From 9b7dcce7ed1255a91286dedba785085fe40198e0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jul 2023 17:40:17 +0200 Subject: [PATCH 0001/1151] Bump version to 2023.9.0dev0 (#97265) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4561e8a53e1..31a158c1ffd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,7 +32,7 @@ env: CACHE_VERSION: 5 PIP_CACHE_VERSION: 4 MYPY_CACHE_VERSION: 4 - HA_SHORT_VERSION: 2023.8 + HA_SHORT_VERSION: 2023.9 DEFAULT_PYTHON: "3.11" ALL_PYTHON_VERSIONS: "['3.11']" # 10.3 is the oldest supported version diff --git a/homeassistant/const.py b/homeassistant/const.py index 513d72555a5..a41710f1280 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,7 +6,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 -MINOR_VERSION: Final = 8 +MINOR_VERSION: Final = 9 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index 1f179518fd9..497a4540dda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.8.0.dev0" +version = "2023.9.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From fd3c2c28118c621e08868f2872f1740fa6914dd3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Jul 2023 21:22:22 +0200 Subject: [PATCH 0002/1151] Fix zodiac import flow/issue (#97282) --- homeassistant/components/zodiac/__init__.py | 40 ++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zodiac/__init__.py b/homeassistant/components/zodiac/__init__.py index 81d5b5bdc21..48d1d8aa7aa 100644 --- a/homeassistant/components/zodiac/__init__.py +++ b/homeassistant/components/zodiac/__init__.py @@ -17,27 +17,27 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the zodiac component.""" - - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.1.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Zodiac", - }, - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config + if DOMAIN in config: + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.1.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Zodiac", + }, + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) ) - ) return True From 7113db8da41c1be5deaa9d81fee6f655e2c85989 Mon Sep 17 00:00:00 2001 From: "J.P. Krauss" Date: Wed, 26 Jul 2023 12:30:25 -0700 Subject: [PATCH 0003/1151] Improve AirNow Configuration Error Handling (#97267) * Fix config flow error handling when no data is returned by AirNow API * Add test for PyAirNow EmptyResponseError * Typo Fix --- homeassistant/components/airnow/config_flow.py | 4 +++- homeassistant/components/airnow/strings.json | 2 +- tests/components/airnow/test_config_flow.py | 13 ++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airnow/config_flow.py b/homeassistant/components/airnow/config_flow.py index 39dbef48647..67bce66e167 100644 --- a/homeassistant/components/airnow/config_flow.py +++ b/homeassistant/components/airnow/config_flow.py @@ -2,7 +2,7 @@ import logging from pyairnow import WebServiceAPI -from pyairnow.errors import AirNowError, InvalidKeyError +from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -35,6 +35,8 @@ async def validate_input(hass: core.HomeAssistant, data): raise InvalidAuth from exc except AirNowError as exc: raise CannotConnect from exc + except EmptyResponseError as exc: + raise InvalidLocation from exc if not test_data: raise InvalidLocation diff --git a/homeassistant/components/airnow/strings.json b/homeassistant/components/airnow/strings.json index aed12596176..072f0988c19 100644 --- a/homeassistant/components/airnow/strings.json +++ b/homeassistant/components/airnow/strings.json @@ -14,7 +14,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_location": "No results found for that location", + "invalid_location": "No results found for that location, try changing the location or station radius.", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index efa462ee4e6..5fda5f532a3 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -1,7 +1,7 @@ """Test the AirNow config flow.""" from unittest.mock import AsyncMock -from pyairnow.errors import AirNowError, InvalidKeyError +from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError import pytest from homeassistant import config_entries, data_entry_flow @@ -55,6 +55,17 @@ async def test_form_cannot_connect(hass: HomeAssistant, config, setup_airnow) -> assert result2["errors"] == {"base": "cannot_connect"} +@pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=EmptyResponseError)]) +async def test_form_empty_result(hass: HomeAssistant, config, setup_airnow) -> None: + """Test we handle empty response error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_location"} + + @pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=RuntimeError)]) async def test_form_unexpected(hass: HomeAssistant, config, setup_airnow) -> None: """Test we handle an unexpected error.""" From dce9a1b9988a4a305bf14a819be153112440dd08 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 26 Jul 2023 22:03:38 +0200 Subject: [PATCH 0004/1151] Rename key of water level sensor in PEGELONLINE (#97289) --- homeassistant/components/pegel_online/coordinator.py | 4 ++-- homeassistant/components/pegel_online/model.py | 2 +- homeassistant/components/pegel_online/sensor.py | 8 ++++---- homeassistant/components/pegel_online/strings.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pegel_online/coordinator.py b/homeassistant/components/pegel_online/coordinator.py index 995953c5e36..8fab3ce36ae 100644 --- a/homeassistant/components/pegel_online/coordinator.py +++ b/homeassistant/components/pegel_online/coordinator.py @@ -31,10 +31,10 @@ class PegelOnlineDataUpdateCoordinator(DataUpdateCoordinator[PegelOnlineData]): async def _async_update_data(self) -> PegelOnlineData: """Fetch data from API endpoint.""" try: - current_measurement = await self.api.async_get_station_measurement( + water_level = await self.api.async_get_station_measurement( self.station.uuid ) except CONNECT_ERRORS as err: raise UpdateFailed(f"Failed to communicate with API: {err}") from err - return {"current_measurement": current_measurement} + return {"water_level": water_level} diff --git a/homeassistant/components/pegel_online/model.py b/homeassistant/components/pegel_online/model.py index c1760d3261b..c8dac75bcf2 100644 --- a/homeassistant/components/pegel_online/model.py +++ b/homeassistant/components/pegel_online/model.py @@ -8,4 +8,4 @@ from aiopegelonline import CurrentMeasurement class PegelOnlineData(TypedDict): """TypedDict for PEGELONLINE Coordinator Data.""" - current_measurement: CurrentMeasurement + water_level: CurrentMeasurement diff --git a/homeassistant/components/pegel_online/sensor.py b/homeassistant/components/pegel_online/sensor.py index 7d48635781b..14ec0c2d032 100644 --- a/homeassistant/components/pegel_online/sensor.py +++ b/homeassistant/components/pegel_online/sensor.py @@ -37,11 +37,11 @@ class PegelOnlineSensorEntityDescription( SENSORS: tuple[PegelOnlineSensorEntityDescription, ...] = ( PegelOnlineSensorEntityDescription( - key="current_measurement", - translation_key="current_measurement", + key="water_level", + translation_key="water_level", state_class=SensorStateClass.MEASUREMENT, - fn_native_unit=lambda data: data["current_measurement"].uom, - fn_native_value=lambda data: data["current_measurement"].value, + fn_native_unit=lambda data: data["water_level"].uom, + fn_native_value=lambda data: data["water_level"].value, icon="mdi:waves-arrow-up", ), ) diff --git a/homeassistant/components/pegel_online/strings.json b/homeassistant/components/pegel_online/strings.json index 71ec95f825c..930e349f9c3 100644 --- a/homeassistant/components/pegel_online/strings.json +++ b/homeassistant/components/pegel_online/strings.json @@ -26,7 +26,7 @@ }, "entity": { "sensor": { - "current_measurement": { + "water_level": { "name": "Water level" } } From 7d8462b11c2453d852d26d6e836599eaebd35a52 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 26 Jul 2023 23:12:01 +0200 Subject: [PATCH 0005/1151] Weather remove forecast deprecation (#97292) --- homeassistant/components/weather/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 89bd601fdae..f0c32f2d8cc 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -272,8 +272,6 @@ class WeatherEntity(Entity): "visibility_unit", "_attr_precipitation_unit", "precipitation_unit", - "_attr_forecast", - "forecast", ) ): if _reported is False: From b31cfe0b24dc9b20302d38bfb60dac7d61142d30 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Thu, 27 Jul 2023 00:15:01 -0400 Subject: [PATCH 0006/1151] Add Schlage integration (#93777) Co-authored-by: J. Nick Koston Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- CODEOWNERS | 2 + homeassistant/components/schlage/__init__.py | 39 +++++++++ .../components/schlage/config_flow.py | 58 +++++++++++++ homeassistant/components/schlage/const.py | 9 ++ .../components/schlage/coordinator.py | 41 +++++++++ homeassistant/components/schlage/lock.py | 84 ++++++++++++++++++ .../components/schlage/manifest.json | 9 ++ homeassistant/components/schlage/strings.json | 19 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/schlage/__init__.py | 1 + tests/components/schlage/conftest.py | 48 +++++++++++ tests/components/schlage/test_config_flow.py | 80 +++++++++++++++++ tests/components/schlage/test_init.py | 61 +++++++++++++ tests/components/schlage/test_lock.py | 86 +++++++++++++++++++ 17 files changed, 550 insertions(+) create mode 100644 homeassistant/components/schlage/__init__.py create mode 100644 homeassistant/components/schlage/config_flow.py create mode 100644 homeassistant/components/schlage/const.py create mode 100644 homeassistant/components/schlage/coordinator.py create mode 100644 homeassistant/components/schlage/lock.py create mode 100644 homeassistant/components/schlage/manifest.json create mode 100644 homeassistant/components/schlage/strings.json create mode 100644 tests/components/schlage/__init__.py create mode 100644 tests/components/schlage/conftest.py create mode 100644 tests/components/schlage/test_config_flow.py create mode 100644 tests/components/schlage/test_init.py create mode 100644 tests/components/schlage/test_lock.py diff --git a/CODEOWNERS b/CODEOWNERS index 10acd5dd65a..f85b796b145 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1077,6 +1077,8 @@ build.json @home-assistant/supervisor /tests/components/scene/ @home-assistant/core /homeassistant/components/schedule/ @home-assistant/core /tests/components/schedule/ @home-assistant/core +/homeassistant/components/schlage/ @dknowles2 +/tests/components/schlage/ @dknowles2 /homeassistant/components/schluter/ @prairieapps /homeassistant/components/scrape/ @fabaff @gjohansson-ST @epenet /tests/components/scrape/ @fabaff @gjohansson-ST @epenet diff --git a/homeassistant/components/schlage/__init__.py b/homeassistant/components/schlage/__init__.py new file mode 100644 index 00000000000..7991645e20d --- /dev/null +++ b/homeassistant/components/schlage/__init__.py @@ -0,0 +1,39 @@ +"""The Schlage integration.""" +from __future__ import annotations + +from pycognito.exceptions import WarrantException +import pyschlage + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, LOGGER +from .coordinator import SchlageDataUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.LOCK] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Schlage from a config entry.""" + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + try: + auth = await hass.async_add_executor_job(pyschlage.Auth, username, password) + except WarrantException as ex: + LOGGER.error("Schlage authentication failed: %s", ex) + return False + + coordinator = SchlageDataUpdateCoordinator(hass, username, pyschlage.Schlage(auth)) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await coordinator.async_config_entry_first_refresh() + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/schlage/config_flow.py b/homeassistant/components/schlage/config_flow.py new file mode 100644 index 00000000000..7e095466087 --- /dev/null +++ b/homeassistant/components/schlage/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow for Schlage integration.""" +from __future__ import annotations + +from typing import Any + +import pyschlage +from pyschlage.exceptions import NotAuthorizedError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN, LOGGER + +STEP_USER_DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Schlage.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + try: + user_id = await self.hass.async_add_executor_job( + _authenticate, username, password + ) + except NotAuthorizedError: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unknown error") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(user_id) + return self.async_create_entry(title=username, data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +def _authenticate(username: str, password: str) -> str: + """Authenticate with the Schlage API.""" + auth = pyschlage.Auth(username, password) + auth.authenticate() + # The user_id property will make a blocking call if it's not already + # cached. To avoid blocking the event loop, we read it here. + return auth.user_id diff --git a/homeassistant/components/schlage/const.py b/homeassistant/components/schlage/const.py new file mode 100644 index 00000000000..1effd4bb334 --- /dev/null +++ b/homeassistant/components/schlage/const.py @@ -0,0 +1,9 @@ +"""Constants for the Schlage integration.""" + +from datetime import timedelta +import logging + +DOMAIN = "schlage" +LOGGER = logging.getLogger(__package__) +MANUFACTURER = "Schlage" +UPDATE_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/schlage/coordinator.py b/homeassistant/components/schlage/coordinator.py new file mode 100644 index 00000000000..8b9cde21f90 --- /dev/null +++ b/homeassistant/components/schlage/coordinator.py @@ -0,0 +1,41 @@ +"""DataUpdateCoordinator for the Schlage integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from pyschlage import Lock, Schlage +from pyschlage.exceptions import Error + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER, UPDATE_INTERVAL + + +@dataclass +class SchlageData: + """Container for cached data from the Schlage API.""" + + locks: dict[str, Lock] + + +class SchlageDataUpdateCoordinator(DataUpdateCoordinator[SchlageData]): + """The Schlage data update coordinator.""" + + def __init__(self, hass: HomeAssistant, username: str, api: Schlage) -> None: + """Initialize the class.""" + super().__init__( + hass, LOGGER, name=f"{DOMAIN} ({username})", update_interval=UPDATE_INTERVAL + ) + self.api = api + + 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: + raise UpdateFailed("Failed to refresh Schlage data") from ex + + 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()}) diff --git a/homeassistant/components/schlage/lock.py b/homeassistant/components/schlage/lock.py new file mode 100644 index 00000000000..ad7ff863d40 --- /dev/null +++ b/homeassistant/components/schlage/lock.py @@ -0,0 +1,84 @@ +"""Platform for Schlage lock integration.""" +from __future__ import annotations + +from typing import Any + +from pyschlage.lock import Lock + +from homeassistant.components.lock import LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import SchlageDataUpdateCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Schlage WiFi locks based on a config entry.""" + coordinator: SchlageDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + SchlageLockEntity(coordinator=coordinator, device_id=device_id) + for device_id in coordinator.data.locks + ) + + +class SchlageLockEntity(CoordinatorEntity[SchlageDataUpdateCoordinator], LockEntity): + """Schlage lock entity.""" + + _attr_has_entity_name = True + _attr_name = None + + def __init__( + self, coordinator: SchlageDataUpdateCoordinator, device_id: str + ) -> None: + """Initialize a Schlage Lock.""" + super().__init__(coordinator=coordinator) + self.device_id = device_id + self._attr_unique_id = device_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + name=self._lock.name, + manufacturer=MANUFACTURER, + model=self._lock.model_name, + sw_version=self._lock.firmware_version, + ) + self._update_attrs() + + @property + def _lock(self) -> Lock: + """Fetch the Schlage lock from our coordinator.""" + return self.coordinator.data.locks[self.device_id] + + @property + def available(self) -> bool: + """Return if entity is available.""" + # When is_locked is None the lock is unavailable. + return super().available and self._lock.is_locked is not None + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attrs() + return super()._handle_coordinator_update() + + def _update_attrs(self) -> None: + """Update our internal state attributes.""" + self._attr_is_locked = self._lock.is_locked + self._attr_is_jammed = self._lock.is_jammed + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the device.""" + await self.hass.async_add_executor_job(self._lock.lock) + await self.coordinator.async_request_refresh() + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the device.""" + await self.hass.async_add_executor_job(self._lock.unlock) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json new file mode 100644 index 00000000000..cbc173b8c34 --- /dev/null +++ b/homeassistant/components/schlage/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "schlage", + "name": "Schlage", + "codeowners": ["@dknowles2"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/schlage", + "iot_class": "cloud_polling", + "requirements": ["pyschlage==2023.5.0"] +} diff --git a/homeassistant/components/schlage/strings.json b/homeassistant/components/schlage/strings.json new file mode 100644 index 00000000000..4f32ad094c0 --- /dev/null +++ b/homeassistant/components/schlage/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 10221d1d589..7de32dc5071 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -395,6 +395,7 @@ FLOWS = { "rympro", "sabnzbd", "samsungtv", + "schlage", "scrape", "screenlogic", "season", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a3a8c334c11..aa3ad84f192 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4849,6 +4849,12 @@ "config_flow": false, "iot_class": "local_push" }, + "schlage": { + "name": "Schlage", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "schluter": { "name": "Schluter", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index edd1edd7c2f..0d85e334e63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1981,6 +1981,9 @@ pysabnzbd==1.1.1 # homeassistant.components.saj pysaj==0.0.16 +# homeassistant.components.schlage +pyschlage==2023.5.0 + # homeassistant.components.sensibo pysensibo==1.0.31 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a07488a1898..45f110e4c5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1473,6 +1473,9 @@ pyrympro==0.0.7 # homeassistant.components.sabnzbd pysabnzbd==1.1.1 +# homeassistant.components.schlage +pyschlage==2023.5.0 + # homeassistant.components.sensibo pysensibo==1.0.31 diff --git a/tests/components/schlage/__init__.py b/tests/components/schlage/__init__.py new file mode 100644 index 00000000000..c6cd3fec0bc --- /dev/null +++ b/tests/components/schlage/__init__.py @@ -0,0 +1 @@ +"""Tests for the Schlage integration.""" diff --git a/tests/components/schlage/conftest.py b/tests/components/schlage/conftest.py new file mode 100644 index 00000000000..681024358c6 --- /dev/null +++ b/tests/components/schlage/conftest.py @@ -0,0 +1,48 @@ +"""Common fixtures for the Schlage tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.schlage.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock ConfigEntry.""" + return MockConfigEntry( + title="asdf@asdf.com", + domain=DOMAIN, + data={ + CONF_USERNAME: "asdf@asdf.com", + CONF_PASSWORD: "hunter2", + }, + unique_id="abc123", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.schlage.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_schlage(): + """Mock pyschlage.Schlage.""" + with patch("pyschlage.Schlage", autospec=True) as mock_schlage: + yield mock_schlage.return_value + + +@pytest.fixture +def mock_pyschlage_auth(): + """Mock pyschlage.Auth.""" + with patch("pyschlage.Auth", autospec=True) as mock_auth: + mock_auth.return_value.user_id = "abc123" + yield mock_auth.return_value diff --git a/tests/components/schlage/test_config_flow.py b/tests/components/schlage/test_config_flow.py new file mode 100644 index 00000000000..b256e8950ed --- /dev/null +++ b/tests/components/schlage/test_config_flow.py @@ -0,0 +1,80 @@ +"""Test the Schlage config flow.""" +from unittest.mock import AsyncMock, Mock + +from pyschlage.exceptions import Error as PyschlageError, NotAuthorizedError +import pytest + +from homeassistant import config_entries +from homeassistant.components.schlage.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_form( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_pyschlage_auth: Mock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + mock_pyschlage_auth.authenticate.assert_called_once_with() + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "test-username" + assert result2["data"] == { + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth( + hass: HomeAssistant, mock_pyschlage_auth: Mock +) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_pyschlage_auth.authenticate.side_effect = NotAuthorizedError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_unknown(hass: HomeAssistant, mock_pyschlage_auth: Mock) -> None: + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_pyschlage_auth.authenticate.side_effect = PyschlageError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/schlage/test_init.py b/tests/components/schlage/test_init.py new file mode 100644 index 00000000000..0811d87ec80 --- /dev/null +++ b/tests/components/schlage/test_init.py @@ -0,0 +1,61 @@ +"""Tests for the Schlage integration.""" + +from unittest.mock import Mock, patch + +from pycognito.exceptions import WarrantException +from pyschlage.exceptions import Error + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@patch( + "pyschlage.Auth", + side_effect=WarrantException, +) +async def test_auth_failed( + mock_auth: Mock, hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test failed auth on setup.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_auth.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_update_data_fails( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_pyschlage_auth: Mock, + mock_schlage: Mock, +) -> None: + """Test that we properly handle API errors.""" + mock_schlage.locks.side_effect = Error + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_schlage.locks.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_pyschlage_auth: Mock, + mock_schlage: Mock, +) -> None: + """Test the Schlage configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/schlage/test_lock.py b/tests/components/schlage/test_lock.py new file mode 100644 index 00000000000..8819d8558fd --- /dev/null +++ b/tests/components/schlage/test_lock.py @@ -0,0 +1,86 @@ +"""Test schlage lock.""" +from unittest.mock import Mock, create_autospec + +from pyschlage.lock import Lock +import pytest + +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.components.schlage.const import 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 tests.common import MockConfigEntry + + +@pytest.fixture +def mock_lock(): + """Mock Lock fixture.""" + mock_lock = create_autospec(Lock) + mock_lock.configure_mock( + device_id="test", + name="Vault Door", + model_name="", + is_locked=False, + is_jammed=False, + battery_level=0, + firmware_version="1.0", + ) + return mock_lock + + +@pytest.fixture +async def mock_entry( + hass: HomeAssistant, mock_pyschlage_auth: Mock, mock_schlage: Mock, mock_lock: Mock +) -> ConfigEntry: + """Create and add a mock ConfigEntry.""" + mock_schlage.locks.return_value = [mock_lock] + entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-username", "password": "test-password"}, + entry_id="test-username", + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert DOMAIN in hass.config_entries.async_domains() + return entry + + +async def test_lock_device_registry( + hass: HomeAssistant, mock_entry: ConfigEntry +) -> None: + """Test lock 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_lock_services( + hass: HomeAssistant, mock_lock: Mock, mock_entry: ConfigEntry +) -> None: + """Test lock services.""" + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + service_data={ATTR_ENTITY_ID: "lock.vault_door"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_lock.lock.assert_called_once_with() + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + service_data={ATTR_ENTITY_ID: "lock.vault_door"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_lock.unlock.assert_called_once_with() + + await hass.config_entries.async_unload(mock_entry.entry_id) From 55beb261901e72dab9df2aac272e1c3c16222580 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 27 Jul 2023 08:54:20 +0200 Subject: [PATCH 0007/1151] Fix authlib version constraint required by point (#97228) --- homeassistant/components/point/__init__.py | 5 ++--- homeassistant/package_constraints.txt | 4 ---- script/gen_requirements_all.py | 4 ---- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 6600a8240a0..627736f605d 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -97,9 +97,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: token_saver=token_saver, ) try: - # pylint: disable-next=fixme - # TODO Remove authlib constraint when refactoring this code - await session.ensure_active_token() + # the call to user() implicitly calls ensure_active_token() in authlib + await session.user() except ConnectTimeout as err: _LOGGER.debug("Connection Timeout") raise ConfigEntryNotReady from err diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a9239bcfda8..3ce056e1a46 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -121,10 +121,6 @@ python-socketio>=4.6.0,<5.0 # https://github.com/home-assistant/core/pull/67046 multidict>=6.0.2 -# Required for compatibility with point integration - ensure_active_token -# https://github.com/home-assistant/core/pull/68176 -authlib<1.0 - # Version 2.0 added typing, prevent accidental fallbacks backoff>=2.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9302d547786..02d528c33e2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -123,10 +123,6 @@ python-socketio>=4.6.0,<5.0 # https://github.com/home-assistant/core/pull/67046 multidict>=6.0.2 -# Required for compatibility with point integration - ensure_active_token -# https://github.com/home-assistant/core/pull/68176 -authlib<1.0 - # Version 2.0 added typing, prevent accidental fallbacks backoff>=2.0 From 3fd33c4ecc5c093a9ccd727021d1bfc1b67839c0 Mon Sep 17 00:00:00 2001 From: Markus Becker Date: Thu, 27 Jul 2023 08:58:52 +0200 Subject: [PATCH 0008/1151] Fix typo Lomng -> Long (#97315) --- homeassistant/components/matter/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index bfdba33327b..c68b38bbb8c 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -52,7 +52,7 @@ "state": { "switch_latched": "Switch latched", "initial_press": "Initial press", - "long_press": "Lomng press", + "long_press": "Long press", "short_release": "Short release", "long_release": "Long release", "multi_press_ongoing": "Multi press ongoing", From 01ba7a869833022118b909fba11b54c55c6e0c8c Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 27 Jul 2023 03:13:49 -0400 Subject: [PATCH 0009/1151] bump python-roborock to 0.30.2 (#97306) --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 5f6aa63ce2f..d26116a7818 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.30.1"] + "requirements": ["python-roborock==0.30.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0d85e334e63..1f443ec83c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.30.1 +python-roborock==0.30.2 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45f110e4c5d..481ef6c71d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1582,7 +1582,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.30.1 +python-roborock==0.30.2 # homeassistant.components.smarttub python-smarttub==0.0.33 From e9c3f0821ff360b77dd451999fcd63a9584a3580 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jul 2023 02:17:27 -0500 Subject: [PATCH 0010/1151] Fix dumping lru stats in the profiler (#97303) --- homeassistant/components/profiler/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index ba5f25a1c02..8c5c206ae9f 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -45,7 +45,6 @@ _KNOWN_LRU_CLASSES = ( "StatesMetaManager", "StateAttributesManager", "StatisticsMetaManager", - "DomainData", "IntegrationMatcher", ) From 265fe51169ff32699f6b59293127667e3ee88275 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 27 Jul 2023 09:21:30 +0200 Subject: [PATCH 0011/1151] Bump aioslimproto to 2.3.3 (#97283) --- homeassistant/components/slimproto/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index 1ef87e84933..b221db96262 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -6,5 +6,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/slimproto", "iot_class": "local_push", - "requirements": ["aioslimproto==2.3.2"] + "requirements": ["aioslimproto==2.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1f443ec83c2..1bf6250db67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -345,7 +345,7 @@ aioshelly==5.4.0 aioskybell==22.7.0 # homeassistant.components.slimproto -aioslimproto==2.3.2 +aioslimproto==2.3.3 # homeassistant.components.steamist aiosteamist==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 481ef6c71d4..47948deb776 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ aioshelly==5.4.0 aioskybell==22.7.0 # homeassistant.components.slimproto -aioslimproto==2.3.2 +aioslimproto==2.3.3 # homeassistant.components.steamist aiosteamist==0.3.2 From c2bbb0b5db25ee375f2bf3e2524403c6bf7998d3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Jul 2023 09:22:22 +0200 Subject: [PATCH 0012/1151] Fix implicit use of device name in TPLink switch (#97293) --- homeassistant/components/tplink/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index d82308a2e32..6c843246663 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -84,6 +84,8 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): class SmartPlugSwitch(CoordinatedTPLinkEntity, SwitchEntity): """Representation of a TPLink Smart Plug switch.""" + _attr_name = None + def __init__( self, device: SmartDevice, From 3fcfe7d0c6221cef54428a59b053304a02a750d3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 27 Jul 2023 09:23:23 +0200 Subject: [PATCH 0013/1151] Set mqtt entity name to `null` when it is a duplicate of the device name (#97304) --- homeassistant/components/mqtt/mixins.py | 9 +++++++++ tests/components/mqtt/test_mixins.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 70b681ffbb2..9f0849a4d4c 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -1135,7 +1135,16 @@ class MqttEntity( "MQTT device information always needs to include a name, got %s, " "if device information is shared between multiple entities, the device " "name must be included in each entity's device configuration", + config, ) + elif config[CONF_DEVICE][CONF_NAME] == entity_name: + _LOGGER.warning( + "MQTT device name is equal to entity name in your config %s, " + "this is not expected. Please correct your configuration. " + "The entity name will be set to `null`", + config, + ) + self._attr_name = None def _setup_common_attributes_from_config(self, config: ConfigType) -> None: """(Re)Setup the common attributes for the entity.""" diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index 5a30a3a65de..23367d7829f 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -212,6 +212,26 @@ async def test_availability_with_shared_state_topic( None, True, ), + ( # entity_name_and_device_name_the_sane + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "Hello world", + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": { + "identifiers": ["helloworld"], + "name": "Hello world", + }, + } + } + }, + "sensor.hello_world", + "Hello world", + "Hello world", + False, + ), ], ids=[ "default_entity_name_without_device_name", @@ -222,6 +242,7 @@ async def test_availability_with_shared_state_topic( "name_set_no_device_name_set", "none_entity_name_with_device_name", "none_entity_name_without_device_name", + "entity_name_and_device_name_the_sane", ], ) @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) From f610a9b1c9b47e5146c52850298448b216c200b8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 27 Jul 2023 09:24:32 +0200 Subject: [PATCH 0014/1151] Fix sql entities not loading (#97316) --- homeassistant/components/sql/sensor.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index cbdef90f623..0c8e90b8895 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -84,7 +84,11 @@ async def async_setup_platform( if value_template is not None: value_template.hass = hass - trigger_entity_config = {CONF_NAME: name, CONF_DEVICE_CLASS: device_class} + trigger_entity_config = { + CONF_NAME: name, + CONF_DEVICE_CLASS: device_class, + CONF_UNIQUE_ID: unique_id, + } if availability: trigger_entity_config[CONF_AVAILABILITY] = availability if icon: @@ -132,7 +136,11 @@ async def async_setup_entry( value_template.hass = hass name_template = Template(name, hass) - trigger_entity_config = {CONF_NAME: name_template, CONF_DEVICE_CLASS: device_class} + trigger_entity_config = { + CONF_NAME: name_template, + CONF_DEVICE_CLASS: device_class, + CONF_UNIQUE_ID: entry.entry_id, + } await async_setup_sensor( hass, @@ -269,7 +277,6 @@ async def async_setup_sensor( column_name, unit, value_template, - unique_id, yaml, state_class, use_database_executor, @@ -322,7 +329,6 @@ class SQLSensor(ManualTriggerEntity, SensorEntity): column: str, unit: str | None, value_template: Template | None, - unique_id: str | None, yaml: bool, state_class: SensorStateClass | None, use_database_executor: bool, @@ -336,14 +342,16 @@ class SQLSensor(ManualTriggerEntity, SensorEntity): self._column_name = column self.sessionmaker = sessmaker self._attr_extra_state_attributes = {} - self._attr_unique_id = unique_id self._use_database_executor = use_database_executor self._lambda_stmt = _generate_lambda_stmt(query) + self._attr_name = ( + None if not yaml else trigger_entity_config[CONF_NAME].template + ) self._attr_has_entity_name = not yaml - if not yaml and unique_id: + if not yaml and trigger_entity_config.get(CONF_UNIQUE_ID): self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, unique_id)}, + identifiers={(DOMAIN, trigger_entity_config[CONF_UNIQUE_ID])}, manufacturer="SQL", name=trigger_entity_config[CONF_NAME].template, ) From e99ba1b0dad9edc71d71509c450dee65162c14a3 Mon Sep 17 00:00:00 2001 From: dougiteixeira <31328123+dougiteixeira@users.noreply.github.com> Date: Thu, 27 Jul 2023 05:21:22 -0300 Subject: [PATCH 0015/1151] Move async_client_device_info_fn to entity.py (#97270) Move client device info --- homeassistant/components/unifi/entity.py | 11 +++++++++++ homeassistant/components/unifi/sensor.py | 15 +-------------- homeassistant/components/unifi/switch.py | 13 +------------ 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index 54b9cb12157..db7b414b3b0 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -74,6 +74,17 @@ def async_wlan_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceIn ) +@callback +def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for client.""" + client = api.clients[obj_id] + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, obj_id)}, + default_manufacturer=client.oui, + default_name=client.name or client.hostname, + ) + + @dataclass class UnifiDescription(Generic[HandlerT, ApiItemT]): """Validate and load entities from different UniFi handlers.""" diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 8cdc0dcbb71..7dc086878e3 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -10,7 +10,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta from typing import Generic -import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients from aiounifi.interfaces.ports import Ports @@ -28,8 +27,6 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, UnitOfInformation, UnitOfPower from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util @@ -39,6 +36,7 @@ from .entity import ( HandlerT, UnifiEntity, UnifiEntityDescription, + async_client_device_info_fn, async_device_available_fn, async_device_device_info_fn, async_wlan_device_info_fn, @@ -83,17 +81,6 @@ def async_wlan_client_value_fn(controller: UniFiController, wlan: Wlan) -> int: ) -@callback -def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: - """Create device registry entry for client.""" - client = api.clients[obj_id] - return DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, obj_id)}, - default_manufacturer=client.oui, - default_name=client.name or client.hostname, - ) - - @dataclass class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): """Validate and load entities from different UniFi handlers.""" diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 64e3ec2455c..140da492a96 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -42,7 +42,6 @@ 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, DeviceEntryType, ) from homeassistant.helpers.entity import DeviceInfo @@ -55,6 +54,7 @@ from .entity import ( SubscriptionT, UnifiEntity, UnifiEntityDescription, + async_client_device_info_fn, async_device_available_fn, async_device_device_info_fn, async_wlan_device_info_fn, @@ -77,17 +77,6 @@ def async_dpi_group_is_on_fn( ) -@callback -def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: - """Create device registry entry for client.""" - client = api.clients[obj_id] - return DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, obj_id)}, - default_manufacturer=client.oui, - default_name=client.name or client.hostname, - ) - - @callback def async_dpi_group_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: """Create device registry entry for DPI group.""" From cd1a99a15fab8da290990e7d80bdd1dcb610c9d1 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Thu, 27 Jul 2023 09:54:44 -0400 Subject: [PATCH 0016/1151] Bump pydrawise to 2023.7.1 (#97334) --- homeassistant/components/hydrawise/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 48c9cdcf042..d9e6d809960 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2023.7.0"] + "requirements": ["pydrawise==2023.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1bf6250db67..acacd333914 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1644,7 +1644,7 @@ pydiscovergy==2.0.1 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2023.7.0 +pydrawise==2023.7.1 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 From 2542c5f259d83dc04b3964658c538ddd63ab9e73 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Thu, 27 Jul 2023 09:57:36 -0400 Subject: [PATCH 0017/1151] Fix Hydrawise zone addressing (#97333) --- .../components/hydrawise/binary_sensor.py | 2 +- homeassistant/components/hydrawise/sensor.py | 2 +- homeassistant/components/hydrawise/switch.py | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index bc9b8722c58..63fe28cd400 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -92,6 +92,6 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): if self.entity_description.key == "status": self._attr_is_on = self.coordinator.api.status == "All good!" elif self.entity_description.key == "is_watering": - relay_data = self.coordinator.api.relays[self.data["relay"] - 1] + relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]] self._attr_is_on = relay_data["timestr"] == "Now" super()._handle_coordinator_update() diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 9214b9daeac..fa82c058f5b 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -77,7 +77,7 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): def _handle_coordinator_update(self) -> None: """Get the latest data and updates the states.""" LOGGER.debug("Updating Hydrawise sensor: %s", self.name) - relay_data = self.coordinator.api.relays[self.data["relay"] - 1] + relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]] if self.entity_description.key == "watering_time": if relay_data["timestr"] == "Now": self._attr_native_value = int(relay_data["run"] / 60) diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index dbd2c08b28e..0dd694a47d6 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -99,26 +99,26 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity): def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - relay_data = self.data["relay"] - 1 + zone_number = self.data["relay"] if self.entity_description.key == "manual_watering": - self.coordinator.api.run_zone(self._default_watering_timer, relay_data) + self.coordinator.api.run_zone(self._default_watering_timer, zone_number) elif self.entity_description.key == "auto_watering": - self.coordinator.api.suspend_zone(0, relay_data) + self.coordinator.api.suspend_zone(0, zone_number) def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - relay_data = self.data["relay"] - 1 + zone_number = self.data["relay"] if self.entity_description.key == "manual_watering": - self.coordinator.api.run_zone(0, relay_data) + self.coordinator.api.run_zone(0, zone_number) elif self.entity_description.key == "auto_watering": - self.coordinator.api.suspend_zone(365, relay_data) + self.coordinator.api.suspend_zone(365, zone_number) @callback def _handle_coordinator_update(self) -> None: """Update device state.""" - relay_data = self.data["relay"] - 1 + zone_number = self.data["relay"] LOGGER.debug("Updating Hydrawise switch: %s", self.name) - timestr = self.coordinator.api.relays[relay_data]["timestr"] + timestr = self.coordinator.api.relays_by_zone_number[zone_number]["timestr"] if self.entity_description.key == "manual_watering": self._attr_is_on = timestr == "Now" elif self.entity_description.key == "auto_watering": From 374255ce878d333e834516abc4d20b2bf879917f Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 27 Jul 2023 16:00:27 +0200 Subject: [PATCH 0018/1151] Duotecno beta fix (#97325) * Fix duotecno * Implement comments * small cover fix --- homeassistant/components/duotecno/__init__.py | 2 +- homeassistant/components/duotecno/cover.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/duotecno/__init__.py b/homeassistant/components/duotecno/__init__.py index 668a38dae5b..98003c3e8c4 100644 --- a/homeassistant/components/duotecno/__init__.py +++ b/homeassistant/components/duotecno/__init__.py @@ -22,10 +22,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await controller.connect( entry.data[CONF_HOST], entry.data[CONF_PORT], entry.data[CONF_PASSWORD] ) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) except (OSError, InvalidPassword, LoadFailure) as err: raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = controller + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/duotecno/cover.py b/homeassistant/components/duotecno/cover.py index 13e3df8fc0a..0fd212df085 100644 --- a/homeassistant/components/duotecno/cover.py +++ b/homeassistant/components/duotecno/cover.py @@ -26,7 +26,7 @@ async def async_setup_entry( """Set up the duoswitch endities.""" cntrl = hass.data[DOMAIN][entry.entry_id] async_add_entities( - DuotecnoCover(channel) for channel in cntrl.get_units("DuoSwitchUnit") + DuotecnoCover(channel) for channel in cntrl.get_units("DuoswitchUnit") ) From cbc8ebb4274bee358d85a04d9fb06305663ac5d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jul 2023 09:30:31 -0500 Subject: [PATCH 0019/1151] Bump aioesphomeapi to 15.1.15 (#97335) changelog: https://github.com/esphome/aioesphomeapi/compare/v15.1.14...v15.1.15 --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b9b235ab41e..d35cf90c60f 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async_interrupt==1.1.1", - "aioesphomeapi==15.1.14", + "aioesphomeapi==15.1.15", "bluetooth-data-tools==1.6.1", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index acacd333914..e8a101afc83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.14 +aioesphomeapi==15.1.15 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47948deb776..674312831d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.14 +aioesphomeapi==15.1.15 # homeassistant.components.flo aioflo==2021.11.0 From cc47ff30b3dc7fb8edd46d7a903f7252f9141c73 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 27 Jul 2023 15:32:53 +0100 Subject: [PATCH 0020/1151] Split availability and data subscriptions in homekit_controller (#97337) --- .../components/homekit_controller/connection.py | 16 ++++++++++++---- .../components/homekit_controller/entity.py | 4 +++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index d101517e002..4ba22317644 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -142,7 +142,7 @@ class HKDevice: function=self.async_update, ) - self._all_subscribers: set[CALLBACK_TYPE] = set() + self._availability_callbacks: set[CALLBACK_TYPE] = set() self._subscriptions: dict[tuple[int, int], set[CALLBACK_TYPE]] = {} @property @@ -189,7 +189,7 @@ class HKDevice: if self.available == available: return self.available = available - for callback_ in self._all_subscribers: + for callback_ in self._availability_callbacks: callback_() async def _async_populate_ble_accessory_state(self, event: Event) -> None: @@ -811,12 +811,10 @@ class HKDevice: self, characteristics: Iterable[tuple[int, int]], callback_: CALLBACK_TYPE ) -> CALLBACK_TYPE: """Add characteristics to the watch list.""" - self._all_subscribers.add(callback_) for aid_iid in characteristics: self._subscriptions.setdefault(aid_iid, set()).add(callback_) def _unsub(): - self._all_subscribers.remove(callback_) for aid_iid in characteristics: self._subscriptions[aid_iid].remove(callback_) if not self._subscriptions[aid_iid]: @@ -824,6 +822,16 @@ class HKDevice: return _unsub + @callback + def async_subscribe_availability(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE: + """Add characteristics to the watch list.""" + self._availability_callbacks.add(callback_) + + def _unsub(): + self._availability_callbacks.remove(callback_) + + return _unsub + async def get_characteristics(self, *args: Any, **kwargs: Any) -> dict[str, Any]: """Read latest state from homekit accessory.""" return await self.pairing.get_characteristics(*args, **kwargs) diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index f6aadfac7ac..046dc9f17ec 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -58,7 +58,9 @@ class HomeKitEntity(Entity): self.all_characteristics, self._async_write_ha_state ) ) - + self.async_on_remove( + self._accessory.async_subscribe_availability(self._async_write_ha_state) + ) self._accessory.add_pollable_characteristics(self.pollable_characteristics) await self._accessory.add_watchable_characteristics( self.watchable_characteristics From 7ada88eab3a5d81ba04e1e6101bffee638bb45e1 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 27 Jul 2023 16:58:09 +0200 Subject: [PATCH 0021/1151] Hue event entity follow up (#97336) --- homeassistant/components/hue/const.py | 4 +- homeassistant/components/hue/logbook.py | 78 ------------- homeassistant/components/hue/strings.json | 3 +- .../components/hue/test_device_trigger_v2.py | 1 + tests/components/hue/test_event.py | 1 + tests/components/hue/test_logbook.py | 107 ------------------ 6 files changed, 6 insertions(+), 188 deletions(-) delete mode 100644 homeassistant/components/hue/logbook.py delete mode 100644 tests/components/hue/test_logbook.py diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index d7d254b64a8..38c2587bc1a 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -43,11 +43,11 @@ REQUEST_REFRESH_DELAY = 0.3 # V2 API SPECIFIC CONSTANTS ################## DEFAULT_BUTTON_EVENT_TYPES = ( - # I have never ever seen the `DOUBLE_SHORT_RELEASE` - # or `DOUBLE_SHORT_RELEASE` events so leave them out here + # I have never ever seen the `DOUBLE_SHORT_RELEASE` event so leave it out here ButtonEvent.INITIAL_PRESS, ButtonEvent.REPEAT, ButtonEvent.SHORT_RELEASE, + ButtonEvent.LONG_PRESS, ButtonEvent.LONG_RELEASE, ) diff --git a/homeassistant/components/hue/logbook.py b/homeassistant/components/hue/logbook.py deleted file mode 100644 index 21d0da074a7..00000000000 --- a/homeassistant/components/hue/logbook.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Describe hue logbook events.""" -from __future__ import annotations - -from collections.abc import Callable - -from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME -from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_TYPE -from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers import device_registry as dr - -from .const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN - -TRIGGER_SUBTYPE = { - "button_1": "first button", - "button_2": "second button", - "button_3": "third button", - "button_4": "fourth button", - "double_buttons_1_3": "first and third buttons", - "double_buttons_2_4": "second and fourth buttons", - "dim_down": "dim down", - "dim_up": "dim up", - "turn_off": "turn off", - "turn_on": "turn on", - "1": "first button", - "2": "second button", - "3": "third button", - "4": "fourth button", - "clock_wise": "Rotation clockwise", - "counter_clock_wise": "Rotation counter-clockwise", -} -TRIGGER_TYPE = { - "remote_button_long_release": "{subtype} released after long press", - "remote_button_short_press": "{subtype} pressed", - "remote_button_short_release": "{subtype} released", - "remote_double_button_long_press": "both {subtype} released after long press", - "remote_double_button_short_press": "both {subtype} released", - "initial_press": "{subtype} pressed initially", - "long_press": "{subtype} long press", - "repeat": "{subtype} held down", - "short_release": "{subtype} released after short press", - "long_release": "{subtype} released after long press", - "double_short_release": "both {subtype} released", - "start": '"{subtype}" pressed initially', -} - -UNKNOWN_TYPE = "unknown type" -UNKNOWN_SUB_TYPE = "unknown sub type" - - -@callback -def async_describe_events( - hass: HomeAssistant, - async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], -) -> None: - """Describe hue logbook events.""" - - @callback - def async_describe_hue_event(event: Event) -> dict[str, str]: - """Describe hue logbook event.""" - data = event.data - name: str | None = None - if dev_ent := dr.async_get(hass).async_get(data[CONF_DEVICE_ID]): - name = dev_ent.name - if name is None: - name = data[CONF_ID] - if CONF_TYPE in data: # v2 - subtype = TRIGGER_SUBTYPE.get(str(data[CONF_SUBTYPE]), UNKNOWN_SUB_TYPE) - message = TRIGGER_TYPE.get(data[CONF_TYPE], UNKNOWN_TYPE).format( - subtype=subtype - ) - else: - message = f"Event {data[CONF_EVENT]}" # v1 - return { - LOGBOOK_ENTRY_NAME: name, - LOGBOOK_ENTRY_MESSAGE: message, - } - - async_describe_event(DOMAIN, ATTR_HUE_EVENT, async_describe_hue_event) diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index a6920293ac1..6d65abc8d5f 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -76,7 +76,8 @@ "initial_press": "Initial press", "repeat": "Repeat", "short_release": "Short press", - "long_release": "Long press", + "long_press": "Long press", + "long_release": "Long release", "double_short_release": "Double press" } } diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index bfc0b612c1f..e89f53af73a 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -94,6 +94,7 @@ async def test_get_triggers( ButtonEvent.INITIAL_PRESS, ButtonEvent.LONG_RELEASE, ButtonEvent.REPEAT, + ButtonEvent.LONG_PRESS, ButtonEvent.SHORT_RELEASE, ) for control_id, resource_id in ( diff --git a/tests/components/hue/test_event.py b/tests/components/hue/test_event.py index e3f50318f61..4dbb104357d 100644 --- a/tests/components/hue/test_event.py +++ b/tests/components/hue/test_event.py @@ -28,6 +28,7 @@ async def test_event( "initial_press", "repeat", "short_release", + "long_press", "long_release", ] # trigger firing 'initial_press' event from the device diff --git a/tests/components/hue/test_logbook.py b/tests/components/hue/test_logbook.py deleted file mode 100644 index 3f49efcdeb7..00000000000 --- a/tests/components/hue/test_logbook.py +++ /dev/null @@ -1,107 +0,0 @@ -"""The tests for hue logbook.""" -from homeassistant.components.hue.const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN -from homeassistant.components.hue.v1.hue_event import CONF_LAST_UPDATED -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_EVENT, - CONF_ID, - CONF_TYPE, - CONF_UNIQUE_ID, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr -from homeassistant.setup import async_setup_component - -from .conftest import setup_platform - -from tests.components.logbook.common import MockRow, mock_humanify - -# v1 event -SAMPLE_V1_EVENT = { - CONF_DEVICE_ID: "fe346f17a9f8c15be633f9cc3f3d6631", - CONF_EVENT: 18, - CONF_ID: "hue_tap", - CONF_LAST_UPDATED: "2019-12-28T22:58:03", - CONF_UNIQUE_ID: "00:00:00:00:00:44:23:08-f2", -} -# v2 event -SAMPLE_V2_EVENT = { - CONF_DEVICE_ID: "f974028e7933aea703a2199a855bc4a3", - CONF_ID: "wall_switch_with_2_controls_button", - CONF_SUBTYPE: 1, - CONF_TYPE: "initial_press", - CONF_UNIQUE_ID: "c658d3d8-a013-4b81-8ac6-78b248537e70", -} - - -async def test_humanify_hue_events( - hass: HomeAssistant, mock_bridge_v2, device_registry: dr.DeviceRegistry -) -> None: - """Test hue events when the devices are present in the registry.""" - await setup_platform(hass, mock_bridge_v2, "sensor") - hass.config.components.add("recorder") - assert await async_setup_component(hass, "logbook", {}) - await hass.async_block_till_done() - entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - - v1_device = device_registry.async_get_or_create( - identifiers={(DOMAIN, "v1")}, name="Remote 1", config_entry_id=entry.entry_id - ) - v2_device = device_registry.async_get_or_create( - identifiers={(DOMAIN, "v2")}, name="Remote 2", config_entry_id=entry.entry_id - ) - - (v1_event, v2_event) = mock_humanify( - hass, - [ - MockRow( - ATTR_HUE_EVENT, - {**SAMPLE_V1_EVENT, CONF_DEVICE_ID: v1_device.id}, - ), - MockRow( - ATTR_HUE_EVENT, - {**SAMPLE_V2_EVENT, CONF_DEVICE_ID: v2_device.id}, - ), - ], - ) - - assert v1_event["name"] == "Remote 1" - assert v1_event["domain"] == DOMAIN - assert v1_event["message"] == "Event 18" - - assert v2_event["name"] == "Remote 2" - assert v2_event["domain"] == DOMAIN - assert v2_event["message"] == "first button pressed initially" - - -async def test_humanify_hue_events_devices_removed( - hass: HomeAssistant, mock_bridge_v2 -) -> None: - """Test hue events when the devices have been removed from the registry.""" - await setup_platform(hass, mock_bridge_v2, "sensor") - hass.config.components.add("recorder") - assert await async_setup_component(hass, "logbook", {}) - await hass.async_block_till_done() - - (v1_event, v2_event) = mock_humanify( - hass, - [ - MockRow( - ATTR_HUE_EVENT, - SAMPLE_V1_EVENT, - ), - MockRow( - ATTR_HUE_EVENT, - SAMPLE_V2_EVENT, - ), - ], - ) - - assert v1_event["name"] == "hue_tap" - assert v1_event["domain"] == DOMAIN - assert v1_event["message"] == "Event 18" - - assert v2_event["name"] == "wall_switch_with_2_controls_button" - assert v2_event["domain"] == DOMAIN - assert v2_event["message"] == "first button pressed initially" From b92e7c5ddf4b0240d8031f48b014597da140d8a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Jul 2023 11:56:45 -0500 Subject: [PATCH 0022/1151] Bump aiohomekit to 2.6.12 (#97342) --- 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 f859919fe07..8cc80ef864e 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.11"], + "requirements": ["aiohomekit==2.6.12"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index e8a101afc83..ef24d1113ea 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.11 +aiohomekit==2.6.12 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 674312831d7..7098648a6c9 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.11 +aiohomekit==2.6.12 # homeassistant.components.emulated_hue # homeassistant.components.http From 737ac8c60039013d148c4b488f685ea74468debc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Jul 2023 18:57:01 +0200 Subject: [PATCH 0023/1151] Fix DeviceInfo configuration_url validation (#97319) --- homeassistant/helpers/device_registry.py | 41 ++++++++------ homeassistant/helpers/entity.py | 3 +- tests/helpers/test_device_registry.py | 68 ++++++++++++++++++++++-- tests/helpers/test_entity_platform.py | 5 -- 4 files changed, 92 insertions(+), 25 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index f1eed86f10c..5764f65957e 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast from urllib.parse import urlparse import attr +from yarl import URL from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -48,6 +49,8 @@ ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 RUNTIME_ONLY_ATTRS = {"suggested_area"} +CONFIGURATION_URL_SCHEMES = {"http", "https", "homeassistant"} + class DeviceEntryDisabler(StrEnum): """What disabled a device entry.""" @@ -168,28 +171,36 @@ def _validate_device_info( ), ) - if (config_url := device_info.get("configuration_url")) is not None: - if type(config_url) is not str or urlparse(config_url).scheme not in [ - "http", - "https", - "homeassistant", - ]: - raise DeviceInfoError( - config_entry.domain if config_entry else "unknown", - device_info, - f"invalid configuration_url '{config_url}'", - ) - return device_info_type +def _validate_configuration_url(value: Any) -> str | None: + """Validate and convert configuration_url.""" + if value is None: + return None + if ( + isinstance(value, URL) + and (value.scheme not in CONFIGURATION_URL_SCHEMES or not value.host) + ) or ( + (parsed_url := urlparse(str(value))) + and ( + parsed_url.scheme not in CONFIGURATION_URL_SCHEMES + or not parsed_url.hostname + ) + ): + raise ValueError(f"invalid configuration_url '{value}'") + return str(value) + + @attr.s(slots=True, frozen=True) class DeviceEntry: """Device Registry Entry.""" area_id: str | None = attr.ib(default=None) config_entries: set[str] = attr.ib(converter=set, factory=set) - configuration_url: str | None = attr.ib(default=None) + configuration_url: str | URL | None = attr.ib( + converter=_validate_configuration_url, default=None + ) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) disabled_by: DeviceEntryDisabler | None = attr.ib(default=None) entry_type: DeviceEntryType | None = attr.ib(default=None) @@ -453,7 +464,7 @@ class DeviceRegistry: self, *, config_entry_id: str, - configuration_url: str | None | UndefinedType = UNDEFINED, + configuration_url: str | URL | None | UndefinedType = UNDEFINED, connections: set[tuple[str, str]] | None | UndefinedType = UNDEFINED, default_manufacturer: str | None | UndefinedType = UNDEFINED, default_model: str | None | UndefinedType = UNDEFINED, @@ -582,7 +593,7 @@ class DeviceRegistry: *, add_config_entry_id: str | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, - configuration_url: str | None | UndefinedType = UNDEFINED, + configuration_url: str | URL | None | UndefinedType = UNDEFINED, disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, entry_type: DeviceEntryType | None | UndefinedType = UNDEFINED, hw_version: str | None | UndefinedType = UNDEFINED, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 8e07897c84f..7d240cc0320 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -15,6 +15,7 @@ from timeit import default_timer as timer from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict, TypeVar, final import voluptuous as vol +from yarl import URL from homeassistant.backports.functools import cached_property from homeassistant.config import DATA_CUSTOMIZE @@ -177,7 +178,7 @@ def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None: class DeviceInfo(TypedDict, total=False): """Entity device information for device registry.""" - configuration_url: str | None + configuration_url: str | URL | None connections: set[tuple[str, str]] default_manufacturer: str default_model: str diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 3e59b08cfa8..0210d7ba75d 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1,9 +1,11 @@ """Tests for the Device Registry.""" +from contextlib import nullcontext import time from typing import Any from unittest.mock import patch import pytest +from yarl import URL from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STARTED @@ -171,7 +173,7 @@ async def test_loading_from_storage( { "area_id": "12345A", "config_entries": ["1234"], - "configuration_url": "configuration_url", + "configuration_url": "https://example.com/config", "connections": [["Zigbee", "01.23.45.67.89"]], "disabled_by": dr.DeviceEntryDisabler.USER, "entry_type": dr.DeviceEntryType.SERVICE, @@ -213,7 +215,7 @@ async def test_loading_from_storage( assert entry == dr.DeviceEntry( area_id="12345A", config_entries={"1234"}, - configuration_url="configuration_url", + configuration_url="https://example.com/config", connections={("Zigbee", "01.23.45.67.89")}, disabled_by=dr.DeviceEntryDisabler.USER, entry_type=dr.DeviceEntryType.SERVICE, @@ -916,7 +918,7 @@ async def test_update( updated_entry = device_registry.async_update_device( entry.id, area_id="12345A", - configuration_url="configuration_url", + configuration_url="https://example.com/config", disabled_by=dr.DeviceEntryDisabler.USER, entry_type=dr.DeviceEntryType.SERVICE, hw_version="hw_version", @@ -935,7 +937,7 @@ async def test_update( assert updated_entry == dr.DeviceEntry( area_id="12345A", config_entries={"1234"}, - configuration_url="configuration_url", + configuration_url="https://example.com/config", connections={("mac", "12:34:56:ab:cd:ef")}, disabled_by=dr.DeviceEntryDisabler.USER, entry_type=dr.DeviceEntryType.SERVICE, @@ -1670,3 +1672,61 @@ async def test_only_disable_device_if_all_config_entries_are_disabled( entry1 = device_registry.async_get(entry1.id) assert not entry1.disabled + + +@pytest.mark.parametrize( + ("configuration_url", "expectation"), + [ + ("http://localhost", nullcontext()), + ("http://localhost:8123", nullcontext()), + ("https://example.com", nullcontext()), + ("http://localhost/config", nullcontext()), + ("http://localhost:8123/config", nullcontext()), + ("https://example.com/config", nullcontext()), + ("homeassistant://config", nullcontext()), + (URL("http://localhost"), nullcontext()), + (URL("http://localhost:8123"), nullcontext()), + (URL("https://example.com"), nullcontext()), + (URL("http://localhost/config"), nullcontext()), + (URL("http://localhost:8123/config"), nullcontext()), + (URL("https://example.com/config"), nullcontext()), + (URL("homeassistant://config"), nullcontext()), + (None, nullcontext()), + ("http://", pytest.raises(ValueError)), + ("https://", pytest.raises(ValueError)), + ("gopher://localhost", pytest.raises(ValueError)), + ("homeassistant://", pytest.raises(ValueError)), + (URL("http://"), pytest.raises(ValueError)), + (URL("https://"), pytest.raises(ValueError)), + (URL("gopher://localhost"), pytest.raises(ValueError)), + (URL("homeassistant://"), pytest.raises(ValueError)), + # Exception implements __str__ + (Exception("https://example.com"), nullcontext()), + (Exception("https://"), pytest.raises(ValueError)), + (Exception(), pytest.raises(ValueError)), + ], +) +async def test_device_info_configuration_url_validation( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + configuration_url: str | URL | None, + expectation, +) -> None: + """Test configuration URL of device info is properly validated.""" + with expectation: + device_registry.async_get_or_create( + config_entry_id="1234", + identifiers={("something", "1234")}, + name="name", + configuration_url=configuration_url, + ) + + update_device = device_registry.async_get_or_create( + config_entry_id="5678", + identifiers={("something", "5678")}, + name="name", + ) + with expectation: + device_registry.async_update_device( + update_device.id, configuration_url=configuration_url + ) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 1f7e579ea95..3eaad662d8b 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1857,11 +1857,6 @@ async def test_device_name_defaulting_config_entry( "name": "bla", "default_name": "yo", }, - # Invalid configuration URL - { - "identifiers": {("hue", "1234")}, - "configuration_url": "foo://192.168.0.100/config", - }, ], ) async def test_device_type_error_checking( From af286a8feb5232e7f173b13b8f90dce981e45254 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Jul 2023 18:57:13 +0200 Subject: [PATCH 0024/1151] Add urllib3<2 package constraint (#97339) --- homeassistant/package_constraints.txt | 4 +++- script/gen_requirements_all.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3ce056e1a46..17f114b99ce 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -59,7 +59,9 @@ zeroconf==0.71.4 pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 -urllib3>=1.26.5 +# Temporary setting an upper bound, to prevent compat issues with urllib3>=2 +# https://github.com/home-assistant/core/issues/97248 +urllib3>=1.26.5,<2 # Constrain httplib2 to protect against GHSA-93xj-8mrv-444m # https://github.com/advisories/GHSA-93xj-8mrv-444m diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 02d528c33e2..b2954dc777b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -61,7 +61,9 @@ CONSTRAINT_BASE = """ pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 -urllib3>=1.26.5 +# Temporary setting an upper bound, to prevent compat issues with urllib3>=2 +# https://github.com/home-assistant/core/issues/97248 +urllib3>=1.26.5,<2 # Constrain httplib2 to protect against GHSA-93xj-8mrv-444m # https://github.com/advisories/GHSA-93xj-8mrv-444m From 7e3fdd85fc2b703b09188fcc61ee293b1382375e Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 27 Jul 2023 13:30:42 -0500 Subject: [PATCH 0025/1151] Add wildcards to sentence triggers (#97236) Co-authored-by: Franck Nijhof --- .../components/conversation/__init__.py | 6 +- .../components/conversation/default_agent.py | 44 ++++++++++--- .../components/conversation/manifest.json | 2 +- .../components/conversation/trigger.py | 21 ++++++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../conversation/snapshots/test_init.ambr | 23 +++++-- .../conversation/test_default_agent.py | 3 +- tests/components/conversation/test_trigger.py | 61 +++++++++++++++++++ 10 files changed, 147 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 30ecf16bb37..29dd56c11ec 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -322,7 +322,11 @@ async def websocket_hass_agent_debug( "intent": { "name": result.intent.name, }, - "entities": { + "slots": { # direct access to values + entity_key: entity.value + for entity_key, entity in result.entities.items() + }, + "details": { entity_key: { "name": entity.name, "value": entity.value, diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index b0a3702b5c9..04aafc8a99d 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -11,7 +11,14 @@ from pathlib import Path import re from typing import IO, Any -from hassil.intents import Intents, ResponseType, SlotList, TextSlotList +from hassil.expression import Expression, ListReference, Sequence +from hassil.intents import ( + Intents, + ResponseType, + SlotList, + TextSlotList, + WildcardSlotList, +) from hassil.recognize import RecognizeResult, recognize_all from hassil.util import merge_dict from home_assistant_intents import get_domains_and_languages, get_intents @@ -48,7 +55,7 @@ _ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"] REGEX_TYPE = type(re.compile("")) TRIGGER_CALLBACK_TYPE = Callable[ # pylint: disable=invalid-name - [str], Awaitable[str | None] + [str, RecognizeResult], Awaitable[str | None] ] @@ -657,6 +664,17 @@ class DefaultAgent(AbstractConversationAgent): } self._trigger_intents = Intents.from_dict(intents_dict) + + # Assume slot list references are wildcards + wildcard_names: set[str] = set() + for trigger_intent in self._trigger_intents.intents.values(): + for intent_data in trigger_intent.data: + for sentence in intent_data.sentences: + _collect_list_references(sentence, wildcard_names) + + for wildcard_name in wildcard_names: + self._trigger_intents.slot_lists[wildcard_name] = WildcardSlotList() + _LOGGER.debug("Rebuilt trigger intents: %s", intents_dict) def _unregister_trigger(self, trigger_data: TriggerData) -> None: @@ -682,14 +700,14 @@ class DefaultAgent(AbstractConversationAgent): assert self._trigger_intents is not None - matched_triggers: set[int] = set() + matched_triggers: dict[int, RecognizeResult] = {} for result in recognize_all(sentence, self._trigger_intents): trigger_id = int(result.intent.name) if trigger_id in matched_triggers: # Already matched a sentence from this trigger break - matched_triggers.add(trigger_id) + matched_triggers[trigger_id] = result if not matched_triggers: # Sentence did not match any trigger sentences @@ -699,14 +717,14 @@ class DefaultAgent(AbstractConversationAgent): "'%s' matched %s trigger(s): %s", sentence, len(matched_triggers), - matched_triggers, + list(matched_triggers), ) # Gather callback responses in parallel trigger_responses = await asyncio.gather( *( - self._trigger_sentences[trigger_id].callback(sentence) - for trigger_id in matched_triggers + self._trigger_sentences[trigger_id].callback(sentence, result) + for trigger_id, result in matched_triggers.items() ) ) @@ -733,3 +751,15 @@ def _make_error_result( response.async_set_error(error_code, response_text) return ConversationResult(response, conversation_id) + + +def _collect_list_references(expression: Expression, list_names: set[str]) -> None: + """Collect list reference names recursively.""" + if isinstance(expression, Sequence): + seq: Sequence = expression + for item in seq.items: + _collect_list_references(item, list_names) + elif isinstance(expression, ListReference): + # {list} + list_ref: ListReference = expression + list_names.add(list_ref.slot_name) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index a8f24a335f0..1eb58e96ff9 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.2.2", "home-assistant-intents==2023.7.25"] + "requirements": ["hassil==1.2.5", "home-assistant-intents==2023.7.25"] } diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py index b64b74c5fa6..71ddb5c1237 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from hassil.recognize import PUNCTUATION +from hassil.recognize import PUNCTUATION, RecognizeResult import voluptuous as vol from homeassistant.const import CONF_COMMAND, CONF_PLATFORM @@ -49,12 +49,29 @@ async def async_attach_trigger( job = HassJob(action) @callback - async def call_action(sentence: str) -> str | None: + async def call_action(sentence: str, result: RecognizeResult) -> str | None: """Call action with right context.""" + + # Add slot values as extra trigger data + details = { + entity_name: { + "name": entity_name, + "text": entity.text.strip(), # remove whitespace + "value": entity.value.strip() + if isinstance(entity.value, str) + else entity.value, + } + for entity_name, entity in result.entities.items() + } + trigger_input: dict[str, Any] = { # Satisfy type checker **trigger_data, "platform": DOMAIN, "sentence": sentence, + "details": details, + "slots": { # direct access to values + entity_name: entity["value"] for entity_name, entity in details.items() + }, } # Wait for the automation to complete diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 17f114b99ce..a0046569eb8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ dbus-fast==1.87.2 fnv-hash-fast==0.4.0 ha-av==10.1.0 hass-nabucasa==0.69.0 -hassil==1.2.2 +hassil==1.2.5 home-assistant-bluetooth==1.10.2 home-assistant-frontend==20230725.0 home-assistant-intents==2023.7.25 diff --git a/requirements_all.txt b/requirements_all.txt index ef24d1113ea..899e444ab94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -958,7 +958,7 @@ hass-nabucasa==0.69.0 hass-splunk==0.1.1 # homeassistant.components.conversation -hassil==1.2.2 +hassil==1.2.5 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7098648a6c9..bb4eefeab72 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -753,7 +753,7 @@ habitipy==0.2.0 hass-nabucasa==0.69.0 # homeassistant.components.conversation -hassil==1.2.2 +hassil==1.2.5 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 8ef0cef52f9..f9fe284bcb0 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -372,7 +372,7 @@ dict({ 'results': list([ dict({ - 'entities': dict({ + 'details': dict({ 'name': dict({ 'name': 'name', 'text': 'my cool light', @@ -382,6 +382,9 @@ 'intent': dict({ 'name': 'HassTurnOn', }), + 'slots': dict({ + 'name': 'my cool light', + }), 'targets': dict({ 'light.kitchen': dict({ 'matched': True, @@ -389,7 +392,7 @@ }), }), dict({ - 'entities': dict({ + 'details': dict({ 'name': dict({ 'name': 'name', 'text': 'my cool light', @@ -399,6 +402,9 @@ 'intent': dict({ 'name': 'HassTurnOff', }), + 'slots': dict({ + 'name': 'my cool light', + }), 'targets': dict({ 'light.kitchen': dict({ 'matched': True, @@ -406,7 +412,7 @@ }), }), dict({ - 'entities': dict({ + 'details': dict({ 'area': dict({ 'name': 'area', 'text': 'kitchen', @@ -421,6 +427,10 @@ 'intent': dict({ 'name': 'HassTurnOn', }), + 'slots': dict({ + 'area': 'kitchen', + 'domain': 'light', + }), 'targets': dict({ 'light.kitchen': dict({ 'matched': True, @@ -428,7 +438,7 @@ }), }), dict({ - 'entities': dict({ + 'details': dict({ 'area': dict({ 'name': 'area', 'text': 'kitchen', @@ -448,6 +458,11 @@ 'intent': dict({ 'name': 'HassGetState', }), + 'slots': dict({ + 'area': 'kitchen', + 'domain': 'light', + 'state': 'on', + }), 'targets': dict({ 'light.kitchen': dict({ 'matched': False, diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index af9af468453..c3c2e621260 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -246,7 +246,8 @@ async def test_trigger_sentences(hass: HomeAssistant, init_components) -> None: for sentence in test_sentences: callback.reset_mock() result = await conversation.async_converse(hass, sentence, None, Context()) - callback.assert_called_once_with(sentence) + assert callback.call_count == 1 + assert callback.call_args[0][0] == sentence assert ( result.response.response_type == intent.IntentResponseType.ACTION_DONE ), sentence diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index 522162fa457..3f4dd9e3a7e 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -61,6 +61,8 @@ async def test_if_fires_on_event(hass: HomeAssistant, calls, setup_comp) -> None "idx": "0", "platform": "conversation", "sentence": "Ha ha ha", + "slots": {}, + "details": {}, } @@ -103,6 +105,8 @@ async def test_same_trigger_multiple_sentences( "idx": "0", "platform": "conversation", "sentence": "hello", + "slots": {}, + "details": {}, } @@ -188,3 +192,60 @@ async def test_fails_on_punctuation(hass: HomeAssistant, command: str) -> None: }, ], ) + + +async def test_wildcards(hass: HomeAssistant, calls, setup_comp) -> None: + """Test wildcards in trigger sentences.""" + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": { + "platform": "conversation", + "command": [ + "play {album} by {artist}", + ], + }, + "action": { + "service": "test.automation", + "data_template": {"data": "{{ trigger }}"}, + }, + } + }, + ) + + await hass.services.async_call( + "conversation", + "process", + { + "text": "play the white album by the beatles", + }, + blocking=True, + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["data"] == { + "alias": None, + "id": "0", + "idx": "0", + "platform": "conversation", + "sentence": "play the white album by the beatles", + "slots": { + "album": "the white album", + "artist": "the beatles", + }, + "details": { + "album": { + "name": "album", + "text": "the white album", + "value": "the white album", + }, + "artist": { + "name": "artist", + "text": "the beatles", + "value": "the beatles", + }, + }, + } From 70cb8afb945a85cde804cfc88f8b96cdadfb07b9 Mon Sep 17 00:00:00 2001 From: "J.P. Krauss" Date: Thu, 27 Jul 2023 12:19:16 -0700 Subject: [PATCH 0026/1151] Add AirNow Reporting Station as sensor (#97273) * Add AirNow Reporting Station as sensor attribute * Make Reporting Station a sensor instead of attribute as requested * Update homeassistant/components/airnow/strings.json Co-authored-by: Joost Lekkerkerker * Fix reporting station attribute names to avoid showing on map * Add attribute name translations * Update homeassistant/components/airnow/strings.json Co-authored-by: G Johansson --------- Co-authored-by: Joost Lekkerkerker Co-authored-by: G Johansson --- homeassistant/components/airnow/sensor.py | 14 ++++++++++++++ homeassistant/components/airnow/strings.json | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index f3d29cc65df..9559d2ecff8 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -30,6 +30,9 @@ from .const import ( ATTR_API_AQI_LEVEL, ATTR_API_O3, ATTR_API_PM25, + ATTR_API_STATION, + ATTR_API_STATION_LATITUDE, + ATTR_API_STATION_LONGITUDE, DEFAULT_NAME, DOMAIN, ) @@ -40,6 +43,7 @@ PARALLEL_UPDATES = 1 ATTR_DESCR = "description" ATTR_LEVEL = "level" +ATTR_STATION = "reporting_station" @dataclass @@ -85,6 +89,16 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = ( value_fn=lambda data: data.get(ATTR_API_O3), extra_state_attributes_fn=None, ), + AirNowEntityDescription( + key=ATTR_API_STATION, + translation_key="station", + icon="mdi:blur", + value_fn=lambda data: data.get(ATTR_API_STATION), + extra_state_attributes_fn=lambda data: { + "lat": data[ATTR_API_STATION_LATITUDE], + "long": data[ATTR_API_STATION_LONGITUDE], + }, + ), ) diff --git a/homeassistant/components/airnow/strings.json b/homeassistant/components/airnow/strings.json index 072f0988c19..9926a2f78aa 100644 --- a/homeassistant/components/airnow/strings.json +++ b/homeassistant/components/airnow/strings.json @@ -25,6 +25,13 @@ "sensor": { "o3": { "name": "[%key:component::sensor::entity_component::ozone::name%]" + }, + "station": { + "name": "PM2.5 reporting station", + "state_attributes": { + "lat": { "name": "[%key:common::config_flow::data::latitude%]" }, + "long": { "name": "[%key:common::config_flow::data::longitude%]" } + } } } } From 3fdcb67322a34fe3b6fed1932e674cf142c42935 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 27 Jul 2023 22:26:35 +0200 Subject: [PATCH 0027/1151] Add breaks_in_ha_version for Dynalite YAML import (#97359) Dynalite breaks in version --- homeassistant/components/dynalite/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py index 8438307c698..7cced80c97e 100644 --- a/homeassistant/components/dynalite/config_flow.py +++ b/homeassistant/components/dynalite/config_flow.py @@ -34,6 +34,7 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.hass, HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2023.12.0", is_fixable=False, is_persistent=False, issue_domain=DOMAIN, From 2d4040c70a21bc55cb556dabe69eb8d5996d1861 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 27 Jul 2023 22:50:50 +0200 Subject: [PATCH 0028/1151] Netatmo add issue for yaml deprecation (#97360) Netatmo add issue --- homeassistant/components/netatmo/__init__.py | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index e26a32965a3..42c48ff9751 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -29,7 +29,13 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CoreState, Event, HomeAssistant, ServiceCall +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + CoreState, + Event, + HomeAssistant, + ServiceCall, +) from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, @@ -38,6 +44,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from . import api @@ -108,6 +115,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "the UI automatically and can be safely removed from your " "configuration.yaml file" ) + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Netatmo", + }, + ) return True From 4ac139592a783eda0089baeef6c854eb0b49dd57 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 27 Jul 2023 22:53:24 +0200 Subject: [PATCH 0029/1151] Plum Lightpad deprecation issue for yaml configuration (#97362) Plum issue yaml config --- .../components/plum_lightpad/__init__.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index aa82d5662c6..241c14f29b9 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -12,9 +12,10 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -47,12 +48,26 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf = config[DOMAIN] - _LOGGER.info("Found Plum Lightpad configuration in config, importing") + _LOGGER.debug("Found Plum Lightpad configuration in config, importing") hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=conf ) ) + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Plum Lightpad", + }, + ) return True From ae3cc0b619ed6382ee8a5f00daa4f28b1204612b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 27 Jul 2023 22:53:51 +0200 Subject: [PATCH 0030/1151] Sure Petcare deprecation issue yaml configuration (#97363) Sure Petcare yaml config issue --- .../components/surepetcare/__init__.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index ec5d9f63920..9189ea38c00 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -18,10 +18,15 @@ from homeassistant.const import ( CONF_USERNAME, Platform, ) -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + ServiceCall, +) from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -88,6 +93,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: data=config[DOMAIN], ) ) + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Sure Petcare", + }, + ) return True From 973370b75e8da78e56ca76cc74d4b254dcab819f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 27 Jul 2023 23:01:17 +0200 Subject: [PATCH 0031/1151] Deprecate Freebox YAML (#97345) * Deprecate Freebox YAML * Update homeassistant/components/freebox/__init__.py --------- Co-authored-by: G Johansson --- homeassistant/components/freebox/__init__.py | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 12463934adb..5465d524faf 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -7,10 +7,16 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, ServiceCall +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + Event, + HomeAssistant, + ServiceCall, +) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, PLATFORMS, SERVICE_REBOOT @@ -43,6 +49,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Freebox", + }, + ) + return True From 053f4b08b63ad2896d4a1b712a066c4cd30615d3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 27 Jul 2023 23:33:08 +0200 Subject: [PATCH 0032/1151] Bump reolink_aio to 0.7.5 (#97357) * bump reolink-aio to 0.7.4 * Bump reolink_aio to 0.7.5 --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 00f0e0f518b..25994d56250 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.3"] + "requirements": ["reolink-aio==0.7.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 899e444ab94..97f132dad19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2281,7 +2281,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.3 +reolink-aio==0.7.5 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb4eefeab72..2ed15835605 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1677,7 +1677,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.3 +reolink-aio==0.7.5 # homeassistant.components.rflink rflink==0.0.65 From 2299430dbeb470ff8b5a62fae1fa80fbfc3f014f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 27 Jul 2023 23:33:49 +0200 Subject: [PATCH 0033/1151] Sonos add yaml config issue (#97365) --- homeassistant/components/sonos/__init__.py | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index e6b328cbcb0..259a9f54044 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -25,7 +25,13 @@ from homeassistant.components import ssdp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + DOMAIN as HOMEASSISTANT_DOMAIN, + Event, + HomeAssistant, + callback, +) from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -33,6 +39,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later, async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .alarms import SonosAlarms @@ -125,6 +132,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) ) + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Sonos", + }, + ) return True From 81c0dcff63e805cec11a5e05c35da8cfb55472d2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 27 Jul 2023 23:34:17 +0200 Subject: [PATCH 0034/1151] Home Connect deprecation issue yaml configuration (#97361) Home Connect deprecation issue --- .../components/home_connect/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 0fa14682f44..7377c4b60d0 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -19,12 +19,13 @@ from homeassistant.const import ( CONF_DEVICE, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, device_registry as dr, ) +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle @@ -133,6 +134,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_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Home Connect", + }, + ) async def _async_service_program(call, method): """Execute calls to services taking a program.""" From 3a5444883634fed79eb69d3fa3102bccc9608aa7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 28 Jul 2023 11:52:23 +0200 Subject: [PATCH 0035/1151] Bump pysensibo to 1.0.32 (#97382) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index f90b887d04c..26182102442 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -15,5 +15,5 @@ "iot_class": "cloud_polling", "loggers": ["pysensibo"], "quality_scale": "platinum", - "requirements": ["pysensibo==1.0.31"] + "requirements": ["pysensibo==1.0.32"] } diff --git a/requirements_all.txt b/requirements_all.txt index 97f132dad19..f7276d583fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1985,7 +1985,7 @@ pysaj==0.0.16 pyschlage==2023.5.0 # homeassistant.components.sensibo -pysensibo==1.0.31 +pysensibo==1.0.32 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ed15835605..274d7dfd9a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1477,7 +1477,7 @@ pysabnzbd==1.1.1 pyschlage==2023.5.0 # homeassistant.components.sensibo -pysensibo==1.0.31 +pysensibo==1.0.32 # homeassistant.components.serial # homeassistant.components.zha From 13349e76ed16183cb7b8542ca2416abd0e4a80ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 12:19:20 -0500 Subject: [PATCH 0036/1151] Avoid firing update coordinator callbacks when nothing has changed (#97268) --- homeassistant/helpers/update_coordinator.py | 22 +++- tests/helpers/test_update_coordinator.py | 115 ++++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 36dd7d27d4a..8057e77de4f 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -54,7 +54,12 @@ class BaseDataUpdateCoordinatorProtocol(Protocol): class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): - """Class to manage fetching data from single endpoint.""" + """Class to manage fetching data from single endpoint. + + Setting :attr:`always_update` to ``False`` will cause coordinator to only + callback listeners when data has changed. This requires that the data + implements ``__eq__`` or uses a python object that already does. + """ def __init__( self, @@ -65,6 +70,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): update_interval: timedelta | None = None, update_method: Callable[[], Awaitable[_DataT]] | None = None, request_refresh_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None, + always_update: bool = True, ) -> None: """Initialize global data updater.""" self.hass = hass @@ -74,6 +80,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): self.update_interval = update_interval self._shutdown_requested = False self.config_entry = config_entries.current_entry.get() + self.always_update = always_update # It's None before the first successful update. # Components should call async_config_entry_first_refresh @@ -277,7 +284,10 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): if log_timing := self.logger.isEnabledFor(logging.DEBUG): start = monotonic() + auth_failed = False + previous_update_success = self.last_update_success + previous_data = self.data try: self.data = await self._async_update_data() @@ -371,7 +381,15 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): if not auth_failed and self._listeners and not self.hass.is_stopping: self._schedule_refresh() - self.async_update_listeners() + if not self.last_update_success and not previous_update_success: + return + + if ( + self.always_update + or self.last_update_success != previous_update_success + or previous_data != self.data + ): + self.async_update_listeners() @callback def async_set_update_error(self, err: Exception) -> None: diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 91f761b5bb6..4258a508c34 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -594,3 +594,118 @@ async def test_async_set_update_error( # Remove callbacks to avoid lingering timers remove_callbacks() + + +async def test_only_callback_on_change_when_always_update_is_false( + crd: update_coordinator.DataUpdateCoordinator[int], caplog: pytest.LogCaptureFixture +) -> None: + """Test we do not callback listeners unless something has actually changed when always_update is false.""" + update_callback = Mock() + crd.always_update = False + remove_callbacks = crd.async_add_listener(update_callback) + mocked_data = None + mocked_exception = None + + async def _update_method() -> int: + nonlocal mocked_data + nonlocal mocked_exception + if mocked_exception is not None: + raise mocked_exception + return mocked_data + + crd.update_method = _update_method + + mocked_data = {"a": 1} + await crd.async_refresh() + update_callback.assert_called_once() + update_callback.reset_mock() + + mocked_data = {"a": 1} + await crd.async_refresh() + update_callback.assert_not_called() + update_callback.reset_mock() + + mocked_data = None + mocked_exception = aiohttp.ClientError("Client Failure #1") + await crd.async_refresh() + update_callback.assert_called_once() + update_callback.reset_mock() + + mocked_data = None + mocked_exception = aiohttp.ClientError("Client Failure #1") + await crd.async_refresh() + update_callback.assert_not_called() + update_callback.reset_mock() + + mocked_exception = None + mocked_data = {"a": 1} + await crd.async_refresh() + update_callback.assert_called_once() + update_callback.reset_mock() + + mocked_data = {"a": 1} + await crd.async_refresh() + update_callback.assert_not_called() + update_callback.reset_mock() + + mocked_data = {"a": 2} + await crd.async_refresh() + update_callback.assert_called_once() + update_callback.reset_mock() + + mocked_data = {"a": 2} + await crd.async_refresh() + update_callback.assert_not_called() + update_callback.reset_mock() + + mocked_data = {"a": 2, "b": 3} + await crd.async_refresh() + update_callback.assert_called_once() + update_callback.reset_mock() + + remove_callbacks() + + +async def test_always_callback_when_always_update_is_true( + crd: update_coordinator.DataUpdateCoordinator[int], caplog: pytest.LogCaptureFixture +) -> None: + """Test we callback listeners even though the data is the same when always_update is True.""" + update_callback = Mock() + remove_callbacks = crd.async_add_listener(update_callback) + mocked_data = None + mocked_exception = None + + async def _update_method() -> int: + nonlocal mocked_data + nonlocal mocked_exception + if mocked_exception is not None: + raise mocked_exception + return mocked_data + + crd.update_method = _update_method + + mocked_data = {"a": 1} + await crd.async_refresh() + update_callback.assert_called_once() + update_callback.reset_mock() + + mocked_data = {"a": 1} + await crd.async_refresh() + update_callback.assert_called_once() + update_callback.reset_mock() + + # But still don't fire it if we are only getting + # failure over and over + mocked_data = None + mocked_exception = aiohttp.ClientError("Client Failure #1") + await crd.async_refresh() + update_callback.assert_called_once() + update_callback.reset_mock() + + mocked_data = None + mocked_exception = aiohttp.ClientError("Client Failure #1") + await crd.async_refresh() + update_callback.assert_not_called() + update_callback.reset_mock() + + remove_callbacks() From 54e718561751e50f9c91343b66072e1504995ad7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 12:33:33 -0500 Subject: [PATCH 0037/1151] Disable always_update in rain machine coordinator (#97410) --- homeassistant/components/rainmachine/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index 61ef1be500a..64917b6d721 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -106,6 +106,7 @@ class RainMachineDataUpdateCoordinator(DataUpdateCoordinator[dict]): name=name, update_interval=update_interval, update_method=update_method, + always_update=False, ) self._rebooting = False From 48eebe43c9fa2ddfeb37039c2dfb9f46d1b881ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 12:38:07 -0500 Subject: [PATCH 0038/1151] Disable always_update in steamist coordinator (#97411) --- homeassistant/components/steamist/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/steamist/coordinator.py b/homeassistant/components/steamist/coordinator.py index 0ab603ddd17..67aedf0af94 100644 --- a/homeassistant/components/steamist/coordinator.py +++ b/homeassistant/components/steamist/coordinator.py @@ -30,6 +30,7 @@ class SteamistDataUpdateCoordinator(DataUpdateCoordinator[SteamistStatus]): _LOGGER, name=f"Steamist {host}", update_interval=timedelta(seconds=5), + always_update=False, ) async def _async_update_data(self) -> SteamistStatus: From e4ac8bdd6bcb2f8dba7797ff3358f8f443ba68d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 12:38:33 -0500 Subject: [PATCH 0039/1151] Disable always_update in flux_led coordinator (#97412) --- homeassistant/components/flux_led/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/flux_led/coordinator.py b/homeassistant/components/flux_led/coordinator.py index 38c5ed70b5e..bf3f1dee94a 100644 --- a/homeassistant/components/flux_led/coordinator.py +++ b/homeassistant/components/flux_led/coordinator.py @@ -41,6 +41,7 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator[None]): request_refresh_debouncer=Debouncer( hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False ), + always_update=False, ) async def _async_update_data(self) -> None: From 2ec2c25f5a00dbbfd9c6ca3e192046b8d161a2b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 12:38:52 -0500 Subject: [PATCH 0040/1151] Disable always_update in nut coordinator (#97413) --- homeassistant/components/nut/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 6bf5b68e927..9ffe1016aec 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -77,6 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name="NUT resource status", update_method=async_update_data, update_interval=timedelta(seconds=scan_interval), + always_update=False, ) # Fetch initial data so we have data when entities subscribe From 8a9153d004662ec410852eafd7f243efd11c422f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 12:39:15 -0500 Subject: [PATCH 0041/1151] Disable always_update in emonitor coordinator (#97414) --- homeassistant/components/emonitor/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index ea19808cd37..3bc5c7862cb 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -31,6 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name=entry.title, update_method=emonitor.async_get_status, update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), + always_update=False, ) await coordinator.async_config_entry_first_refresh() From 5829efb5d7ce94bc618a6be9c893dde11f933dc8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 12:40:50 -0500 Subject: [PATCH 0042/1151] Disable always_update in lookin coordinator (#97415) --- homeassistant/components/lookin/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/lookin/coordinator.py b/homeassistant/components/lookin/coordinator.py index 1bdbb36dd71..d556899a914 100644 --- a/homeassistant/components/lookin/coordinator.py +++ b/homeassistant/components/lookin/coordinator.py @@ -60,6 +60,7 @@ class LookinDataUpdateCoordinator(DataUpdateCoordinator[_DataT]): name=name, update_interval=update_interval, update_method=update_method, + always_update=False, ) @callback From 44a38859864997fbddc87f8163751ad53177760b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 12:41:05 -0500 Subject: [PATCH 0043/1151] Disable always_update in powerwall coordinator (#97416) --- homeassistant/components/powerwall/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 8d66a12faad..33395f5fe6a 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -169,6 +169,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name="Powerwall site", update_method=manager.async_update_data, update_interval=timedelta(seconds=UPDATE_INTERVAL), + always_update=False, ) await coordinator.async_config_entry_first_refresh() From e1f14ed990845fca3e817085796544d72a9c5171 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 12:41:25 -0500 Subject: [PATCH 0044/1151] Disable always_update in cert_expiry coordinator (#97417) --- homeassistant/components/cert_expiry/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 4fc89bc918b..267a2d56236 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -65,10 +65,7 @@ class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime | None]): name = f"{self.host}{display_port}" super().__init__( - hass, - _LOGGER, - name=name, - update_interval=SCAN_INTERVAL, + hass, _LOGGER, name=name, update_interval=SCAN_INTERVAL, always_update=False ) async def _async_update_data(self) -> datetime | None: From 8101376ad5883c0444b7e3b1ee08a00d2393b32b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 28 Jul 2023 19:41:41 +0200 Subject: [PATCH 0045/1151] Small cleanup in event entity (#97409) --- homeassistant/components/event/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/event/__init__.py b/homeassistant/components/event/__init__.py index 98dd6036bc9..f6ba2d79bfe 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -45,7 +45,6 @@ __all__ = [ "EventDeviceClass", "EventEntity", "EventEntityDescription", - "EventEntityFeature", ] # mypy: disallow-any-generics @@ -104,7 +103,7 @@ class EventExtraStoredData(ExtraStoredData): class EventEntity(RestoreEntity): - """Representation of a Event entity.""" + """Representation of an Event entity.""" entity_description: EventEntityDescription _attr_device_class: EventDeviceClass | None From c1ce14983cbcabf6a6d4ebe74ec2fe4e370d06ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 12:41:45 -0500 Subject: [PATCH 0046/1151] Disable always_update in filesize coordinator (#97418) --- homeassistant/components/filesize/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 0b5c39f3629..49f14e0031a 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -91,6 +91,7 @@ class FileSizeCoordinator(DataUpdateCoordinator): _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=60), + always_update=False, ) self._path = path From 1b10c44a1634752e68d1f439c5c6d1840c53a3aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 12:42:04 -0500 Subject: [PATCH 0047/1151] Disable always_update in esphome dashboard coordinator (#97419) --- homeassistant/components/esphome/dashboard.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 4cbb9cbe847..41b0617e630 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -172,6 +172,7 @@ class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): _LOGGER, name="ESPHome Dashboard", update_interval=timedelta(minutes=5), + always_update=False, ) self.addon_slug = addon_slug self.url = url From cd311f4868dd4c26ab188fcba84357b8fc351295 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 28 Jul 2023 23:01:21 +0200 Subject: [PATCH 0048/1151] meteo_france add yaml config removal issue (#97428) meteo_france yaml removal issue --- .../components/meteo_france/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index ccd23762850..6ad3868f13d 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -8,9 +8,10 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -52,6 +53,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, context={"source": SOURCE_IMPORT}, data=city_conf ) ) + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Météo-France", + }, + ) return True From 594d98822b36233d2c8419025bf74a770e2f395f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 28 Jul 2023 23:21:15 +0200 Subject: [PATCH 0049/1151] OctoPrint add yaml config removal issue (#97431) OctoPrint yaml config removal issue --- homeassistant/components/octoprint/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index dd6ab5794fc..4c57b6e57dc 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -24,11 +24,12 @@ from homeassistant.const import ( CONF_VERIFY_SSL, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, 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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify as util_slugify @@ -149,6 +150,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: }, ) ) + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "OctoPrint", + }, + ) return True From 05e9b63b169100fea7fb30e9ccabae5661564f1a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 28 Jul 2023 23:21:34 +0200 Subject: [PATCH 0050/1151] MELCloud add yaml config removal issue (#97430) melcloud yaml config removal issue --- homeassistant/components/melcloud/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index eea169c3591..ddadfa74266 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -13,12 +13,13 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, 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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle @@ -61,6 +62,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: data={CONF_USERNAME: username, CONF_TOKEN: token}, ) ) + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "MELCloud", + }, + ) return True From 7966d8da76e33ad4a4ac00435d7bfc3334ddd274 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 28 Jul 2023 23:21:49 +0200 Subject: [PATCH 0051/1151] LiteJet add yaml config removal issue (#97429) litejet yaml config removal issue --- homeassistant/components/litejet/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index 291333d0b74..a7ea6ecd034 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -6,9 +6,10 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import CONF_EXCLUDE_NAMES, CONF_INCLUDE_SWITCHES, DOMAIN, PLATFORMS @@ -43,6 +44,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] ) ) + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "LiteJet", + }, + ) return True From a3110ef1c12ded46b8bcf8121a4878482da7dee4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 16:23:37 -0500 Subject: [PATCH 0052/1151] Disable always_update in oncue coordinator (#97434) --- homeassistant/components/oncue/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/oncue/__init__.py b/homeassistant/components/oncue/__init__.py index eb9ac37db18..a87b2a9e02c 100644 --- a/homeassistant/components/oncue/__init__.py +++ b/homeassistant/components/oncue/__init__.py @@ -39,6 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name=f"Oncue {entry.data[CONF_USERNAME]}", update_interval=timedelta(minutes=10), update_method=client.async_fetch_all, + always_update=False, ) await coordinator.async_config_entry_first_refresh() From 1b6f15e3da5bc6caca6d7602c42d89492d286256 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 16:28:41 -0500 Subject: [PATCH 0053/1151] Disable always_update in enphase_envoy coordinator (#97425) see https://github.com/home-assistant/developers.home-assistant/pull/1863 --- homeassistant/components/enphase_envoy/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 1a4feb59376..e94cb9c47d8 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -64,6 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name=f"envoy {name}", update_method=async_update_data, update_interval=SCAN_INTERVAL, + always_update=False, ) try: From fc38451faf9459a53bc0bc29fdfd2509b701f628 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 28 Jul 2023 23:29:01 +0200 Subject: [PATCH 0054/1151] Disable always_update in yale_smart_alarm coordinator (#97426) always update --- homeassistant/components/yale_smart_alarm/coordinator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py index 1a350d1db98..e1cff8fb2a5 100644 --- a/homeassistant/components/yale_smart_alarm/coordinator.py +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, YALE_BASE_ERRORS -class YaleDataUpdateCoordinator(DataUpdateCoordinator): +class YaleDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """A Yale Data Update Coordinator.""" def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -28,6 +28,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): LOGGER, name=DOMAIN, update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + always_update=False, ) async def _async_update_data(self) -> dict[str, Any]: From a2555e71e2df4fdb3ac747cbc1ddf8c03a8eedbe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 16:30:29 -0500 Subject: [PATCH 0055/1151] Small cleanups to ambient station (#97421) --- .../components/ambient_station/__init__.py | 37 +++++++------------ .../ambient_station/binary_sensor.py | 9 ++--- .../components/ambient_station/sensor.py | 8 ++-- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index f68ae3df114..364e5b2abb6 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -148,6 +148,7 @@ class AmbientStation: """Define a handler to fire when the data is received.""" mac = data["macAddress"] + # If data has not changed, don't update: if data == self.stations[mac][ATTR_LAST_DATA]: return @@ -228,33 +229,23 @@ class AmbientWeatherEntity(Entity): self._mac_address = mac_address self.entity_description = description + @callback + def _async_update(self) -> None: + """Update the state.""" + last_data = self._ambient.stations[self._mac_address][ATTR_LAST_DATA] + key = self.entity_description.key + available_key = TYPE_SOLARRADIATION if key == TYPE_SOLARRADIATION_LX else key + self._attr_available = last_data[available_key] is not None + self.update_from_latest_data() + self.async_write_ha_state() + async def async_added_to_hass(self) -> None: """Register callbacks.""" - - @callback - def update() -> None: - """Update the state.""" - if self.entity_description.key == TYPE_SOLARRADIATION_LX: - self._attr_available = ( - self._ambient.stations[self._mac_address][ATTR_LAST_DATA][ - TYPE_SOLARRADIATION - ] - is not None - ) - else: - self._attr_available = ( - self._ambient.stations[self._mac_address][ATTR_LAST_DATA][ - self.entity_description.key - ] - is not None - ) - - self.update_from_latest_data() - self.async_write_ha_state() - self.async_on_remove( async_dispatcher_connect( - self.hass, f"ambient_station_data_update_{self._mac_address}", update + self.hass, + f"ambient_station_data_update_{self._mac_address}", + self._async_update, ) ) diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 8876c1a5c62..ca32f16c758 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -409,9 +409,6 @@ class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorEntity): @callback def update_from_latest_data(self) -> None: """Fetch new state data for the entity.""" - self._attr_is_on = ( - self._ambient.stations[self._mac_address][ATTR_LAST_DATA][ - self.entity_description.key - ] - == self.entity_description.on_state - ) + description = self.entity_description + last_data = self._ambient.stations[self._mac_address][ATTR_LAST_DATA] + self._attr_is_on = last_data[description.key] == description.on_state diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 0fc6e7643db..8bdc66133d6 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -694,11 +694,9 @@ class AmbientWeatherSensor(AmbientWeatherEntity, SensorEntity): @callback def update_from_latest_data(self) -> None: """Fetch new state data for the sensor.""" - raw = self._ambient.stations[self._mac_address][ATTR_LAST_DATA][ - self.entity_description.key - ] - - if self.entity_description.key == TYPE_LASTRAIN: + key = self.entity_description.key + raw = self._ambient.stations[self._mac_address][ATTR_LAST_DATA][key] + if key == TYPE_LASTRAIN: self._attr_native_value = datetime.strptime(raw, "%Y-%m-%dT%H:%M:%S.%f%z") else: self._attr_native_value = raw From c11222c1d0a07733fb57cd3a7fa2c5f11bc29b8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 16:37:08 -0500 Subject: [PATCH 0056/1151] Bump nexia to 2.0.7 (#97432) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 2e54e773a44..5464a241b7a 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/nexia", "iot_class": "cloud_polling", "loggers": ["nexia"], - "requirements": ["nexia==2.0.6"] + "requirements": ["nexia==2.0.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index f7276d583fd..89bb3324528 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1258,7 +1258,7 @@ nettigo-air-monitor==2.1.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.6 +nexia==2.0.7 # homeassistant.components.nextcloud nextcloudmonitor==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 274d7dfd9a9..7b1e5082cf3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -966,7 +966,7 @@ netmap==0.7.0.2 nettigo-air-monitor==2.1.0 # homeassistant.components.nexia -nexia==2.0.6 +nexia==2.0.7 # homeassistant.components.nextcloud nextcloudmonitor==1.4.0 From 0a56361ca460e1e08905750155cee71f0ca5d665 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Jul 2023 17:46:26 -0500 Subject: [PATCH 0057/1151] Disable always_update in nexia coordinator (#97436) --- homeassistant/components/nexia/coordinator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nexia/coordinator.py b/homeassistant/components/nexia/coordinator.py index b83ebcf9c40..cd515e44b14 100644 --- a/homeassistant/components/nexia/coordinator.py +++ b/homeassistant/components/nexia/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from nexia.home import NexiaHome @@ -14,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_UPDATE_RATE = 120 -class NexiaDataUpdateCoordinator(DataUpdateCoordinator[None]): +class NexiaDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """DataUpdateCoordinator for nexia homes.""" def __init__( @@ -29,8 +30,9 @@ class NexiaDataUpdateCoordinator(DataUpdateCoordinator[None]): _LOGGER, name="Nexia update", update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), + always_update=False, ) - async def _async_update_data(self) -> None: + async def _async_update_data(self) -> dict[str, Any]: """Fetch data from API endpoint.""" return await self.nexia_home.update() From 78003886a5614c64260782da53a4726a9c659a80 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 29 Jul 2023 02:22:54 +0200 Subject: [PATCH 0058/1151] GDACS add yaml config issue (#97424) gdacs remove yaml issue --- homeassistant/components/gdacs/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py index 9474e006dbb..f25341455bb 100644 --- a/homeassistant/components/gdacs/__init__.py +++ b/homeassistant/components/gdacs/__init__.py @@ -13,10 +13,11 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, UnitOfLength, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.util.unit_conversion import DistanceConverter from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM @@ -77,6 +78,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: }, ) ) + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Global Disaster Alert and Coordination System", + }, + ) return True From 0e8bbbd3d99b84ea9ade82eeef54efdbd30e74ba Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sat, 29 Jul 2023 00:09:25 -0400 Subject: [PATCH 0059/1151] Add a battery sensor to Schlage (#97369) --- homeassistant/components/schlage/__init__.py | 2 +- homeassistant/components/schlage/entity.py | 41 ++++++++++++ homeassistant/components/schlage/lock.py | 32 ++-------- homeassistant/components/schlage/sensor.py | 66 ++++++++++++++++++++ tests/components/schlage/conftest.py | 37 ++++++++++- tests/components/schlage/test_lock.py | 49 ++------------- tests/components/schlage/test_sensor.py | 30 +++++++++ 7 files changed, 182 insertions(+), 75 deletions(-) create mode 100644 homeassistant/components/schlage/entity.py create mode 100644 homeassistant/components/schlage/sensor.py create mode 100644 tests/components/schlage/test_sensor.py diff --git a/homeassistant/components/schlage/__init__.py b/homeassistant/components/schlage/__init__.py index 7991645e20d..95cfd16958c 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] +PLATFORMS: list[Platform] = [Platform.LOCK, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/schlage/entity.py b/homeassistant/components/schlage/entity.py new file mode 100644 index 00000000000..3a1a11bc098 --- /dev/null +++ b/homeassistant/components/schlage/entity.py @@ -0,0 +1,41 @@ +"""Base entity class for Schlage.""" + +from pyschlage.lock import Lock + +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import SchlageDataUpdateCoordinator + + +class SchlageEntity(CoordinatorEntity[SchlageDataUpdateCoordinator]): + """Base Schlage entity.""" + + _attr_has_entity_name = True + + def __init__( + self, coordinator: SchlageDataUpdateCoordinator, device_id: str + ) -> None: + """Initialize a Schlage entity.""" + super().__init__(coordinator=coordinator) + self.device_id = device_id + self._attr_unique_id = device_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + name=self._lock.name, + manufacturer=MANUFACTURER, + model=self._lock.model_name, + sw_version=self._lock.firmware_version, + ) + + @property + def _lock(self) -> Lock: + """Fetch the Schlage lock from our coordinator.""" + return self.coordinator.data.locks[self.device_id] + + @property + def available(self) -> bool: + """Return if entity is available.""" + # When is_locked is None the lock is unavailable. + return super().available and self._lock.is_locked is not None diff --git a/homeassistant/components/schlage/lock.py b/homeassistant/components/schlage/lock.py index ad7ff863d40..65758c3442f 100644 --- a/homeassistant/components/schlage/lock.py +++ b/homeassistant/components/schlage/lock.py @@ -3,17 +3,14 @@ from __future__ import annotations from typing import Any -from pyschlage.lock import Lock - from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, MANUFACTURER +from .const import DOMAIN from .coordinator import SchlageDataUpdateCoordinator +from .entity import SchlageEntity async def async_setup_entry( @@ -29,39 +26,18 @@ async def async_setup_entry( ) -class SchlageLockEntity(CoordinatorEntity[SchlageDataUpdateCoordinator], LockEntity): +class SchlageLockEntity(SchlageEntity, LockEntity): """Schlage lock entity.""" - _attr_has_entity_name = True _attr_name = None def __init__( self, coordinator: SchlageDataUpdateCoordinator, device_id: str ) -> None: """Initialize a Schlage Lock.""" - super().__init__(coordinator=coordinator) - self.device_id = device_id - self._attr_unique_id = device_id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device_id)}, - name=self._lock.name, - manufacturer=MANUFACTURER, - model=self._lock.model_name, - sw_version=self._lock.firmware_version, - ) + super().__init__(coordinator=coordinator, device_id=device_id) self._update_attrs() - @property - def _lock(self) -> Lock: - """Fetch the Schlage lock from our coordinator.""" - return self.coordinator.data.locks[self.device_id] - - @property - def available(self) -> bool: - """Return if entity is available.""" - # When is_locked is None the lock is unavailable. - return super().available and self._lock.is_locked is not None - @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" diff --git a/homeassistant/components/schlage/sensor.py b/homeassistant/components/schlage/sensor.py new file mode 100644 index 00000000000..aa2ff87e5bf --- /dev/null +++ b/homeassistant/components/schlage/sensor.py @@ -0,0 +1,66 @@ +"""Platform for Schlage sensor integration.""" + +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SchlageDataUpdateCoordinator +from .entity import SchlageEntity + +_SENSOR_DESCRIPTIONS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="battery_level", + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors based on a config entry.""" + coordinator: SchlageDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + SchlageBatterySensor( + coordinator=coordinator, + description=description, + device_id=device_id, + ) + for description in _SENSOR_DESCRIPTIONS + for device_id in coordinator.data.locks + ) + + +class SchlageBatterySensor(SchlageEntity, SensorEntity): + """Schlage battery sensor entity.""" + + def __init__( + self, + coordinator: SchlageDataUpdateCoordinator, + description: SensorEntityDescription, + device_id: str, + ) -> None: + """Initialize a Schlage battery sensor.""" + super().__init__(coordinator=coordinator, device_id=device_id) + self.entity_description = description + self._attr_unique_id = f"{device_id}_{description.key}" + self._attr_native_value = getattr(self._lock, self.entity_description.key) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = getattr(self._lock, self.entity_description.key) + return super()._handle_coordinator_update() diff --git a/tests/components/schlage/conftest.py b/tests/components/schlage/conftest.py index 681024358c6..3445d653a81 100644 --- a/tests/components/schlage/conftest.py +++ b/tests/components/schlage/conftest.py @@ -1,11 +1,13 @@ """Common fixtures for the Schlage tests.""" from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, create_autospec, patch +from pyschlage.lock import Lock import pytest from homeassistant.components.schlage.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -24,6 +26,23 @@ def mock_config_entry() -> MockConfigEntry: ) +@pytest.fixture +async def mock_added_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_pyschlage_auth: Mock, + mock_schlage: Mock, + mock_lock: Mock, +) -> MockConfigEntry: + """Mock ConfigEntry that's been added to HA.""" + mock_schlage.locks.return_value = [mock_lock] + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert DOMAIN in hass.config_entries.async_domains() + return mock_config_entry + + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: """Override async_setup_entry.""" @@ -46,3 +65,19 @@ def mock_pyschlage_auth(): with patch("pyschlage.Auth", autospec=True) as mock_auth: mock_auth.return_value.user_id = "abc123" yield mock_auth.return_value + + +@pytest.fixture +def mock_lock(): + """Mock Lock fixture.""" + mock_lock = create_autospec(Lock) + mock_lock.configure_mock( + device_id="test", + name="Vault Door", + model_name="", + is_locked=False, + is_jammed=False, + battery_level=20, + firmware_version="1.0", + ) + return mock_lock diff --git a/tests/components/schlage/test_lock.py b/tests/components/schlage/test_lock.py index 8819d8558fd..b164b4f6b79 100644 --- a/tests/components/schlage/test_lock.py +++ b/tests/components/schlage/test_lock.py @@ -1,56 +1,15 @@ """Test schlage lock.""" -from unittest.mock import Mock, create_autospec - -from pyschlage.lock import Lock -import pytest +from unittest.mock import Mock from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.components.schlage.const import 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 tests.common import MockConfigEntry - - -@pytest.fixture -def mock_lock(): - """Mock Lock fixture.""" - mock_lock = create_autospec(Lock) - mock_lock.configure_mock( - device_id="test", - name="Vault Door", - model_name="", - is_locked=False, - is_jammed=False, - battery_level=0, - firmware_version="1.0", - ) - return mock_lock - - -@pytest.fixture -async def mock_entry( - hass: HomeAssistant, mock_pyschlage_auth: Mock, mock_schlage: Mock, mock_lock: Mock -) -> ConfigEntry: - """Create and add a mock ConfigEntry.""" - mock_schlage.locks.return_value = [mock_lock] - entry = MockConfigEntry( - domain=DOMAIN, - data={"username": "test-username", "password": "test-password"}, - entry_id="test-username", - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert DOMAIN in hass.config_entries.async_domains() - return entry - async def test_lock_device_registry( - hass: HomeAssistant, mock_entry: ConfigEntry + hass: HomeAssistant, mock_added_config_entry: ConfigEntry ) -> None: """Test lock is added to device registry.""" device_registry = dr.async_get(hass) @@ -62,7 +21,7 @@ async def test_lock_device_registry( async def test_lock_services( - hass: HomeAssistant, mock_lock: Mock, mock_entry: ConfigEntry + hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry ) -> None: """Test lock services.""" await hass.services.async_call( @@ -83,4 +42,4 @@ async def test_lock_services( await hass.async_block_till_done() mock_lock.unlock.assert_called_once_with() - await hass.config_entries.async_unload(mock_entry.entry_id) + await hass.config_entries.async_unload(mock_added_config_entry.entry_id) diff --git a/tests/components/schlage/test_sensor.py b/tests/components/schlage/test_sensor.py new file mode 100644 index 00000000000..775438795ff --- /dev/null +++ b/tests/components/schlage/test_sensor.py @@ -0,0 +1,30 @@ +"""Test schlage sensor.""" + +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + + +async def test_sensor_device_registry( + hass: HomeAssistant, mock_added_config_entry: ConfigEntry +) -> None: + """Test sensor 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_battery_sensor( + hass: HomeAssistant, mock_added_config_entry: ConfigEntry +) -> None: + """Test the battery sensor.""" + battery_sensor = hass.states.get("sensor.vault_door_battery") + assert battery_sensor is not None + assert battery_sensor.state == "20" + assert battery_sensor.attributes["unit_of_measurement"] == PERCENTAGE + assert battery_sensor.attributes["device_class"] == SensorDeviceClass.BATTERY From b7ed163cafa6705b5162c3fb27683c9dffa2c3cc Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sat, 29 Jul 2023 12:32:55 +0200 Subject: [PATCH 0060/1151] bmw_connected_drive: Add WASHING_FLUID to correct binary sensor attribute (#97448) BMW: Add WASHING_FLUID to correct binary sensor attribute Co-authored-by: rikroe --- .../components/bmw_connected_drive/binary_sensor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index c3be7ae189b..d3711a8f2e6 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -37,11 +37,14 @@ ALLOWED_CONDITION_BASED_SERVICE_KEYS = { "TIRE_WEAR_REAR", "VEHICLE_CHECK", "VEHICLE_TUV", - "WASHING_FLUID", } LOGGED_CONDITION_BASED_SERVICE_WARNINGS: set[str] = set() -ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = {"ENGINE_OIL", "TIRE_PRESSURE"} +ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = { + "ENGINE_OIL", + "TIRE_PRESSURE", + "WASHING_FLUID", +} LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS: set[str] = set() From 750260b266794a608f9f454dfc4d8e2685673719 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 29 Jul 2023 17:03:29 +0200 Subject: [PATCH 0061/1151] Add more sensors to PEGELONLINE (#97295) * add further sensors * adjust and improve tests * add device classes were applicable * fix doc string * name for ph comes from device class * use icon from device class for ph sensor --- .../components/pegel_online/coordinator.py | 13 +- .../components/pegel_online/manifest.json | 2 +- .../components/pegel_online/model.py | 11 - .../components/pegel_online/sensor.py | 91 ++++++-- .../components/pegel_online/strings.json | 18 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/pegel_online/__init__.py | 10 +- tests/components/pegel_online/const.py | 203 ++++++++++++++++++ .../pegel_online/test_config_flow.py | 45 +--- tests/components/pegel_online/test_init.py | 31 +-- tests/components/pegel_online/test_sensor.py | 144 ++++++++++--- 12 files changed, 444 insertions(+), 128 deletions(-) delete mode 100644 homeassistant/components/pegel_online/model.py create mode 100644 tests/components/pegel_online/const.py diff --git a/homeassistant/components/pegel_online/coordinator.py b/homeassistant/components/pegel_online/coordinator.py index 8fab3ce36ae..9463aa48872 100644 --- a/homeassistant/components/pegel_online/coordinator.py +++ b/homeassistant/components/pegel_online/coordinator.py @@ -1,18 +1,17 @@ """DataUpdateCoordinator for pegel_online.""" import logging -from aiopegelonline import CONNECT_ERRORS, PegelOnline, Station +from aiopegelonline import CONNECT_ERRORS, PegelOnline, Station, StationMeasurements from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import MIN_TIME_BETWEEN_UPDATES -from .model import PegelOnlineData _LOGGER = logging.getLogger(__name__) -class PegelOnlineDataUpdateCoordinator(DataUpdateCoordinator[PegelOnlineData]): +class PegelOnlineDataUpdateCoordinator(DataUpdateCoordinator[StationMeasurements]): """DataUpdateCoordinator for the pegel_online integration.""" def __init__( @@ -28,13 +27,9 @@ class PegelOnlineDataUpdateCoordinator(DataUpdateCoordinator[PegelOnlineData]): update_interval=MIN_TIME_BETWEEN_UPDATES, ) - async def _async_update_data(self) -> PegelOnlineData: + async def _async_update_data(self) -> StationMeasurements: """Fetch data from API endpoint.""" try: - water_level = await self.api.async_get_station_measurement( - self.station.uuid - ) + return await self.api.async_get_station_measurements(self.station.uuid) except CONNECT_ERRORS as err: raise UpdateFailed(f"Failed to communicate with API: {err}") from err - - return {"water_level": water_level} diff --git a/homeassistant/components/pegel_online/manifest.json b/homeassistant/components/pegel_online/manifest.json index a51954496cd..9546017d4ff 100644 --- a/homeassistant/components/pegel_online/manifest.json +++ b/homeassistant/components/pegel_online/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["aiopegelonline"], - "requirements": ["aiopegelonline==0.0.5"] + "requirements": ["aiopegelonline==0.0.6"] } diff --git a/homeassistant/components/pegel_online/model.py b/homeassistant/components/pegel_online/model.py deleted file mode 100644 index c8dac75bcf2..00000000000 --- a/homeassistant/components/pegel_online/model.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Models for PEGELONLINE.""" - -from typing import TypedDict - -from aiopegelonline import CurrentMeasurement - - -class PegelOnlineData(TypedDict): - """TypedDict for PEGELONLINE Coordinator Data.""" - - water_level: CurrentMeasurement diff --git a/homeassistant/components/pegel_online/sensor.py b/homeassistant/components/pegel_online/sensor.py index 14ec0c2d032..cf229f16d12 100644 --- a/homeassistant/components/pegel_online/sensor.py +++ b/homeassistant/components/pegel_online/sensor.py @@ -1,10 +1,12 @@ """PEGELONLINE sensor entities.""" from __future__ import annotations -from collections.abc import Callable from dataclasses import dataclass +from aiopegelonline.models import CurrentMeasurement + from homeassistant.components.sensor import ( + SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, @@ -17,15 +19,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import PegelOnlineDataUpdateCoordinator from .entity import PegelOnlineEntity -from .model import PegelOnlineData @dataclass class PegelOnlineRequiredKeysMixin: """Mixin for required keys.""" - fn_native_unit: Callable[[PegelOnlineData], str] - fn_native_value: Callable[[PegelOnlineData], float] + measurement_key: str @dataclass @@ -36,14 +36,71 @@ class PegelOnlineSensorEntityDescription( SENSORS: tuple[PegelOnlineSensorEntityDescription, ...] = ( + PegelOnlineSensorEntityDescription( + key="air_temperature", + translation_key="air_temperature", + measurement_key="air_temperature", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + icon="mdi:thermometer-lines", + entity_registry_enabled_default=False, + ), + PegelOnlineSensorEntityDescription( + key="clearance_height", + translation_key="clearance_height", + measurement_key="clearance_height", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DISTANCE, + icon="mdi:bridge", + ), + PegelOnlineSensorEntityDescription( + key="oxygen_level", + translation_key="oxygen_level", + measurement_key="oxygen_level", + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:water-opacity", + entity_registry_enabled_default=False, + ), + PegelOnlineSensorEntityDescription( + key="ph_value", + measurement_key="ph_value", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.PH, + entity_registry_enabled_default=False, + ), + PegelOnlineSensorEntityDescription( + key="water_speed", + translation_key="water_speed", + measurement_key="water_speed", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.SPEED, + icon="mdi:waves-arrow-right", + entity_registry_enabled_default=False, + ), + PegelOnlineSensorEntityDescription( + key="water_flow", + translation_key="water_flow", + measurement_key="water_flow", + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:waves", + entity_registry_enabled_default=False, + ), PegelOnlineSensorEntityDescription( key="water_level", translation_key="water_level", + measurement_key="water_level", state_class=SensorStateClass.MEASUREMENT, - fn_native_unit=lambda data: data["water_level"].uom, - fn_native_value=lambda data: data["water_level"].value, icon="mdi:waves-arrow-up", ), + PegelOnlineSensorEntityDescription( + key="water_temperature", + translation_key="water_temperature", + measurement_key="water_temperature", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TEMPERATURE, + icon="mdi:thermometer-water", + entity_registry_enabled_default=False, + ), ) @@ -51,9 +108,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the PEGELONLINE sensor.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: PegelOnlineDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( - [PegelOnlineSensor(coordinator, description) for description in SENSORS] + [ + PegelOnlineSensor(coordinator, description) + for description in SENSORS + if getattr(coordinator.data, description.measurement_key) is not None + ] ) @@ -71,9 +133,9 @@ class PegelOnlineSensor(PegelOnlineEntity, SensorEntity): super().__init__(coordinator) self.entity_description = description self._attr_unique_id = f"{self.station.uuid}_{description.key}" - self._attr_native_unit_of_measurement = self.entity_description.fn_native_unit( - coordinator.data - ) + + if description.device_class != SensorDeviceClass.PH: + self._attr_native_unit_of_measurement = self.measurement.uom if self.station.latitude and self.station.longitude: self._attr_extra_state_attributes.update( @@ -83,7 +145,12 @@ class PegelOnlineSensor(PegelOnlineEntity, SensorEntity): } ) + @property + def measurement(self) -> CurrentMeasurement: + """Return the measurement data of the entity.""" + return getattr(self.coordinator.data, self.entity_description.measurement_key) + @property def native_value(self) -> float: """Return the state of the device.""" - return self.entity_description.fn_native_value(self.coordinator.data) + return self.measurement.value diff --git a/homeassistant/components/pegel_online/strings.json b/homeassistant/components/pegel_online/strings.json index 930e349f9c3..e777f6169ba 100644 --- a/homeassistant/components/pegel_online/strings.json +++ b/homeassistant/components/pegel_online/strings.json @@ -26,8 +26,26 @@ }, "entity": { "sensor": { + "air_temperature": { + "name": "Air temperature" + }, + "clearance_height": { + "name": "Clearance height" + }, + "oxygen_level": { + "name": "Oxygen level" + }, + "water_speed": { + "name": "Water flow speed" + }, + "water_flow": { + "name": "Water volume flow" + }, "water_level": { "name": "Water level" + }, + "water_temperature": { + "name": "Water temperature" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 89bb3324528..e5e4d16ab87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -304,7 +304,7 @@ aiooncue==0.3.5 aioopenexchangerates==0.4.0 # homeassistant.components.pegel_online -aiopegelonline==0.0.5 +aiopegelonline==0.0.6 # homeassistant.components.acmeda aiopulse==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b1e5082cf3..aa61a3417b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,7 +279,7 @@ aiooncue==0.3.5 aioopenexchangerates==0.4.0 # homeassistant.components.pegel_online -aiopegelonline==0.0.5 +aiopegelonline==0.0.6 # homeassistant.components.acmeda aiopulse==0.4.3 diff --git a/tests/components/pegel_online/__init__.py b/tests/components/pegel_online/__init__.py index ac3f9bda7dd..aed9949c927 100644 --- a/tests/components/pegel_online/__init__.py +++ b/tests/components/pegel_online/__init__.py @@ -8,13 +8,13 @@ class PegelOnlineMock: self, nearby_stations=None, station_details=None, - station_measurement=None, + station_measurements=None, side_effect=None, ) -> None: """Init the mock.""" self.nearby_stations = nearby_stations self.station_details = station_details - self.station_measurement = station_measurement + self.station_measurements = station_measurements self.side_effect = side_effect async def async_get_nearby_stations(self, *args): @@ -29,11 +29,11 @@ class PegelOnlineMock: raise self.side_effect return self.station_details - async def async_get_station_measurement(self, *args): - """Mock async_get_station_measurement.""" + async def async_get_station_measurements(self, *args): + """Mock async_get_station_measurements.""" if self.side_effect: raise self.side_effect - return self.station_measurement + return self.station_measurements def override_side_effect(self, side_effect): """Override the side_effect.""" diff --git a/tests/components/pegel_online/const.py b/tests/components/pegel_online/const.py new file mode 100644 index 00000000000..4ab28301f90 --- /dev/null +++ b/tests/components/pegel_online/const.py @@ -0,0 +1,203 @@ +"""Constants for pegel_online tests.""" + +from aiopegelonline.models import Station, StationMeasurements + +from homeassistant.components.pegel_online.const import CONF_STATION + +MOCK_STATION_DETAILS_MEISSEN = Station( + { + "uuid": "85d686f1-xxxx-xxxx-xxxx-3207b50901a7", + "number": "501060", + "shortname": "MEISSEN", + "longname": "MEISSEN", + "km": 82.2, + "agency": "STANDORT DRESDEN", + "longitude": 13.475467710324812, + "latitude": 51.16440557554545, + "water": {"shortname": "ELBE", "longname": "ELBE"}, + } +) + +MOCK_STATION_DETAILS_DRESDEN = Station( + { + "uuid": "70272185-xxxx-xxxx-xxxx-43bea330dcae", + "number": "501060", + "shortname": "DRESDEN", + "longname": "DRESDEN", + "km": 55.63, + "agency": "STANDORT DRESDEN", + "longitude": 13.738831783620384, + "latitude": 51.054459765598125, + "water": {"shortname": "ELBE", "longname": "ELBE"}, + } +) +MOCK_CONFIG_ENTRY_DATA_DRESDEN = {CONF_STATION: "70272185-xxxx-xxxx-xxxx-43bea330dcae"} +MOCK_STATION_MEASUREMENT_DRESDEN = StationMeasurements( + [ + { + "shortname": "W", + "longname": "WASSERSTAND ROHDATEN", + "unit": "cm", + "equidistance": 15, + "currentMeasurement": { + "timestamp": "2023-07-26T21:15:00+02:00", + "value": 62, + "stateMnwMhw": "low", + "stateNswHsw": "normal", + }, + "gaugeZero": { + "unit": "m. ü. NHN", + "value": 102.7, + "validFrom": "2019-11-01", + }, + }, + { + "shortname": "Q", + "longname": "ABFLUSS_ROHDATEN", + "unit": "m³/s", + "equidistance": 15, + "currentMeasurement": { + "timestamp": "2023-07-26T06:00:00+02:00", + "value": 88.4, + }, + }, + ] +) + +MOCK_STATION_DETAILS_HANAU_BRIDGE = Station( + { + "uuid": "07374faf-xxxx-xxxx-xxxx-adc0e0784c4b", + "number": "24700347", + "shortname": "HANAU BRÜCKE DFH", + "longname": "HANAU BRÜCKE DFH", + "km": 56.398, + "agency": "ASCHAFFENBURG", + "water": {"shortname": "MAIN", "longname": "MAIN"}, + } +) +MOCK_CONFIG_ENTRY_DATA_HANAU_BRIDGE = { + CONF_STATION: "07374faf-xxxx-xxxx-xxxx-adc0e0784c4b" +} +MOCK_STATION_MEASUREMENT_HANAU_BRIDGE = StationMeasurements( + [ + { + "shortname": "DFH", + "longname": "DURCHFAHRTSHÖHE", + "unit": "cm", + "equidistance": 15, + "currentMeasurement": { + "timestamp": "2023-07-26T19:45:00+02:00", + "value": 715, + }, + "gaugeZero": { + "unit": "m. ü. NHN", + "value": 106.501, + "validFrom": "2019-11-01", + }, + } + ] +) + + +MOCK_STATION_DETAILS_WUERZBURG = Station( + { + "uuid": "915d76e1-xxxx-xxxx-xxxx-4d144cd771cc", + "number": "24300600", + "shortname": "WÜRZBURG", + "longname": "WÜRZBURG", + "km": 251.97, + "agency": "SCHWEINFURT", + "longitude": 9.925968763247354, + "latitude": 49.79620901036012, + "water": {"shortname": "MAIN", "longname": "MAIN"}, + } +) +MOCK_CONFIG_ENTRY_DATA_WUERZBURG = { + CONF_STATION: "915d76e1-xxxx-xxxx-xxxx-4d144cd771cc" +} +MOCK_STATION_MEASUREMENT_WUERZBURG = StationMeasurements( + [ + { + "shortname": "W", + "longname": "WASSERSTAND ROHDATEN", + "unit": "cm", + "equidistance": 15, + "currentMeasurement": { + "timestamp": "2023-07-26T19:15:00+02:00", + "value": 159, + "stateMnwMhw": "normal", + "stateNswHsw": "normal", + }, + "gaugeZero": { + "unit": "m. ü. NHN", + "value": 164.511, + "validFrom": "2019-11-01", + }, + }, + { + "shortname": "LT", + "longname": "LUFTTEMPERATUR", + "unit": "°C", + "equidistance": 60, + "currentMeasurement": { + "timestamp": "2023-07-26T19:00:00+02:00", + "value": 21.2, + }, + }, + { + "shortname": "WT", + "longname": "WASSERTEMPERATUR", + "unit": "°C", + "equidistance": 60, + "currentMeasurement": { + "timestamp": "2023-07-26T19:00:00+02:00", + "value": 22.1, + }, + }, + { + "shortname": "VA", + "longname": "FLIESSGESCHWINDIGKEIT", + "unit": "m/s", + "equidistance": 15, + "currentMeasurement": { + "timestamp": "2023-07-26T19:15:00+02:00", + "value": 0.58, + }, + }, + { + "shortname": "O2", + "longname": "SAUERSTOFFGEHALT", + "unit": "mg/l", + "equidistance": 60, + "currentMeasurement": { + "timestamp": "2023-07-26T19:00:00+02:00", + "value": 8.4, + }, + }, + { + "shortname": "PH", + "longname": "PH-WERT", + "unit": "--", + "equidistance": 60, + "currentMeasurement": { + "timestamp": "2023-07-26T19:00:00+02:00", + "value": 8.1, + }, + }, + { + "shortname": "Q", + "longname": "ABFLUSS", + "unit": "m³/s", + "equidistance": 15, + "currentMeasurement": { + "timestamp": "2023-07-26T19:00:00+02:00", + "value": 102, + }, + }, + ] +) + +MOCK_NEARBY_STATIONS = { + "70272185-xxxx-xxxx-xxxx-43bea330dcae": MOCK_STATION_DETAILS_DRESDEN, + "85d686f1-xxxx-xxxx-xxxx-3207b50901a7": MOCK_STATION_DETAILS_MEISSEN, +} diff --git a/tests/components/pegel_online/test_config_flow.py b/tests/components/pegel_online/test_config_flow.py index ffc2f88d5a8..cb467e462f0 100644 --- a/tests/components/pegel_online/test_config_flow.py +++ b/tests/components/pegel_online/test_config_flow.py @@ -2,7 +2,6 @@ from unittest.mock import patch from aiohttp.client_exceptions import ClientError -from aiopegelonline import Station from homeassistant.components.pegel_online.const import ( CONF_STATION, @@ -19,6 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import PegelOnlineMock +from .const import MOCK_CONFIG_ENTRY_DATA_DRESDEN, MOCK_NEARBY_STATIONS from tests.common import MockConfigEntry @@ -27,38 +27,7 @@ MOCK_USER_DATA_STEP1 = { CONF_RADIUS: 25, } -MOCK_USER_DATA_STEP2 = {CONF_STATION: "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8"} - -MOCK_CONFIG_ENTRY_DATA = {CONF_STATION: "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8"} - -MOCK_NEARBY_STATIONS = { - "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8": Station( - { - "uuid": "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", - "number": "501060", - "shortname": "DRESDEN", - "longname": "DRESDEN", - "km": 55.63, - "agency": "STANDORT DRESDEN", - "longitude": 13.738831783620384, - "latitude": 51.054459765598125, - "water": {"shortname": "ELBE", "longname": "ELBE"}, - } - ), - "85d686f1-xxxx-xxxx-xxxx-3207b50901a7": Station( - { - "uuid": "85d686f1-xxxx-xxxx-xxxx-3207b50901a7", - "number": "501060", - "shortname": "MEISSEN", - "longname": "MEISSEN", - "km": 82.2, - "agency": "STANDORT DRESDEN", - "longitude": 13.475467710324812, - "latitude": 51.16440557554545, - "water": {"shortname": "ELBE", "longname": "ELBE"}, - } - ), -} +MOCK_USER_DATA_STEP2 = {CONF_STATION: "70272185-xxxx-xxxx-xxxx-43bea330dcae"} async def test_user(hass: HomeAssistant) -> None: @@ -85,7 +54,7 @@ async def test_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA_STEP2 ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_STATION] == "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8" + assert result["data"][CONF_STATION] == "70272185-xxxx-xxxx-xxxx-43bea330dcae" assert result["title"] == "DRESDEN ELBE" await hass.async_block_till_done() @@ -97,8 +66,8 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: """Test starting a flow by user with an already configured statioon.""" mock_config = MockConfigEntry( domain=DOMAIN, - data=MOCK_CONFIG_ENTRY_DATA, - unique_id=MOCK_CONFIG_ENTRY_DATA[CONF_STATION], + data=MOCK_CONFIG_ENTRY_DATA_DRESDEN, + unique_id=MOCK_CONFIG_ENTRY_DATA_DRESDEN[CONF_STATION], ) mock_config.add_to_hass(hass) @@ -159,7 +128,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA_STEP2 ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_STATION] == "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8" + assert result["data"][CONF_STATION] == "70272185-xxxx-xxxx-xxxx-43bea330dcae" assert result["title"] == "DRESDEN ELBE" await hass.async_block_till_done() @@ -201,7 +170,7 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA_STEP2 ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_STATION] == "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8" + assert result["data"][CONF_STATION] == "70272185-xxxx-xxxx-xxxx-43bea330dcae" assert result["title"] == "DRESDEN ELBE" await hass.async_block_till_done() diff --git a/tests/components/pegel_online/test_init.py b/tests/components/pegel_online/test_init.py index 93ade373315..2b5ba3642ec 100644 --- a/tests/components/pegel_online/test_init.py +++ b/tests/components/pegel_online/test_init.py @@ -2,7 +2,6 @@ from unittest.mock import patch from aiohttp.client_exceptions import ClientError -from aiopegelonline import CurrentMeasurement, Station from homeassistant.components.pegel_online.const import ( CONF_STATION, @@ -14,39 +13,27 @@ from homeassistant.core import HomeAssistant from homeassistant.util import utcnow from . import PegelOnlineMock +from .const import ( + MOCK_CONFIG_ENTRY_DATA_DRESDEN, + MOCK_STATION_DETAILS_DRESDEN, + MOCK_STATION_MEASUREMENT_DRESDEN, +) from tests.common import MockConfigEntry, async_fire_time_changed -MOCK_CONFIG_ENTRY_DATA = {CONF_STATION: "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8"} - -MOCK_STATION_DETAILS = Station( - { - "uuid": "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", - "number": "501060", - "shortname": "DRESDEN", - "longname": "DRESDEN", - "km": 55.63, - "agency": "STANDORT DRESDEN", - "longitude": 13.738831783620384, - "latitude": 51.054459765598125, - "water": {"shortname": "ELBE", "longname": "ELBE"}, - } -) -MOCK_STATION_MEASUREMENT = CurrentMeasurement("cm", 56) - async def test_update_error(hass: HomeAssistant) -> None: """Tests error during update entity.""" entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_CONFIG_ENTRY_DATA, - unique_id=MOCK_CONFIG_ENTRY_DATA[CONF_STATION], + data=MOCK_CONFIG_ENTRY_DATA_DRESDEN, + unique_id=MOCK_CONFIG_ENTRY_DATA_DRESDEN[CONF_STATION], ) entry.add_to_hass(hass) with patch("homeassistant.components.pegel_online.PegelOnline") as pegelonline: pegelonline.return_value = PegelOnlineMock( - station_details=MOCK_STATION_DETAILS, - station_measurement=MOCK_STATION_MEASUREMENT, + station_details=MOCK_STATION_DETAILS_DRESDEN, + station_measurements=MOCK_STATION_MEASUREMENT_DRESDEN, ) assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/pegel_online/test_sensor.py b/tests/components/pegel_online/test_sensor.py index 216ca3427c5..a02c7538280 100644 --- a/tests/components/pegel_online/test_sensor.py +++ b/tests/components/pegel_online/test_sensor.py @@ -1,53 +1,141 @@ """Test pegel_online component.""" from unittest.mock import patch -from aiopegelonline import CurrentMeasurement, Station +from aiopegelonline.models import Station, StationMeasurements +import pytest from homeassistant.components.pegel_online.const import CONF_STATION, DOMAIN -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant from . import PegelOnlineMock +from .const import ( + MOCK_CONFIG_ENTRY_DATA_DRESDEN, + MOCK_CONFIG_ENTRY_DATA_HANAU_BRIDGE, + MOCK_CONFIG_ENTRY_DATA_WUERZBURG, + MOCK_STATION_DETAILS_DRESDEN, + MOCK_STATION_DETAILS_HANAU_BRIDGE, + MOCK_STATION_DETAILS_WUERZBURG, + MOCK_STATION_MEASUREMENT_DRESDEN, + MOCK_STATION_MEASUREMENT_HANAU_BRIDGE, + MOCK_STATION_MEASUREMENT_WUERZBURG, +) from tests.common import MockConfigEntry -MOCK_CONFIG_ENTRY_DATA = {CONF_STATION: "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8"} -MOCK_STATION_DETAILS = Station( - { - "uuid": "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", - "number": "501060", - "shortname": "DRESDEN", - "longname": "DRESDEN", - "km": 55.63, - "agency": "STANDORT DRESDEN", - "longitude": 13.738831783620384, - "latitude": 51.054459765598125, - "water": {"shortname": "ELBE", "longname": "ELBE"}, - } +@pytest.mark.parametrize( + ( + "mock_config_entry_data", + "mock_station_details", + "mock_station_measurement", + "expected_states", + ), + [ + ( + MOCK_CONFIG_ENTRY_DATA_DRESDEN, + MOCK_STATION_DETAILS_DRESDEN, + MOCK_STATION_MEASUREMENT_DRESDEN, + { + "sensor.dresden_elbe_water_volume_flow": ( + "DRESDEN ELBE Water volume flow", + "88.4", + "m³/s", + ), + "sensor.dresden_elbe_water_level": ( + "DRESDEN ELBE Water level", + "62", + "cm", + ), + }, + ), + ( + MOCK_CONFIG_ENTRY_DATA_HANAU_BRIDGE, + MOCK_STATION_DETAILS_HANAU_BRIDGE, + MOCK_STATION_MEASUREMENT_HANAU_BRIDGE, + { + "sensor.hanau_brucke_dfh_main_clearance_height": ( + "HANAU BRÜCKE DFH MAIN Clearance height", + "715", + "cm", + ), + }, + ), + ( + MOCK_CONFIG_ENTRY_DATA_WUERZBURG, + MOCK_STATION_DETAILS_WUERZBURG, + MOCK_STATION_MEASUREMENT_WUERZBURG, + { + "sensor.wurzburg_main_air_temperature": ( + "WÜRZBURG MAIN Air temperature", + "21.2", + "°C", + ), + "sensor.wurzburg_main_oxygen_level": ( + "WÜRZBURG MAIN Oxygen level", + "8.4", + "mg/l", + ), + "sensor.wurzburg_main_ph": ( + "WÜRZBURG MAIN pH", + "8.1", + None, + ), + "sensor.wurzburg_main_water_flow_speed": ( + "WÜRZBURG MAIN Water flow speed", + "0.58", + "m/s", + ), + "sensor.wurzburg_main_water_volume_flow": ( + "WÜRZBURG MAIN Water volume flow", + "102", + "m³/s", + ), + "sensor.wurzburg_main_water_level": ( + "WÜRZBURG MAIN Water level", + "159", + "cm", + ), + "sensor.wurzburg_main_water_temperature": ( + "WÜRZBURG MAIN Water temperature", + "22.1", + "°C", + ), + }, + ), + ], ) -MOCK_STATION_MEASUREMENT = CurrentMeasurement("cm", 56) - - -async def test_sensor(hass: HomeAssistant) -> None: +async def test_sensor( + hass: HomeAssistant, + mock_config_entry_data: dict, + mock_station_details: Station, + mock_station_measurement: StationMeasurements, + expected_states: dict, + entity_registry_enabled_by_default: None, +) -> None: """Tests sensor entity.""" entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_CONFIG_ENTRY_DATA, - unique_id=MOCK_CONFIG_ENTRY_DATA[CONF_STATION], + data=mock_config_entry_data, + unique_id=mock_config_entry_data[CONF_STATION], ) entry.add_to_hass(hass) with patch("homeassistant.components.pegel_online.PegelOnline") as pegelonline: pegelonline.return_value = PegelOnlineMock( - station_details=MOCK_STATION_DETAILS, - station_measurement=MOCK_STATION_MEASUREMENT, + station_details=mock_station_details, + station_measurements=mock_station_measurement, ) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.dresden_elbe_water_level") - assert state.name == "DRESDEN ELBE Water level" - assert state.state == "56" - assert state.attributes[ATTR_LATITUDE] == 51.054459765598125 - assert state.attributes[ATTR_LONGITUDE] == 13.738831783620384 + assert len(hass.states.async_all()) == len(expected_states) + + for state_name, state_data in expected_states.items(): + state = hass.states.get(state_name) + assert state.name == state_data[0] + assert state.state == state_data[1] + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == state_data[2] + if mock_station_details.latitude is not None: + assert state.attributes[ATTR_LATITUDE] == mock_station_details.latitude + assert state.attributes[ATTR_LONGITUDE] == mock_station_details.longitude From f52876c7f6120cac5ffc68e910376d561fc0f8fa Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Sat, 29 Jul 2023 17:37:40 +0200 Subject: [PATCH 0062/1151] Add entity description to EZVIZ SwitchEntity (#95672) * Initial commit * Update switch entity * Add entity description * Redundant get. Key will always be there. * fixed dumb condition mistake. * Removed names from entity description * Implement suggestions * async_add_entities has iterator so cleanup * Update strings.json --- homeassistant/components/ezviz/strings.json | 38 +++++ homeassistant/components/ezviz/switch.py | 169 ++++++++++++++++---- 2 files changed, 173 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index d60c4816d24..590f95029c6 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -163,6 +163,44 @@ "last_alarm_type_name": { "name": "Last alarm type name" } + }, + "switch": { + "status_light": { + "name": "Status light" + }, + "privacy": { + "name": "Privacy" + }, + "infrared_light": { + "name": "Infrared light" + }, + "sleep": { + "name": "Sleep" + }, + "audio": { + "name": "Audio" + }, + "motion_tracking": { + "name": "Motion tracking" + }, + "all_day_video_recording": { + "name": "All day video recording" + }, + "auto_sleep": { + "name": "Auto sleep" + }, + "flicker_light_on_movement": { + "name": "Flicker light on movement" + }, + "pir_motion_activated_light": { + "name": "PIR motion activated light" + }, + "tamper_alarm": { + "name": "Tamper alarm" + }, + "follow_movement": { + "name": "Follow movement" + } } }, "services": { diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index 58b28477412..337a7080506 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -1,14 +1,20 @@ """Support for EZVIZ Switch sensors.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any -from pyezviz.constants import DeviceSwitchType +from pyezviz.constants import DeviceSwitchType, SupportExt from pyezviz.exceptions import HTTPError, PyEzvizError -from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_COORDINATOR, DOMAIN @@ -16,6 +22,96 @@ from .coordinator import EzvizDataUpdateCoordinator from .entity import EzvizEntity +@dataclass +class EzvizSwitchEntityDescriptionMixin: + """Mixin values for EZVIZ Switch entities.""" + + supported_ext: str | None + + +@dataclass +class EzvizSwitchEntityDescription( + SwitchEntityDescription, EzvizSwitchEntityDescriptionMixin +): + """Describe a EZVIZ switch.""" + + +SWITCH_TYPES: dict[int, EzvizSwitchEntityDescription] = { + 3: EzvizSwitchEntityDescription( + key="3", + translation_key="status_light", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=None, + ), + 7: EzvizSwitchEntityDescription( + key="7", + translation_key="privacy", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=str(SupportExt.SupportPtzPrivacy.value), + ), + 10: EzvizSwitchEntityDescription( + key="10", + translation_key="infrared_light", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=str(SupportExt.SupportCloseInfraredLight.value), + ), + 21: EzvizSwitchEntityDescription( + key="21", + translation_key="sleep", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=str(SupportExt.SupportSleep.value), + ), + 22: EzvizSwitchEntityDescription( + key="22", + translation_key="audio", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=str(SupportExt.SupportAudioOnoff.value), + ), + 25: EzvizSwitchEntityDescription( + key="25", + translation_key="motion_tracking", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=str(SupportExt.SupportIntelligentTrack.value), + ), + 29: EzvizSwitchEntityDescription( + key="29", + translation_key="all_day_video_recording", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=str(SupportExt.SupportFulldayRecord.value), + ), + 32: EzvizSwitchEntityDescription( + key="32", + translation_key="auto_sleep", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=str(SupportExt.SupportAutoSleep.value), + ), + 301: EzvizSwitchEntityDescription( + key="301", + translation_key="flicker_light_on_movement", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=str(SupportExt.SupportActiveDefense.value), + ), + 305: EzvizSwitchEntityDescription( + key="305", + translation_key="pir_motion_activated_light", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=str(SupportExt.SupportLightRelate.value), + ), + 306: EzvizSwitchEntityDescription( + key="306", + translation_key="tamper_alarm", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=str(SupportExt.SupportTamperAlarm.value), + ), + 650: EzvizSwitchEntityDescription( + key="650", + translation_key="follow_movement", + device_class=SwitchDeviceClass.SWITCH, + supported_ext=str(SupportExt.SupportTracking.value), + ), +} + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -24,61 +120,66 @@ async def async_setup_entry( DATA_COORDINATOR ] - supported_switches = {switches.value for switches in DeviceSwitchType} - async_add_entities( - [ - EzvizSwitch(coordinator, camera, switch) - for camera in coordinator.data - for switch in coordinator.data[camera].get("switches") - if switch in supported_switches - ] + EzvizSwitch(coordinator, camera, switch_number) + for camera in coordinator.data + for switch_number in coordinator.data[camera]["switches"] + if switch_number in SWITCH_TYPES + if SWITCH_TYPES[switch_number].supported_ext + in coordinator.data[camera]["supportExt"] + or SWITCH_TYPES[switch_number].supported_ext is None ) class EzvizSwitch(EzvizEntity, SwitchEntity): """Representation of a EZVIZ sensor.""" - _attr_device_class = SwitchDeviceClass.SWITCH + _attr_has_entity_name = True def __init__( - self, coordinator: EzvizDataUpdateCoordinator, serial: str, switch: str + self, coordinator: EzvizDataUpdateCoordinator, serial: str, switch_number: int ) -> None: """Initialize the switch.""" super().__init__(coordinator, serial) - self._name = switch - self._attr_name = f"{self._camera_name} {DeviceSwitchType(switch).name.title()}" + self._switch_number = switch_number self._attr_unique_id = ( - f"{serial}_{self._camera_name}.{DeviceSwitchType(switch).name}" + f"{serial}_{self._camera_name}.{DeviceSwitchType(switch_number).name}" ) - - @property - def is_on(self) -> bool: - """Return the state of the switch.""" - return self.data["switches"][self._name] + self.entity_description = SWITCH_TYPES[switch_number] + self._attr_is_on = self.data["switches"][switch_number] async def async_turn_on(self, **kwargs: Any) -> None: """Change a device switch on the camera.""" try: - update_ok = await self.hass.async_add_executor_job( - self.coordinator.ezviz_client.switch_status, self._serial, self._name, 1 - ) + if await self.hass.async_add_executor_job( + self.coordinator.ezviz_client.switch_status, + self._serial, + self._switch_number, + 1, + ): + self._attr_is_on = True + self.async_write_ha_state() except (HTTPError, PyEzvizError) as err: - raise PyEzvizError(f"Failed to turn on switch {self._name}") from err - - if update_ok: - await self.coordinator.async_request_refresh() + raise HomeAssistantError(f"Failed to turn on switch {self.name}") from err async def async_turn_off(self, **kwargs: Any) -> None: """Change a device switch on the camera.""" try: - update_ok = await self.hass.async_add_executor_job( - self.coordinator.ezviz_client.switch_status, self._serial, self._name, 0 - ) + if await self.hass.async_add_executor_job( + self.coordinator.ezviz_client.switch_status, + self._serial, + self._switch_number, + 0, + ): + self._attr_is_on = False + self.async_write_ha_state() except (HTTPError, PyEzvizError) as err: - raise PyEzvizError(f"Failed to turn off switch {self._name}") from err + raise HomeAssistantError(f"Failed to turn off switch {self.name}") from err - if update_ok: - await self.coordinator.async_request_refresh() + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = self.data["switches"].get(self._switch_number) + super()._handle_coordinator_update() From 598e26e481d0dc0d7ec7ea90f9919620daac2838 Mon Sep 17 00:00:00 2001 From: mbo18 Date: Sat, 29 Jul 2023 17:40:14 +0200 Subject: [PATCH 0063/1151] Add device and state class to humidity sensor (#97331) Add device and state class to humidity --- homeassistant/components/meteo_france/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 89faf6d80eb..67fcd9d71fc 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -141,7 +141,8 @@ SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = ( key="humidity", name="Humidity", native_unit_of_measurement=PERCENTAGE, - icon="mdi:water-percent", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, data_path="current_forecast:humidity", ), ) From 12426dfad46251ad71981c33ef26a1385fbf9f23 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 29 Jul 2023 19:22:53 +0000 Subject: [PATCH 0064/1151] Add entity translations for AccuWeather (#95940) * Add entity name translations * Some improvements * Update tests * Suggested changes * day 0 -> today * night 0 -> tonight * Fix precipitation --- .../components/accuweather/sensor.py | 464 ++++++----- .../components/accuweather/strings.json | 767 ++++++++++++++++-- tests/components/accuweather/test_sensor.py | 102 +-- 3 files changed, 1044 insertions(+), 289 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 9eca5e772b0..c983f0bc291 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -59,193 +59,280 @@ class AccuWeatherSensorDescription( """Class describing AccuWeather sensor entities.""" attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {} + day: int | None = None FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( - AccuWeatherSensorDescription( - key="AirQuality", - icon="mdi:air-filter", - name="Air quality", - value_fn=lambda data: cast(str, data[ATTR_CATEGORY]), - device_class=SensorDeviceClass.ENUM, - options=["good", "hazardous", "high", "low", "moderate", "unhealthy"], - translation_key="air_quality", + *( + AccuWeatherSensorDescription( + key="AirQuality", + icon="mdi:air-filter", + value_fn=lambda data: cast(str, data[ATTR_CATEGORY]), + device_class=SensorDeviceClass.ENUM, + options=["good", "hazardous", "high", "low", "moderate", "unhealthy"], + translation_key=f"air_quality_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="CloudCoverDay", - icon="mdi:weather-cloudy", - name="Cloud cover day", - entity_registry_enabled_default=False, - native_unit_of_measurement=PERCENTAGE, - value_fn=lambda data: cast(int, data), + *( + AccuWeatherSensorDescription( + key="CloudCoverDay", + icon="mdi:weather-cloudy", + entity_registry_enabled_default=False, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda data: cast(int, data), + translation_key=f"cloud_cover_day_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="CloudCoverNight", - icon="mdi:weather-cloudy", - name="Cloud cover night", - entity_registry_enabled_default=False, - native_unit_of_measurement=PERCENTAGE, - value_fn=lambda data: cast(int, data), + *( + AccuWeatherSensorDescription( + key="CloudCoverNight", + icon="mdi:weather-cloudy", + entity_registry_enabled_default=False, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda data: cast(int, data), + translation_key=f"cloud_cover_night_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="Grass", - icon="mdi:grass", - name="Grass pollen", - entity_registry_enabled_default=False, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - value_fn=lambda data: cast(int, data[ATTR_VALUE]), - attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, - translation_key="grass_pollen", + *( + AccuWeatherSensorDescription( + key="Grass", + icon="mdi:grass", + entity_registry_enabled_default=False, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + value_fn=lambda data: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, + translation_key=f"grass_pollen_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="HoursOfSun", - icon="mdi:weather-partly-cloudy", - name="Hours of sun", - native_unit_of_measurement=UnitOfTime.HOURS, - value_fn=lambda data: cast(float, data), + *( + AccuWeatherSensorDescription( + key="HoursOfSun", + icon="mdi:weather-partly-cloudy", + native_unit_of_measurement=UnitOfTime.HOURS, + value_fn=lambda data: cast(float, data), + translation_key=f"hours_of_sun_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="LongPhraseDay", - name="Condition day", - value_fn=lambda data: cast(str, data), + *( + AccuWeatherSensorDescription( + key="LongPhraseDay", + value_fn=lambda data: cast(str, data), + translation_key=f"condition_day_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="LongPhraseNight", - name="Condition night", - value_fn=lambda data: cast(str, data), + *( + AccuWeatherSensorDescription( + key="LongPhraseNight", + value_fn=lambda data: cast(str, data), + translation_key=f"condition_night_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="Mold", - icon="mdi:blur", - name="Mold pollen", - entity_registry_enabled_default=False, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - value_fn=lambda data: cast(int, data[ATTR_VALUE]), - attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, - translation_key="mold_pollen", + *( + AccuWeatherSensorDescription( + key="Mold", + icon="mdi:blur", + entity_registry_enabled_default=False, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + value_fn=lambda data: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, + translation_key=f"mold_pollen_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="Ragweed", - icon="mdi:sprout", - name="Ragweed pollen", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - value_fn=lambda data: cast(int, data[ATTR_VALUE]), - attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, - translation_key="ragweed_pollen", + *( + AccuWeatherSensorDescription( + key="Ragweed", + icon="mdi:sprout", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + value_fn=lambda data: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, + translation_key=f"ragweed_pollen_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureMax", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel temperature max", - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), + *( + AccuWeatherSensorDescription( + key="RealFeelTemperatureMax", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key=f"realfeel_temperature_max_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureMin", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel temperature min", - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), + *( + AccuWeatherSensorDescription( + key="RealFeelTemperatureMin", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key=f"realfeel_temperature_min_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureShadeMax", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel temperature shade max", - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), + *( + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMax", + device_class=SensorDeviceClass.TEMPERATURE, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key=f"realfeel_temperature_shade_max_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureShadeMin", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel temperature shade min", - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), + *( + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMin", + device_class=SensorDeviceClass.TEMPERATURE, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key=f"realfeel_temperature_shade_min_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="SolarIrradianceDay", - icon="mdi:weather-sunny", - name="Solar irradiance day", - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), + *( + AccuWeatherSensorDescription( + key="SolarIrradianceDay", + icon="mdi:weather-sunny", + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key=f"solar_irradiance_day_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="SolarIrradianceNight", - icon="mdi:weather-sunny", - name="Solar irradiance night", - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, - value_fn=lambda data: cast(float, data[ATTR_VALUE]), + *( + AccuWeatherSensorDescription( + key="SolarIrradianceNight", + icon="mdi:weather-sunny", + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER, + value_fn=lambda data: cast(float, data[ATTR_VALUE]), + translation_key=f"solar_irradiance_night_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="ThunderstormProbabilityDay", - icon="mdi:weather-lightning", - name="Thunderstorm probability day", - native_unit_of_measurement=PERCENTAGE, - value_fn=lambda data: cast(int, data), + *( + AccuWeatherSensorDescription( + key="ThunderstormProbabilityDay", + icon="mdi:weather-lightning", + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda data: cast(int, data), + translation_key=f"thunderstorm_probability_day_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="ThunderstormProbabilityNight", - icon="mdi:weather-lightning", - name="Thunderstorm probability night", - native_unit_of_measurement=PERCENTAGE, - value_fn=lambda data: cast(int, data), + *( + AccuWeatherSensorDescription( + key="ThunderstormProbabilityNight", + icon="mdi:weather-lightning", + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda data: cast(int, data), + translation_key=f"thunderstorm_probability_night_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="Tree", - icon="mdi:tree-outline", - name="Tree pollen", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - value_fn=lambda data: cast(int, data[ATTR_VALUE]), - attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, - translation_key="tree_pollen", + *( + AccuWeatherSensorDescription( + key="Tree", + icon="mdi:tree-outline", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + value_fn=lambda data: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, + translation_key=f"tree_pollen_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="UVIndex", - icon="mdi:weather-sunny", - name="UV index", - native_unit_of_measurement=UV_INDEX, - value_fn=lambda data: cast(int, data[ATTR_VALUE]), - attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, - translation_key="uv_index", + *( + AccuWeatherSensorDescription( + key="UVIndex", + icon="mdi:weather-sunny", + native_unit_of_measurement=UV_INDEX, + value_fn=lambda data: cast(int, data[ATTR_VALUE]), + attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]}, + translation_key=f"uv_index_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="WindGustDay", - device_class=SensorDeviceClass.WIND_SPEED, - name="Wind gust day", - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, - value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), - attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + *( + AccuWeatherSensorDescription( + key="WindGustDay", + device_class=SensorDeviceClass.WIND_SPEED, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + translation_key=f"wind_gust_speed_day_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="WindGustNight", - device_class=SensorDeviceClass.WIND_SPEED, - name="Wind gust night", - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, - value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), - attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + *( + AccuWeatherSensorDescription( + key="WindGustNight", + device_class=SensorDeviceClass.WIND_SPEED, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + translation_key=f"wind_gust_speed_night_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="WindDay", - device_class=SensorDeviceClass.WIND_SPEED, - name="Wind day", - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, - value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), - attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + *( + AccuWeatherSensorDescription( + key="WindDay", + device_class=SensorDeviceClass.WIND_SPEED, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + translation_key=f"wind_speed_day_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), - AccuWeatherSensorDescription( - key="WindNight", - device_class=SensorDeviceClass.WIND_SPEED, - name="Wind night", - native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, - value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), - attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + *( + AccuWeatherSensorDescription( + key="WindNight", + device_class=SensorDeviceClass.WIND_SPEED, + native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, + value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]), + attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]}, + translation_key=f"wind_speed_night_{day}d", + day=day, + ) + for day in range(MAX_FORECAST_DAYS + 1) ), ) @@ -253,118 +340,117 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( AccuWeatherSensorDescription( key="ApparentTemperature", device_class=SensorDeviceClass.TEMPERATURE, - name="Apparent temperature", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), + translation_key="apparent_temperature", ), AccuWeatherSensorDescription( key="Ceiling", device_class=SensorDeviceClass.DISTANCE, icon="mdi:weather-fog", - name="Cloud ceiling", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfLength.METERS, value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), suggested_display_precision=0, + translation_key="cloud_ceiling", ), AccuWeatherSensorDescription( key="CloudCover", icon="mdi:weather-cloudy", - name="Cloud cover", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: cast(int, data), + translation_key="cloud_cover", ), AccuWeatherSensorDescription( key="DewPoint", device_class=SensorDeviceClass.TEMPERATURE, - name="Dew point", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), + translation_key="dew_point", ), AccuWeatherSensorDescription( key="RealFeelTemperature", device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel temperature", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), + translation_key="realfeel_temperature", ), AccuWeatherSensorDescription( key="RealFeelTemperatureShade", device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel temperature shade", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), + translation_key="realfeel_temperature_shade", ), AccuWeatherSensorDescription( key="Precipitation", device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, - name="Precipitation", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), attr_fn=lambda data: {"type": data["PrecipitationType"]}, + translation_key="precipitation", ), AccuWeatherSensorDescription( key="PressureTendency", device_class=SensorDeviceClass.ENUM, icon="mdi:gauge", - name="Pressure tendency", options=["falling", "rising", "steady"], - translation_key="pressure_tendency", value_fn=lambda data: cast(str, data["LocalizedText"]).lower(), + translation_key="pressure_tendency", ), AccuWeatherSensorDescription( key="UVIndex", icon="mdi:weather-sunny", - name="UV index", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UV_INDEX, value_fn=lambda data: cast(int, data), attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]}, + translation_key="uv_index", ), AccuWeatherSensorDescription( key="WetBulbTemperature", device_class=SensorDeviceClass.TEMPERATURE, - name="Wet bulb temperature", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), + translation_key="wet_bulb_temperature", ), AccuWeatherSensorDescription( key="WindChillTemperature", device_class=SensorDeviceClass.TEMPERATURE, - name="Wind chill temperature", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]), + translation_key="wind_chill_temperature", ), AccuWeatherSensorDescription( key="Wind", device_class=SensorDeviceClass.WIND_SPEED, - name="Wind", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]), + translation_key="wind_speed", ), AccuWeatherSensorDescription( key="WindGust", device_class=SensorDeviceClass.WIND_SPEED, - name="Wind gust", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR, value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]), + translation_key="wind_gust_speed", ), ) @@ -381,14 +467,12 @@ async def async_setup_entry( ] if coordinator.forecast: - # Some air quality/allergy sensors are only available for certain - # locations. - sensors.extend( - AccuWeatherSensor(coordinator, description, forecast_day=day) - for day in range(MAX_FORECAST_DAYS + 1) - for description in FORECAST_SENSOR_TYPES - if description.key in coordinator.data[ATTR_FORECAST][0] - ) + for description in FORECAST_SENSOR_TYPES: + # Some air quality/allergy sensors are only available for certain + # locations. + if description.key not in coordinator.data[ATTR_FORECAST][description.day]: + continue + sensors.append(AccuWeatherSensor(coordinator, description)) async_add_entities(sensors) @@ -406,25 +490,21 @@ class AccuWeatherSensor( self, coordinator: AccuWeatherDataUpdateCoordinator, description: AccuWeatherSensorDescription, - forecast_day: int | None = None, ) -> None: """Initialize.""" super().__init__(coordinator) + self.forecast_day = description.day self.entity_description = description self._sensor_data = _get_sensor_data( - coordinator.data, description.key, forecast_day + coordinator.data, description.key, self.forecast_day ) - if forecast_day is not None: - self._attr_name = f"{description.name} {forecast_day}d" - self._attr_unique_id = ( - f"{coordinator.location_key}-{description.key}-{forecast_day}".lower() - ) + if self.forecast_day is not None: + self._attr_unique_id = f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower() else: self._attr_unique_id = ( f"{coordinator.location_key}-{description.key}".lower() ) self._attr_device_info = coordinator.device_info - self.forecast_day = forecast_day @property def native_value(self) -> str | int | float | None: diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index e9c4ace9b99..24024ba722f 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -24,14 +24,8 @@ }, "entity": { "sensor": { - "pressure_tendency": { - "state": { - "steady": "Steady", - "rising": "Rising", - "falling": "Falling" - } - }, - "air_quality": { + "air_quality_0d": { + "name": "Air quality today", "state": { "good": "Good", "hazardous": "Hazardous", @@ -41,80 +35,761 @@ "unhealthy": "Unhealthy" } }, - "grass_pollen": { + "air_quality_1d": { + "name": "Air quality day 1", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + }, + "air_quality_2d": { + "name": "Air quality day 2", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + }, + "air_quality_3d": { + "name": "Air quality day 3", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + }, + "air_quality_4d": { + "name": "Air quality day 4", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + }, + "apparent_temperature": { + "name": "Apparent temperature" + }, + "cloud_ceiling": { + "name": "Cloud ceiling" + }, + "cloud_cover": { + "name": "Cloud cover" + }, + "cloud_cover_day_0d": { + "name": "Cloud cover today" + }, + "cloud_cover_day_1d": { + "name": "Cloud cover day 1" + }, + "cloud_cover_day_2d": { + "name": "Cloud cover day 2" + }, + "cloud_cover_day_3d": { + "name": "Cloud cover day 3" + }, + "cloud_cover_day_4d": { + "name": "Cloud cover day 4" + }, + "cloud_cover_night_0d": { + "name": "Cloud cover tonight" + }, + "cloud_cover_night_1d": { + "name": "Cloud cover night 1" + }, + "cloud_cover_night_2d": { + "name": "Cloud cover night 2" + }, + "cloud_cover_night_3d": { + "name": "Cloud cover night 3" + }, + "cloud_cover_night_4d": { + "name": "Cloud cover night 4" + }, + "condition_day_0d": { + "name": "Condition today" + }, + "condition_day_1d": { + "name": "Condition day 1" + }, + "condition_day_2d": { + "name": "Condition day 2" + }, + "condition_day_3d": { + "name": "Condition day 3" + }, + "condition_day_4d": { + "name": "Condition day 4" + }, + "condition_night_0d": { + "name": "Condition tonight" + }, + "condition_night_1d": { + "name": "Condition night 1" + }, + "condition_night_2d": { + "name": "Condition night 2" + }, + "condition_night_3d": { + "name": "Condition night 3" + }, + "condition_night_4d": { + "name": "Condition night 4" + }, + "dew_point": { + "name": "Dew point" + }, + "grass_pollen_0d": { + "name": "Grass pollen today", "state_attributes": { "level": { "name": "Level", "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" } } } }, - "mold_pollen": { + "grass_pollen_1d": { + "name": "Grass pollen day 1", "state_attributes": { "level": { - "name": "Level", + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" } } } }, - "ragweed_pollen": { + "grass_pollen_2d": { + "name": "Grass pollen day 2", "state_attributes": { "level": { - "name": "Level", + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" } } } }, - "tree_pollen": { + "grass_pollen_3d": { + "name": "Grass pollen day 3", "state_attributes": { "level": { - "name": "Level", + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "grass_pollen_4d": { + "name": "Grass pollen day 4", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "hours_of_sun_0d": { + "name": "Hours of sun today" + }, + "hours_of_sun_1d": { + "name": "Hours of sun day 1" + }, + "hours_of_sun_2d": { + "name": "Hours of sun day 2" + }, + "hours_of_sun_3d": { + "name": "Hours of sun day 3" + }, + "hours_of_sun_4d": { + "name": "Hours of sun day 4" + }, + "mold_pollen_0d": { + "name": "Mold pollen today", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "mold_pollen_1d": { + "name": "Mold pollen day 1", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "mold_pollen_2d": { + "name": "Mold pollen day 2", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "mold_pollen_3d": { + "name": "Mold pollen day 3", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "mold_pollen_4d": { + "name": "Mold pollen day 4", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "precipitation": { + "name": "[%key:component::sensor::entity_component::precipitation::name%]" + }, + "pressure_tendency": { + "name": "Pressure tendency", + "state": { + "steady": "Steady", + "rising": "Rising", + "falling": "Falling" + } + }, + "ragweed_pollen_0d": { + "name": "Ragweed pollen today", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "ragweed_pollen_1d": { + "name": "Ragweed pollen day 1", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "ragweed_pollen_2d": { + "name": "Ragweed pollen day 2", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "ragweed_pollen_3d": { + "name": "Ragweed pollen day 3", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "ragweed_pollen_4d": { + "name": "Ragweed pollen day 4", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "realfeel_temperature": { + "name": "RealFeel temperature" + }, + "realfeel_temperature_max_0d": { + "name": "RealFeel temperature max today" + }, + "realfeel_temperature_max_1d": { + "name": "RealFeel temperature max day 1" + }, + "realfeel_temperature_max_2d": { + "name": "RealFeel temperature max day 2" + }, + "realfeel_temperature_max_3d": { + "name": "RealFeel temperature max day 3" + }, + "realfeel_temperature_max_4d": { + "name": "RealFeel temperature max day 4" + }, + "realfeel_temperature_min_0d": { + "name": "RealFeel temperature min today" + }, + "realfeel_temperature_min_1d": { + "name": "RealFeel temperature min day 1" + }, + "realfeel_temperature_min_2d": { + "name": "RealFeel temperature min day 2" + }, + "realfeel_temperature_min_3d": { + "name": "RealFeel temperature min day 3" + }, + "realfeel_temperature_min_4d": { + "name": "RealFeel temperature min day 4" + }, + "realfeel_temperature_shade": { + "name": "RealFeel temperature shade" + }, + "realfeel_temperature_shade_max_0d": { + "name": "RealFeel temperature shade max today" + }, + "realfeel_temperature_shade_max_1d": { + "name": "RealFeel temperature shade max day 1" + }, + "realfeel_temperature_shade_max_2d": { + "name": "RealFeel temperature shade max day 2" + }, + "realfeel_temperature_shade_max_3d": { + "name": "RealFeel temperature shade max day 3" + }, + "realfeel_temperature_shade_max_4d": { + "name": "RealFeel temperature shade max day 4" + }, + "realfeel_temperature_shade_min_0d": { + "name": "RealFeel temperature shade min today" + }, + "realfeel_temperature_shade_min_1d": { + "name": "RealFeel temperature shade min day 1" + }, + "realfeel_temperature_shade_min_2d": { + "name": "RealFeel temperature shade min day 2" + }, + "realfeel_temperature_shade_min_3d": { + "name": "RealFeel temperature shade min day 3" + }, + "realfeel_temperature_shade_min_4d": { + "name": "RealFeel temperature shade min day 4" + }, + "solar_irradiance_day_0d": { + "name": "Solar irradiance today" + }, + "solar_irradiance_day_1d": { + "name": "Solar irradiance day 1" + }, + "solar_irradiance_day_2d": { + "name": "Solar irradiance day 2" + }, + "solar_irradiance_day_3d": { + "name": "Solar irradiance day 3" + }, + "solar_irradiance_day_4d": { + "name": "Solar irradiance day 4" + }, + "solar_irradiance_night_0d": { + "name": "Solar irradiance tonight" + }, + "solar_irradiance_night_1d": { + "name": "Solar irradiance night 1" + }, + "solar_irradiance_night_2d": { + "name": "Solar irradiance night 2" + }, + "solar_irradiance_night_3d": { + "name": "Solar irradiance night 3" + }, + "solar_irradiance_night_4d": { + "name": "Solar irradiance night 4" + }, + "thunderstorm_probability_day_0d": { + "name": "Thunderstorm probability today" + }, + "thunderstorm_probability_day_1d": { + "name": "Thunderstorm probability day 1" + }, + "thunderstorm_probability_day_2d": { + "name": "Thunderstorm probability day 2" + }, + "thunderstorm_probability_day_3d": { + "name": "Thunderstorm probability day 3" + }, + "thunderstorm_probability_day_4d": { + "name": "Thunderstorm probability day 4" + }, + "thunderstorm_probability_night_0d": { + "name": "Thunderstorm probability tonight" + }, + "thunderstorm_probability_night_1d": { + "name": "Thunderstorm probability night 1" + }, + "thunderstorm_probability_night_2d": { + "name": "Thunderstorm probability night 2" + }, + "thunderstorm_probability_night_3d": { + "name": "Thunderstorm probability night 3" + }, + "thunderstorm_probability_night_4d": { + "name": "Thunderstorm probability night 4" + }, + "tree_pollen_0d": { + "name": "Tree pollen today", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "tree_pollen_1d": { + "name": "Tree pollen day 1", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "tree_pollen_2d": { + "name": "Tree pollen day 2", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "tree_pollen_3d": { + "name": "Tree pollen day 3", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "tree_pollen_4d": { + "name": "Tree pollen day 4", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" } } } }, "uv_index": { + "name": "UV index", "state_attributes": { "level": { - "name": "Level", + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", "state": { - "good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]", - "hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]", - "high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]", - "low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]", - "moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]", - "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]" + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" } } } + }, + "uv_index_0d": { + "name": "UV index today", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "uv_index_1d": { + "name": "UV index day 1", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "uv_index_2d": { + "name": "UV index day 2", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "uv_index_3d": { + "name": "UV index day 3", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "uv_index_4d": { + "name": "UV index day 4", + "state_attributes": { + "level": { + "name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]", + "state": { + "good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]", + "hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]", + "high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]", + "low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]", + "moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]", + "unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]" + } + } + } + }, + "wet_bulb_temperature": { + "name": "Wet bulb temperature" + }, + "wind_speed": { + "name": "[%key:component::weather::entity_component::_::state_attributes::wind_speed::name%]" + }, + "wind_chill_temperature": { + "name": "Wind chill temperature" + }, + "wind_gust_speed": { + "name": "[%key:component::weather::entity_component::_::state_attributes::wind_gust_speed::name%]" + }, + "wind_gust_speed_day_0d": { + "name": "Wind gust speed today" + }, + "wind_gust_speed_day_1d": { + "name": "Wind gust speed day 1" + }, + "wind_gust_speed_day_2d": { + "name": "Wind gust speed day 2" + }, + "wind_gust_speed_day_3d": { + "name": "Wind gust speed day 3" + }, + "wind_gust_speed_day_4d": { + "name": "Wind gust speed day 4" + }, + "wind_gust_speed_night_0d": { + "name": "Wind gust speed tonight" + }, + "wind_gust_speed_night_1d": { + "name": "Wind gust speed night 1" + }, + "wind_gust_speed_night_2d": { + "name": "Wind gust speed night 2" + }, + "wind_gust_speed_night_3d": { + "name": "Wind gust speed night 3" + }, + "wind_gust_speed_night_4d": { + "name": "Wind gust speed night 4" + }, + "wind_speed_day_0d": { + "name": "Wind speed today" + }, + "wind_speed_day_1d": { + "name": "Wind speed day 1" + }, + "wind_speed_day_2d": { + "name": "Wind speed day 2" + }, + "wind_speed_day_3d": { + "name": "Wind speed day 3" + }, + "wind_speed_day_4d": { + "name": "Wind speed day 4" + }, + "wind_speed_night_0d": { + "name": "Wind speed tonight" + }, + "wind_speed_night_1d": { + "name": "Wind speed night 1" + }, + "wind_speed_night_2d": { + "name": "Wind speed night 2" + }, + "wind_speed_night_3d": { + "name": "Wind speed night 3" + }, + "wind_speed_night_4d": { + "name": "Wind speed night 4" } } }, diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 35f86bdb039..a7a94894be4 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -192,7 +192,7 @@ async def test_sensor_without_forecast( assert entry assert entry.unique_id == "0123456-windchilltemperature" - state = hass.states.get("sensor.home_wind_gust") + state = hass.states.get("sensor.home_wind_gust_speed") assert state assert state.state == "20.3" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -204,11 +204,11 @@ async def test_sensor_without_forecast( assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - entry = registry.async_get("sensor.home_wind_gust") + entry = registry.async_get("sensor.home_wind_gust_speed") assert entry assert entry.unique_id == "0123456-windgust" - state = hass.states.get("sensor.home_wind") + state = hass.states.get("sensor.home_wind_speed") assert state assert state.state == "14.5" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -220,7 +220,7 @@ async def test_sensor_without_forecast( assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - entry = registry.async_get("sensor.home_wind") + entry = registry.async_get("sensor.home_wind_speed") assert entry assert entry.unique_id == "0123456-wind" @@ -232,7 +232,7 @@ async def test_sensor_with_forecast( await init_integration(hass, forecast=True) registry = er.async_get(hass) - state = hass.states.get("sensor.home_hours_of_sun_0d") + state = hass.states.get("sensor.home_hours_of_sun_today") assert state assert state.state == "7.2" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -240,11 +240,11 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.HOURS assert state.attributes.get(ATTR_STATE_CLASS) is None - entry = registry.async_get("sensor.home_hours_of_sun_0d") + entry = registry.async_get("sensor.home_hours_of_sun_today") assert entry assert entry.unique_id == "0123456-hoursofsun-0" - state = hass.states.get("sensor.home_realfeel_temperature_max_0d") + state = hass.states.get("sensor.home_realfeel_temperature_max_today") assert state assert state.state == "29.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -252,10 +252,10 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None - entry = registry.async_get("sensor.home_realfeel_temperature_max_0d") + entry = registry.async_get("sensor.home_realfeel_temperature_max_today") assert entry - state = hass.states.get("sensor.home_realfeel_temperature_min_0d") + state = hass.states.get("sensor.home_realfeel_temperature_min_today") assert state assert state.state == "15.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -263,11 +263,11 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None - entry = registry.async_get("sensor.home_realfeel_temperature_min_0d") + entry = registry.async_get("sensor.home_realfeel_temperature_min_today") assert entry assert entry.unique_id == "0123456-realfeeltemperaturemin-0" - state = hass.states.get("sensor.home_thunderstorm_probability_day_0d") + state = hass.states.get("sensor.home_thunderstorm_probability_today") assert state assert state.state == "40" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -275,11 +275,11 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_STATE_CLASS) is None - entry = registry.async_get("sensor.home_thunderstorm_probability_day_0d") + entry = registry.async_get("sensor.home_thunderstorm_probability_today") assert entry assert entry.unique_id == "0123456-thunderstormprobabilityday-0" - state = hass.states.get("sensor.home_thunderstorm_probability_night_0d") + state = hass.states.get("sensor.home_thunderstorm_probability_tonight") assert state assert state.state == "40" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -287,11 +287,11 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_STATE_CLASS) is None - entry = registry.async_get("sensor.home_thunderstorm_probability_night_0d") + entry = registry.async_get("sensor.home_thunderstorm_probability_tonight") assert entry assert entry.unique_id == "0123456-thunderstormprobabilitynight-0" - state = hass.states.get("sensor.home_uv_index_0d") + state = hass.states.get("sensor.home_uv_index_today") assert state assert state.state == "5" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -300,11 +300,11 @@ async def test_sensor_with_forecast( assert state.attributes.get("level") == "moderate" assert state.attributes.get(ATTR_STATE_CLASS) is None - entry = registry.async_get("sensor.home_uv_index_0d") + entry = registry.async_get("sensor.home_uv_index_today") assert entry assert entry.unique_id == "0123456-uvindex-0" - state = hass.states.get("sensor.home_air_quality_0d") + state = hass.states.get("sensor.home_air_quality_today") assert state assert state.state == "good" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -319,7 +319,7 @@ async def test_sensor_with_forecast( "unhealthy", ] - state = hass.states.get("sensor.home_cloud_cover_day_0d") + state = hass.states.get("sensor.home_cloud_cover_today") assert state assert state.state == "58" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -327,11 +327,11 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" assert state.attributes.get(ATTR_STATE_CLASS) is None - entry = registry.async_get("sensor.home_cloud_cover_day_0d") + entry = registry.async_get("sensor.home_cloud_cover_today") assert entry assert entry.unique_id == "0123456-cloudcoverday-0" - state = hass.states.get("sensor.home_cloud_cover_night_0d") + state = hass.states.get("sensor.home_cloud_cover_tonight") assert state assert state.state == "65" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -339,10 +339,10 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" assert state.attributes.get(ATTR_STATE_CLASS) is None - entry = registry.async_get("sensor.home_cloud_cover_night_0d") + entry = registry.async_get("sensor.home_cloud_cover_tonight") assert entry - state = hass.states.get("sensor.home_grass_pollen_0d") + state = hass.states.get("sensor.home_grass_pollen_today") assert state assert state.state == "0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -354,11 +354,11 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_ICON) == "mdi:grass" assert state.attributes.get(ATTR_STATE_CLASS) is None - entry = registry.async_get("sensor.home_grass_pollen_0d") + entry = registry.async_get("sensor.home_grass_pollen_today") assert entry assert entry.unique_id == "0123456-grass-0" - state = hass.states.get("sensor.home_mold_pollen_0d") + state = hass.states.get("sensor.home_mold_pollen_today") assert state assert state.state == "0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -369,11 +369,11 @@ async def test_sensor_with_forecast( assert state.attributes.get("level") == "low" assert state.attributes.get(ATTR_ICON) == "mdi:blur" - entry = registry.async_get("sensor.home_mold_pollen_0d") + entry = registry.async_get("sensor.home_mold_pollen_today") assert entry assert entry.unique_id == "0123456-mold-0" - state = hass.states.get("sensor.home_ragweed_pollen_0d") + state = hass.states.get("sensor.home_ragweed_pollen_today") assert state assert state.state == "0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -384,11 +384,11 @@ async def test_sensor_with_forecast( assert state.attributes.get("level") == "low" assert state.attributes.get(ATTR_ICON) == "mdi:sprout" - entry = registry.async_get("sensor.home_ragweed_pollen_0d") + entry = registry.async_get("sensor.home_ragweed_pollen_today") assert entry assert entry.unique_id == "0123456-ragweed-0" - state = hass.states.get("sensor.home_realfeel_temperature_shade_max_0d") + state = hass.states.get("sensor.home_realfeel_temperature_shade_max_today") assert state assert state.state == "28.0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -396,22 +396,22 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None - entry = registry.async_get("sensor.home_realfeel_temperature_shade_max_0d") + entry = registry.async_get("sensor.home_realfeel_temperature_shade_max_today") assert entry assert entry.unique_id == "0123456-realfeeltemperatureshademax-0" - state = hass.states.get("sensor.home_realfeel_temperature_shade_min_0d") + state = hass.states.get("sensor.home_realfeel_temperature_shade_min_today") assert state assert state.state == "15.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - entry = registry.async_get("sensor.home_realfeel_temperature_shade_min_0d") + entry = registry.async_get("sensor.home_realfeel_temperature_shade_min_today") assert entry assert entry.unique_id == "0123456-realfeeltemperatureshademin-0" - state = hass.states.get("sensor.home_tree_pollen_0d") + state = hass.states.get("sensor.home_tree_pollen_today") assert state assert state.state == "0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -423,11 +423,11 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_ICON) == "mdi:tree-outline" assert state.attributes.get(ATTR_STATE_CLASS) is None - entry = registry.async_get("sensor.home_tree_pollen_0d") + entry = registry.async_get("sensor.home_tree_pollen_today") assert entry assert entry.unique_id == "0123456-tree-0" - state = hass.states.get("sensor.home_wind_day_0d") + state = hass.states.get("sensor.home_wind_speed_today") assert state assert state.state == "13.0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -439,11 +439,11 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - entry = registry.async_get("sensor.home_wind_day_0d") + entry = registry.async_get("sensor.home_wind_speed_today") assert entry assert entry.unique_id == "0123456-windday-0" - state = hass.states.get("sensor.home_wind_night_0d") + state = hass.states.get("sensor.home_wind_speed_tonight") assert state assert state.state == "7.4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -456,11 +456,11 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - entry = registry.async_get("sensor.home_wind_night_0d") + entry = registry.async_get("sensor.home_wind_speed_tonight") assert entry assert entry.unique_id == "0123456-windnight-0" - state = hass.states.get("sensor.home_wind_gust_day_0d") + state = hass.states.get("sensor.home_wind_gust_speed_today") assert state assert state.state == "29.6" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -473,11 +473,11 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - entry = registry.async_get("sensor.home_wind_gust_day_0d") + entry = registry.async_get("sensor.home_wind_gust_speed_today") assert entry assert entry.unique_id == "0123456-windgustday-0" - state = hass.states.get("sensor.home_wind_gust_night_0d") + state = hass.states.get("sensor.home_wind_gust_speed_tonight") assert state assert state.state == "18.5" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -490,15 +490,15 @@ async def test_sensor_with_forecast( assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED - entry = registry.async_get("sensor.home_wind_gust_night_0d") + entry = registry.async_get("sensor.home_wind_gust_speed_tonight") assert entry assert entry.unique_id == "0123456-windgustnight-0" - entry = registry.async_get("sensor.home_air_quality_0d") + entry = registry.async_get("sensor.home_air_quality_today") assert entry assert entry.unique_id == "0123456-airquality-0" - state = hass.states.get("sensor.home_solar_irradiance_day_0d") + state = hass.states.get("sensor.home_solar_irradiance_today") assert state assert state.state == "7447.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -508,11 +508,11 @@ async def test_sensor_with_forecast( == UnitOfIrradiance.WATTS_PER_SQUARE_METER ) - entry = registry.async_get("sensor.home_solar_irradiance_day_0d") + entry = registry.async_get("sensor.home_solar_irradiance_today") assert entry assert entry.unique_id == "0123456-solarirradianceday-0" - state = hass.states.get("sensor.home_solar_irradiance_night_0d") + state = hass.states.get("sensor.home_solar_irradiance_tonight") assert state assert state.state == "271.6" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION @@ -522,11 +522,11 @@ async def test_sensor_with_forecast( == UnitOfIrradiance.WATTS_PER_SQUARE_METER ) - entry = registry.async_get("sensor.home_solar_irradiance_night_0d") + entry = registry.async_get("sensor.home_solar_irradiance_tonight") assert entry assert entry.unique_id == "0123456-solarirradiancenight-0" - state = hass.states.get("sensor.home_condition_day_0d") + state = hass.states.get("sensor.home_condition_today") assert state assert ( state.state @@ -534,16 +534,16 @@ async def test_sensor_with_forecast( ) assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - entry = registry.async_get("sensor.home_condition_day_0d") + entry = registry.async_get("sensor.home_condition_today") assert entry assert entry.unique_id == "0123456-longphraseday-0" - state = hass.states.get("sensor.home_condition_night_0d") + state = hass.states.get("sensor.home_condition_tonight") assert state assert state.state == "Partly cloudy" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - entry = registry.async_get("sensor.home_condition_night_0d") + entry = registry.async_get("sensor.home_condition_tonight") assert entry assert entry.unique_id == "0123456-longphrasenight-0" @@ -629,7 +629,7 @@ async def test_sensor_imperial_units(hass: HomeAssistant) -> None: assert state.state == "10498.687664042" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.FEET - state = hass.states.get("sensor.home_wind") + state = hass.states.get("sensor.home_wind_speed") assert state assert state.state == "9.0" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfSpeed.MILES_PER_HOUR From 5e3bcc1224754e816bfe38713db67ec0b4af5d98 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 30 Jul 2023 15:30:13 +0200 Subject: [PATCH 0065/1151] Update zigpy to 0.56.3 (#97480) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 7694a85b8ed..874990402fc 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -25,10 +25,10 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.101", "zigpy-deconz==0.21.0", - "zigpy==0.56.2", + "zigpy==0.56.3", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", - "zigpy-znp==0.11.3" + "zigpy-znp==0.11.4" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index e5e4d16ab87..9301f6b8670 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2776,10 +2776,10 @@ zigpy-xbee==0.18.1 zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.3 +zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.56.2 +zigpy==0.56.3 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa61a3417b2..e90478359c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2043,10 +2043,10 @@ zigpy-xbee==0.18.1 zigpy-zigate==0.11.0 # homeassistant.components.zha -zigpy-znp==0.11.3 +zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.56.2 +zigpy==0.56.3 # homeassistant.components.zwave_js zwave-js-server-python==0.49.0 From afdbbefc3198050c37602f82399d0516474be978 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jul 2023 09:28:45 -0700 Subject: [PATCH 0066/1151] Revert using has_entity_name in ESPHome when `friendly_name` is not set (#97488) --- homeassistant/components/esphome/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 6b0a4cd6b26..b308d8dc08c 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -140,7 +140,6 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" _attr_should_poll = False - _attr_has_entity_name = True _static_info: _InfoT _state: _StateT _has_state: bool @@ -169,6 +168,7 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)} ) self._entry_id = entry_data.entry_id + self._attr_has_entity_name = bool(device_info.friendly_name) async def async_added_to_hass(self) -> None: """Register callbacks.""" From 84576672deb4903680fef98601865836a47a7f54 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jul 2023 09:39:40 -0700 Subject: [PATCH 0067/1151] Bump dbus-fast to 1.87.5 (#97364) --- 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 cbeab2abec0..bc07e2b94ae 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.6.1", - "dbus-fast==1.87.2" + "dbus-fast==1.87.5" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a0046569eb8..be1dff7623d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.6.1 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.2 -dbus-fast==1.87.2 +dbus-fast==1.87.5 fnv-hash-fast==0.4.0 ha-av==10.1.0 hass-nabucasa==0.69.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9301f6b8670..ce9f6d1bddd 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.87.2 +dbus-fast==1.87.5 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e90478359c4..4e9a7e76261 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.87.2 +dbus-fast==1.87.5 # homeassistant.components.debugpy debugpy==1.6.7 From 68cb7a7ddeafaa93318c5d04555f6757e3924c56 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 30 Jul 2023 18:40:38 +0200 Subject: [PATCH 0068/1151] Update ha-av to 10.1.1 (#97481) --- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index ea02bfedefb..a89ee370920 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "iot_class": "local_push", - "requirements": ["ha-av==10.1.0", "Pillow==10.0.0"] + "requirements": ["ha-av==10.1.1", "Pillow==10.0.0"] } diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index c07a083ac52..96474ceb7eb 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["PyTurboJPEG==1.7.1", "ha-av==10.1.0", "numpy==1.23.2"] + "requirements": ["PyTurboJPEG==1.7.1", "ha-av==10.1.1", "numpy==1.23.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index be1dff7623d..04c0b0fd44f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ ciso8601==2.3.0 cryptography==41.0.2 dbus-fast==1.87.5 fnv-hash-fast==0.4.0 -ha-av==10.1.0 +ha-av==10.1.1 hass-nabucasa==0.69.0 hassil==1.2.5 home-assistant-bluetooth==1.10.2 diff --git a/requirements_all.txt b/requirements_all.txt index ce9f6d1bddd..054188e1585 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -937,7 +937,7 @@ h2==4.1.0 # homeassistant.components.generic # homeassistant.components.stream -ha-av==10.1.0 +ha-av==10.1.1 # homeassistant.components.ffmpeg ha-ffmpeg==3.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e9a7e76261..4544b33e3b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -735,7 +735,7 @@ h2==4.1.0 # homeassistant.components.generic # homeassistant.components.stream -ha-av==10.1.0 +ha-av==10.1.1 # homeassistant.components.ffmpeg ha-ffmpeg==3.1.0 From 28dc8192124cfdbfb840077e07fcf96c7b2a57e5 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 30 Jul 2023 09:41:14 -0700 Subject: [PATCH 0069/1151] Bump opower to 0.0.16 (#97437) --- 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 08f25d20eff..c054af8f43c 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.15"] + "requirements": ["opower==0.0.16"] } diff --git a/requirements_all.txt b/requirements_all.txt index 054188e1585..99e3fc3c744 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1368,7 +1368,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.15 +opower==0.0.16 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4544b33e3b4..6e632f0806a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1037,7 +1037,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.15 +opower==0.0.16 # homeassistant.components.oralb oralb-ble==0.17.6 From 9d2f52dbc5dc353067e0e277dab3db2327853639 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Jul 2023 11:42:28 -0500 Subject: [PATCH 0070/1151] Allow deleting config entry devices in jellyfin (#97377) --- homeassistant/components/jellyfin/__init__.py | 16 +++++ .../components/jellyfin/coordinator.py | 3 + tests/components/jellyfin/test_init.py | 60 +++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index 4ee97020724..f25c3410edb 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -4,6 +4,7 @@ from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, LOGGER, PLATFORMS @@ -60,3 +61,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return True + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove device from a config entry.""" + data = hass.data[DOMAIN][config_entry.entry_id] + coordinator = data.coordinators["sessions"] + + return not device_entry.identifiers.intersection( + ( + (DOMAIN, coordinator.server_id), + *((DOMAIN, id) for id in coordinator.device_ids), + ) + ) diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py index 3d5b150f39f..f4ab98ca268 100644 --- a/homeassistant/components/jellyfin/coordinator.py +++ b/homeassistant/components/jellyfin/coordinator.py @@ -47,6 +47,7 @@ class JellyfinDataUpdateCoordinator(DataUpdateCoordinator[JellyfinDataT], ABC): self.user_id: str = user_id self.session_ids: set[str] = set() + self.device_ids: set[str] = set() async def _async_update_data(self) -> JellyfinDataT: """Get the latest data from Jellyfin.""" @@ -75,4 +76,6 @@ class SessionsDataUpdateCoordinator( and session["Client"] != USER_APP_NAME } + self.device_ids = {session["DeviceId"] for session in sessions_by_id.values()} + return sessions_by_id diff --git a/tests/components/jellyfin/test_init.py b/tests/components/jellyfin/test_init.py index 56e352bd71f..9af73391d18 100644 --- a/tests/components/jellyfin/test_init.py +++ b/tests/components/jellyfin/test_init.py @@ -4,10 +4,29 @@ from unittest.mock import MagicMock from homeassistant.components.jellyfin.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from . import async_load_json_fixture from tests.common import MockConfigEntry +from tests.typing import MockHAClientWebSocket, WebSocketGenerator + + +async def remove_device( + ws_client: MockHAClientWebSocket, device_id: str, config_entry_id: str +) -> bool: + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 1, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] async def test_config_entry_not_ready( @@ -66,3 +85,44 @@ async def test_load_unload_config_entry( await hass.async_block_till_done() assert mock_config_entry.entry_id not in hass.data[DOMAIN] assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_device_remove_devices( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_config_entry: MockConfigEntry, + mock_jellyfin: MagicMock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test we can only remove a device that no longer exists.""" + assert await async_setup_component(hass, "config", {}) + + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + device_entry = device_registry.async_get_device( + identifiers={ + ( + DOMAIN, + "DEVICE-UUID", + ) + }, + ) + assert ( + await remove_device( + await hass_ws_client(hass), device_entry.id, mock_config_entry.entry_id + ) + is False + ) + old_device_entry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, "OLD-DEVICE-UUID")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), old_device_entry.id, mock_config_entry.entry_id + ) + is True + ) From 2b4387f7c2fd2fccf295fe7149d2a08433da6a43 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 30 Jul 2023 18:43:42 +0200 Subject: [PATCH 0071/1151] Return the actual media url from media extractor (#97408) --- homeassistant/components/media_extractor/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index a35650f0092..d00f1b33ccc 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -127,7 +127,12 @@ class MediaExtractor: _LOGGER.error("Could not extract stream for the query: %s", query) raise MEQueryException() from err - return requested_stream["webpage_url"] + if "formats" in requested_stream: + best_stream = requested_stream["formats"][ + len(requested_stream["formats"]) - 1 + ] + return best_stream["url"] + return requested_stream["url"] return stream_selector From 1f11ce63fcc84988cb934ffe524185b95ae20993 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 30 Jul 2023 18:47:34 +0200 Subject: [PATCH 0072/1151] Manual trigger entity fix name influence entity_id (#97398) --- homeassistant/components/scrape/sensor.py | 1 - homeassistant/components/sql/sensor.py | 9 ++++----- homeassistant/helpers/template_entity.py | 5 +++++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index a68083856f7..cc4cd269606 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -157,7 +157,6 @@ class ScrapeSensor( """Initialize a web scrape sensor.""" CoordinatorEntity.__init__(self, coordinator) ManualTriggerEntity.__init__(self, hass, trigger_entity_config) - self._attr_name = trigger_entity_config[CONF_NAME].template self._attr_native_unit_of_measurement = unit_of_measurement self._attr_state_class = state_class self._select = select diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 0c8e90b8895..aecc34d7009 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -344,16 +344,15 @@ class SQLSensor(ManualTriggerEntity, SensorEntity): self._attr_extra_state_attributes = {} self._use_database_executor = use_database_executor self._lambda_stmt = _generate_lambda_stmt(query) - self._attr_name = ( - None if not yaml else trigger_entity_config[CONF_NAME].template - ) - self._attr_has_entity_name = not yaml + if not yaml: + self._attr_name = None + self._attr_has_entity_name = True if not yaml and trigger_entity_config.get(CONF_UNIQUE_ID): self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, trigger_entity_config[CONF_UNIQUE_ID])}, manufacturer="SQL", - name=trigger_entity_config[CONF_NAME].template, + name=self.name, ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index b7be7c2c9a6..2e5cebf8571 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -626,6 +626,11 @@ class ManualTriggerEntity(TriggerBaseEntity): ) -> None: """Initialize the entity.""" TriggerBaseEntity.__init__(self, hass, config) + # Need initial rendering on `name` as it influence the `entity_id` + self._rendered[CONF_NAME] = config[CONF_NAME].async_render( + {}, + parse_result=CONF_NAME in self._parse_result, + ) @callback def _process_manual_data(self, value: Any | None = None) -> None: From c32b15c7545e1f70e2f00614993f4e7c4573bae2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 30 Jul 2023 18:49:00 +0200 Subject: [PATCH 0073/1151] Reolink long poll recover (#97465) --- homeassistant/components/reolink/host.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 81fbda63fef..df03095b7f9 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -267,7 +267,19 @@ class ReolinkHost: async def _async_start_long_polling(self): """Start ONVIF long polling task.""" if self._long_poll_task is None: - await self._api.subscribe(sub_type=SubType.long_poll) + try: + await self._api.subscribe(sub_type=SubType.long_poll) + except ReolinkError as err: + # make sure the long_poll_task is always created to try again later + if not self._lost_subscription: + self._lost_subscription = True + _LOGGER.error( + "Reolink %s event long polling subscription lost: %s", + self._api.nvr_name, + str(err), + ) + else: + self._lost_subscription = False self._long_poll_task = asyncio.create_task(self._async_long_polling()) async def _async_stop_long_polling(self): @@ -319,7 +331,13 @@ class ReolinkHost: try: await self._renew(SubType.push) if self._long_poll_task is not None: - await self._renew(SubType.long_poll) + if not self._api.subscribed(SubType.long_poll): + _LOGGER.debug("restarting long polling task") + # To prevent 5 minute request timeout + await self._async_stop_long_polling() + await self._async_start_long_polling() + else: + await self._renew(SubType.long_poll) except SubscriptionError as err: if not self._lost_subscription: self._lost_subscription = True From f4e79bbab82386a123bd12b89de473744b61641c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 30 Jul 2023 18:49:27 +0200 Subject: [PATCH 0074/1151] Regard long poll without events as valid (#97383) --- homeassistant/components/reolink/host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index df03095b7f9..5882e5e66a4 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -475,7 +475,7 @@ class ReolinkHost: self._long_poll_error = False - if not self._long_poll_received and channels != []: + if not self._long_poll_received: self._long_poll_received = True ir.async_delete_issue(self._hass, DOMAIN, "webhook_url") From a70c10d3c9080ec3270ea225a84a3e3d8ebce88c Mon Sep 17 00:00:00 2001 From: Niels Perfors Date: Sun, 30 Jul 2023 18:53:26 +0200 Subject: [PATCH 0075/1151] Upgrade Verisure to 2.6.4 (#97278) --- homeassistant/components/verisure/coordinator.py | 16 +++------------- homeassistant/components/verisure/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index 47fbde3ef20..bc3b68922b0 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -7,7 +7,6 @@ from time import sleep from verisure import ( Error as VerisureError, LoginError as VerisureLoginError, - ResponseError as VerisureResponseError, Session as Verisure, ) @@ -50,7 +49,7 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): except VerisureLoginError as ex: LOGGER.error("Could not log in to verisure, %s", ex) raise ConfigEntryAuthFailed("Credentials expired for Verisure") from ex - except VerisureResponseError as ex: + except VerisureError as ex: LOGGER.error("Could not log in to verisure, %s", ex) return False @@ -65,11 +64,9 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): try: await self.hass.async_add_executor_job(self.verisure.update_cookie) except VerisureLoginError as ex: - LOGGER.error("Credentials expired for Verisure, %s", ex) raise ConfigEntryAuthFailed("Credentials expired for Verisure") from ex - except VerisureResponseError as ex: - LOGGER.error("Could not log in to verisure, %s", ex) - raise ConfigEntryAuthFailed("Could not log in to verisure") from ex + except VerisureError as ex: + raise UpdateFailed("Unable to update cookie") from ex try: overview = await self.hass.async_add_executor_job( self.verisure.request, @@ -81,13 +78,6 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): self.verisure.smart_lock(), self.verisure.smartplugs(), ) - except VerisureResponseError as err: - LOGGER.debug("Cookie expired or service unavailable, %s", err) - overview = self._overview - try: - await self.hass.async_add_executor_job(self.verisure.update_cookie) - except VerisureResponseError as ex: - raise ConfigEntryAuthFailed("Credentials for Verisure expired.") from ex except VerisureError as err: LOGGER.error("Could not read overview, %s", err) raise UpdateFailed("Could not read overview") from err diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 66dccdc07de..98440f67e4c 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["verisure"], - "requirements": ["vsure==2.6.1"] + "requirements": ["vsure==2.6.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 99e3fc3c744..40e02707438 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2637,7 +2637,7 @@ volkszaehler==0.4.0 volvooncall==0.10.3 # homeassistant.components.verisure -vsure==2.6.1 +vsure==2.6.4 # homeassistant.components.vasttrafik vtjp==0.1.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e632f0806a..18453a03abe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1937,7 +1937,7 @@ voip-utils==0.1.0 volvooncall==0.10.3 # homeassistant.components.verisure -vsure==2.6.1 +vsure==2.6.4 # homeassistant.components.vulcan vulcan-api==2.3.0 From 71cc227f091f7c6e98c847c0a946425a191036a0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 30 Jul 2023 18:54:45 +0200 Subject: [PATCH 0076/1151] Update aiopvpc to 4.2.2 (#97482) --- homeassistant/components/pvpc_hourly_pricing/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 64e6e19086f..8db978135f6 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["aiopvpc"], "quality_scale": "platinum", - "requirements": ["aiopvpc==4.1.0"] + "requirements": ["aiopvpc==4.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 40e02707438..7a7dd204315 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -316,7 +316,7 @@ aiopurpleair==2022.12.1 aiopvapi==2.0.4 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==4.1.0 +aiopvpc==4.2.2 # homeassistant.components.lidarr # homeassistant.components.radarr diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18453a03abe..b8564a77338 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -291,7 +291,7 @@ aiopurpleair==2022.12.1 aiopvapi==2.0.4 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==4.1.0 +aiopvpc==4.2.2 # homeassistant.components.lidarr # homeassistant.components.radarr From 1553ff1001a63bb760a748c0e13a3c4255152e72 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 30 Jul 2023 18:55:13 +0200 Subject: [PATCH 0077/1151] Update pydantic to 1.10.12 (#97479) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index bf71ed4d255..79a26736b2b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.2 mock-open==1.4.0 mypy==1.4.1 pre-commit==3.3.3 -pydantic==1.10.11 +pydantic==1.10.12 pylint==2.17.4 pylint-per-file-ignores==1.2.1 pipdeptree==2.11.0 From cb033f7a7b837053351e9db99432fb0fd9037c82 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sun, 30 Jul 2023 19:02:43 +0200 Subject: [PATCH 0078/1151] Change IoT class for ToD to calculated (#97422) --- homeassistant/components/tod/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tod/manifest.json b/homeassistant/components/tod/manifest.json index a38531e8883..3d82c387ab7 100644 --- a/homeassistant/components/tod/manifest.json +++ b/homeassistant/components/tod/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tod", "integration_type": "helper", - "iot_class": "local_push", + "iot_class": "calculated", "quality_scale": "internal" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index aa3ad84f192..40775163374 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6696,7 +6696,7 @@ "tod": { "integration_type": "helper", "config_flow": true, - "iot_class": "local_push" + "iot_class": "calculated" }, "utility_meter": { "integration_type": "helper", From 15b7035ad0332be47a682d6a4b58b3f20fa02baf Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 30 Jul 2023 19:03:25 +0200 Subject: [PATCH 0079/1151] Change IoT class for Moon to calculated (#97405) --- homeassistant/components/moon/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 5da6a6b3359..6102b37fb13 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/moon", "integration_type": "service", - "iot_class": "local_polling", + "iot_class": "calculated", "quality_scale": "internal" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 40775163374..6e16752ca49 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3485,7 +3485,7 @@ "moon": { "integration_type": "service", "config_flow": true, - "iot_class": "local_polling" + "iot_class": "calculated" }, "mopeka": { "name": "Mopeka", From 04e72fec5722155bee55f019097cb84ae1bc4447 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 30 Jul 2023 19:10:45 +0200 Subject: [PATCH 0080/1151] Add entity translation to Moon (#97404) --- homeassistant/components/moon/sensor.py | 1 - homeassistant/components/moon/strings.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 10251fc679d..56b5fa52325 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -47,7 +47,6 @@ class MoonSensorEntity(SensorEntity): """Representation of a Moon sensor.""" _attr_has_entity_name = True - _attr_name = "Phase" _attr_device_class = SensorDeviceClass.ENUM _attr_options = [ STATE_NEW_MOON, diff --git a/homeassistant/components/moon/strings.json b/homeassistant/components/moon/strings.json index 1210fb6403e..d4d59f83674 100644 --- a/homeassistant/components/moon/strings.json +++ b/homeassistant/components/moon/strings.json @@ -13,6 +13,7 @@ "entity": { "sensor": { "phase": { + "name": "Phase", "state": { "first_quarter": "First quarter", "full_moon": "Full moon", From a4b2ded503069e2fa17b7017a9db10035d63dd8c Mon Sep 17 00:00:00 2001 From: Meow Date: Sun, 30 Jul 2023 19:27:30 +0200 Subject: [PATCH 0081/1151] Refactor deprecated RESULT_TYPE_* (#97367) --- tests/components/airzone/test_config_flow.py | 29 +++--- tests/components/bsblan/test_config_flow.py | 17 ++-- .../devolo_home_network/test_config_flow.py | 6 +- tests/components/dlna_dmr/test_config_flow.py | 89 ++++++++++--------- .../dremel_3d_printer/test_config_flow.py | 16 ++-- tests/components/generic/test_config_flow.py | 75 ++++++++-------- .../geo_json_events/test_config_flow.py | 15 ++-- tests/components/lidarr/test_config_flow.py | 22 ++--- .../components/pushbullet/test_config_flow.py | 13 +-- tests/components/radarr/test_config_flow.py | 26 +++--- tests/components/renson/test_config_flow.py | 10 +-- tests/components/roborock/test_config_flow.py | 25 +++--- tests/components/tautulli/test_config_flow.py | 32 +++---- tests/components/upcloud/test_config_flow.py | 17 ++-- tests/components/wemo/test_config_flow.py | 10 +-- 15 files changed, 202 insertions(+), 200 deletions(-) diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index 5460272e74e..d703a232c7b 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -10,13 +10,14 @@ from aioairzone.exceptions import ( SystemOutOfRange, ) -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.airzone.config_flow import short_mac from homeassistant.components.airzone.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .util import CONFIG, CONFIG_ID1, HVAC_MOCK, HVAC_VERSION_MOCK, HVAC_WEBSERVER_MOCK @@ -56,7 +57,7 @@ async def test_form(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" assert result["errors"] == {} @@ -70,7 +71,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {CONFIG[CONF_HOST]}:{CONFIG[CONF_PORT]}" assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] assert result["data"][CONF_PORT] == CONFIG[CONF_PORT] @@ -102,7 +103,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_ID: "invalid_system_id"} @@ -113,7 +114,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: result["flow_id"], CONFIG_ID1 ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -121,7 +122,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert ( result["title"] == f"Airzone {CONFIG_ID1[CONF_HOST]}:{CONFIG_ID1[CONF_PORT]}" @@ -178,7 +179,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -204,7 +205,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_HOST: TEST_IP, CONF_PORT: TEST_PORT, @@ -226,7 +227,7 @@ async def test_dhcp_flow_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -243,7 +244,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -288,7 +289,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {short_mac(HVAC_WEBSERVER_MOCK[API_MAC])}" assert result["data"][CONF_HOST] == TEST_IP assert result["data"][CONF_PORT] == TEST_PORT @@ -309,7 +310,7 @@ async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -335,7 +336,7 @@ async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {CONF_ID: "invalid_system_id"} @@ -356,7 +357,7 @@ async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {short_mac(DHCP_SERVICE_INFO.macaddress)}" assert result["data"][CONF_HOST] == TEST_IP assert result["data"][CONF_PORT] == TEST_PORT diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index bcd6dec14b1..dce881f2f7d 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -3,17 +3,12 @@ from unittest.mock import AsyncMock, MagicMock from bsblan import BSBLANConnectionError -from homeassistant import data_entry_flow from homeassistant.components.bsblan import config_flow from homeassistant.components.bsblan.const import CONF_PASSKEY, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.device_registry import format_mac from tests.common import MockConfigEntry @@ -30,7 +25,7 @@ async def test_full_user_flow_implementation( context={"source": SOURCE_USER}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( @@ -44,7 +39,7 @@ async def test_full_user_flow_implementation( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == format_mac("00:80:41:19:69:90") assert result2.get("data") == { CONF_HOST: "127.0.0.1", @@ -68,7 +63,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_connection_error( @@ -90,7 +85,7 @@ async def test_connection_error( }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert result.get("step_id") == "user" @@ -114,5 +109,5 @@ async def test_user_device_exists_abort( }, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 91d7d6f39cf..9050181cc8f 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -7,7 +7,7 @@ from unittest.mock import patch from devolo_plc_api.exceptions.device import DeviceNotFound import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.devolo_home_network import config_flow from homeassistant.components.devolo_home_network.const import ( DOMAIN, @@ -191,7 +191,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.devolo_home_network.async_setup_entry", @@ -203,7 +203,7 @@ async def test_form_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 43e60638ba9..be49a6ca257 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -10,7 +10,7 @@ from async_upnp_client.client import UpnpDevice from async_upnp_client.exceptions import UpnpError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.dlna_dmr.const import ( CONF_BROWSE_UNFILTERED, @@ -21,6 +21,7 @@ from homeassistant.components.dlna_dmr.const import ( ) from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_MAC, CONF_TYPE, CONF_URL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import ( MOCK_DEVICE_HOST_ADDR, @@ -101,7 +102,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -109,7 +110,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -136,7 +137,7 @@ async def test_user_flow_discovered_manual( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -144,7 +145,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -152,7 +153,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -177,7 +178,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -185,7 +186,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) result["flow_id"], user_input={CONF_HOST: MOCK_DEVICE_NAME} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -208,7 +209,7 @@ async def test_user_flow_uncontactable( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -216,7 +217,7 @@ async def test_user_flow_uncontactable( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "manual" @@ -241,7 +242,7 @@ async def test_user_flow_embedded_st( result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -249,7 +250,7 @@ async def test_user_flow_embedded_st( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -272,7 +273,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -280,7 +281,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "not_dmr"} assert result["step_id"] == "manual" @@ -295,7 +296,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -303,7 +304,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -327,7 +328,7 @@ async def test_ssdp_flow_unavailable( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError @@ -337,7 +338,7 @@ async def test_ssdp_flow_unavailable( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -368,7 +369,7 @@ async def test_ssdp_flow_existing( }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -388,7 +389,7 @@ async def test_ssdp_flow_duplicate_location( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION @@ -414,7 +415,7 @@ async def test_ssdp_duplicate_mac_ignored_entry( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -437,7 +438,7 @@ async def test_ssdp_duplicate_mac_configured_entry( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -453,7 +454,7 @@ async def test_ssdp_add_mac( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -474,7 +475,7 @@ async def test_ssdp_dont_remove_mac( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -502,7 +503,7 @@ async def test_ssdp_flow_upnp_udn( }, ), ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -518,7 +519,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_dmr" # Service list does not contain services @@ -530,7 +531,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_dmr" # AVTransport service is missing @@ -546,7 +547,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -568,7 +569,7 @@ async def test_ssdp_single_service(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -582,7 +583,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "alternative_integration" discovery = dataclasses.replace(MOCK_DISCOVERY) @@ -595,7 +596,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "alternative_integration" for manufacturer, model in [ @@ -613,7 +614,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "alternative_integration" @@ -635,7 +636,7 @@ async def test_ignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -659,7 +660,7 @@ async def test_ignore_flow_no_ssdp( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: None, @@ -680,7 +681,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME # Device was found via SSDP, matching the 2nd device type tried @@ -698,7 +699,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -706,7 +707,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -733,7 +734,7 @@ async def test_unignore_flow_offline( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME # Device is not in the SSDP discoveries (perhaps HA restarted between ignore and unignore) @@ -745,7 +746,7 @@ async def test_unignore_flow_offline( context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "discovery_error" @@ -759,7 +760,7 @@ async def test_get_mac_address_ipv4( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" mock_get_mac_address.assert_called_once_with(ip=MOCK_DEVICE_HOST_ADDR) @@ -783,7 +784,7 @@ async def test_get_mac_address_ipv6( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" # The scope must be removed for get_mac_address to work correctly @@ -824,7 +825,7 @@ async def test_options_flow( config_entry_mock.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry_mock.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -838,7 +839,7 @@ async def test_options_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_url"} @@ -853,7 +854,7 @@ async def test_options_flow( }, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_LISTEN_PORT: 2222, CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", diff --git a/tests/components/dremel_3d_printer/test_config_flow.py b/tests/components/dremel_3d_printer/test_config_flow.py index 8161662a14a..e968e0af491 100644 --- a/tests/components/dremel_3d_printer/test_config_flow.py +++ b/tests/components/dremel_3d_printer/test_config_flow.py @@ -3,11 +3,11 @@ from unittest.mock import patch from requests.exceptions import ConnectTimeout -from homeassistant import data_entry_flow from homeassistant.components.dremel_3d_printer.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import CONF_DATA, patch_async_setup_entry @@ -22,7 +22,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant, connection) -> DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch_async_setup_entry(): @@ -30,7 +30,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant, connection) -> result["flow_id"], user_input=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "DREMEL 3D45" assert result["data"] == CONF_DATA @@ -42,7 +42,7 @@ async def test_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -53,7 +53,7 @@ async def test_cannot_connect(hass: HomeAssistant, connection) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -62,7 +62,7 @@ async def test_cannot_connect(hass: HomeAssistant, connection) -> None: result["flow_id"], user_input=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == CONF_DATA @@ -73,7 +73,7 @@ async def test_unknown_error(hass: HomeAssistant, connection) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -82,6 +82,6 @@ async def test_unknown_error(hass: HomeAssistant, connection) -> None: result["flow_id"], user_input=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "DREMEL 3D45" assert result["data"] == CONF_DATA diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 54a9c5c0796..db9787fb283 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -9,7 +9,7 @@ import httpx import pytest import respx -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.camera import async_get_image from homeassistant.components.generic.config_flow import slug from homeassistant.components.generic.const import ( @@ -35,6 +35,7 @@ from homeassistant.const import ( HTTP_BASIC_AUTHENTICATION, ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er, issue_registry as ir from tests.common import MockConfigEntry @@ -79,7 +80,7 @@ async def test_form( user_flow["flow_id"], TESTDATA, ) - assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["type"] == FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" client = await hass_client() preview_id = result1["flow_id"] @@ -92,7 +93,7 @@ async def test_form( user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -133,13 +134,13 @@ async def test_form_only_stillimage( data, ) await hass.async_block_till_done() - assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["type"] == FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -166,13 +167,13 @@ async def test_form_reject_still_preview( user_flow["flow_id"], TESTDATA, ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: False}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" @@ -193,7 +194,7 @@ async def test_form_still_preview_cam_off( user_flow["flow_id"], TESTDATA, ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" preview_id = result1["flow_id"] # Try to view the image, should be unavailable. @@ -214,14 +215,14 @@ async def test_form_only_stillimage_gif( user_flow["flow_id"], data, ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["options"][CONF_CONTENT_TYPE] == "image/gif" @@ -239,14 +240,14 @@ async def test_form_only_svg_whitespace( user_flow["flow_id"], data, ) - assert result1["type"] == data_entry_flow.FlowResultType.FORM + assert result1["type"] == FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY @respx.mock @@ -274,14 +275,14 @@ async def test_form_only_still_sample( user_flow["flow_id"], data, ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY @respx.mock @@ -362,13 +363,13 @@ async def test_form_rtsp_mode( result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -399,14 +400,14 @@ async def test_form_only_stream( user_flow["flow_id"], data, ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["step_id"] == "user_confirm_still" result3 = await hass.config_entries.flow.async_configure( result1["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "127_0_0_1" assert result3["options"] == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, @@ -442,7 +443,7 @@ async def test_form_still_and_stream_not_provided( CONF_VERIFY_SSL: False, }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "no_still_image_or_stream_url"} @@ -638,7 +639,7 @@ async def test_options_template_error( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" # try updating the still image url @@ -649,16 +650,16 @@ async def test_options_template_error( result["flow_id"], user_input=data, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "confirm_still" result2a = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={CONF_CONFIRMED_OK: True} ) - assert result2a["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2a["type"] == FlowResultType.CREATE_ENTRY result3 = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] == FlowResultType.FORM assert result3["step_id"] == "init" # verify that an invalid template reports the correct UI error. @@ -667,7 +668,7 @@ async def test_options_template_error( result3["flow_id"], user_input=data, ) - assert result4.get("type") == data_entry_flow.FlowResultType.FORM + assert result4.get("type") == FlowResultType.FORM assert result4["errors"] == {"still_image_url": "template_error"} # verify that an invalid template reports the correct UI error. @@ -678,7 +679,7 @@ async def test_options_template_error( user_input=data, ) - assert result5.get("type") == data_entry_flow.FlowResultType.FORM + assert result5.get("type") == FlowResultType.FORM assert result5["errors"] == {"stream_source": "template_error"} # verify that an relative stream url is rejected. @@ -688,7 +689,7 @@ async def test_options_template_error( result5["flow_id"], user_input=data, ) - assert result6.get("type") == data_entry_flow.FlowResultType.FORM + assert result6.get("type") == FlowResultType.FORM assert result6["errors"] == {"stream_source": "relative_url"} # verify that an malformed stream url is rejected. @@ -698,7 +699,7 @@ async def test_options_template_error( result6["flow_id"], user_input=data, ) - assert result7.get("type") == data_entry_flow.FlowResultType.FORM + assert result7.get("type") == FlowResultType.FORM assert result7["errors"] == {"stream_source": "malformed_url"} @@ -736,7 +737,7 @@ async def test_options_only_stream( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" # try updating the config options @@ -745,13 +746,13 @@ async def test_options_only_stream( result["flow_id"], user_input=data, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "confirm_still" result3 = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={CONF_CONFIRMED_OK: True} ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" @@ -766,7 +767,7 @@ async def test_import(hass: HomeAssistant, fakeimg_png) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Yaml Defined Name" await hass.async_block_till_done() @@ -778,7 +779,7 @@ async def test_import(hass: HomeAssistant, fakeimg_png) -> None: # Any name defined in yaml should end up as the entity id. assert hass.states.get("camera.yaml_defined_name") - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] == FlowResultType.ABORT # These above can be deleted after deprecation period is finished. @@ -873,7 +874,7 @@ async def test_use_wallclock_as_timestamps_option( result = await hass.config_entries.options.async_init( mock_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" with patch( "homeassistant.components.generic.async_setup_entry", return_value=True @@ -882,12 +883,12 @@ async def test_use_wallclock_as_timestamps_option( result["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["type"] == FlowResultType.FORM # Test what happens if user rejects the preview result3 = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={CONF_CONFIRMED_OK: False} ) - assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["type"] == FlowResultType.FORM assert result3["step_id"] == "init" with patch( "homeassistant.components.generic.async_setup_entry", return_value=True @@ -896,10 +897,10 @@ async def test_use_wallclock_as_timestamps_option( result3["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, ) - assert result4["type"] == data_entry_flow.FlowResultType.FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == "confirm_still" result5 = await hass.config_entries.options.async_configure( result4["flow_id"], user_input={CONF_CONFIRMED_OK: True}, ) - assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result5["type"] == FlowResultType.CREATE_ENTRY diff --git a/tests/components/geo_json_events/test_config_flow.py b/tests/components/geo_json_events/test_config_flow.py index 440e8c76086..765f7c11482 100644 --- a/tests/components/geo_json_events/test_config_flow.py +++ b/tests/components/geo_json_events/test_config_flow.py @@ -3,7 +3,7 @@ from datetime import timedelta import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.geo_json_events import DOMAIN from homeassistant.const import ( CONF_LATITUDE, @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_URL, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.geo_json_events.conftest import URL @@ -31,7 +32,7 @@ async def test_duplicate_error_user( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -44,7 +45,7 @@ async def test_duplicate_error_user( }, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -64,7 +65,7 @@ async def test_duplicate_error_import( CONF_RADIUS: 25, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -82,7 +83,7 @@ async def test_step_import(hass: HomeAssistant) -> None: CONF_SCAN_INTERVAL: timedelta(minutes=4), }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert ( result["title"] == "http://geo.json.local/geo_json_events.json (-41.2, 174.7)" ) @@ -100,7 +101,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -113,7 +114,7 @@ async def test_step_user(hass: HomeAssistant) -> None: }, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert ( result["title"] == "http://geo.json.local/geo_json_events.json (-41.2, 174.7)" ) diff --git a/tests/components/lidarr/test_config_flow.py b/tests/components/lidarr/test_config_flow.py index d3c4352dc1e..89bb6614739 100644 --- a/tests/components/lidarr/test_config_flow.py +++ b/tests/components/lidarr/test_config_flow.py @@ -1,9 +1,9 @@ """Test Lidarr config flow.""" -from homeassistant import data_entry_flow from homeassistant.components.lidarr.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .conftest import CONF_DATA, MOCK_INPUT, ComponentSetup @@ -15,14 +15,14 @@ async def test_flow_user_form(hass: HomeAssistant, connection) -> None: context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -34,7 +34,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant, invalid_auth) -> None context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -47,7 +47,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant, cannot_connect) -> data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -60,7 +60,7 @@ async def test_wrong_app(hass: HomeAssistant, wrong_app) -> None: data=MOCK_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "wrong_app" @@ -73,7 +73,7 @@ async def test_zeroconf_failed(hass: HomeAssistant, zeroconf_failed) -> None: data=MOCK_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "zeroconf_failed" @@ -88,7 +88,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -108,18 +108,18 @@ async def test_flow_reauth( }, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: "abc123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_KEY] == "abc123" diff --git a/tests/components/pushbullet/test_config_flow.py b/tests/components/pushbullet/test_config_flow.py index f250c22c443..d7baef682b8 100644 --- a/tests/components/pushbullet/test_config_flow.py +++ b/tests/components/pushbullet/test_config_flow.py @@ -4,10 +4,11 @@ from unittest.mock import patch from pushbullet import InvalidKeyError, PushbulletError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.pushbullet.const import DOMAIN from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_CONFIG @@ -33,7 +34,7 @@ async def test_flow_user(hass: HomeAssistant, requests_mock_fixture) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "pushbullet" assert result["data"] == MOCK_CONFIG @@ -58,7 +59,7 @@ async def test_flow_user_already_configured( result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -83,7 +84,7 @@ async def test_flow_name_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_config, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -99,7 +100,7 @@ async def test_flow_invalid_key(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -116,6 +117,6 @@ async def test_flow_conn_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/radarr/test_config_flow.py b/tests/components/radarr/test_config_flow.py index 0e328b50f94..5527e311114 100644 --- a/tests/components/radarr/test_config_flow.py +++ b/tests/components/radarr/test_config_flow.py @@ -3,11 +3,11 @@ from unittest.mock import patch from aiopyarr import exceptions -from homeassistant import data_entry_flow from homeassistant.components.radarr.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import ( API_KEY, @@ -33,7 +33,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_cannot_connect( @@ -48,7 +48,7 @@ async def test_cannot_connect( data=MOCK_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -62,7 +62,7 @@ async def test_invalid_auth( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=MOCK_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -79,7 +79,7 @@ async def test_wrong_app(hass: HomeAssistant) -> None: data={CONF_URL: URL, CONF_VERIFY_SSL: False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "wrong_app" @@ -96,7 +96,7 @@ async def test_zero_conf_failure(hass: HomeAssistant) -> None: data={CONF_URL: URL, CONF_VERIFY_SSL: False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "zeroconf_failed" @@ -113,7 +113,7 @@ async def test_unknown_error(hass: HomeAssistant) -> None: data=MOCK_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -130,7 +130,7 @@ async def test_zero_conf(hass: HomeAssistant) -> None: data={CONF_URL: URL, CONF_VERIFY_SSL: False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -150,14 +150,14 @@ async def test_full_reauth_flow_implementation( data=entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch_async_setup_entry() as mock_setup_entry: @@ -166,7 +166,7 @@ async def test_full_reauth_flow_implementation( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA | {CONF_API_KEY: "test-api-key-reauth"} @@ -185,7 +185,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch_async_setup_entry(): @@ -194,7 +194,7 @@ async def test_full_user_flow_implementation( user_input=MOCK_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["data"][CONF_URL] == "http://192.168.1.189:7887/test" diff --git a/tests/components/renson/test_config_flow.py b/tests/components/renson/test_config_flow.py index 6b9f54cd454..578c6125427 100644 --- a/tests/components/renson/test_config_flow.py +++ b/tests/components/renson/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.renson.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -12,7 +12,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Renson" assert result2["data"] == { "host": "1.1.1.1", @@ -55,7 +55,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -76,5 +76,5 @@ async def test_form_unknown(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/roborock/test_config_flow.py b/tests/components/roborock/test_config_flow.py index 2f297135d15..bbaa8935461 100644 --- a/tests/components/roborock/test_config_flow.py +++ b/tests/components/roborock/test_config_flow.py @@ -10,9 +10,10 @@ from roborock.exceptions import ( RoborockUrlException, ) -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.roborock.const import CONF_ENTRY_CODE, DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from .mock_data import MOCK_CONFIG, USER_DATA, USER_EMAIL @@ -28,7 +29,7 @@ async def test_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.request_code" @@ -37,7 +38,7 @@ async def test_config_flow_success( result["flow_id"], {"username": USER_EMAIL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "code" assert result["errors"] == {} with patch( @@ -48,7 +49,7 @@ async def test_config_flow_success( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_EMAIL assert result["data"] == MOCK_CONFIG assert result["result"] @@ -81,7 +82,7 @@ async def test_config_flow_failures_request_code( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.request_code", @@ -90,7 +91,7 @@ async def test_config_flow_failures_request_code( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": USER_EMAIL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == request_code_errors # Recover from error with patch( @@ -100,7 +101,7 @@ async def test_config_flow_failures_request_code( result["flow_id"], {"username": USER_EMAIL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "code" assert result["errors"] == {} with patch( @@ -111,7 +112,7 @@ async def test_config_flow_failures_request_code( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_EMAIL assert result["data"] == MOCK_CONFIG assert result["result"] @@ -142,7 +143,7 @@ async def test_config_flow_failures_code_login( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.request_code" @@ -151,7 +152,7 @@ async def test_config_flow_failures_code_login( result["flow_id"], {"username": USER_EMAIL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "code" assert result["errors"] == {} # Raise exception for invalid code @@ -162,7 +163,7 @@ async def test_config_flow_failures_code_login( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == code_login_errors with patch( "homeassistant.components.roborock.config_flow.RoborockApiClient.code_login", @@ -172,7 +173,7 @@ async def test_config_flow_failures_code_login( result["flow_id"], user_input={CONF_ENTRY_CODE: "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_EMAIL assert result["data"] == MOCK_CONFIG assert result["result"] diff --git a/tests/components/tautulli/test_config_flow.py b/tests/components/tautulli/test_config_flow.py index d39f9c1e3a1..0ca2d0438a7 100644 --- a/tests/components/tautulli/test_config_flow.py +++ b/tests/components/tautulli/test_config_flow.py @@ -3,11 +3,11 @@ from unittest.mock import AsyncMock, patch from pytautulli import exceptions -from homeassistant import data_entry_flow from homeassistant.components.tautulli.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from . import CONF_DATA, NAME, patch_config_flow_tautulli, setup_integration @@ -20,7 +20,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -31,7 +31,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -43,7 +43,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -54,7 +54,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -66,7 +66,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -77,7 +77,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -89,7 +89,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -100,7 +100,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -116,7 +116,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -128,7 +128,7 @@ async def test_flow_user_multiple_entries_allowed(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -144,7 +144,7 @@ async def test_flow_user_multiple_entries_allowed(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == input @@ -164,7 +164,7 @@ async def test_flow_reauth( }, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -179,7 +179,7 @@ async def test_flow_reauth( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.FlowResultType.ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == CONF_DATA assert len(mock_entry.mock_calls) == 1 @@ -205,7 +205,7 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "invalid_auth" @@ -214,5 +214,5 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/upcloud/test_config_flow.py b/tests/components/upcloud/test_config_flow.py index eadbe1c8fe6..cc869cdb99b 100644 --- a/tests/components/upcloud/test_config_flow.py +++ b/tests/components/upcloud/test_config_flow.py @@ -6,10 +6,11 @@ import requests_mock from requests_mock import ANY from upcloud_api import UpCloudAPIError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.upcloud.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +30,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -42,7 +43,7 @@ async def test_connection_error( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -63,7 +64,7 @@ async def test_login_error( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -78,7 +79,7 @@ async def test_success( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -96,7 +97,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -108,7 +109,7 @@ async def test_options(hass: HomeAssistant) -> None: ) -async def test_already_configured(hass, requests_mock): +async def test_already_configured(hass: HomeAssistant, requests_mock) -> None: """Test duplicate entry aborts and updates data.""" config_entry = MockConfigEntry( @@ -127,7 +128,7 @@ async def test_already_configured(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=new_user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_USERNAME] == new_user_input[CONF_USERNAME] assert config_entry.data[CONF_PASSWORD] == new_user_input[CONF_PASSWORD] diff --git a/tests/components/wemo/test_config_flow.py b/tests/components/wemo/test_config_flow.py index 71f3a378b74..5cb2b54c9a0 100644 --- a/tests/components/wemo/test_config_flow.py +++ b/tests/components/wemo/test_config_flow.py @@ -2,11 +2,11 @@ from dataclasses import asdict -from homeassistant import data_entry_flow from homeassistant.components.wemo.const import DOMAIN from homeassistant.components.wemo.wemo_device import Options from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, patch @@ -21,7 +21,7 @@ async def test_not_discovered(hass: HomeAssistant) -> None: with patch("homeassistant.components.wemo.config_flow.pywemo") as mock_pywemo: mock_pywemo.discover_devices.return_value = [] result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -33,14 +33,14 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=asdict(options) ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert Options(**result["data"]) == options @@ -51,7 +51,7 @@ async def test_invalid_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" # enable_subscription must be True if enable_long_press is True (default). From eb56c7e1b74abfebf70b9b13ad7579b76e6f7783 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 30 Jul 2023 12:27:57 -0700 Subject: [PATCH 0082/1151] Bump androidtvremote2==0.0.13 (#97494) --- homeassistant/components/androidtv_remote/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv_remote/manifest.json b/homeassistant/components/androidtv_remote/manifest.json index 3feddacd4e5..cb7a969379e 100644 --- a/homeassistant/components/androidtv_remote/manifest.json +++ b/homeassistant/components/androidtv_remote/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_push", "loggers": ["androidtvremote2"], "quality_scale": "platinum", - "requirements": ["androidtvremote2==0.0.12"], + "requirements": ["androidtvremote2==0.0.13"], "zeroconf": ["_androidtvremote2._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 7a7dd204315..8d9e6471841 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -399,7 +399,7 @@ amcrest==1.9.7 androidtv[async]==0.0.70 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.12 +androidtvremote2==0.0.13 # homeassistant.components.anel_pwrctrl anel-pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b8564a77338..0dbddf51c02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -368,7 +368,7 @@ amberelectric==1.0.4 androidtv[async]==0.0.70 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.12 +androidtvremote2==0.0.13 # homeassistant.components.anova anova-wifi==0.10.0 From f218fb8ceecaa2e0a52e2915790be1d155c8c55b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Jul 2023 23:56:30 -0700 Subject: [PATCH 0083/1151] Fix typo in PassiveBluetoothDataProcessor (#97508) --- .../components/bluetooth/passive_update_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 607abaa0168..c553e2469eb 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -202,7 +202,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): def async_add_entities_listener( self, entity_class: type[PassiveBluetoothProcessorEntity], - async_add_entites: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> Callable[[], None]: """Add a listener for new entities.""" created: set[PassiveBluetoothEntityKey] = set() @@ -220,7 +220,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): entities.append(entity_class(self, entity_key, description)) created.add(entity_key) if entities: - async_add_entites(entities) + async_add_entities(entities) return self.async_add_listener(_async_add_or_update_entities) From b266514068d1ef7faf762ce099322c0796a91dc4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 31 Jul 2023 09:07:13 +0200 Subject: [PATCH 0084/1151] Delay creation of Reolink repair issues (#97476) * delay creation of repair issues * fix tests --- homeassistant/components/reolink/host.py | 37 ++++++++++++------------ tests/components/reolink/test_init.py | 11 ++++++- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 5882e5e66a4..cfc702aac39 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -26,6 +26,7 @@ from .exceptions import ReolinkSetupException, ReolinkWebhookException, UserNotA DEFAULT_TIMEOUT = 60 FIRST_ONVIF_TIMEOUT = 10 +FIRST_ONVIF_LONG_POLL_TIMEOUT = 90 SUBSCRIPTION_RENEW_THRESHOLD = 300 POLL_INTERVAL_NO_PUSH = 5 LONG_POLL_COOLDOWN = 0.75 @@ -205,7 +206,7 @@ class ReolinkHost: # ONVIF push is not received, start long polling and schedule check await self._async_start_long_polling() self._cancel_long_poll_check = async_call_later( - self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif_long_poll + self._hass, FIRST_ONVIF_LONG_POLL_TIMEOUT, self._async_check_onvif_long_poll ) self._cancel_onvif_check = None @@ -215,7 +216,7 @@ class ReolinkHost: if not self._long_poll_received: _LOGGER.debug( "Did not receive state through ONVIF long polling after %i seconds", - FIRST_ONVIF_TIMEOUT, + FIRST_ONVIF_LONG_POLL_TIMEOUT, ) ir.async_create_issue( self._hass, @@ -230,8 +231,24 @@ class ReolinkHost: "network_link": "https://my.home-assistant.io/redirect/network/", }, ) + if self._base_url.startswith("https"): + ir.async_create_issue( + self._hass, + DOMAIN, + "https_webhook", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="https_webhook", + translation_placeholders={ + "base_url": self._base_url, + "network_link": "https://my.home-assistant.io/redirect/network/", + }, + ) + else: + ir.async_delete_issue(self._hass, DOMAIN, "https_webhook") else: ir.async_delete_issue(self._hass, DOMAIN, "webhook_url") + ir.async_delete_issue(self._hass, DOMAIN, "https_webhook") # If no ONVIF push or long polling state is received, start fast polling await self._async_poll_all_motion() @@ -426,22 +443,6 @@ class ReolinkHost: webhook_path = webhook.async_generate_path(event_id) self._webhook_url = f"{self._base_url}{webhook_path}" - if self._base_url.startswith("https"): - ir.async_create_issue( - self._hass, - DOMAIN, - "https_webhook", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="https_webhook", - translation_placeholders={ - "base_url": self._base_url, - "network_link": "https://my.home-assistant.io/redirect/network/", - }, - ) - else: - ir.async_delete_issue(self._hass, DOMAIN, "https_webhook") - _LOGGER.debug("Registered webhook: %s", event_id) def unregister_webhook(self): diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 1e588d5e3a1..f5f581760c1 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -116,7 +116,14 @@ async def test_https_repair_issue( hass, {"country": "GB", "internal_url": "https://test_homeassistant_address"} ) - assert await hass.config_entries.async_setup(config_entry.entry_id) + 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) @@ -150,6 +157,8 @@ async def test_webhook_repair_issue( """Test repairs issue is raised when the webhook url is unreachable.""" 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", ): From 7bda873c2a53ef42d9d97d63addf514f64cce419 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jul 2023 01:02:15 -0700 Subject: [PATCH 0085/1151] Fix bthome not remembering a device is a sleepy device (#97517) --- homeassistant/components/bthome/__init__.py | 7 +++++++ homeassistant/components/bthome/binary_sensor.py | 5 +---- homeassistant/components/bthome/const.py | 1 + homeassistant/components/bthome/coordinator.py | 10 ++++++++++ homeassistant/components/bthome/sensor.py | 5 +---- tests/components/bthome/test_sensor.py | 6 +++++- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/bthome/__init__.py b/homeassistant/components/bthome/__init__.py index 3e2e17a9a21..751c8f74bf9 100644 --- a/homeassistant/components/bthome/__init__.py +++ b/homeassistant/components/bthome/__init__.py @@ -20,6 +20,7 @@ from .const import ( BTHOME_BLE_EVENT, CONF_BINDKEY, CONF_DISCOVERED_EVENT_CLASSES, + CONF_SLEEPY_DEVICE, DOMAIN, BTHomeBleEvent, ) @@ -43,6 +44,11 @@ def process_service_info( entry.entry_id ] discovered_device_classes = coordinator.discovered_device_classes + if entry.data.get(CONF_SLEEPY_DEVICE, False) != data.sleepy_device: + hass.config_entries.async_update_entry( + entry, + data=entry.data | {CONF_SLEEPY_DEVICE: data.sleepy_device}, + ) if update.events: address = service_info.device.address for device_key, event in update.events.items(): @@ -113,6 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, []) ), connectable=False, + entry=entry, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/bthome/binary_sensor.py b/homeassistant/components/bthome/binary_sensor.py index d9d24e95007..277c2af7ff2 100644 --- a/homeassistant/components/bthome/binary_sensor.py +++ b/homeassistant/components/bthome/binary_sensor.py @@ -203,7 +203,4 @@ class BTHomeBluetoothBinarySensorEntity( @property def available(self) -> bool: """Return True if entity is available.""" - coordinator: BTHomePassiveBluetoothProcessorCoordinator = ( - self.processor.coordinator - ) - return coordinator.device_data.sleepy_device or super().available + return self.processor.coordinator.sleepy_device or super().available diff --git a/homeassistant/components/bthome/const.py b/homeassistant/components/bthome/const.py index 75a8ab4fc86..780833bf92e 100644 --- a/homeassistant/components/bthome/const.py +++ b/homeassistant/components/bthome/const.py @@ -7,6 +7,7 @@ DOMAIN = "bthome" CONF_BINDKEY: Final = "bindkey" CONF_DISCOVERED_EVENT_CLASSES: Final = "known_events" +CONF_SLEEPY_DEVICE: Final = "sleepy_device" CONF_SUBTYPE: Final = "subtype" EVENT_TYPE: Final = "event_type" diff --git a/homeassistant/components/bthome/coordinator.py b/homeassistant/components/bthome/coordinator.py index dafa932a73e..bb743be7c7f 100644 --- a/homeassistant/components/bthome/coordinator.py +++ b/homeassistant/components/bthome/coordinator.py @@ -13,8 +13,11 @@ from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothProcessorCoordinator, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from .const import CONF_SLEEPY_DEVICE + class BTHomePassiveBluetoothProcessorCoordinator(PassiveBluetoothProcessorCoordinator): """Define a BTHome Bluetooth Passive Update Processor Coordinator.""" @@ -28,12 +31,19 @@ class BTHomePassiveBluetoothProcessorCoordinator(PassiveBluetoothProcessorCoordi update_method: Callable[[BluetoothServiceInfoBleak], Any], device_data: BTHomeBluetoothDeviceData, discovered_device_classes: set[str], + entry: ConfigEntry, connectable: bool = False, ) -> None: """Initialize the BTHome Bluetooth Passive Update Processor Coordinator.""" super().__init__(hass, logger, address, mode, update_method, connectable) self.discovered_device_classes = discovered_device_classes self.device_data = device_data + self.entry = entry + + @property + def sleepy_device(self) -> bool: + """Return True if the device is a sleepy device.""" + return self.entry.data.get(CONF_SLEEPY_DEVICE, self.device_data.sleepy_device) class BTHomePassiveBluetoothDataProcessor(PassiveBluetoothDataProcessor): diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index fc8673e801b..95cba20055f 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -400,7 +400,4 @@ class BTHomeBluetoothSensorEntity( @property def available(self) -> bool: """Return True if entity is available.""" - coordinator: BTHomePassiveBluetoothProcessorCoordinator = ( - self.processor.coordinator - ) - return coordinator.device_data.sleepy_device or super().available + return self.processor.coordinator.sleepy_device or super().available diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index 4450bfcc936..582dcabbb33 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -9,7 +9,7 @@ import pytest from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, ) -from homeassistant.components.bthome.const import DOMAIN +from homeassistant.components.bthome.const import CONF_SLEEPY_DEVICE, DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ( ATTR_FRIENDLY_NAME, @@ -1138,6 +1138,8 @@ async def test_unavailable(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + assert CONF_SLEEPY_DEVICE not in entry.data + async def test_sleepy_device(hass: HomeAssistant) -> None: """Test sleepy device does not go to unavailable after 60 minutes.""" @@ -1191,3 +1193,5 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + assert entry.data[CONF_SLEEPY_DEVICE] is True From 28bebf338fd1aca55c5ebcfc3dd31273c419e5cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jul 2023 01:02:36 -0700 Subject: [PATCH 0086/1151] Fix xiaomi_ble not remembering a device is a sleepy device (#97518) --- homeassistant/components/xiaomi_ble/__init__.py | 7 +++++++ homeassistant/components/xiaomi_ble/binary_sensor.py | 5 +---- homeassistant/components/xiaomi_ble/const.py | 1 + homeassistant/components/xiaomi_ble/coordinator.py | 10 ++++++++++ homeassistant/components/xiaomi_ble/sensor.py | 5 +---- tests/components/xiaomi_ble/test_binary_sensor.py | 6 +++++- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 1810d52323c..b12f4df7db1 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -19,6 +19,7 @@ from homeassistant.helpers.device_registry import DeviceRegistry, async_get from .const import ( CONF_DISCOVERED_EVENT_CLASSES, + CONF_SLEEPY_DEVICE, DOMAIN, XIAOMI_BLE_EVENT, XiaomiBleEvent, @@ -43,6 +44,11 @@ def process_service_info( entry.entry_id ] discovered_device_classes = coordinator.discovered_device_classes + if entry.data.get(CONF_SLEEPY_DEVICE, False) != data.sleepy_device: + hass.config_entries.async_update_entry( + entry, + data=entry.data | {CONF_SLEEPY_DEVICE: data.sleepy_device}, + ) if update.events: address = service_info.device.address for device_key, event in update.events.items(): @@ -157,6 +163,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # since we will trade the BLEDevice for a connectable one # if we need to poll it connectable=False, + entry=entry, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index f7c4c87014c..5ff418fe831 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -136,7 +136,4 @@ class XiaomiBluetoothSensorEntity( @property def available(self) -> bool: """Return True if entity is available.""" - coordinator: XiaomiActiveBluetoothProcessorCoordinator = ( - self.processor.coordinator - ) - return coordinator.device_data.sleepy_device or super().available + return self.processor.coordinator.sleepy_device or super().available diff --git a/homeassistant/components/xiaomi_ble/const.py b/homeassistant/components/xiaomi_ble/const.py index 1566478bcea..346d8a61318 100644 --- a/homeassistant/components/xiaomi_ble/const.py +++ b/homeassistant/components/xiaomi_ble/const.py @@ -7,6 +7,7 @@ DOMAIN = "xiaomi_ble" CONF_DISCOVERED_EVENT_CLASSES: Final = "known_events" +CONF_SLEEPY_DEVICE: Final = "sleepy_device" CONF_EVENT_PROPERTIES: Final = "event_properties" EVENT_PROPERTIES: Final = "event_properties" EVENT_TYPE: Final = "event_type" diff --git a/homeassistant/components/xiaomi_ble/coordinator.py b/homeassistant/components/xiaomi_ble/coordinator.py index 2a4b35f6171..94e70ca9835 100644 --- a/homeassistant/components/xiaomi_ble/coordinator.py +++ b/homeassistant/components/xiaomi_ble/coordinator.py @@ -15,9 +15,12 @@ from homeassistant.components.bluetooth.active_update_processor import ( from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer +from .const import CONF_SLEEPY_DEVICE + class XiaomiActiveBluetoothProcessorCoordinator(ActiveBluetoothProcessorCoordinator): """Define a Xiaomi Bluetooth Active Update Processor Coordinator.""" @@ -39,6 +42,7 @@ class XiaomiActiveBluetoothProcessorCoordinator(ActiveBluetoothProcessorCoordina ] | None = None, poll_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None, + entry: ConfigEntry, connectable: bool = True, ) -> None: """Initialize the Xiaomi Bluetooth Active Update Processor Coordinator.""" @@ -55,6 +59,12 @@ class XiaomiActiveBluetoothProcessorCoordinator(ActiveBluetoothProcessorCoordina ) self.discovered_device_classes = discovered_device_classes self.device_data = device_data + self.entry = entry + + @property + def sleepy_device(self) -> bool: + """Return True if the device is a sleepy device.""" + return self.entry.data.get(CONF_SLEEPY_DEVICE, self.device_data.sleepy_device) class XiaomiPassiveBluetoothDataProcessor(PassiveBluetoothDataProcessor): diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index f0f0d7fa71e..86ffdedafd1 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -200,7 +200,4 @@ class XiaomiBluetoothSensorEntity( @property def available(self) -> bool: """Return True if entity is available.""" - coordinator: XiaomiActiveBluetoothProcessorCoordinator = ( - self.processor.coordinator - ) - return coordinator.device_data.sleepy_device or super().available + return self.processor.coordinator.sleepy_device or super().available diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 424f6e22b26..5dd1b965f25 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -7,7 +7,7 @@ from unittest.mock import patch from homeassistant.components.bluetooth import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, ) -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, STATE_OFF, @@ -313,6 +313,8 @@ async def test_unavailable(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + assert CONF_SLEEPY_DEVICE not in entry.data + async def test_sleepy_device(hass: HomeAssistant) -> None: """Test sleepy device does not go to unavailable after 60 minutes.""" @@ -363,3 +365,5 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + assert entry.data[CONF_SLEEPY_DEVICE] is True From 5c4e47c127d3d8c0b1a9a609233f93cc2bceed68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jul 2023 01:02:57 -0700 Subject: [PATCH 0087/1151] Use internal imports in Bluetooth update coordinator to avoid future circular imports (#97506) --- .../components/bluetooth/update_coordinator.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index 0c41b58c63d..dd9e7ecfe5d 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -6,16 +6,20 @@ import logging from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from . import ( - BluetoothCallbackMatcher, - BluetoothChange, - BluetoothScanningMode, - BluetoothServiceInfoBleak, +from .api import ( async_address_present, async_last_service_info, async_register_callback, async_track_unavailable, ) +from .match import ( + BluetoothCallbackMatcher, +) +from .models import ( + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, +) class BasePassiveBluetoothCoordinator(ABC): From 37cdd511830e2efb223ebce72d3cc2e10750304c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jul 2023 01:03:19 -0700 Subject: [PATCH 0088/1151] Combine Bluetooth update coordinator subscriptions to reduce code duplication (#97503) --- .../bluetooth/update_coordinator.py | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index dd9e7ecfe5d..71729a1dba8 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -41,8 +41,7 @@ class BasePassiveBluetoothCoordinator(ABC): self.logger = logger self.address = address self.connectable = connectable - self._cancel_track_unavailable: CALLBACK_TYPE | None = None - self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None + self._on_stop: list[CALLBACK_TYPE] = [] self.mode = mode self._last_unavailable_time = 0.0 self._last_name = address @@ -97,27 +96,31 @@ class BasePassiveBluetoothCoordinator(ABC): @callback def _async_start(self) -> None: """Start the callbacks.""" - self._cancel_bluetooth_advertisements = async_register_callback( - self.hass, - self._async_handle_bluetooth_event, - BluetoothCallbackMatcher( - address=self.address, connectable=self.connectable - ), - self.mode, + self._on_stop.append( + async_register_callback( + self.hass, + self._async_handle_bluetooth_event, + BluetoothCallbackMatcher( + address=self.address, connectable=self.connectable + ), + self.mode, + ) ) - self._cancel_track_unavailable = async_track_unavailable( - self.hass, self._async_handle_unavailable, self.address, self.connectable + self._on_stop.append( + async_track_unavailable( + self.hass, + self._async_handle_unavailable, + self.address, + self.connectable, + ) ) @callback def _async_stop(self) -> None: """Stop the callbacks.""" - if self._cancel_bluetooth_advertisements is not None: - self._cancel_bluetooth_advertisements() - self._cancel_bluetooth_advertisements = None - if self._cancel_track_unavailable is not None: - self._cancel_track_unavailable() - self._cancel_track_unavailable = None + for unsub in self._on_stop: + unsub() + self._on_stop.clear() @callback def _async_handle_unavailable( From ed3ebdfea52b222560ee6cae21c84f1e73df4d9a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 31 Jul 2023 11:15:59 +0200 Subject: [PATCH 0089/1151] Remove myself from scrape codeowners (#97524) --- CODEOWNERS | 4 ++-- homeassistant/components/scrape/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index f85b796b145..aa2630ba46d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1080,8 +1080,8 @@ build.json @home-assistant/supervisor /homeassistant/components/schlage/ @dknowles2 /tests/components/schlage/ @dknowles2 /homeassistant/components/schluter/ @prairieapps -/homeassistant/components/scrape/ @fabaff @gjohansson-ST @epenet -/tests/components/scrape/ @fabaff @gjohansson-ST @epenet +/homeassistant/components/scrape/ @fabaff @gjohansson-ST +/tests/components/scrape/ @fabaff @gjohansson-ST /homeassistant/components/screenlogic/ @dieselrabbit @bdraco /tests/components/screenlogic/ @dieselrabbit @bdraco /homeassistant/components/script/ @home-assistant/core diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 42f9fdb05d5..23845cc2eac 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -2,7 +2,7 @@ "domain": "scrape", "name": "Scrape", "after_dependencies": ["rest"], - "codeowners": ["@fabaff", "@gjohansson-ST", "@epenet"], + "codeowners": ["@fabaff", "@gjohansson-ST"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/scrape", "iot_class": "cloud_polling", From 927905ac84e2c03649e2fb2ec8147178d389cac5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 31 Jul 2023 12:17:51 +0200 Subject: [PATCH 0090/1151] Handle http error in Renault initialisation (#97530) --- homeassistant/components/renault/__init__.py | 5 ++++- tests/components/renault/test_init.py | 21 +++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py index b02938b1652..f69451290bc 100644 --- a/homeassistant/components/renault/__init__.py +++ b/homeassistant/components/renault/__init__.py @@ -26,7 +26,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b raise ConfigEntryAuthFailed() hass.data.setdefault(DOMAIN, {}) - await renault_hub.async_initialise(config_entry) + try: + await renault_hub.async_initialise(config_entry) + except aiohttp.ClientResponseError as exc: + raise ConfigEntryNotReady() from exc hass.data[DOMAIN][config_entry.entry_id] = renault_hub diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index 7f2aee9d7bd..415b07dc7e6 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -1,7 +1,7 @@ """Tests for Renault setup process.""" from collections.abc import Generator from typing import Any -from unittest.mock import patch +from unittest.mock import Mock, patch import aiohttp import pytest @@ -76,3 +76,22 @@ async def test_setup_entry_exception( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) + + +@pytest.mark.usefixtures("patch_renault_account") +async def test_setup_entry_kamereon_exception( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Test ConfigEntryNotReady when API raises an exception during entry setup.""" + # In this case we are testing the condition where renault_hub fails to retrieve + # list of vehicles (see Gateway Time-out on #97324). + with patch( + "renault_api.renault_client.RenaultClient.get_api_account", + side_effect=aiohttp.ClientResponseError(Mock(), (), status=504), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) From 83db3c48c2273041c0f165ceb062092581afb6ea Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 31 Jul 2023 12:35:30 +0200 Subject: [PATCH 0091/1151] Fix unused variable in Renault tests (#97529) --- tests/components/renault/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index 415b07dc7e6..e1c782d06d5 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -68,7 +68,7 @@ async def test_setup_entry_exception( # ConfigEntryNotReady. with patch( "renault_api.renault_session.RenaultSession.login", - side_effect=aiohttp.ClientConnectionError, + side_effect=side_effect, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 7b25702605ee765b5bf65b6359aab5026686a936 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 31 Jul 2023 03:38:31 -0700 Subject: [PATCH 0092/1151] Bump pywemo to 1.2.0 (#97520) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index bb19d2e1655..3dbd8aa32bc 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -9,7 +9,7 @@ }, "iot_class": "local_push", "loggers": ["pywemo"], - "requirements": ["pywemo==1.1.0"], + "requirements": ["pywemo==1.2.0"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 8d9e6471841..8a1a8ef3024 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2227,7 +2227,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==1.1.0 +pywemo==1.2.0 # homeassistant.components.wilight pywilight==0.0.74 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0dbddf51c02..532c02b66a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1638,7 +1638,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==1.1.0 +pywemo==1.2.0 # homeassistant.components.wilight pywilight==0.0.74 From 787486b68df9eafea387b86c98326f5d8656ff6e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 31 Jul 2023 12:50:44 +0200 Subject: [PATCH 0093/1151] Remove myself from rest codeowners (#97528) --- CODEOWNERS | 2 -- homeassistant/components/rest/manifest.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index aa2630ba46d..2121415fd75 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1028,8 +1028,6 @@ build.json @home-assistant/supervisor /homeassistant/components/repairs/ @home-assistant/core /tests/components/repairs/ @home-assistant/core /homeassistant/components/repetier/ @MTrab @ShadowBr0ther -/homeassistant/components/rest/ @epenet -/tests/components/rest/ @epenet /homeassistant/components/rflink/ @javicalle /tests/components/rflink/ @javicalle /homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json index b6ec7eb8ecb..c8796c7161c 100644 --- a/homeassistant/components/rest/manifest.json +++ b/homeassistant/components/rest/manifest.json @@ -1,7 +1,7 @@ { "domain": "rest", "name": "RESTful", - "codeowners": ["@epenet"], + "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/rest", "iot_class": "local_polling", "requirements": ["jsonpath==0.82", "xmltodict==0.13.0"] From 651d4134cf8344171e7350079fb2de1a53db3fb2 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 31 Jul 2023 14:21:34 +0200 Subject: [PATCH 0094/1151] Avoid leaking exception trace for philips_js (#97491) Avoid leaking exception trace --- homeassistant/components/philips_js/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 8ecc8a0e8c4..6f72f31ae8f 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -7,7 +7,12 @@ from datetime import timedelta import logging from typing import Any -from haphilipsjs import AutenticationFailure, ConnectionFailure, PhilipsTV +from haphilipsjs import ( + AutenticationFailure, + ConnectionFailure, + GeneralFailure, + PhilipsTV, +) from haphilipsjs.typing import SystemType from homeassistant.config_entries import ConfigEntry @@ -22,7 +27,7 @@ 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.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, DOMAIN @@ -187,3 +192,5 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): pass except AutenticationFailure as exception: raise ConfigEntryAuthFailed(str(exception)) from exception + except GeneralFailure as exception: + raise UpdateFailed(str(exception)) from exception From e7069d48be5a613d32889afd8e6de27089430146 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jul 2023 07:04:41 -0700 Subject: [PATCH 0095/1151] Load homekit_controller test data using its json loader (#97534) --- tests/components/homekit_controller/common.py | 4 +- .../specific_devices/test_connectsense.py | 81 ++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 0c27e0a3648..2b532769220 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -3,12 +3,12 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -import json import logging import os from typing import Any, Final from unittest import mock +from aiohomekit.hkjson import loads as hkloads from aiohomekit.model import ( Accessories, AccessoriesState, @@ -185,7 +185,7 @@ async def setup_accessories_from_file(hass, path): accessories_fixture = await hass.async_add_executor_job( load_fixture, os.path.join("homekit_controller", path) ) - accessories_json = json.loads(accessories_fixture) + accessories_json = hkloads(accessories_fixture) accessories = Accessories.from_list(accessories_json) return accessories diff --git a/tests/components/homekit_controller/specific_devices/test_connectsense.py b/tests/components/homekit_controller/specific_devices/test_connectsense.py index aa24a3dea68..44157c32203 100644 --- a/tests/components/homekit_controller/specific_devices/test_connectsense.py +++ b/tests/components/homekit_controller/specific_devices/test_connectsense.py @@ -20,7 +20,86 @@ from ..common import ( async def test_connectsense_setup(hass: HomeAssistant) -> None: """Test that the accessory can be correctly setup in HA.""" accessories = await setup_accessories_from_file(hass, "connectsense.json") - await setup_test_accessories(hass, accessories) + config_entry, pairing = await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="InWall Outlet-0394DE", + model="CS-IWO", + manufacturer="ConnectSense", + sw_version="1.0.0", + hw_version="", + serial_number="1020301376", + devices=[], + entities=[ + EntityTestInfo( + entity_id="sensor.inwall_outlet_0394de_current", + friendly_name="InWall Outlet-0394DE Current", + unique_id="00:00:00:00:00:00_1_13_18", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state="0.03", + ), + EntityTestInfo( + entity_id="sensor.inwall_outlet_0394de_power", + friendly_name="InWall Outlet-0394DE Power", + unique_id="00:00:00:00:00:00_1_13_19", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, + unit_of_measurement=POWER_WATT, + state="0.8", + ), + EntityTestInfo( + entity_id="sensor.inwall_outlet_0394de_energy_kwh", + friendly_name="InWall Outlet-0394DE Energy kWh", + unique_id="00:00:00:00:00:00_1_13_20", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state="379.69299", + ), + EntityTestInfo( + entity_id="switch.inwall_outlet_0394de_outlet_a", + friendly_name="InWall Outlet-0394DE Outlet A", + unique_id="00:00:00:00:00:00_1_13", + state="on", + ), + EntityTestInfo( + entity_id="sensor.inwall_outlet_0394de_current_2", + friendly_name="InWall Outlet-0394DE Current", + unique_id="00:00:00:00:00:00_1_25_30", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state="0.05", + ), + EntityTestInfo( + entity_id="sensor.inwall_outlet_0394de_power_2", + friendly_name="InWall Outlet-0394DE Power", + unique_id="00:00:00:00:00:00_1_25_31", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, + unit_of_measurement=POWER_WATT, + state="0.8", + ), + EntityTestInfo( + entity_id="sensor.inwall_outlet_0394de_energy_kwh_2", + friendly_name="InWall Outlet-0394DE Energy kWh", + unique_id="00:00:00:00:00:00_1_25_32", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state="175.85001", + ), + EntityTestInfo( + entity_id="switch.inwall_outlet_0394de_outlet_b", + friendly_name="InWall Outlet-0394DE Outlet B", + unique_id="00:00:00:00:00:00_1_25", + state="on", + ), + ], + ), + ) + + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() await assert_devices_and_entities_created( hass, From 9df1805b3bcfe588dcacdb27ce5753140a3c55b5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 31 Jul 2023 16:06:06 +0200 Subject: [PATCH 0096/1151] Remove myself from const and util codeowners (#97527) --- CODEOWNERS | 2 -- script/hassfest/codeowners.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 2121415fd75..5ba36fb30c1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -19,8 +19,6 @@ build.json @home-assistant/supervisor # Other code /homeassistant/scripts/check_config.py @kellerza -/homeassistant/const.py @epenet -/homeassistant/util/ @epenet # Integrations /homeassistant/components/abode/ @shred86 diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index f95a7b3b542..458391b1fb4 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -25,8 +25,6 @@ build.json @home-assistant/supervisor # Other code /homeassistant/scripts/check_config.py @kellerza -/homeassistant/const.py @epenet -/homeassistant/util/ @epenet # Integrations """.strip() From dba6330fc8e097517dc3b9e753d48b418abc5dc5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:25:57 +0200 Subject: [PATCH 0097/1151] Update pydiscovergy to 2.0.3 (#97509) * Update pydiscovergy to 2.0.3 * Fix mypy * Fix tests --- homeassistant/components/discovergy/coordinator.py | 4 ++-- homeassistant/components/discovergy/diagnostics.py | 5 +++-- homeassistant/components/discovergy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/discovergy/coordinator.py b/homeassistant/components/discovergy/coordinator.py index 6ee5a4c3e84..d2548d0bacd 100644 --- a/homeassistant/components/discovergy/coordinator.py +++ b/homeassistant/components/discovergy/coordinator.py @@ -50,9 +50,9 @@ class DiscovergyUpdateCoordinator(DataUpdateCoordinator[Reading]): return await self.discovergy_client.meter_last_reading(self.meter.meter_id) except AccessTokenExpired as err: raise ConfigEntryAuthFailed( - f"Auth expired while fetching last reading for meter {self.meter.get_meter_id()}" + f"Auth expired while fetching last reading for meter {self.meter.meter_id}" ) from err except HTTPError as err: raise UpdateFailed( - f"Error while fetching last reading for meter {self.meter.get_meter_id()}" + f"Error while fetching last reading for meter {self.meter.meter_id}" ) from err diff --git a/homeassistant/components/discovergy/diagnostics.py b/homeassistant/components/discovergy/diagnostics.py index a7c79bf3b13..5d4a34b07dd 100644 --- a/homeassistant/components/discovergy/diagnostics.py +++ b/homeassistant/components/discovergy/diagnostics.py @@ -1,6 +1,7 @@ """Diagnostics support for discovergy.""" from __future__ import annotations +from dataclasses import asdict from typing import Any from pydiscovergy.models import Meter @@ -36,11 +37,11 @@ async def async_get_config_entry_diagnostics( for meter in meters: # make a dict of meter data and redact some data - flattened_meter.append(async_redact_data(meter.__dict__, TO_REDACT_METER)) + flattened_meter.append(async_redact_data(asdict(meter), TO_REDACT_METER)) # get last reading for meter and make a dict of it coordinator = data.coordinators[meter.meter_id] - last_readings[meter.meter_id] = coordinator.data.__dict__ + last_readings[meter.meter_id] = asdict(coordinator.data) return { "entry": async_redact_data(entry.as_dict(), TO_REDACT_CONFIG_ENTRY), diff --git a/homeassistant/components/discovergy/manifest.json b/homeassistant/components/discovergy/manifest.json index 23d7f1ad5bf..d5bdc018eda 100644 --- a/homeassistant/components/discovergy/manifest.json +++ b/homeassistant/components/discovergy/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/discovergy", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["pydiscovergy==2.0.1"] + "requirements": ["pydiscovergy==2.0.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8a1a8ef3024..9d69d8b1f64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1638,7 +1638,7 @@ pydelijn==1.1.0 pydexcom==0.2.3 # homeassistant.components.discovergy -pydiscovergy==2.0.1 +pydiscovergy==2.0.3 # homeassistant.components.doods pydoods==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 532c02b66a6..4d91f9b13ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1217,7 +1217,7 @@ pydeconz==113 pydexcom==0.2.3 # homeassistant.components.discovergy -pydiscovergy==2.0.1 +pydiscovergy==2.0.3 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 From 4d9b73033d6ebb8594e94795f46cb6b8d908b91b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:59:40 +0200 Subject: [PATCH 0098/1151] Update python-typing-update to 0.6.0 (#97531) --- .pre-commit-config.yaml | 4 ++-- homeassistant/helpers/entity_registry.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7b351b755f..b4a6704717f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,7 +43,7 @@ repos: hooks: - id: prettier - repo: https://github.com/cdce8p/python-typing-update - rev: v0.5.0 + rev: v0.6.0 hooks: # Run `python-typing-update` hook manually from time to time # to update python typing syntax. @@ -52,7 +52,7 @@ repos: - id: python-typing-update stages: [manual] args: - - --py310-plus + - --py311-plus - --force - --keep-updates files: ^(homeassistant|tests|script)/.+\.py$ diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index a46dd3c3a52..ff2ca255279 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -15,10 +15,9 @@ from datetime import datetime, timedelta from enum import StrEnum import logging import time -from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast +from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict, TypeVar, cast import attr -from typing_extensions import NotRequired import voluptuous as vol from homeassistant.const import ( From 085e274f444ca786d5a94a3e58e4c4a57285e32b Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 31 Jul 2023 12:38:50 -0400 Subject: [PATCH 0099/1151] Bump pyschlage to 2023.7.0 (#97366) --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index cbc173b8c34..e8b1443358d 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2023.5.0"] + "requirements": ["pyschlage==2023.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9d69d8b1f64..8e3126680ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1982,7 +1982,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2023.5.0 +pyschlage==2023.7.0 # homeassistant.components.sensibo pysensibo==1.0.32 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d91f9b13ce..ffef1a4a0a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1474,7 +1474,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2023.5.0 +pyschlage==2023.7.0 # homeassistant.components.sensibo pysensibo==1.0.32 From c2e9fd85c20a28619c43499b689f58245a158d41 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 31 Jul 2023 18:44:03 +0200 Subject: [PATCH 0100/1151] Fix RootFolder not iterable in Radarr (#97537) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/radarr/coordinator.py | 12 +- tests/components/radarr/__init__.py | 34 +++-- .../radarr/fixtures/single-movie.json | 116 ++++++++++++++++++ .../fixtures/single-rootfolder-linux.json | 6 + .../fixtures/single-rootfolder-windows.json | 6 + tests/components/radarr/test_sensor.py | 43 +++++-- 6 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 tests/components/radarr/fixtures/single-movie.json create mode 100644 tests/components/radarr/fixtures/single-rootfolder-linux.json create mode 100644 tests/components/radarr/fixtures/single-rootfolder-windows.json diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index 5537a18725c..c318d662028 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from datetime import timedelta -from typing import Generic, TypeVar, cast +from typing import Generic, TypeVar from aiopyarr import Health, RadarrMovie, RootFolder, SystemStatus, exceptions from aiopyarr.models.host_configuration import PyArrHostConfiguration @@ -71,7 +71,10 @@ class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[RootFolder async def _fetch_data(self) -> list[RootFolder]: """Fetch the data.""" - return cast(list[RootFolder], await self.api_client.async_get_root_folders()) + root_folders = await self.api_client.async_get_root_folders() + if isinstance(root_folders, RootFolder): + root_folders = [root_folders] + return root_folders class HealthDataUpdateCoordinator(RadarrDataUpdateCoordinator[list[Health]]): @@ -87,4 +90,7 @@ class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator[int]): async def _fetch_data(self) -> int: """Fetch the movies data.""" - return len(cast(list[RadarrMovie], await self.api_client.async_get_movies())) + movies = await self.api_client.async_get_movies() + if isinstance(movies, RadarrMovie): + return 1 + return len(movies) diff --git a/tests/components/radarr/__init__.py b/tests/components/radarr/__init__.py index 7e574b1e3e0..069eeabe8d8 100644 --- a/tests/components/radarr/__init__.py +++ b/tests/components/radarr/__init__.py @@ -41,6 +41,7 @@ def mock_connection( error: bool = False, invalid_auth: bool = False, windows: bool = False, + single_return: bool = False, ) -> None: """Mock radarr connection.""" if error: @@ -75,22 +76,27 @@ def mock_connection( headers={"Content-Type": CONTENT_TYPE_JSON}, ) + root_folder_fixture = "rootfolder-linux" + if windows: - aioclient_mock.get( - f"{url}/api/v3/rootfolder", - text=load_fixture("radarr/rootfolder-windows.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - else: - aioclient_mock.get( - f"{url}/api/v3/rootfolder", - text=load_fixture("radarr/rootfolder-linux.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) + root_folder_fixture = "rootfolder-windows" + + if single_return: + root_folder_fixture = f"single-{root_folder_fixture}" + + aioclient_mock.get( + f"{url}/api/v3/rootfolder", + text=load_fixture(f"radarr/{root_folder_fixture}.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + movie_fixture = "movie" + if single_return: + movie_fixture = f"single-{movie_fixture}" aioclient_mock.get( f"{url}/api/v3/movie", - text=load_fixture("radarr/movie.json"), + text=load_fixture(f"radarr/{movie_fixture}.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) @@ -139,6 +145,7 @@ async def setup_integration( connection_error: bool = False, invalid_auth: bool = False, windows: bool = False, + single_return: bool = False, ) -> MockConfigEntry: """Set up the radarr integration in Home Assistant.""" entry = MockConfigEntry( @@ -159,6 +166,7 @@ async def setup_integration( error=connection_error, invalid_auth=invalid_auth, windows=windows, + single_return=single_return, ) if not skip_entry_setup: @@ -183,7 +191,7 @@ def patch_radarr(): def create_entry(hass: HomeAssistant) -> MockConfigEntry: - """Create Efergy entry in Home Assistant.""" + """Create Radarr entry in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, data={ diff --git a/tests/components/radarr/fixtures/single-movie.json b/tests/components/radarr/fixtures/single-movie.json new file mode 100644 index 00000000000..db9e720d285 --- /dev/null +++ b/tests/components/radarr/fixtures/single-movie.json @@ -0,0 +1,116 @@ +{ + "id": 0, + "title": "string", + "originalTitle": "string", + "alternateTitles": [ + { + "sourceType": "tmdb", + "movieId": 1, + "title": "string", + "sourceId": 0, + "votes": 0, + "voteCount": 0, + "language": { + "id": 1, + "name": "English" + }, + "id": 1 + } + ], + "sortTitle": "string", + "sizeOnDisk": 0, + "overview": "string", + "inCinemas": "2020-11-06T00:00:00Z", + "physicalRelease": "2019-03-19T00:00:00Z", + "images": [ + { + "coverType": "poster", + "url": "string", + "remoteUrl": "string" + } + ], + "website": "string", + "year": 0, + "hasFile": true, + "youTubeTrailerId": "string", + "studio": "string", + "path": "string", + "rootFolderPath": "string", + "qualityProfileId": 0, + "monitored": true, + "minimumAvailability": "announced", + "isAvailable": true, + "folderName": "string", + "runtime": 0, + "cleanTitle": "string", + "imdbId": "string", + "tmdbId": 0, + "titleSlug": "string", + "certification": "string", + "genres": ["string"], + "tags": [0], + "added": "2018-12-28T05:56:49Z", + "ratings": { + "votes": 0, + "value": 0 + }, + "movieFile": { + "movieId": 0, + "relativePath": "string", + "path": "string", + "size": 916662234, + "dateAdded": "2020-11-26T02:00:35Z", + "indexerFlags": 1, + "quality": { + "quality": { + "id": 14, + "name": "WEBRip-720p", + "source": "webrip", + "resolution": 720, + "modifier": "none" + }, + "revision": { + "version": 1, + "real": 0, + "isRepack": false + } + }, + "mediaInfo": { + "audioBitrate": 0, + "audioChannels": 2, + "audioCodec": "AAC", + "audioLanguages": "", + "audioStreamCount": 1, + "videoBitDepth": 8, + "videoBitrate": 1000000, + "videoCodec": "x264", + "videoFps": 25.0, + "resolution": "1280x534", + "runTime": "1:49:06", + "scanType": "Progressive", + "subtitles": "" + }, + "originalFilePath": "string", + "qualityCutoffNotMet": true, + "languages": [ + { + "id": 26, + "name": "Hindi" + } + ], + "edition": "", + "id": 35361 + }, + "collection": { + "name": "string", + "tmdbId": 0, + "images": [ + { + "coverType": "poster", + "url": "string", + "remoteUrl": "string" + } + ] + }, + "status": "deleted" +} diff --git a/tests/components/radarr/fixtures/single-rootfolder-linux.json b/tests/components/radarr/fixtures/single-rootfolder-linux.json new file mode 100644 index 00000000000..085467fda6a --- /dev/null +++ b/tests/components/radarr/fixtures/single-rootfolder-linux.json @@ -0,0 +1,6 @@ +{ + "path": "/downloads", + "freeSpace": 282500064232, + "unmappedFolders": [], + "id": 1 +} diff --git a/tests/components/radarr/fixtures/single-rootfolder-windows.json b/tests/components/radarr/fixtures/single-rootfolder-windows.json new file mode 100644 index 00000000000..25a93baa10d --- /dev/null +++ b/tests/components/radarr/fixtures/single-rootfolder-windows.json @@ -0,0 +1,6 @@ +{ + "path": "D:\\Downloads\\TV", + "freeSpace": 282500064232, + "unmappedFolders": [], + "id": 1 +} diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index d3dde74dcbf..f4f863d9bb6 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -1,4 +1,5 @@ """The tests for Radarr sensor platform.""" +import pytest from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT @@ -9,15 +10,43 @@ from . import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker +@pytest.mark.parametrize( + ("windows", "single", "root_folder"), + [ + ( + False, + False, + "downloads", + ), + ( + False, + True, + "downloads", + ), + ( + True, + False, + "tv", + ), + ( + True, + True, + "tv", + ), + ], +) async def test_sensors( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, entity_registry_enabled_by_default: None, + windows: bool, + single: bool, + root_folder: str, ) -> None: """Test for successfully setting up the Radarr platform.""" - await setup_integration(hass, aioclient_mock) + await setup_integration(hass, aioclient_mock, windows=windows, single_return=single) - state = hass.states.get("sensor.mock_title_disk_space_downloads") + state = hass.states.get(f"sensor.mock_title_disk_space_{root_folder}") assert state.state == "263.10" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "GB" state = hass.states.get("sensor.mock_title_movies") @@ -26,13 +55,3 @@ async def test_sensors( state = hass.states.get("sensor.mock_title_start_time") assert state.state == "2020-09-01T23:50:20+00:00" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP - - -async def test_windows( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test for successfully setting up the Radarr platform on Windows.""" - await setup_integration(hass, aioclient_mock, windows=True) - - state = hass.states.get("sensor.mock_title_disk_space_tv") - assert state.state == "263.10" From 094f2cbad733e63bff20b70caf398c9b2ade1cbb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jul 2023 09:49:02 -0700 Subject: [PATCH 0101/1151] Fix saving subclassed datetime objects in storage (#97502) --- homeassistant/helpers/json.py | 2 ++ tests/common.py | 11 +++++++++-- tests/helpers/test_json.py | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 38c23050885..33054bcb1b0 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -53,6 +53,8 @@ def json_encoder_default(obj: Any) -> Any: return obj.as_dict() if isinstance(obj, Path): return obj.as_posix() + if isinstance(obj, datetime.datetime): + return obj.isoformat() raise TypeError diff --git a/tests/common.py b/tests/common.py index 4fdccced370..542aa0afcee 100644 --- a/tests/common.py +++ b/tests/common.py @@ -67,7 +67,7 @@ from homeassistant.helpers import ( storage, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.json import JSONEncoder, _orjson_default_encoder from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.setup import setup_component from homeassistant.util.async_ import run_callback_threadsafe @@ -1260,7 +1260,14 @@ def mock_storage( # To ensure that the data can be serialized _LOGGER.debug("Writing data to %s: %s", store.key, data_to_write) raise_contains_mocks(data_to_write) - data[store.key] = json.loads(json.dumps(data_to_write, cls=store._encoder)) + encoder = store._encoder + if encoder and encoder is not JSONEncoder: + # If they pass a custom encoder that is not the + # default JSONEncoder, we use the slow path of json.dumps + dump = ft.partial(json.dumps, cls=store._encoder) + else: + dump = _orjson_default_encoder + data[store.key] = json.loads(dump(data_to_write)) async def mock_remove(store: storage.Store) -> None: """Remove data.""" diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 419122b018b..7e248c8c381 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -215,6 +215,20 @@ def test_custom_encoder(tmp_path: Path) -> None: assert data == "9" +def test_saving_subclassed_datetime(tmp_path: Path) -> None: + """Test saving subclassed datetime objects.""" + + class SubClassDateTime(datetime.datetime): + """Subclass datetime.""" + + time = SubClassDateTime.fromtimestamp(0) + + fname = tmp_path / "test6.json" + save_json(fname, {"time": time}) + data = load_json(fname) + assert data == {"time": time.isoformat()} + + def test_default_encoder_is_passed(tmp_path: Path) -> None: """Test we use orjson if they pass in the default encoder.""" fname = tmp_path / "test6.json" From 121fc7778d2573e833d36ac2a331f5f12a24223c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 31 Jul 2023 21:01:25 +0200 Subject: [PATCH 0102/1151] Bump reolink_aio to 0.7.6 + Timeout (#97464) --- homeassistant/components/reolink/__init__.py | 11 +++++------ homeassistant/components/reolink/host.py | 6 ++++-- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 2de87659919..88eec9780a1 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -9,6 +9,7 @@ import logging from typing import Literal import async_timeout +from reolink_aio.api import RETRY_ATTEMPTS from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError from homeassistant.config_entries import ConfigEntry @@ -77,15 +78,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_device_config_update() -> None: """Update the host state cache and renew the ONVIF-subscription.""" - async with async_timeout.timeout(host.api.timeout): + async with async_timeout.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): try: await host.update_states() except ReolinkError as err: - raise UpdateFailed( - f"Error updating Reolink {host.api.nvr_name}" - ) from err + raise UpdateFailed(str(err)) from err - async with async_timeout.timeout(host.api.timeout): + async with async_timeout.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): await host.renew() async def async_check_firmware_update() -> str | Literal[False]: @@ -93,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if not host.api.supported(None, "update"): return False - async with async_timeout.timeout(host.api.timeout): + async with async_timeout.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): try: return await host.api.check_new_firmware() except (ReolinkError, asyncio.exceptions.CancelledError) as err: diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index cfc702aac39..9bcafb8f00d 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -24,7 +24,7 @@ from homeassistant.helpers.network import NoURLAvailableError, get_url from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN from .exceptions import ReolinkSetupException, ReolinkWebhookException, UserNotAdmin -DEFAULT_TIMEOUT = 60 +DEFAULT_TIMEOUT = 30 FIRST_ONVIF_TIMEOUT = 10 FIRST_ONVIF_LONG_POLL_TIMEOUT = 90 SUBSCRIPTION_RENEW_THRESHOLD = 300 @@ -470,7 +470,9 @@ class ReolinkHost: await asyncio.sleep(LONG_POLL_ERROR_COOLDOWN) continue except Exception as ex: - _LOGGER.exception("Error while requesting ONVIF pull point: %s", ex) + _LOGGER.exception( + "Unexpected exception while requesting ONVIF pull point: %s", ex + ) await self._api.unsubscribe(sub_type=SubType.long_poll) raise ex diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 25994d56250..fa61f873cca 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.5"] + "requirements": ["reolink-aio==0.7.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8e3126680ee..44722351f8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2281,7 +2281,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.5 +reolink-aio==0.7.6 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ffef1a4a0a7..9e568692491 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1677,7 +1677,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.5 +reolink-aio==0.7.6 # homeassistant.components.rflink rflink==0.0.65 From a8a3b6778ef2d2cf146319d28a55d13e80a9a182 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 31 Jul 2023 21:16:58 +0200 Subject: [PATCH 0103/1151] Fix unit tests for wake_on_lan (#97542) --- tests/components/wake_on_lan/conftest.py | 19 ++++- tests/components/wake_on_lan/test_switch.py | 89 ++++++++++----------- 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/tests/components/wake_on_lan/conftest.py b/tests/components/wake_on_lan/conftest.py index 582698e39d5..5fa44f10c2c 100644 --- a/tests/components/wake_on_lan/conftest.py +++ b/tests/components/wake_on_lan/conftest.py @@ -1,7 +1,8 @@ """Test fixtures for Wake on Lan.""" from __future__ import annotations -from unittest.mock import AsyncMock, patch +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -11,3 +12,19 @@ def mock_send_magic_packet() -> AsyncMock: """Mock magic packet.""" with patch("wakeonlan.send_magic_packet") as mock_send: yield mock_send + + +@pytest.fixture +def subprocess_call_return_value() -> int | None: + """Return value for subprocess.""" + return 1 + + +@pytest.fixture(autouse=True) +def mock_subprocess_call( + subprocess_call_return_value: int, +) -> Generator[None, None, MagicMock]: + """Mock magic packet.""" + with patch("homeassistant.components.wake_on_lan.switch.sp.call") as mock_sp: + mock_sp.return_value = subprocess_call_return_value + yield mock_sp diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index 8a7fe185662..b2702ed1815 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -1,7 +1,6 @@ """The tests for the wake on lan switch platform.""" from __future__ import annotations -import subprocess from unittest.mock import AsyncMock, patch from homeassistant.components import switch @@ -38,7 +37,7 @@ async def test_valid_hostname( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): + with patch("homeassistant.components.wake_on_lan.switch.sp.call", return_value=0): await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, @@ -85,17 +84,16 @@ async def test_broadcast_config_ip_and_port( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wake_on_lan"}, - blocking=True, - ) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wake_on_lan"}, + blocking=True, + ) - mock_send_magic_packet.assert_called_with( - mac, ip_address=broadcast_address, port=port - ) + mock_send_magic_packet.assert_called_with( + mac, ip_address=broadcast_address, port=port + ) async def test_broadcast_config_ip( @@ -122,15 +120,14 @@ async def test_broadcast_config_ip( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wake_on_lan"}, - blocking=True, - ) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wake_on_lan"}, + blocking=True, + ) - mock_send_magic_packet.assert_called_with(mac, ip_address=broadcast_address) + mock_send_magic_packet.assert_called_with(mac, ip_address=broadcast_address) async def test_broadcast_config_port( @@ -151,15 +148,14 @@ async def test_broadcast_config_port( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wake_on_lan"}, - blocking=True, - ) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wake_on_lan"}, + blocking=True, + ) - mock_send_magic_packet.assert_called_with(mac, port=port) + mock_send_magic_packet.assert_called_with(mac, port=port) async def test_off_script( @@ -185,7 +181,7 @@ async def test_off_script( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): + with patch("homeassistant.components.wake_on_lan.switch.sp.call", return_value=0): await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, @@ -197,7 +193,7 @@ async def test_off_script( assert state.state == STATE_ON assert len(calls) == 0 - with patch.object(subprocess, "call", return_value=2): + with patch("homeassistant.components.wake_on_lan.switch.sp.call", return_value=1): await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_OFF, @@ -230,23 +226,22 @@ async def test_no_hostname_state( state = hass.states.get("switch.wake_on_lan") assert state.state == STATE_OFF - with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wake_on_lan"}, - blocking=True, - ) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wake_on_lan"}, + blocking=True, + ) - state = hass.states.get("switch.wake_on_lan") - assert state.state == STATE_ON + state = hass.states.get("switch.wake_on_lan") + assert state.state == STATE_ON - await hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.wake_on_lan"}, - blocking=True, - ) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wake_on_lan"}, + blocking=True, + ) - state = hass.states.get("switch.wake_on_lan") - assert state.state == STATE_OFF + state = hass.states.get("switch.wake_on_lan") + assert state.state == STATE_OFF From f0640fc057ff8f200eb35dd6c57388432e2c33ed Mon Sep 17 00:00:00 2001 From: janmolemans Date: Mon, 31 Jul 2023 22:23:32 +0200 Subject: [PATCH 0104/1151] Add frequency sensors to Nibe (#89072) * added frequency (for compressors etc) --------- Co-authored-by: Joakim Plate --- homeassistant/components/nibe_heatpump/sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py index 8aabad2c9fc..d9e89a2d56c 100644 --- a/homeassistant/components/nibe_heatpump/sensor.py +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, + UnitOfFrequency, UnitOfPower, UnitOfTemperature, UnitOfTime, @@ -110,6 +111,13 @@ UNIT_DESCRIPTIONS = { state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfTime.HOURS, ), + "Hz": SensorEntityDescription( + key="Hz", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfFrequency.HERTZ, + ), } From 3a2549829e0eec540220fe675be6d18ba2f9c116 Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 1 Aug 2023 00:45:17 -0700 Subject: [PATCH 0105/1151] Bump opower to 0.0.18 (#97548) --- 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 c054af8f43c..c0eb319c10c 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.16"] + "requirements": ["opower==0.0.18"] } diff --git a/requirements_all.txt b/requirements_all.txt index 44722351f8e..574846a9eec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1368,7 +1368,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.16 +opower==0.0.18 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e568692491..f2abd659b7f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1037,7 +1037,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.16 +opower==0.0.18 # homeassistant.components.oralb oralb-ble==0.17.6 From 5ce8e0e33e990a957c9298a90d91bbcadaf59474 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jul 2023 21:49:20 -1000 Subject: [PATCH 0106/1151] Bump HAP-python to 4.7.1 (#97545) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 19fd0b518b2..04ba4cc1a6a 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["pyhap"], "requirements": [ - "HAP-python==4.7.0", + "HAP-python==4.7.1", "fnv-hash-fast==0.4.0", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index 574846a9eec..1de1ce13613 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -26,7 +26,7 @@ CO2Signal==0.4.2 DoorBirdPy==2.1.0 # homeassistant.components.homekit -HAP-python==4.7.0 +HAP-python==4.7.1 # homeassistant.components.tasmota HATasmota==0.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2abd659b7f..5dd365df097 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -25,7 +25,7 @@ CO2Signal==0.4.2 DoorBirdPy==2.1.0 # homeassistant.components.homekit -HAP-python==4.7.0 +HAP-python==4.7.1 # homeassistant.components.tasmota HATasmota==0.6.5 From 9c6c391eeacf0862dc1d6fb138a1a8369abd3433 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 1 Aug 2023 10:03:08 +0200 Subject: [PATCH 0107/1151] Offer work- a-round for MQTT entity names that start with the device name (#97495) Co-authored-by: SukramJ Co-authored-by: Franck Nijhof --- homeassistant/components/mqtt/client.py | 26 ++++++ homeassistant/components/mqtt/mixins.py | 42 ++++++++- homeassistant/components/mqtt/models.py | 1 + homeassistant/components/mqtt/strings.json | 16 ++++ tests/components/mqtt/test_mixins.py | 99 ++++++++++++++++++++-- 5 files changed, 173 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index e8eabe887f2..07fbc0ca8c5 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -36,6 +36,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -64,6 +65,7 @@ from .const import ( DEFAULT_WILL, DEFAULT_WS_HEADERS, DEFAULT_WS_PATH, + DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, PROTOCOL_5, @@ -93,6 +95,10 @@ SUBSCRIBE_COOLDOWN = 0.1 UNSUBSCRIBE_COOLDOWN = 0.1 TIMEOUT_ACK = 10 +MQTT_ENTRIES_NAMING_BLOG_URL = ( + "https://developers.home-assistant.io/blog/2023-057-21-change-naming-mqtt-entities/" +) + SubscribePayloadType = str | bytes # Only bytes if encoding is None @@ -404,6 +410,7 @@ class MQTT: @callback def ha_started(_: Event) -> None: + self.register_naming_issues() self._ha_started.set() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) @@ -416,6 +423,25 @@ class MQTT: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) ) + def register_naming_issues(self) -> None: + """Register issues with MQTT entity naming.""" + mqtt_data = get_mqtt_data(self.hass) + for issue_key, items in mqtt_data.issues.items(): + config_list = "\n".join([f"- {item}" for item in items]) + async_create_issue( + self.hass, + DOMAIN, + issue_key, + breaks_in_ha_version="2024.2.0", + is_fixable=False, + translation_key=issue_key, + translation_placeholders={ + "config": config_list, + }, + learn_more_url=MQTT_ENTRIES_NAMING_BLOG_URL, + severity=IssueSeverity.WARNING, + ) + def start( self, mqtt_data: MqttData, diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 9f0849a4d4c..70156703155 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -1014,6 +1014,7 @@ class MqttEntity( _attr_should_poll = False _default_name: str | None _entity_id_format: str + _issue_key: str | None def __init__( self, @@ -1027,6 +1028,7 @@ class MqttEntity( self._config: ConfigType = config self._attr_unique_id = config.get(CONF_UNIQUE_ID) self._sub_state: dict[str, EntitySubscription] = {} + self._discovery = discovery_data is not None # Load config self._setup_from_config(self._config) @@ -1050,6 +1052,7 @@ class MqttEntity( @final async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" + self.collect_issues() await super().async_added_to_hass() self._prepare_subscribe_topics() await self._subscribe_topics() @@ -1122,6 +1125,7 @@ class MqttEntity( def _set_entity_name(self, config: ConfigType) -> None: """Help setting the entity name if needed.""" + self._issue_key = None entity_name: str | None | UndefinedType = config.get(CONF_NAME, UNDEFINED) # Only set _attr_name if it is needed if entity_name is not UNDEFINED: @@ -1130,6 +1134,7 @@ class MqttEntity( # Assign the default name self._attr_name = self._default_name if CONF_DEVICE in config: + device_name: str if CONF_NAME not in config[CONF_DEVICE]: _LOGGER.info( "MQTT device information always needs to include a name, got %s, " @@ -1137,14 +1142,47 @@ class MqttEntity( "name must be included in each entity's device configuration", config, ) - elif config[CONF_DEVICE][CONF_NAME] == entity_name: + elif (device_name := config[CONF_DEVICE][CONF_NAME]) == entity_name: + self._attr_name = None + self._issue_key = ( + "entity_name_is_device_name_discovery" + if self._discovery + else "entity_name_is_device_name_yaml" + ) _LOGGER.warning( "MQTT device name is equal to entity name in your config %s, " "this is not expected. Please correct your configuration. " "The entity name will be set to `null`", config, ) - self._attr_name = None + elif isinstance(entity_name, str) and entity_name.startswith(device_name): + self._attr_name = ( + new_entity_name := entity_name[len(device_name) :].lstrip() + ) + if device_name[:1].isupper(): + # Ensure a capital if the device name first char is a capital + new_entity_name = new_entity_name[:1].upper() + new_entity_name[1:] + self._issue_key = ( + "entity_name_startswith_device_name_discovery" + if self._discovery + else "entity_name_startswith_device_name_yaml" + ) + _LOGGER.warning( + "MQTT entity name starts with the device name in your config %s, " + "this is not expected. Please correct your configuration. " + "The device name prefix will be stripped off the entity name " + "and becomes '%s'", + config, + new_entity_name, + ) + + def collect_issues(self) -> None: + """Process issues for MQTT entities.""" + if self._issue_key is None: + return + mqtt_data = get_mqtt_data(self.hass) + issues = mqtt_data.issues.setdefault(self._issue_key, set()) + issues.add(self.entity_id) def _setup_common_attributes_from_config(self, config: ConfigType) -> None: """(Re)Setup the common attributes for the entity.""" diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 5a966a4455c..9afa3de3f48 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -305,6 +305,7 @@ class MqttData: ) discovery_unsubscribe: list[CALLBACK_TYPE] = field(default_factory=list) integration_unsubscribe: dict[str, CALLBACK_TYPE] = field(default_factory=dict) + issues: dict[str, set[str]] = field(default_factory=dict) last_discovery: float = 0.0 reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field( diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index f314ddd47d3..55677798a08 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -7,6 +7,22 @@ "deprecation_mqtt_legacy_vacuum_discovery": { "title": "MQTT vacuum entities with legacy schema added through MQTT discovery", "description": "MQTT vacuum entities that use the legacy schema are deprecated, please adjust your devices to use the correct schema and restart Home Assistant to fix this issue." + }, + "entity_name_is_device_name_yaml": { + "title": "Manual configured MQTT entities with a name that is equal to the device name", + "description": "Some MQTT entities have an entity name equal to the device name. This is not expected. The entity name is set to `null` as a work-a-round to avoid a duplicate name. Please update your configuration and restart Home Assistant to fix this issue.\n\nList of affected entities:\n\n{config}" + }, + "entity_name_startswith_device_name_yaml": { + "title": "Manual configured MQTT entities with a name that starts with the device name", + "description": "Some MQTT entities have an entity name that starts with the device name. This is not expected. To avoid a duplicate name the device name prefix is stripped of the entity name as a work-a-round. Please update your configuration and restart Home Assistant to fix this issue. \n\nList of affected entities:\n\n{config}" + }, + "entity_name_is_device_name_discovery": { + "title": "Discovered MQTT entities with a name that is equal to the device name", + "description": "Some MQTT entities have an entity name equal to the device name. This is not expected. The entity name is set to `null` as a work-a-round to avoid a duplicate name. Please inform the maintainer of the software application that supplies the affected entities to fix this issue.\n\nList of affected entities:\n\n{config}" + }, + "entity_name_startswith_device_name_discovery": { + "title": "Discovered entities with a name that starts with the device name", + "description": "Some MQTT entities have an entity name that starts with the device name. This is not expected. To avoid a duplicate name the device name prefix is stripped of the entity name as a work-a-round. Please inform the maintainer of the software application that supplies the affected entities to fix this issue. \n\nList of affected entities:\n\n{config}" } }, "config": { diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index 23367d7829f..18269eb6970 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -6,14 +6,19 @@ import pytest from homeassistant.components import mqtt, sensor from homeassistant.components.mqtt.sensor import DEFAULT_NAME as DEFAULT_SENSOR_NAME -from homeassistant.const import EVENT_STATE_CHANGED, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, + EVENT_STATE_CHANGED, + Platform, +) +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import ( device_registry as dr, + issue_registry as ir, ) -from tests.common import async_fire_mqtt_message -from tests.typing import MqttMockHAClientGenerator +from tests.common import MockConfigEntry, async_capture_events, async_fire_mqtt_message +from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient @pytest.mark.parametrize( @@ -80,7 +85,14 @@ async def test_availability_with_shared_state_topic( @pytest.mark.parametrize( - ("hass_config", "entity_id", "friendly_name", "device_name", "assert_log"), + ( + "hass_config", + "entity_id", + "friendly_name", + "device_name", + "assert_log", + "issue_events", + ), [ ( # default_entity_name_without_device_name { @@ -96,6 +108,7 @@ async def test_availability_with_shared_state_topic( DEFAULT_SENSOR_NAME, None, True, + 0, ), ( # default_entity_name_with_device_name { @@ -111,6 +124,7 @@ async def test_availability_with_shared_state_topic( "Test MQTT Sensor", "Test", False, + 0, ), ( # name_follows_device_class { @@ -127,6 +141,7 @@ async def test_availability_with_shared_state_topic( "Test Humidity", "Test", False, + 0, ), ( # name_follows_device_class_without_device_name { @@ -143,6 +158,7 @@ async def test_availability_with_shared_state_topic( "Humidity", None, True, + 0, ), ( # name_overrides_device_class { @@ -160,6 +176,7 @@ async def test_availability_with_shared_state_topic( "Test MySensor", "Test", False, + 0, ), ( # name_set_no_device_name_set { @@ -177,6 +194,7 @@ async def test_availability_with_shared_state_topic( "MySensor", None, True, + 0, ), ( # none_entity_name_with_device_name { @@ -194,6 +212,7 @@ async def test_availability_with_shared_state_topic( "Test", "Test", False, + 0, ), ( # none_entity_name_without_device_name { @@ -211,8 +230,9 @@ async def test_availability_with_shared_state_topic( "mqtt veryunique", None, True, + 0, ), - ( # entity_name_and_device_name_the_sane + ( # entity_name_and_device_name_the_same { mqtt.DOMAIN: { sensor.DOMAIN: { @@ -231,6 +251,49 @@ async def test_availability_with_shared_state_topic( "Hello world", "Hello world", False, + 1, + ), + ( # entity_name_startswith_device_name1 + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "World automation", + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": { + "identifiers": ["helloworld"], + "name": "World", + }, + } + } + }, + "sensor.world_automation", + "World automation", + "World", + False, + 1, + ), + ( # entity_name_startswith_device_name2 + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "world automation", + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": { + "identifiers": ["helloworld"], + "name": "world", + }, + } + } + }, + "sensor.world_automation", + "world automation", + "world", + False, + 1, ), ], ids=[ @@ -242,24 +305,39 @@ async def test_availability_with_shared_state_topic( "name_set_no_device_name_set", "none_entity_name_with_device_name", "none_entity_name_without_device_name", - "entity_name_and_device_name_the_sane", + "entity_name_and_device_name_the_same", + "entity_name_startswith_device_name1", + "entity_name_startswith_device_name2", ], ) @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) async def test_default_entity_and_device_name( hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, + mqtt_client_mock: MqttMockPahoClient, + mqtt_config_entry_data, caplog: pytest.LogCaptureFixture, entity_id: str, friendly_name: str, device_name: str | None, assert_log: bool, + issue_events: int, ) -> None: """Test device name setup with and without a device_class set. This is a test helper for the _setup_common_attributes_from_config mixin. """ - await mqtt_mock_entry() + # mqtt_mock = await mqtt_mock_entry() + + events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED) + hass.state = CoreState.starting + await hass.async_block_till_done() + + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "mock-broker"}) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() registry = dr.async_get(hass) @@ -274,3 +352,6 @@ async def test_default_entity_and_device_name( assert ( "MQTT device information always needs to include a name" in caplog.text ) is assert_log + + # Assert that an issues ware registered + assert len(events) == issue_events From 5aa3e367547dd3e57becd87e82c7dddd71d2f11e Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Tue, 1 Aug 2023 03:04:04 -0500 Subject: [PATCH 0108/1151] Bump life360 package to 6.0.0 (#97549) --- homeassistant/components/life360/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index bfecce8d3ed..18b83013d70 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/life360", "iot_class": "cloud_polling", "loggers": ["life360"], - "requirements": ["life360==5.5.0"] + "requirements": ["life360==6.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1de1ce13613..0bde0bfc52b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1129,7 +1129,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.5.0 +life360==6.0.0 # homeassistant.components.osramlightify lightify==1.0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5dd365df097..6d357a0dfd4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -879,7 +879,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.5.0 +life360==6.0.0 # homeassistant.components.logi_circle logi-circle==0.2.3 From 8ad37d7640067613fd80020724fa65d89966a7d1 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 1 Aug 2023 03:05:01 -0500 Subject: [PATCH 0109/1151] Send language to Wyoming STT (#97344) --- homeassistant/components/wyoming/stt.py | 7 ++++++- tests/components/wyoming/conftest.py | 14 ++++++++++++++ tests/components/wyoming/snapshots/test_stt.ambr | 7 +++++++ tests/components/wyoming/test_stt.py | 16 ++++++++++------ 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/wyoming/stt.py b/homeassistant/components/wyoming/stt.py index 3f5487881a3..e64a2f14667 100644 --- a/homeassistant/components/wyoming/stt.py +++ b/homeassistant/components/wyoming/stt.py @@ -2,7 +2,7 @@ from collections.abc import AsyncIterable import logging -from wyoming.asr import Transcript +from wyoming.asr import Transcribe, Transcript from wyoming.audio import AudioChunk, AudioStart, AudioStop from wyoming.client import AsyncTcpClient @@ -89,6 +89,10 @@ class WyomingSttProvider(stt.SpeechToTextEntity): """Process an audio stream to STT service.""" try: async with AsyncTcpClient(self.service.host, self.service.port) as client: + # Set transcription language + await client.write_event(Transcribe(language=metadata.language).event()) + + # Begin audio stream await client.write_event( AudioStart( rate=SAMPLE_RATE, @@ -106,6 +110,7 @@ class WyomingSttProvider(stt.SpeechToTextEntity): ) await client.write_event(chunk.event()) + # End audio stream await client.write_event(AudioStop().event()) while True: diff --git a/tests/components/wyoming/conftest.py b/tests/components/wyoming/conftest.py index 0dd9041a0d5..6b4e705914f 100644 --- a/tests/components/wyoming/conftest.py +++ b/tests/components/wyoming/conftest.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch import pytest +from homeassistant.components import stt from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -69,3 +70,16 @@ async def init_wyoming_tts(hass: HomeAssistant, tts_config_entry: ConfigEntry): return_value=TTS_INFO, ): await hass.config_entries.async_setup(tts_config_entry.entry_id) + + +@pytest.fixture +def metadata(hass: HomeAssistant) -> stt.SpeechMetadata: + """Get default STT metadata.""" + return stt.SpeechMetadata( + language=hass.config.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, + ) diff --git a/tests/components/wyoming/snapshots/test_stt.ambr b/tests/components/wyoming/snapshots/test_stt.ambr index 08fe6a1ef8e..784f89b2ab8 100644 --- a/tests/components/wyoming/snapshots/test_stt.ambr +++ b/tests/components/wyoming/snapshots/test_stt.ambr @@ -1,6 +1,13 @@ # serializer version: 1 # name: test_streaming_audio list([ + dict({ + 'data': dict({ + 'language': 'en', + }), + 'payload': None, + 'type': 'transcibe', + }), dict({ 'data': dict({ 'channels': 1, diff --git a/tests/components/wyoming/test_stt.py b/tests/components/wyoming/test_stt.py index 021419f3a5e..1938d44d310 100644 --- a/tests/components/wyoming/test_stt.py +++ b/tests/components/wyoming/test_stt.py @@ -27,7 +27,9 @@ async def test_support(hass: HomeAssistant, init_wyoming_stt) -> None: assert entity.supported_channels == [stt.AudioChannels.CHANNEL_MONO] -async def test_streaming_audio(hass: HomeAssistant, init_wyoming_stt, snapshot) -> None: +async def test_streaming_audio( + hass: HomeAssistant, init_wyoming_stt, metadata, snapshot +) -> None: """Test streaming audio.""" entity = stt.async_get_speech_to_text_entity(hass, "stt.test_asr") assert entity is not None @@ -40,7 +42,7 @@ async def test_streaming_audio(hass: HomeAssistant, init_wyoming_stt, snapshot) "homeassistant.components.wyoming.stt.AsyncTcpClient", MockAsyncTcpClient([Transcript(text="Hello world").event()]), ) as mock_client: - result = await entity.async_process_audio_stream(None, audio_stream()) + result = await entity.async_process_audio_stream(metadata, audio_stream()) assert result.result == stt.SpeechResultState.SUCCESS assert result.text == "Hello world" @@ -48,7 +50,7 @@ async def test_streaming_audio(hass: HomeAssistant, init_wyoming_stt, snapshot) async def test_streaming_audio_connection_lost( - hass: HomeAssistant, init_wyoming_stt + hass: HomeAssistant, init_wyoming_stt, metadata ) -> None: """Test streaming audio and losing connection.""" entity = stt.async_get_speech_to_text_entity(hass, "stt.test_asr") @@ -61,13 +63,15 @@ async def test_streaming_audio_connection_lost( "homeassistant.components.wyoming.stt.AsyncTcpClient", MockAsyncTcpClient([None]), ): - result = await entity.async_process_audio_stream(None, audio_stream()) + result = await entity.async_process_audio_stream(metadata, audio_stream()) assert result.result == stt.SpeechResultState.ERROR assert result.text is None -async def test_streaming_audio_oserror(hass: HomeAssistant, init_wyoming_stt) -> None: +async def test_streaming_audio_oserror( + hass: HomeAssistant, init_wyoming_stt, metadata +) -> None: """Test streaming audio and error raising.""" entity = stt.async_get_speech_to_text_entity(hass, "stt.test_asr") assert entity is not None @@ -81,7 +85,7 @@ async def test_streaming_audio_oserror(hass: HomeAssistant, init_wyoming_stt) -> "homeassistant.components.wyoming.stt.AsyncTcpClient", mock_client, ), patch.object(mock_client, "read_event", side_effect=OSError("Boom!")): - result = await entity.async_process_audio_stream(None, audio_stream()) + result = await entity.async_process_audio_stream(metadata, audio_stream()) assert result.result == stt.SpeechResultState.ERROR assert result.text is None From 122794ada8628359ad66ebb5f67117dd6819bf1d Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Tue, 1 Aug 2023 20:06:19 +1200 Subject: [PATCH 0110/1151] Fix Starlink ping drop rate reporting (#97555) --- homeassistant/components/starlink/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/starlink/sensor.py b/homeassistant/components/starlink/sensor.py index efcf92600b8..ab76a8dffdd 100644 --- a/homeassistant/components/starlink/sensor.py +++ b/homeassistant/components/starlink/sensor.py @@ -130,6 +130,6 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( translation_key="ping_drop_rate", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, - value_fn=lambda data: data.status["pop_ping_drop_rate"], + value_fn=lambda data: data.status["pop_ping_drop_rate"] * 100, ), ) From 8b8a51ffc880f612bf81aa49441b0878eddf1424 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 1 Aug 2023 01:08:08 -0700 Subject: [PATCH 0111/1151] Bump pywemo to 1.2.1 (#97550) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 3dbd8aa32bc..cb189116eeb 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -9,7 +9,7 @@ }, "iot_class": "local_push", "loggers": ["pywemo"], - "requirements": ["pywemo==1.2.0"], + "requirements": ["pywemo==1.2.1"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 0bde0bfc52b..e5384749bd7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2227,7 +2227,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==1.2.0 +pywemo==1.2.1 # homeassistant.components.wilight pywilight==0.0.74 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d357a0dfd4..33125b7e37f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1638,7 +1638,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==1.2.0 +pywemo==1.2.1 # homeassistant.components.wilight pywilight==0.0.74 From 7e134a3d4416358efeb2c519013eb443e1b9a8d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Jul 2023 22:27:25 -1000 Subject: [PATCH 0112/1151] Cleanups to the Bluetooth processor coordinators (#97546) --- .../bluetooth/passive_update_processor.py | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index c553e2469eb..29634c9a18c 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -13,11 +13,15 @@ from .const import DOMAIN from .update_coordinator import BasePassiveBluetoothCoordinator if TYPE_CHECKING: - from collections.abc import Callable, Mapping + from collections.abc import Callable from homeassistant.helpers.entity_platform import AddEntitiesCallback - from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak + from .models import ( + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, + ) @dataclasses.dataclass(slots=True, frozen=True) @@ -41,16 +45,23 @@ class PassiveBluetoothDataUpdate(Generic[_T]): """Generic bluetooth data.""" devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict) - entity_descriptions: Mapping[ + entity_descriptions: dict[ PassiveBluetoothEntityKey, EntityDescription ] = dataclasses.field(default_factory=dict) - entity_names: Mapping[PassiveBluetoothEntityKey, str | None] = dataclasses.field( + entity_names: dict[PassiveBluetoothEntityKey, str | None] = dataclasses.field( default_factory=dict ) - entity_data: Mapping[PassiveBluetoothEntityKey, _T] = dataclasses.field( + entity_data: dict[PassiveBluetoothEntityKey, _T] = dataclasses.field( default_factory=dict ) + def update(self, new_data: PassiveBluetoothDataUpdate[_T]) -> None: + """Update the data.""" + self.devices.update(new_data.devices) + self.entity_descriptions.update(new_data.entity_descriptions) + self.entity_data.update(new_data.entity_data) + self.entity_names.update(new_data.entity_names) + class PassiveBluetoothProcessorCoordinator( Generic[_T], BasePassiveBluetoothCoordinator @@ -87,10 +98,11 @@ class PassiveBluetoothProcessorCoordinator( @callback def async_register_processor( - self, processor: PassiveBluetoothDataProcessor + self, + processor: PassiveBluetoothDataProcessor, ) -> Callable[[], None]: """Register a processor that subscribes to updates.""" - processor.coordinator = self + processor.async_register_coordinator(self) @callback def remove_processor() -> None: @@ -155,7 +167,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): The processor will call the update_method every time the bluetooth device receives a new advertisement data from the coordinator with the data - returned by he update_method of the coordinator. + returned by the update_method of the coordinator. As the size of each advertisement is limited, the update_method should return a PassiveBluetoothDataUpdate object that contains only data that @@ -165,13 +177,17 @@ class PassiveBluetoothDataProcessor(Generic[_T]): """ coordinator: PassiveBluetoothProcessorCoordinator + data: PassiveBluetoothDataUpdate[_T] + entity_names: dict[PassiveBluetoothEntityKey, str | None] + entity_data: dict[PassiveBluetoothEntityKey, _T] + entity_descriptions: dict[PassiveBluetoothEntityKey, EntityDescription] + devices: dict[str | None, DeviceInfo] def __init__( self, update_method: Callable[[_T], PassiveBluetoothDataUpdate[_T]], ) -> None: """Initialize the coordinator.""" - self.coordinator: PassiveBluetoothProcessorCoordinator self._listeners: list[ Callable[[PassiveBluetoothDataUpdate[_T] | None], None] ] = [] @@ -180,14 +196,22 @@ class PassiveBluetoothDataProcessor(Generic[_T]): list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], ] = {} self.update_method = update_method - self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} - self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} - self.entity_descriptions: dict[ - PassiveBluetoothEntityKey, EntityDescription - ] = {} - self.devices: dict[str | None, DeviceInfo] = {} self.last_update_success = True + @callback + def async_register_coordinator( + self, + coordinator: PassiveBluetoothProcessorCoordinator, + ) -> None: + """Register a coordinator.""" + self.coordinator = coordinator + self.data = PassiveBluetoothDataUpdate() + data = self.data + self.entity_names = data.entity_names + self.entity_data = data.entity_data + self.entity_descriptions = data.entity_descriptions + self.devices = data.devices + @property def available(self) -> bool: """Return if the device is available.""" @@ -296,10 +320,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): "Processing %s data recovered", self.coordinator.name ) - self.devices.update(new_data.devices) - self.entity_descriptions.update(new_data.entity_descriptions) - self.entity_data.update(new_data.entity_data) - self.entity_names.update(new_data.entity_names) + self.data.update(new_data) self.async_update_listeners(new_data) From 3f5c0a1285d865d4a472ba569f9d44f8dab501b5 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Tue, 1 Aug 2023 10:04:30 +0100 Subject: [PATCH 0113/1151] Fixes London Air parsing error (#97557) --- homeassistant/components/london_air/sensor.py | 10 ++++++---- tests/fixtures/london_air.json | 8 ++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index e970f040b5f..98cc4c4b4e8 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -218,10 +218,12 @@ def parse_api_response(response): for authority in AUTHORITIES: for entry in response["HourlyAirQualityIndex"]["LocalAuthority"]: if entry["@LocalAuthorityName"] == authority: - if isinstance(entry["Site"], dict): - entry_sites_data = [entry["Site"]] - else: - entry_sites_data = entry["Site"] + entry_sites_data = [] + if "Site" in entry: + if isinstance(entry["Site"], dict): + entry_sites_data = [entry["Site"]] + else: + entry_sites_data = entry["Site"] data[authority] = parse_site(entry_sites_data) diff --git a/tests/fixtures/london_air.json b/tests/fixtures/london_air.json index 3a3d9afb643..7045a90e6e9 100644 --- a/tests/fixtures/london_air.json +++ b/tests/fixtures/london_air.json @@ -3,6 +3,14 @@ "@GroupName": "London", "@TimeToLive": "38", "LocalAuthority": [ + { + "@LocalAuthorityCode": "7", + "@LocalAuthorityName": "City of London", + "@LaCentreLatitude": "51.51333", + "@LaCentreLongitude": "-0.088947", + "@LaCentreLatitudeWGS84": "6712603.132989", + "@LaCentreLongitudeWGS84": "-9901.534748" + }, { "@LocalAuthorityCode": "24", "@LocalAuthorityName": "Merton", From 1773f1ead2018ff48a727bfeb32ec7d0852b27bc Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 1 Aug 2023 11:46:37 +0200 Subject: [PATCH 0114/1151] Update frontend to 20230801.0 (#97561) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 47e742bdb76..2210a44039e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230725.0"] + "requirements": ["home-assistant-frontend==20230801.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 04c0b0fd44f..3a887804470 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.69.0 hassil==1.2.5 home-assistant-bluetooth==1.10.2 -home-assistant-frontend==20230725.0 +home-assistant-frontend==20230801.0 home-assistant-intents==2023.7.25 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index e5384749bd7..bdc304d6185 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -988,7 +988,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230725.0 +home-assistant-frontend==20230801.0 # homeassistant.components.conversation home-assistant-intents==2023.7.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33125b7e37f..18cde278e25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -774,7 +774,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230725.0 +home-assistant-frontend==20230801.0 # homeassistant.components.conversation home-assistant-intents==2023.7.25 From b20a286b5bbc70e48565f8bc390e1b20b37e07e6 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 1 Aug 2023 14:39:31 +0200 Subject: [PATCH 0115/1151] Bump pyduotecno to 2023.8.0 (beta fix) (#97564) * Bump pyduotecno to 2023.7.4 * Bump to 2023.8.0 --- homeassistant/components/duotecno/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index a630a3dedbd..3089e3b515b 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/duotecno", "iot_class": "local_push", - "requirements": ["pyduotecno==2023.7.3"] + "requirements": ["pyduotecno==2023.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index bdc304d6185..e50cec4c222 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1650,7 +1650,7 @@ pydrawise==2023.7.1 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.7.3 +pyduotecno==2023.8.0 # homeassistant.components.ebox pyebox==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18cde278e25..89e947e699c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1223,7 +1223,7 @@ pydiscovergy==2.0.3 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.7.3 +pyduotecno==2023.8.0 # homeassistant.components.econet pyeconet==0.1.20 From 708b00d7ab1e9c4c449775292ff8fe471851b431 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Aug 2023 09:08:12 -1000 Subject: [PATCH 0116/1151] Use legacy rules for ESPHome entity_id construction if `friendly_name` is unset (#97578) --- homeassistant/components/esphome/entity.py | 23 ++++++++++++++++++---- tests/components/esphome/test_entity.py | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index b308d8dc08c..c35b4dc9b13 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -161,14 +161,29 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): assert entry_data.device_info is not None device_info = entry_data.device_info self._device_info = device_info - if object_id := entity_info.object_id: - # Use the object_id to suggest the entity_id - self.entity_id = f"{domain}.{device_info.name}_{object_id}" self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)} ) self._entry_id = entry_data.entry_id - self._attr_has_entity_name = bool(device_info.friendly_name) + # + # If `friendly_name` is set, we use the Friendly naming rules, if + # `friendly_name` is not set we make an exception to the naming rules for + # backwards compatibility and use the Legacy naming rules. + # + # Friendly naming + # - Friendly name is prepended to entity names + # - Device Name is prepended to entity ids + # - Entity id is constructed from device name and object id + # + # Legacy naming + # - Device name is not prepended to entity names + # - Device name is not prepended to entity ids + # - Entity id is constructed from entity name + # + if not device_info.friendly_name: + return + self._attr_has_entity_name = True + self.entity_id = f"{domain}.{device_info.name}_{entity_info.object_id}" async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index e55d4583275..ac121a93eff 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -216,6 +216,6 @@ async def test_esphome_device_without_friendly_name( states=states, device_info={"friendly_name": None}, ) - state = hass.states.get("binary_sensor.test_mybinary_sensor") + state = hass.states.get("binary_sensor.my_binary_sensor") assert state is not None assert state.state == STATE_ON From 6c95e07b7dab861731f39f131ed0a729374eb824 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 1 Aug 2023 21:18:33 +0200 Subject: [PATCH 0117/1151] Fix UniFi image platform failing to setup on read-only account (#97580) --- homeassistant/components/unifi/image.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index 25c368880fa..dc4fb93eded 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -83,6 +83,10 @@ async def async_setup_entry( ) -> None: """Set up image platform for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + + if controller.site_role != "admin": + return + controller.register_platform_add_entities( UnifiImageEntity, ENTITY_DESCRIPTIONS, async_add_entities ) From 7096daa0c1f87c542b89bd309e871772f2354704 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 1 Aug 2023 21:31:45 +0200 Subject: [PATCH 0118/1151] Unignore today's collection for Rova (#97567) --- homeassistant/components/rova/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 21effd3da3a..f68ffbd0eaf 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle -from homeassistant.util.dt import get_time_zone, now +from homeassistant.util.dt import get_time_zone # Config for rova requests. CONF_ZIP_CODE = "zip_code" @@ -150,8 +150,7 @@ class RovaData: tzinfo=get_time_zone("Europe/Amsterdam") ) code = item["GarbageTypeCode"].lower() - - if code not in self.data and date > now(): + if code not in self.data: self.data[code] = date _LOGGER.debug("Updated Rova calendar: %s", self.data) From cad845f5c976c9cb58c54553f6d1eda7d1846346 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 1 Aug 2023 22:26:36 +0200 Subject: [PATCH 0119/1151] Bump zha-quirks to 0.0.102 (#97588) --- 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 874990402fc..5d0fdc646cf 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -23,7 +23,7 @@ "bellows==0.35.8", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.101", + "zha-quirks==0.0.102", "zigpy-deconz==0.21.0", "zigpy==0.56.3", "zigpy-xbee==0.18.1", diff --git a/requirements_all.txt b/requirements_all.txt index e50cec4c222..32a8ed34f54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2758,7 +2758,7 @@ zeroconf==0.71.4 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.101 +zha-quirks==0.0.102 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89e947e699c..2df563a56bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2031,7 +2031,7 @@ zeroconf==0.71.4 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.101 +zha-quirks==0.0.102 # homeassistant.components.zha zigpy-deconz==0.21.0 From 33e5e3c5c2ba940b01e7151de028505c978c1a18 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Aug 2023 22:29:16 +0200 Subject: [PATCH 0120/1151] Ensure we have an valid configuration URL in NetGear (#97590) --- homeassistant/components/netgear/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index ef31a887691..522b60749d0 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -62,6 +62,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) + configuration_url = None + if host := entry.data[CONF_HOST]: + configuration_url = f"http://{host}/" + assert entry.unique_id device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -72,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: model=router.model, sw_version=router.firmware_version, hw_version=router.hardware_version, - configuration_url=f"http://{entry.data[CONF_HOST]}/", + configuration_url=configuration_url, ) async def async_update_devices() -> bool: From af5fc7e759d52c9357fc9d6d8cb7d578123011a9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Aug 2023 23:15:31 +0200 Subject: [PATCH 0121/1151] Ensure load the device registry if it contains invalid configuration URLs (#97589) --- homeassistant/helpers/device_registry.py | 9 ++++-- tests/helpers/test_device_registry.py | 41 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 5764f65957e..4dd9233c6ab 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -198,9 +198,7 @@ class DeviceEntry: area_id: str | None = attr.ib(default=None) config_entries: set[str] = attr.ib(converter=set, factory=set) - configuration_url: str | URL | None = attr.ib( - converter=_validate_configuration_url, default=None - ) + configuration_url: str | None = attr.ib(default=None) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) disabled_by: DeviceEntryDisabler | None = attr.ib(default=None) entry_type: DeviceEntryType | None = attr.ib(default=None) @@ -482,6 +480,8 @@ class DeviceRegistry: via_device: tuple[str, str] | None | UndefinedType = UNDEFINED, ) -> DeviceEntry: """Get device. Create if it doesn't exist.""" + if configuration_url is not UNDEFINED: + configuration_url = _validate_configuration_url(configuration_url) # Reconstruct a DeviceInfo dict from the arguments. # When we upgrade to Python 3.12, we can change this method to instead @@ -681,6 +681,9 @@ class DeviceRegistry: new_values["identifiers"] = new_identifiers old_values["identifiers"] = old.identifiers + if configuration_url is not UNDEFINED: + configuration_url = _validate_configuration_url(configuration_url) + for attr_name, value in ( ("area_id", area_id), ("configuration_url", configuration_url), diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 0210d7ba75d..9ebee025bd5 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1730,3 +1730,44 @@ async def test_device_info_configuration_url_validation( device_registry.async_update_device( update_device.id, configuration_url=configuration_url ) + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_loading_invalid_configuration_url_from_storage( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> None: + """Test loading stored devices with an invalid URL.""" + hass_storage[dr.STORAGE_KEY] = { + "version": dr.STORAGE_VERSION_MAJOR, + "minor_version": dr.STORAGE_VERSION_MINOR, + "data": { + "devices": [ + { + "area_id": None, + "config_entries": ["1234"], + "configuration_url": "invalid", + "connections": [], + "disabled_by": None, + "entry_type": dr.DeviceEntryType.SERVICE, + "hw_version": None, + "id": "abcdefghijklm", + "identifiers": [["serial", "12:34:56:AB:CD:EF"]], + "manufacturer": None, + "model": None, + "name_by_user": None, + "name": None, + "sw_version": None, + "via_device_id": None, + } + ], + "deleted_devices": [], + }, + } + + await dr.async_load(hass) + 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")} + ) + assert entry.configuration_url == "invalid" From 7a92bda514bd8cb62c7306d8b2d4a082d4ae64ff Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Wed, 2 Aug 2023 18:12:49 +1200 Subject: [PATCH 0122/1151] Fix Starlink Roaming name being blank (#97597) --- homeassistant/components/starlink/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/starlink/strings.json b/homeassistant/components/starlink/strings.json index aa89d87b6be..a9e50f5d39f 100644 --- a/homeassistant/components/starlink/strings.json +++ b/homeassistant/components/starlink/strings.json @@ -16,7 +16,7 @@ }, "entity": { "binary_sensor": { - "roaming_mode": { + "roaming": { "name": "Roaming mode" }, "currently_obstructed": { From 93d7165fe9a7b98a92010909196b9d1dcd1d4cd3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Aug 2023 20:19:22 -1000 Subject: [PATCH 0123/1151] Bump zeroconf to 0.72.0 (#97594) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 92daffc6c8b..73ebe15d0c7 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.71.4"] + "requirements": ["zeroconf==0.72.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3a887804470..076e534a6b0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.71.4 +zeroconf==0.72.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 32a8ed34f54..2b30ee02eaf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2752,7 +2752,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.71.4 +zeroconf==0.72.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2df563a56bb..4c062ca0003 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2025,7 +2025,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.71.4 +zeroconf==0.72.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From ac9ec6e402e9526788cdd4ffd37f57e0de5e8c26 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 2 Aug 2023 08:23:37 +0200 Subject: [PATCH 0124/1151] Bump pyDuotecno to 2023.8.1 (#97583) --- homeassistant/components/duotecno/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index 3089e3b515b..0edc9ba7f8c 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/duotecno", "iot_class": "local_push", - "requirements": ["pyduotecno==2023.8.0"] + "requirements": ["pyduotecno==2023.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b30ee02eaf..89bfcc924f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1650,7 +1650,7 @@ pydrawise==2023.7.1 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.8.0 +pyduotecno==2023.8.1 # homeassistant.components.ebox pyebox==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c062ca0003..d47d5224e52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1223,7 +1223,7 @@ pydiscovergy==2.0.3 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.8.0 +pyduotecno==2023.8.1 # homeassistant.components.econet pyeconet==0.1.20 From 49b9dd2a4fa7813d9368e3f8f9e462e75a3f9663 Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Wed, 2 Aug 2023 18:26:50 +1200 Subject: [PATCH 0125/1151] Add Starlink to .strict-typing (#97598) --- .strict-typing | 1 + homeassistant/components/starlink/config_flow.py | 1 + homeassistant/components/starlink/coordinator.py | 4 ++-- mypy.ini | 10 ++++++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.strict-typing b/.strict-typing index dffeb08e014..eec8bd906fe 100644 --- a/.strict-typing +++ b/.strict-typing @@ -296,6 +296,7 @@ homeassistant.components.sonarr.* homeassistant.components.speedtestdotnet.* homeassistant.components.sql.* homeassistant.components.ssdp.* +homeassistant.components.starlink.* homeassistant.components.statistics.* homeassistant.components.steamist.* homeassistant.components.stookalert.* diff --git a/homeassistant/components/starlink/config_flow.py b/homeassistant/components/starlink/config_flow.py index 4154ef09adf..987a84796f1 100644 --- a/homeassistant/components/starlink/config_flow.py +++ b/homeassistant/components/starlink/config_flow.py @@ -44,6 +44,7 @@ class StarlinkConfigFlow(ConfigFlow, domain=DOMAIN): async def get_device_id(self, url: str) -> str | None: """Get the device UID, or None if no device exists at the given URL.""" context = ChannelContext(target=url) + response: str | None try: response = await self.hass.async_add_executor_job(get_id, context) except GrpcError: diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index 56d25bf2d1a..f6f3623f8d4 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -57,7 +57,7 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): except GrpcError as exc: raise UpdateFailed from exc - async def async_stow_starlink(self, stow: bool): + async def async_stow_starlink(self, stow: bool) -> None: """Set whether Starlink system tied to this coordinator should be stowed.""" async with async_timeout.timeout(4): try: @@ -67,7 +67,7 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): except GrpcError as exc: raise HomeAssistantError from exc - async def async_reboot_starlink(self): + async def async_reboot_starlink(self) -> None: """Reboot the Starlink system tied to this coordinator.""" async with async_timeout.timeout(4): try: diff --git a/mypy.ini b/mypy.ini index 7d1ec19c4d5..639f27bbabb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2722,6 +2722,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.starlink.*] +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.statistics.*] check_untyped_defs = true disallow_incomplete_defs = true From db4c9c67a2dd58ba944176c14788432c8c8eaaf0 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 2 Aug 2023 08:29:00 +0200 Subject: [PATCH 0126/1151] Do not set hass data before first coordinator refresh (#97343) --- .../devolo_home_network/__init__.py | 6 ++++-- homeassistant/components/dexcom/__init__.py | 21 +++++++++---------- homeassistant/components/fritz/__init__.py | 4 ++-- homeassistant/components/hassio/__init__.py | 2 +- homeassistant/components/juicenet/__init__.py | 4 ++-- .../landisgyr_heat_meter/__init__.py | 2 +- homeassistant/components/loqed/__init__.py | 3 ++- homeassistant/components/mill/__init__.py | 2 +- .../components/nextcloud/__init__.py | 3 ++- homeassistant/components/nextdns/__init__.py | 18 +++++++--------- homeassistant/components/pi_hole/__init__.py | 4 ++-- homeassistant/components/syncthru/__init__.py | 2 +- .../components/volvooncall/__init__.py | 6 +++--- 13 files changed, 38 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 00d96ea53b3..e70b28f9c3c 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -64,6 +64,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Unable to connect to {entry.data[CONF_IP_ADDRESS]}" ) from err + hass.data[DOMAIN][entry.entry_id] = {"device": device} + async def async_update_connected_plc_devices() -> LogicalNetwork: """Fetch data from API endpoint.""" assert device.plcnet @@ -155,11 +157,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=SHORT_UPDATE_INTERVAL, ) - hass.data[DOMAIN][entry.entry_id] = {"device": device, "coordinators": coordinators} - for coordinator in coordinators.values(): await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id]["coordinators"] = coordinators + await hass.config_entries.async_forward_entry_setups(entry, platforms(device)) entry.async_on_unload( diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 137a884d201..2d7c2120758 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -50,22 +50,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SessionError as error: raise UpdateFailed(error) from error + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=async_update_data, + update_interval=SCAN_INTERVAL, + ) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { - COORDINATOR: DataUpdateCoordinator( - hass, - _LOGGER, - name=DOMAIN, - update_method=async_update_data, - update_interval=SCAN_INTERVAL, - ), + COORDINATOR: coordinator, UNDO_UPDATE_LISTENER: entry.add_update_listener(update_listener), } - await hass.data[DOMAIN][entry.entry_id][ - COORDINATOR - ].async_config_entry_first_refresh() - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 858bd74bb38..137aaa5ba2e 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -43,6 +43,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ): raise ConfigEntryAuthFailed("Missing UPnP configuration") + await avm_wrapper.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = avm_wrapper @@ -51,8 +53,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) - await avm_wrapper.async_config_entry_first_refresh() - # Load the other platforms like switch await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 9227b7da617..08349c0f467 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -647,8 +647,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" dev_reg = dr.async_get(hass) coordinator = HassioDataUpdateCoordinator(hass, entry, dev_reg) - hass.data[ADDONS_COORDINATOR] = coordinator await coordinator.async_config_entry_first_refresh() + hass.data[ADDONS_COORDINATOR] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 483782c948a..c1744b30b1a 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -87,13 +87,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=timedelta(seconds=30), ) + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id] = { JUICENET_API: juicenet, JUICENET_COORDINATOR: coordinator, } - await coordinator.async_config_entry_first_refresh() - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 0279af2e610..28317238bf9 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -26,10 +26,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = ultraheat_api.HeatMeterService(reader) coordinator = UltraheatCoordinator(hass, api) + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - await coordinator.async_config_entry_first_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/loqed/__init__.py b/homeassistant/components/loqed/__init__.py index e6c69e0751e..3ee65f751ae 100644 --- a/homeassistant/components/loqed/__init__.py +++ b/homeassistant/components/loqed/__init__.py @@ -35,10 +35,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator = LoqedDataCoordinator(hass, api, lock, entry) await coordinator.ensure_webhooks() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator 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) return True diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 8fd1d1a3e22..0482e573766 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -73,8 +73,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=update_interval, ) - hass.data[DOMAIN][conn_type][key] = data_coordinator await data_coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][conn_type][key] = data_coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 8e2f39cf9b5..866fe3befa9 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -50,10 +50,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ncm, entry, ) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator 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) return True diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 9e67ccfa4fc..72b3f52d3e8 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -145,7 +145,7 @@ class NextDnsConnectionUpdateCoordinator(NextDnsUpdateCoordinator[ConnectionStat _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] -COORDINATORS = [ +COORDINATORS: list[tuple[str, type[NextDnsUpdateCoordinator], timedelta]] = [ (ATTR_CONNECTION, NextDnsConnectionUpdateCoordinator, UPDATE_INTERVAL_CONNECTION), (ATTR_DNSSEC, NextDnsDnssecUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_ENCRYPTION, NextDnsEncryptionUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), @@ -168,24 +168,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (ApiError, ClientConnectorError, asyncio.TimeoutError) as err: raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} - tasks = [] + coordinators = {} # Independent DataUpdateCoordinator is used for each API endpoint to avoid # unnecessary requests when entities using this endpoint are disabled. for coordinator_name, coordinator_class, update_interval in COORDINATORS: - hass.data[DOMAIN][entry.entry_id][coordinator_name] = coordinator_class( - hass, nextdns, profile_id, update_interval - ) - tasks.append( - hass.data[DOMAIN][entry.entry_id][ - coordinator_name - ].async_config_entry_first_refresh() - ) + coordinator = coordinator_class(hass, nextdns, profile_id, update_interval) + tasks.append(coordinator.async_config_entry_first_refresh()) + coordinators[coordinator_name] = coordinator await asyncio.gather(*tasks) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 96cdd7ab105..ab289c004e1 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -123,14 +123,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=MIN_TIME_BETWEEN_UPDATES, ) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_KEY_API: api, DATA_KEY_COORDINATOR: coordinator, } - await coordinator.async_config_entry_first_refresh() - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index f77f68450a4..52d54e5e58d 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -55,8 +55,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_data, update_interval=timedelta(seconds=30), ) - hass.data[DOMAIN][entry.entry_id] = coordinator await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id] = coordinator if isinstance(coordinator.last_exception, SyncThruAPINotSupported): # this means that the printer does not support the syncthru JSON API # and the config should simply be discarded diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index ab4fa781110..b943240167a 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -70,12 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: volvo_data = VolvoData(hass, connection, entry) - coordinator = hass.data[DOMAIN][entry.entry_id] = VolvoUpdateCoordinator( - hass, volvo_data - ) + coordinator = VolvoUpdateCoordinator(hass, volvo_data) await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True From f9ac102c27abd570480dd4809541c5c9141db92e Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 2 Aug 2023 09:09:13 +0200 Subject: [PATCH 0127/1151] Fix duotecno's name to be sync with the docs (#97602) --- homeassistant/components/duotecno/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index 0edc9ba7f8c..c0bd29547c5 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -1,6 +1,6 @@ { "domain": "duotecno", - "name": "duotecno", + "name": "Duotecno", "codeowners": ["@cereal2nd"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/duotecno", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6e16752ca49..0d0e1ca2d71 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1241,7 +1241,7 @@ "iot_class": "local_polling" }, "duotecno": { - "name": "duotecno", + "name": "Duotecno", "integration_type": "hub", "config_flow": true, "iot_class": "local_push" From e4303e45348396e1d201e71d6ea90ee464cf3364 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 2 Aug 2023 11:26:25 +0200 Subject: [PATCH 0128/1151] Add rounding back when unique_id is not set (#97603) --- .../components/history_stats/sensor.py | 5 +- tests/components/history_stats/test_sensor.py | 87 +++++++++++++++---- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 958f46a5e04..baa39468bc1 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -174,7 +174,10 @@ class HistoryStatsSensor(HistoryStatsSensorBase): return if self._type == CONF_TYPE_TIME: - self._attr_native_value = state.seconds_matched / 3600 + value = state.seconds_matched / 3600 + if self._attr_unique_id is None: + value = round(value, 2) + self._attr_native_value = value elif self._type == CONF_TYPE_RATIO: self._attr_native_value = pretty_ratio(state.seconds_matched, state.period) elif self._type == CONF_TYPE_COUNT: diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index ddd11c0d768..bb4b5b275d2 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -386,6 +386,7 @@ async def test_measure(recorder_mock: Recorder, hass: HomeAssistant) -> None: "start": "{{ as_timestamp(utcnow()) - 3600 }}", "end": "{{ utcnow() }}", "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", }, { "platform": "history_stats", @@ -413,7 +414,7 @@ async def test_measure(recorder_mock: Recorder, hass: HomeAssistant) -> None: await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -724,7 +725,17 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin "start": "{{ utcnow().replace(hour=0, minute=0, second=0) }}", "end": "{{ utcnow() }}", "type": "time", - } + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.state", + "name": "sensor2", + "state": "on", + "start": "{{ utcnow().replace(hour=0, minute=0, second=0) }}", + "end": "{{ utcnow() }}", + "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", + }, ] }, ) @@ -734,6 +745,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "0.0" + assert hass.states.get("sensor.sensor2").state == "0.0" one_hour_in = start_time + timedelta(minutes=60) with freeze_time(one_hour_in): @@ -741,6 +753,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "1.0" + assert hass.states.get("sensor.sensor2").state == "1.0" turn_off_time = start_time + timedelta(minutes=90) with freeze_time(turn_off_time): @@ -750,6 +763,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "1.5" + assert hass.states.get("sensor.sensor2").state == "1.5" turn_back_on_time = start_time + timedelta(minutes=105) with freeze_time(turn_back_on_time): @@ -757,19 +771,22 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "1.5" + assert hass.states.get("sensor.sensor2").state == "1.5" with freeze_time(turn_back_on_time): hass.states.async_set("binary_sensor.state", "on") await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "1.5" + assert hass.states.get("sensor.sensor2").state == "1.5" next_update_time = start_time + timedelta(minutes=107) with freeze_time(next_update_time): async_fire_time_changed(hass, next_update_time) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "1.53333333333333" + assert hass.states.get("sensor.sensor1").state == "1.53" + assert hass.states.get("sensor.sensor2").state == "1.53333333333333" end_time = start_time + timedelta(minutes=120) with freeze_time(end_time): @@ -777,6 +794,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "1.75" + assert hass.states.get("sensor.sensor2").state == "1.75" async def test_async_start_from_history_and_switch_to_watching_state_changes_multiple( @@ -960,7 +978,17 @@ async def test_does_not_work_into_the_future( "start": "{{ utcnow().replace(hour=23, minute=0, second=0) }}", "duration": {"hours": 1}, "type": "time", - } + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.state", + "name": "sensor2", + "state": "on", + "start": "{{ utcnow().replace(hour=23, minute=0, second=0) }}", + "duration": {"hours": 1}, + "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", + }, ] }, ) @@ -969,6 +997,7 @@ async def test_does_not_work_into_the_future( await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN one_hour_in = start_time + timedelta(minutes=60) with freeze_time(one_hour_in): @@ -976,6 +1005,7 @@ async def test_does_not_work_into_the_future( await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN turn_off_time = start_time + timedelta(minutes=90) with freeze_time(turn_off_time): @@ -985,6 +1015,7 @@ async def test_does_not_work_into_the_future( await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN turn_back_on_time = start_time + timedelta(minutes=105) with freeze_time(turn_back_on_time): @@ -992,12 +1023,14 @@ async def test_does_not_work_into_the_future( await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN with freeze_time(turn_back_on_time): hass.states.async_set("binary_sensor.state", "on") await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN end_time = start_time + timedelta(minutes=120) with freeze_time(end_time): @@ -1005,13 +1038,15 @@ async def test_does_not_work_into_the_future( await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN in_the_window = start_time + timedelta(hours=23, minutes=5) with freeze_time(in_the_window): async_fire_time_changed(hass, in_the_window) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.0833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.08" + assert hass.states.get("sensor.sensor2").state == "0.0833333333333333" past_the_window = start_time + timedelta(hours=25) with patch( @@ -1143,6 +1178,7 @@ async def test_measure_sliding_window( "start": "{{ as_timestamp(now()) - 3600 }}", "end": "{{ as_timestamp(now()) + 3600 }}", "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", }, { "platform": "history_stats", @@ -1175,7 +1211,7 @@ async def test_measure_sliding_window( await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" @@ -1188,7 +1224,7 @@ async def test_measure_sliding_window( async_fire_time_changed(hass, past_next_update) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" @@ -1242,6 +1278,7 @@ async def test_measure_from_end_going_backwards( "duration": {"hours": 1}, "end": "{{ utcnow() }}", "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", }, { "platform": "history_stats", @@ -1269,7 +1306,7 @@ async def test_measure_from_end_going_backwards( await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1282,7 +1319,7 @@ async def test_measure_from_end_going_backwards( async_fire_time_changed(hass, past_next_update) await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1335,6 +1372,7 @@ async def test_measure_cet(recorder_mock: Recorder, hass: HomeAssistant) -> None "start": "{{ as_timestamp(utcnow()) - 3600 }}", "end": "{{ utcnow() }}", "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", }, { "platform": "history_stats", @@ -1362,7 +1400,7 @@ async def test_measure_cet(recorder_mock: Recorder, hass: HomeAssistant) -> None await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() - assert hass.states.get("sensor.sensor1").state == "0.833333333333333" + assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.833333333333333" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1425,20 +1463,33 @@ async def test_end_time_with_microseconds_zeroed( "end": "{{ now().replace(microsecond=0) }}", "type": "time", }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.heatpump_compressor_state", + "name": "heatpump_compressor_today2", + "state": "on", + "start": "{{ now().replace(hour=0, minute=0, second=0, microsecond=0) }}", + "end": "{{ now().replace(microsecond=0) }}", + "type": "time", + "unique_id": "6b1f54e3-4065-43ca-8492-d0d4506a573a", + }, ] }, ) await hass.async_block_till_done() await async_update_entity(hass, "sensor.heatpump_compressor_today") await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" assert ( - hass.states.get("sensor.heatpump_compressor_today").state + hass.states.get("sensor.heatpump_compressor_today2").state == "1.83333333333333" ) + async_fire_time_changed(hass, time_200) await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" assert ( - hass.states.get("sensor.heatpump_compressor_today").state + hass.states.get("sensor.heatpump_compressor_today2").state == "1.83333333333333" ) hass.states.async_set("binary_sensor.heatpump_compressor_state", "off") @@ -1448,8 +1499,9 @@ async def test_end_time_with_microseconds_zeroed( with freeze_time(time_400): async_fire_time_changed(hass, time_400) await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" assert ( - hass.states.get("sensor.heatpump_compressor_today").state + hass.states.get("sensor.heatpump_compressor_today2").state == "1.83333333333333" ) hass.states.async_set("binary_sensor.heatpump_compressor_state", "on") @@ -1458,8 +1510,9 @@ async def test_end_time_with_microseconds_zeroed( with freeze_time(time_600): async_fire_time_changed(hass, time_600) await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "3.83" assert ( - hass.states.get("sensor.heatpump_compressor_today").state + hass.states.get("sensor.heatpump_compressor_today2").state == "3.83333333333333" ) @@ -1473,6 +1526,7 @@ async def test_end_time_with_microseconds_zeroed( async_fire_time_changed(hass, rolled_to_next_day) await hass.async_block_till_done() assert hass.states.get("sensor.heatpump_compressor_today").state == "0.0" + assert hass.states.get("sensor.heatpump_compressor_today2").state == "0.0" rolled_to_next_day_plus_12 = start_of_today + timedelta( days=1, hours=12, microseconds=0 @@ -1481,6 +1535,7 @@ async def test_end_time_with_microseconds_zeroed( async_fire_time_changed(hass, rolled_to_next_day_plus_12) await hass.async_block_till_done() assert hass.states.get("sensor.heatpump_compressor_today").state == "12.0" + assert hass.states.get("sensor.heatpump_compressor_today2").state == "12.0" rolled_to_next_day_plus_14 = start_of_today + timedelta( days=1, hours=14, microseconds=0 @@ -1489,6 +1544,7 @@ async def test_end_time_with_microseconds_zeroed( async_fire_time_changed(hass, rolled_to_next_day_plus_14) await hass.async_block_till_done() assert hass.states.get("sensor.heatpump_compressor_today").state == "14.0" + assert hass.states.get("sensor.heatpump_compressor_today2").state == "14.0" rolled_to_next_day_plus_16_860000 = start_of_today + timedelta( days=1, hours=16, microseconds=860000 @@ -1503,8 +1559,9 @@ async def test_end_time_with_microseconds_zeroed( with freeze_time(rolled_to_next_day_plus_18): async_fire_time_changed(hass, rolled_to_next_day_plus_18) await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "16.0" assert ( - hass.states.get("sensor.heatpump_compressor_today").state + hass.states.get("sensor.heatpump_compressor_today2").state == "16.0002388888929" ) From 3ce05314e05382c3d258a9cd35d2ec9697eb6614 Mon Sep 17 00:00:00 2001 From: Bruno Enten Date: Wed, 2 Aug 2023 13:36:05 +0200 Subject: [PATCH 0129/1151] use write_registers also for target temp (#97475) --- homeassistant/components/modbus/__init__.py | 2 + homeassistant/components/modbus/climate.py | 32 +++++-- homeassistant/components/modbus/const.py | 1 + tests/components/modbus/test_climate.py | 101 ++++++++++++++++++++ 4 files changed, 126 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index e8c53469769..d9e81b74ce9 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -104,6 +104,7 @@ from .const import ( # noqa: F401 CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, CONF_TARGET_TEMP, + CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_VERIFY, CONF_WRITE_REGISTERS, CONF_WRITE_TYPE, @@ -228,6 +229,7 @@ CLIMATE_SCHEMA = vol.All( BASE_STRUCT_SCHEMA.extend( { vol.Required(CONF_TARGET_TEMP): cv.positive_int, + vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean, vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_int, vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int, vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 0a8b8dabeeb..27a82c7f53b 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -45,6 +45,7 @@ from .const import ( CONF_MIN_TEMP, CONF_STEP, CONF_TARGET_TEMP, + CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_WRITE_REGISTERS, DataType, ) @@ -84,6 +85,9 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): """Initialize the modbus thermostat.""" super().__init__(hub, config) self._target_temperature_register = config[CONF_TARGET_TEMP] + self._target_temperature_write_registers = config[ + CONF_TARGET_TEMP_WRITE_REGISTERS + ] self._unit = config[CONF_TEMPERATURE_UNIT] self._attr_current_temperature = None @@ -107,7 +111,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self._attr_hvac_modes = cast(list[HVACMode], []) self._attr_hvac_mode = None self._hvac_mode_mapping: list[tuple[int, HVACMode]] = [] - self._hvac_mode_write_type = mode_config[CONF_WRITE_REGISTERS] + self._hvac_mode_write_registers = mode_config[CONF_WRITE_REGISTERS] mode_value_config = mode_config[CONF_HVAC_MODE_VALUES] for hvac_mode_kw, hvac_mode in ( @@ -133,7 +137,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): if CONF_HVAC_ONOFF_REGISTER in config: self._hvac_onoff_register = config[CONF_HVAC_ONOFF_REGISTER] - self._hvac_onoff_write_type = config[CONF_WRITE_REGISTERS] + self._hvac_onoff_write_registers = config[CONF_WRITE_REGISTERS] if HVACMode.OFF not in self._attr_hvac_modes: self._attr_hvac_modes.append(HVACMode.OFF) else: @@ -150,7 +154,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): """Set new target hvac mode.""" if self._hvac_onoff_register is not None: # Turn HVAC Off by writing 0 to the On/Off register, or 1 otherwise. - if self._hvac_onoff_write_type: + if self._hvac_onoff_write_registers: await self._hub.async_pymodbus_call( self._slave, self._hvac_onoff_register, @@ -169,7 +173,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): # Write a value to the mode register for the desired mode. for value, mode in self._hvac_mode_mapping: if mode == hvac_mode: - if self._hvac_mode_write_type: + if self._hvac_mode_write_registers: await self._hub.async_pymodbus_call( self._slave, self._hvac_mode_register, @@ -212,12 +216,20 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): DataType.INT16, DataType.UINT16, ): - result = await self._hub.async_pymodbus_call( - self._slave, - self._target_temperature_register, - int(float(registers[0])), - CALL_TYPE_WRITE_REGISTER, - ) + if self._target_temperature_write_registers: + result = await self._hub.async_pymodbus_call( + self._slave, + self._target_temperature_register, + [int(float(registers[0]))], + CALL_TYPE_WRITE_REGISTERS, + ) + else: + result = await self._hub.async_pymodbus_call( + self._slave, + self._target_temperature_register, + int(float(registers[0])), + CALL_TYPE_WRITE_REGISTER, + ) else: result = await self._hub.async_pymodbus_call( self._slave, diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 4191e1df56f..264268f323e 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -55,6 +55,7 @@ CONF_SWAP_NONE = "none" CONF_SWAP_WORD = "word" CONF_SWAP_WORD_BYTE = "word_byte" CONF_TARGET_TEMP = "target_temp_register" +CONF_TARGET_TEMP_WRITE_REGISTERS = "target_temp_write_registers" CONF_HVAC_MODE_REGISTER = "hvac_mode_register" CONF_HVAC_MODE_VALUES = "values" CONF_HVAC_ONOFF_REGISTER = "hvac_onoff_register" diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 62b3fb6e7f4..ce43cf7c1d2 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -22,6 +22,8 @@ from homeassistant.components.modbus.const import ( CONF_HVAC_ONOFF_REGISTER, CONF_LAZY_ERROR, CONF_TARGET_TEMP, + CONF_TARGET_TEMP_WRITE_REGISTERS, + CONF_WRITE_REGISTERS, MODBUS_DOMAIN, DataType, ) @@ -78,6 +80,19 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") } ], }, + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_HVAC_ONOFF_REGISTER: 12, + CONF_TARGET_TEMP_WRITE_REGISTERS: True, + CONF_WRITE_REGISTERS: True, + } + ], + }, { CONF_CLIMATES: [ { @@ -101,6 +116,30 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") } ], }, + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_HVAC_ONOFF_REGISTER: 12, + CONF_HVAC_MODE_REGISTER: { + CONF_ADDRESS: 11, + CONF_WRITE_REGISTERS: True, + CONF_HVAC_MODE_VALUES: { + "state_off": 0, + "state_heat": 1, + "state_cool": 2, + "state_heat_cool": 3, + "state_dry": 4, + "state_fan_only": 5, + "state_auto": 6, + }, + }, + } + ], + }, ], ) async def test_config_climate(hass: HomeAssistant, mock_modbus) -> None: @@ -353,6 +392,22 @@ async def test_service_climate_update( ] }, ), + ( + 25, + [0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_DATA_TYPE: DataType.INT16, + CONF_TARGET_TEMP_WRITE_REGISTERS: True, + } + ] + }, + ), ], ) async def test_service_climate_set_temperature( @@ -417,6 +472,52 @@ async def test_service_climate_set_temperature( ] }, ), + ( + HVACMode.HEAT, + [0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_HVAC_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_HVAC_MODE_VALUES: { + CONF_HVAC_MODE_COOL: 1, + CONF_HVAC_MODE_HEAT: 2, + }, + CONF_WRITE_REGISTERS: True, + }, + CONF_HVAC_ONOFF_REGISTER: 119, + } + ] + }, + ), + ( + HVACMode.OFF, + [0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_HVAC_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_HVAC_MODE_VALUES: { + CONF_HVAC_MODE_COOL: 1, + CONF_HVAC_MODE_HEAT: 2, + }, + }, + CONF_HVAC_ONOFF_REGISTER: 119, + CONF_WRITE_REGISTERS: True, + } + ] + }, + ), ], ) async def test_service_set_mode( From 02f8000f6c71de2d053d0fe9da04165a9b5591db Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Aug 2023 14:43:42 +0200 Subject: [PATCH 0130/1151] Update frontend to 20230802.0 (#97614) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2210a44039e..84d1d4f5e27 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230801.0"] + "requirements": ["home-assistant-frontend==20230802.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 076e534a6b0..7401c747890 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.69.0 hassil==1.2.5 home-assistant-bluetooth==1.10.2 -home-assistant-frontend==20230801.0 +home-assistant-frontend==20230802.0 home-assistant-intents==2023.7.25 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 89bfcc924f1..e35a3824571 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -988,7 +988,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230801.0 +home-assistant-frontend==20230802.0 # homeassistant.components.conversation home-assistant-intents==2023.7.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d47d5224e52..19772ce2c5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -774,7 +774,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230801.0 +home-assistant-frontend==20230802.0 # homeassistant.components.conversation home-assistant-intents==2023.7.25 From 1a77121c02ff0d5bec3fc4ddabfa23c52138b811 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 2 Aug 2023 20:28:18 +0200 Subject: [PATCH 0131/1151] Fix aiohttp code DeprecationWarnings (#97621) --- homeassistant/components/bond/fan.py | 2 +- homeassistant/components/bond/light.py | 8 ++++---- homeassistant/components/bond/switch.py | 2 +- homeassistant/components/netatmo/__init__.py | 4 ++-- tests/components/bond/common.py | 2 +- tests/components/netatmo/test_init.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 1512cf7b2b4..bc6235cb219 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -153,7 +153,7 @@ class BondFan(BondEntity, FanEntity): except ClientResponseError as ex: raise HomeAssistantError( "The bond API returned an error calling set_power_state_belief for" - f" {self.entity_id}. Code: {ex.code} Message: {ex.message}" + f" {self.entity_id}. Code: {ex.status} Message: {ex.message}" ) from ex async def async_set_speed_belief(self, speed: int) -> None: diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 2380321cc4c..c5816153c8d 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -138,7 +138,7 @@ class BondBaseLight(BondEntity, LightEntity): except ClientResponseError as ex: raise HomeAssistantError( "The bond API returned an error calling set_brightness_belief for" - f" {self.entity_id}. Code: {ex.code} Message: {ex.message}" + f" {self.entity_id}. Code: {ex.status} Message: {ex.message}" ) from ex async def async_set_power_belief(self, power_state: bool) -> None: @@ -150,7 +150,7 @@ class BondBaseLight(BondEntity, LightEntity): except ClientResponseError as ex: raise HomeAssistantError( "The bond API returned an error calling set_light_state_belief for" - f" {self.entity_id}. Code: {ex.code} Message: {ex.message}" + f" {self.entity_id}. Code: {ex.status} Message: {ex.message}" ) from ex @@ -313,7 +313,7 @@ class BondFireplace(BondEntity, LightEntity): except ClientResponseError as ex: raise HomeAssistantError( "The bond API returned an error calling set_brightness_belief for" - f" {self.entity_id}. Code: {ex.code} Message: {ex.message}" + f" {self.entity_id}. Code: {ex.status} Message: {ex.message}" ) from ex async def async_set_power_belief(self, power_state: bool) -> None: @@ -325,5 +325,5 @@ class BondFireplace(BondEntity, LightEntity): except ClientResponseError as ex: raise HomeAssistantError( "The bond API returned an error calling set_power_state_belief for" - f" {self.entity_id}. Code: {ex.code} Message: {ex.message}" + f" {self.entity_id}. Code: {ex.status} Message: {ex.message}" ) from ex diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py index c0ff6368e5a..887532defd1 100644 --- a/homeassistant/components/bond/switch.py +++ b/homeassistant/components/bond/switch.py @@ -65,5 +65,5 @@ class BondSwitch(BondEntity, SwitchEntity): except ClientResponseError as ex: raise HomeAssistantError( "The bond API returned an error calling set_power_state_belief for" - f" {self.entity_id}. Code: {ex.code} Message: {ex.message}" + f" {self.entity_id}. Code: {ex.status} Message: {ex.message}" ) from ex diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 42c48ff9751..f575f227753 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -149,8 +149,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await session.async_ensure_token_valid() except aiohttp.ClientResponseError as ex: - _LOGGER.debug("API error: %s (%s)", ex.code, ex.message) - if ex.code in ( + _LOGGER.debug("API error: %s (%s)", ex.status, ex.message) + if ex.status in ( HTTPStatus.BAD_REQUEST, HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 9b2e82abc84..6fbcb928b5a 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -220,7 +220,7 @@ def patch_bond_action_returns_clientresponseerror(): return patch( "homeassistant.components.bond.Bond.action", side_effect=ClientResponseError( - request_info=None, history=None, code=405, message="Method Not Allowed" + request_info=None, history=None, status=405, message="Method Not Allowed" ), ) diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index f4d24e87b91..c6146dca339 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -415,7 +415,7 @@ async def test_setup_component_invalid_token(hass: HomeAssistant, config_entry) headers={}, real_url="http://example.com", ), - code=400, + status=400, history=(), ) From 0afa9647248b8eb0eff3b42ac99c7964381fe0f9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 2 Aug 2023 20:29:03 +0200 Subject: [PATCH 0132/1151] Fix async_timeout DeprecationWarnings (#97622) --- homeassistant/components/media_player/__init__.py | 13 +++++++------ homeassistant/components/upb/config_flow.py | 5 +++-- homeassistant/components/webostv/media_player.py | 9 +++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 39b67477f97..501fa5c3fb8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1258,12 +1258,13 @@ async def async_fetch_image( """Retrieve an image.""" content, content_type = (None, None) websession = async_get_clientsession(hass) - with suppress(asyncio.TimeoutError), async_timeout.timeout(10): - response = await websession.get(url) - if response.status == HTTPStatus.OK: - content = await response.read() - if content_type := response.headers.get(CONTENT_TYPE): - content_type = content_type.split(";")[0] + with suppress(asyncio.TimeoutError): + async with async_timeout.timeout(10): + response = await websession.get(url) + if response.status == HTTPStatus.OK: + content = await response.read() + if content_type := response.headers.get(CONTENT_TYPE): + content_type = content_type.split(";")[0] if content is None: url_parts = URL(url) diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py index 4a3d970a068..728d46acd76 100644 --- a/homeassistant/components/upb/config_flow.py +++ b/homeassistant/components/upb/config_flow.py @@ -44,8 +44,9 @@ async def _validate_input(data): upb.connect(_connected_callback) - with suppress(asyncio.TimeoutError), async_timeout.timeout(VALIDATE_TIMEOUT): - await connected_event.wait() + with suppress(asyncio.TimeoutError): + async with async_timeout.timeout(VALIDATE_TIMEOUT): + await connected_event.wait() upb.disconnect() diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index c2851cb4c6e..664b45e92cb 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -479,10 +479,11 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): ssl_context = SSLContext() websession = async_get_clientsession(self.hass) - with suppress(asyncio.TimeoutError), async_timeout.timeout(10): - response = await websession.get(url, ssl=ssl_context) - if response.status == HTTPStatus.OK: - content = await response.read() + with suppress(asyncio.TimeoutError): + async with async_timeout.timeout(10): + response = await websession.get(url, ssl=ssl_context) + if response.status == HTTPStatus.OK: + content = await response.read() if content is None: _LOGGER.warning("Error retrieving proxied image from %s", url) From 91a83e1ad290de987fed48fee7f042e1ca5b01cc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 2 Aug 2023 20:29:36 +0200 Subject: [PATCH 0133/1151] Fix httpx DeprecationWarning (#97625) --- homeassistant/components/rest/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 342808f3250..827f4bad0b3 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -190,7 +190,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): req: httpx.Response = await getattr(websession, self._method)( self._resource, auth=self._auth, - data=bytes(body, "utf-8"), + content=bytes(body, "utf-8"), headers=rendered_headers, params=rendered_params, ) From 887e48c440cb9f4aa8e5a49b2b1b0bc00d8a596f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 2 Aug 2023 20:30:13 +0200 Subject: [PATCH 0134/1151] Replace deprecated aiohttp_unused_port fixture (#97626) --- tests/components/http/test_init.py | 10 ++++++---- tests/components/image_processing/test_init.py | 16 ++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 8f9fff79580..3fc8d7689d6 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -85,11 +85,13 @@ class TestView(http.HomeAssistantView): async def test_registering_view_while_running( - hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, aiohttp_unused_port + hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, unused_tcp_port_factory ) -> None: """Test that we can register a view while the server is running.""" await async_setup_component( - hass, http.DOMAIN, {http.DOMAIN: {http.CONF_SERVER_PORT: aiohttp_unused_port()}} + hass, + http.DOMAIN, + {http.DOMAIN: {http.CONF_SERVER_PORT: unused_tcp_port_factory()}}, ) await hass.async_start() @@ -443,11 +445,11 @@ async def test_cors_defaults(hass: HomeAssistant) -> None: async def test_storing_config( - hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, aiohttp_unused_port + hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, unused_tcp_port_factory ) -> None: """Test that we store last working config.""" config = { - http.CONF_SERVER_PORT: aiohttp_unused_port(), + http.CONF_SERVER_PORT: unused_tcp_port_factory(), "use_x_forwarded_for": True, "trusted_proxies": ["192.168.1.100"], } diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index a59761d1c74..40b4c47a3c9 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -23,9 +23,9 @@ async def setup_homeassistant(hass: HomeAssistant): @pytest.fixture -def aiohttp_unused_port(event_loop, aiohttp_unused_port, socket_enabled): +def aiohttp_unused_port_factory(event_loop, unused_tcp_port_factory, socket_enabled): """Return aiohttp_unused_port and allow opening sockets.""" - return aiohttp_unused_port + return unused_tcp_port_factory def get_url(hass): @@ -34,12 +34,12 @@ def get_url(hass): return f"{hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" -async def setup_image_processing(hass, aiohttp_unused_port): +async def setup_image_processing(hass, aiohttp_unused_port_factory): """Set up things to be run when tests are started.""" await async_setup_component( hass, http.DOMAIN, - {http.DOMAIN: {http.CONF_SERVER_PORT: aiohttp_unused_port()}}, + {http.DOMAIN: {http.CONF_SERVER_PORT: aiohttp_unused_port_factory()}}, ) config = {ip.DOMAIN: {"platform": "test"}, "camera": {"platform": "demo"}} @@ -85,11 +85,11 @@ async def test_setup_component_with_service(hass: HomeAssistant) -> None: async def test_get_image_from_camera( mock_camera_read, hass: HomeAssistant, - aiohttp_unused_port, + aiohttp_unused_port_factory, enable_custom_integrations: None, ) -> None: """Grab an image from camera entity.""" - await setup_image_processing(hass, aiohttp_unused_port) + await setup_image_processing(hass, aiohttp_unused_port_factory) common.async_scan(hass, entity_id="image_processing.test") await hass.async_block_till_done() @@ -108,11 +108,11 @@ async def test_get_image_from_camera( async def test_get_image_without_exists_camera( mock_image, hass: HomeAssistant, - aiohttp_unused_port, + aiohttp_unused_port_factory, enable_custom_integrations: None, ) -> None: """Try to get image without exists camera.""" - await setup_image_processing(hass, aiohttp_unused_port) + await setup_image_processing(hass, aiohttp_unused_port_factory) hass.states.async_remove("camera.demo_camera") From 9d9af0c8841eac740338fbcb04a1631a6c6084a1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 2 Aug 2023 20:30:41 +0200 Subject: [PATCH 0135/1151] Fix pylint DeprecationWarnings (#97627) --- tests/pylint/test_enforce_type_hints.py | 26 ++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index b80d8f01445..d23d5a849dd 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -108,7 +108,7 @@ def test_ignore_no_annotations( ) -> None: """Ensure that _is_valid_type is not run if there are no annotations.""" # Set ignore option - type_hint_checker.config.ignore_missing_annotations = True + type_hint_checker.linter.config.ignore_missing_annotations = True func_node = astroid.extract_node( code, @@ -143,7 +143,7 @@ def test_bypass_ignore_no_annotations( but `ignore-missing-annotations` option is forced to False. """ # Set bypass option - type_hint_checker.config.ignore_missing_annotations = False + type_hint_checker.linter.config.ignore_missing_annotations = False func_node = astroid.extract_node( code, @@ -485,7 +485,7 @@ def test_invalid_entity_properties( ) -> None: """Check missing entity properties when ignore_missing_annotations is False.""" # Set bypass option - type_hint_checker.config.ignore_missing_annotations = False + type_hint_checker.linter.config.ignore_missing_annotations = False class_node, prop_node, func_node = astroid.extract_node( """ @@ -552,7 +552,7 @@ def test_ignore_invalid_entity_properties( ) -> None: """Check invalid entity properties are ignored by default.""" # Set ignore option - type_hint_checker.config.ignore_missing_annotations = True + type_hint_checker.linter.config.ignore_missing_annotations = True class_node = astroid.extract_node( """ @@ -590,7 +590,7 @@ def test_named_arguments( ) -> None: """Check missing entity properties when ignore_missing_annotations is False.""" # Set bypass option - type_hint_checker.config.ignore_missing_annotations = False + type_hint_checker.linter.config.ignore_missing_annotations = False class_node, func_node, percentage_node, preset_mode_node = astroid.extract_node( """ @@ -676,7 +676,7 @@ def test_invalid_mapping_return_type( ) -> None: """Check that Mapping[xxx, Any] doesn't accept invalid Mapping or dict.""" # Set bypass option - type_hint_checker.config.ignore_missing_annotations = False + type_hint_checker.linter.config.ignore_missing_annotations = False class_node, property_node = astroid.extract_node( f""" @@ -734,7 +734,7 @@ def test_valid_mapping_return_type( ) -> None: """Check that Mapping[xxx, Any] accepts both Mapping and dict.""" # Set bypass option - type_hint_checker.config.ignore_missing_annotations = False + type_hint_checker.linter.config.ignore_missing_annotations = False class_node = astroid.extract_node( f""" @@ -774,7 +774,7 @@ def test_valid_long_tuple( ) -> None: """Check invalid entity properties are ignored by default.""" # Set ignore option - type_hint_checker.config.ignore_missing_annotations = False + type_hint_checker.linter.config.ignore_missing_annotations = False class_node, _, _, _ = astroid.extract_node( """ @@ -821,7 +821,7 @@ def test_invalid_long_tuple( ) -> None: """Check invalid entity properties are ignored by default.""" # Set ignore option - type_hint_checker.config.ignore_missing_annotations = False + type_hint_checker.linter.config.ignore_missing_annotations = False class_node, rgbw_node, rgbww_node = astroid.extract_node( """ @@ -882,7 +882,7 @@ def test_invalid_device_class( ) -> None: """Ensure invalid hints are rejected for entity device_class.""" # Set bypass option - type_hint_checker.config.ignore_missing_annotations = False + type_hint_checker.linter.config.ignore_missing_annotations = False class_node, prop_node = astroid.extract_node( """ @@ -925,7 +925,7 @@ def test_media_player_entity( ) -> None: """Ensure valid hints are accepted for media_player entity.""" # Set bypass option - type_hint_checker.config.ignore_missing_annotations = False + type_hint_checker.linter.config.ignore_missing_annotations = False class_node = astroid.extract_node( """ @@ -952,7 +952,7 @@ def test_media_player_entity( def test_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: """Ensure valid hints are accepted for number entity.""" # Set bypass option - type_hint_checker.config.ignore_missing_annotations = False + type_hint_checker.linter.config.ignore_missing_annotations = False # Ensure that device class is valid despite Entity inheritance # Ensure that `int` is valid for `float` return type @@ -989,7 +989,7 @@ def test_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) - def test_vacuum_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: """Ensure valid hints are accepted for vacuum entity.""" # Set bypass option - type_hint_checker.config.ignore_missing_annotations = False + type_hint_checker.linter.config.ignore_missing_annotations = False # Ensure that `dict | list | None` is valid for params class_node = astroid.extract_node( From 82f27115f5b53074fa385cd571c37115544efbe8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 2 Aug 2023 23:48:37 +0200 Subject: [PATCH 0136/1151] Add device naming to Yeelight (#97639) --- homeassistant/components/yeelight/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 35739b0f596..2180eecff22 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -930,6 +930,8 @@ class YeelightColorLightWithoutNightlightSwitch( ): """Representation of a Color Yeelight light.""" + _attr_name = None + class YeelightColorLightWithNightlightSwitch( YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight From 7cf2199e8b3e0a9498ad089ff2290f27829def8a Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 2 Aug 2023 19:01:30 -0500 Subject: [PATCH 0137/1151] Bump intents to 2023.8.2 (#97636) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/conversation/snapshots/test_init.ambr | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 1eb58e96ff9..9e0909b6dfc 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.2.5", "home-assistant-intents==2023.7.25"] + "requirements": ["hassil==1.2.5", "home-assistant-intents==2023.8.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7401c747890..fd214bf68be 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ hass-nabucasa==0.69.0 hassil==1.2.5 home-assistant-bluetooth==1.10.2 home-assistant-frontend==20230802.0 -home-assistant-intents==2023.7.25 +home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index e35a3824571..1da35f94dfc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -991,7 +991,7 @@ holidays==0.28 home-assistant-frontend==20230802.0 # homeassistant.components.conversation -home-assistant-intents==2023.7.25 +home-assistant-intents==2023.8.2 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19772ce2c5e..0567b57ca3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ holidays==0.28 home-assistant-frontend==20230802.0 # homeassistant.components.conversation -home-assistant-intents==2023.7.25 +home-assistant-intents==2023.8.2 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index f9fe284bcb0..f7145a9ab56 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -30,6 +30,7 @@ 'id': 'homeassistant', 'name': 'Home Assistant', 'supported_languages': list([ + 'af', 'ar', 'bg', 'bn', From 3c2cebea721d9d86e6fc7d26733d8cc60921c76f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 3 Aug 2023 09:10:31 +0200 Subject: [PATCH 0138/1151] Fix abode DeprecationWarnings (#97620) --- homeassistant/components/abode/__init__.py | 10 +++++----- homeassistant/components/abode/alarm_control_panel.py | 2 +- homeassistant/components/abode/sensor.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 38e88944867..2b7e2a07467 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -287,14 +287,14 @@ class AbodeDevice(AbodeEntity): """Initialize Abode device.""" super().__init__(data) self._device = device - self._attr_unique_id = device.device_uuid + self._attr_unique_id = device.uuid async def async_added_to_hass(self) -> None: """Subscribe to device events.""" await super().async_added_to_hass() await self.hass.async_add_executor_job( self._data.abode.events.add_device_callback, - self._device.device_id, + self._device.id, self._update_callback, ) @@ -302,7 +302,7 @@ class AbodeDevice(AbodeEntity): """Unsubscribe from device events.""" await super().async_will_remove_from_hass() await self.hass.async_add_executor_job( - self._data.abode.events.remove_all_device_callbacks, self._device.device_id + self._data.abode.events.remove_all_device_callbacks, self._device.id ) def update(self) -> None: @@ -313,7 +313,7 @@ class AbodeDevice(AbodeEntity): def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes.""" return { - "device_id": self._device.device_id, + "device_id": self._device.id, "battery_low": self._device.battery_low, "no_response": self._device.no_response, "device_type": self._device.type, @@ -323,7 +323,7 @@ class AbodeDevice(AbodeEntity): def device_info(self) -> entity.DeviceInfo: """Return device registry information for this entity.""" return entity.DeviceInfo( - identifiers={(DOMAIN, self._device.device_id)}, + identifiers={(DOMAIN, self._device.id)}, manufacturer="Abode", model=self._device.type, name=self._device.name, diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index 66a2e3b0db5..d0137395446 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -69,7 +69,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes.""" return { - "device_id": self._device.device_id, + "device_id": self._device.id, "battery_backup": self._device.battery, "cellular_backup": self._device.is_cellular, } diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 1e238783221..bf885485fc3 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -63,7 +63,7 @@ class AbodeSensor(AbodeDevice, SensorEntity): """Initialize a sensor for an Abode device.""" super().__init__(data, device) self.entity_description = description - self._attr_unique_id = f"{device.device_uuid}-{description.key}" + self._attr_unique_id = f"{device.uuid}-{description.key}" if description.key == CONST.TEMP_STATUS_KEY: self._attr_native_unit_of_measurement = device.temp_unit elif description.key == CONST.HUMI_STATUS_KEY: From ad0873549da624e0bf3432d441c976fcf03c8bdc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 3 Aug 2023 09:11:41 +0200 Subject: [PATCH 0139/1151] Fix ssl DeprecationWarnings (#97623) --- homeassistant/components/webostv/media_player.py | 4 ++-- homeassistant/util/ssl.py | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 664b45e92cb..579c97c5277 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -8,7 +8,7 @@ from datetime import timedelta from functools import wraps from http import HTTPStatus import logging -from ssl import SSLContext +import ssl from typing import Any, Concatenate, ParamSpec, TypeVar, cast from aiowebostv import WebOsClient, WebOsTvPairError @@ -476,7 +476,7 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): content = None ssl_context = None if url.startswith("https"): - ssl_context = SSLContext() + ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) websession = async_get_clientsession(self.hass) with suppress(asyncio.TimeoutError): diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 84585d7a8c7..2b503716063 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -127,14 +127,9 @@ def server_context_modern() -> ssl.SSLContext: Modern guidelines are followed. """ context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.minimum_version = ssl.TLSVersion.TLSv1_2 - context.options |= ( - ssl.OP_NO_SSLv2 - | ssl.OP_NO_SSLv3 - | ssl.OP_NO_TLSv1 - | ssl.OP_NO_TLSv1_1 - | ssl.OP_CIPHER_SERVER_PREFERENCE - ) + context.options |= ssl.OP_CIPHER_SERVER_PREFERENCE if hasattr(ssl, "OP_NO_COMPRESSION"): context.options |= ssl.OP_NO_COMPRESSION From 4196c43416bdb8d63c8f3f0930b446ce1b1f1929 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 3 Aug 2023 09:12:08 +0200 Subject: [PATCH 0140/1151] Fix deluge DeprecationWarning (#97624) --- homeassistant/components/deluge/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/deluge/config_flow.py b/homeassistant/components/deluge/config_flow.py index 30b48ab60d9..5de61350039 100644 --- a/homeassistant/components/deluge/config_flow.py +++ b/homeassistant/components/deluge/config_flow.py @@ -87,7 +87,7 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] api = DelugeRPCClient( - host=host, port=port, username=username, password=password + host=host, port=port, username=username, password=password, decode_utf8=True ) try: await self.hass.async_add_executor_job(api.connect) From 357bfc46bfd92131f34ef36a17008c090854e0a9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 3 Aug 2023 09:13:23 +0200 Subject: [PATCH 0141/1151] Revert "Add device naming to Yeelight" (#97647) Revert "Add device naming to Yeelight (#97639)" This reverts commit 82f27115f5b53074fa385cd571c37115544efbe8. --- homeassistant/components/yeelight/light.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 2180eecff22..35739b0f596 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -930,8 +930,6 @@ class YeelightColorLightWithoutNightlightSwitch( ): """Representation of a Color Yeelight light.""" - _attr_name = None - class YeelightColorLightWithNightlightSwitch( YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight From 1daac466355af037475834eec7e33362e969333b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 3 Aug 2023 09:14:00 +0200 Subject: [PATCH 0142/1151] Replace deprecated pkg_resources with importlib.metadata (#97628) --- homeassistant/requirements.py | 5 ++--- homeassistant/util/package.py | 25 +++++++++++++------------ tests/util/test_package.py | 28 ++++++++++++++-------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 30c5d0a2448..954de3bf5a6 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -7,7 +7,7 @@ import logging import os from typing import Any, cast -import pkg_resources +from packaging.requirements import Requirement from .core import HomeAssistant, callback from .exceptions import HomeAssistantError @@ -232,8 +232,7 @@ class RequirementsManager: skipped_requirements = [ req for req in requirements - if pkg_resources.Requirement.parse(req).project_name - in self.hass.config.skip_pip_packages + if Requirement(req).name in self.hass.config.skip_pip_packages ] for req in skipped_requirements: diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 45ceb471fd8..7de75c1e24f 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from functools import cache -from importlib.metadata import PackageNotFoundError, version +from importlib.metadata import PackageNotFoundError, distribution, version import logging import os from pathlib import Path @@ -11,7 +11,7 @@ from subprocess import PIPE, Popen import sys from urllib.parse import urlparse -import pkg_resources +from packaging.requirements import InvalidRequirement, Requirement _LOGGER = logging.getLogger(__name__) @@ -37,26 +37,27 @@ def is_installed(package: str) -> bool: Returns False when the package is not installed or doesn't meet req. """ try: - pkg_resources.get_distribution(package) + distribution(package) return True - except (IndexError, pkg_resources.ResolutionError, pkg_resources.ExtractionError): - req = pkg_resources.Requirement.parse(package) - except ValueError: - # This is a zip file. We no longer use this in Home Assistant, - # leaving it in for custom components. - req = pkg_resources.Requirement.parse(urlparse(package).fragment) + except (IndexError, PackageNotFoundError): + try: + req = Requirement(package) + except InvalidRequirement: + # This is a zip file. We no longer use this in Home Assistant, + # leaving it in for custom components. + req = Requirement(urlparse(package).fragment) try: - installed_version = version(req.project_name) + installed_version = version(req.name) # This will happen when an install failed or # was aborted while in progress see # https://github.com/home-assistant/core/issues/47699 if installed_version is None: _LOGGER.error( # type: ignore[unreachable] - "Installed version for %s resolved to None", req.project_name + "Installed version for %s resolved to None", req.name ) return False - return installed_version in req + return req.specifier.contains(installed_version, prereleases=True) except PackageNotFoundError: return False diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 2a190d2aea5..ff26cba0dd4 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -1,12 +1,12 @@ """Test Home Assistant package util methods.""" import asyncio +from importlib.metadata import PackageNotFoundError, metadata import logging import os from subprocess import PIPE import sys from unittest.mock import MagicMock, call, patch -import pkg_resources import pytest import homeassistant.util.package as package @@ -246,9 +246,9 @@ async def test_async_get_user_site(mock_env_copy) -> None: def test_check_package_global() -> None: """Test for an installed package.""" - first_package = list(pkg_resources.working_set)[0] - installed_package = first_package.project_name - installed_version = first_package.version + pkg = metadata("homeassistant") + installed_package = pkg["name"] + installed_version = pkg["version"] assert package.is_installed(installed_package) assert package.is_installed(f"{installed_package}=={installed_version}") @@ -264,13 +264,13 @@ def test_check_package_zip() -> None: def test_get_distribution_falls_back_to_version() -> None: """Test for get_distribution failing and fallback to version.""" - first_package = list(pkg_resources.working_set)[0] - installed_package = first_package.project_name - installed_version = first_package.version + pkg = metadata("homeassistant") + installed_package = pkg["name"] + installed_version = pkg["version"] with patch( - "homeassistant.util.package.pkg_resources.get_distribution", - side_effect=pkg_resources.ExtractionError, + "homeassistant.util.package.distribution", + side_effect=PackageNotFoundError, ): assert package.is_installed(installed_package) assert package.is_installed(f"{installed_package}=={installed_version}") @@ -281,13 +281,13 @@ def test_get_distribution_falls_back_to_version() -> None: def test_check_package_previous_failed_install() -> None: """Test for when a previously install package failed and left cruft behind.""" - first_package = list(pkg_resources.working_set)[0] - installed_package = first_package.project_name - installed_version = first_package.version + pkg = metadata("homeassistant") + installed_package = pkg["name"] + installed_version = pkg["version"] with patch( - "homeassistant.util.package.pkg_resources.get_distribution", - side_effect=pkg_resources.ExtractionError, + "homeassistant.util.package.distribution", + side_effect=PackageNotFoundError, ), patch("homeassistant.util.package.version", return_value=None): assert not package.is_installed(installed_package) assert not package.is_installed(f"{installed_package}=={installed_version}") From 564e0110a497ce203125266ed5381a465a243052 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 3 Aug 2023 11:34:20 +0200 Subject: [PATCH 0143/1151] Revert "OctoPrint add yaml config removal issue" (#97674) Revert "OctoPrint add yaml config removal issue (#97431)" This reverts commit 594d98822b36233d2c8419025bf74a770e2f395f. --- homeassistant/components/octoprint/__init__.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 4c57b6e57dc..dd6ab5794fc 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -24,12 +24,11 @@ from homeassistant.const import ( CONF_VERIFY_SSL, Platform, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify as util_slugify @@ -150,20 +149,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: }, ) ) - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.2.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "OctoPrint", - }, - ) return True From 05e2acb091579acacc17f836427bd047a26cb883 Mon Sep 17 00:00:00 2001 From: Michael Arthur Date: Thu, 3 Aug 2023 22:06:42 +1200 Subject: [PATCH 0144/1151] Add hour of free power select to Electric Kiwi (#97515) * add select sensor to Electric Kiwi * Update homeassistant/components/electric_kiwi/select.py Co-authored-by: Joost Lekkerkerker * simplify the HOP select since there is only one * remove handle coordinator state --------- Co-authored-by: Joost Lekkerkerker --- .coveragerc | 1 + .../components/electric_kiwi/__init__.py | 4 +- .../components/electric_kiwi/select.py | 69 +++++++++++++++++++ .../components/electric_kiwi/strings.json | 5 ++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/electric_kiwi/select.py diff --git a/.coveragerc b/.coveragerc index fb1869b2489..9c6e7a1a223 100644 --- a/.coveragerc +++ b/.coveragerc @@ -270,6 +270,7 @@ omit = homeassistant/components/electric_kiwi/oauth2.py homeassistant/components/electric_kiwi/sensor.py homeassistant/components/electric_kiwi/coordinator.py + homeassistant/components/electric_kiwi/select.py homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/__init__.py homeassistant/components/elkm1/alarm_control_panel.py diff --git a/homeassistant/components/electric_kiwi/__init__.py b/homeassistant/components/electric_kiwi/__init__.py index 3ae6b1c70cf..5af02f69bcf 100644 --- a/homeassistant/components/electric_kiwi/__init__.py +++ b/homeassistant/components/electric_kiwi/__init__.py @@ -15,9 +15,7 @@ from . import api from .const import DOMAIN from .coordinator import ElectricKiwiHOPDataCoordinator -PLATFORMS: list[Platform] = [ - Platform.SENSOR, -] +PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SELECT] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/electric_kiwi/select.py b/homeassistant/components/electric_kiwi/select.py new file mode 100644 index 00000000000..a474c315258 --- /dev/null +++ b/homeassistant/components/electric_kiwi/select.py @@ -0,0 +1,69 @@ +"""Support for Electric Kiwi hour of free power.""" +from __future__ import annotations + +import logging + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTRIBUTION, DOMAIN +from .coordinator import ElectricKiwiHOPDataCoordinator + +_LOGGER = logging.getLogger(__name__) +ATTR_EK_HOP_SELECT = "hop_select" + +HOP_SELECT = SelectEntityDescription( + entity_category=EntityCategory.CONFIG, + key=ATTR_EK_HOP_SELECT, + translation_key="hopselector", +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Electric Kiwi select setup.""" + hop_coordinator: ElectricKiwiHOPDataCoordinator = hass.data[DOMAIN][entry.entry_id] + + _LOGGER.debug("Setting up HOP entity") + entities = [ElectricKiwiSelectHOPEntity(hop_coordinator, HOP_SELECT)] + async_add_entities(entities) + + +class ElectricKiwiSelectHOPEntity( + CoordinatorEntity[ElectricKiwiHOPDataCoordinator], SelectEntity +): + """Entity object for seeing and setting the hour of free power.""" + + entity_description: SelectEntityDescription + _attr_has_entity_name = True + _attr_attribution = ATTRIBUTION + values_dict: dict[str, int] + + def __init__( + self, + hop_coordinator: ElectricKiwiHOPDataCoordinator, + description: SelectEntityDescription, + ) -> None: + """Initialise the HOP selection entity.""" + super().__init__(hop_coordinator) + self._attr_unique_id = f"{self.coordinator._ek_api.customer_number}_{self.coordinator._ek_api.connection_id}_{description.key}" + self.entity_description = description + self._state = None + self.values_dict = self.coordinator.get_hop_options() + self._attr_options = list(self.values_dict.keys()) + + @property + def current_option(self) -> str | None: + """Return the currently selected option.""" + return f"{self.coordinator.data.start.start_time} - {self.coordinator.data.end.end_time}" + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + value = self.values_dict[option] + await self.coordinator.async_update_hop(value) + self.async_write_ha_state() diff --git a/homeassistant/components/electric_kiwi/strings.json b/homeassistant/components/electric_kiwi/strings.json index 19056180f17..81de5cef896 100644 --- a/homeassistant/components/electric_kiwi/strings.json +++ b/homeassistant/components/electric_kiwi/strings.json @@ -31,6 +31,11 @@ "hopfreepowerend": { "name": "Hour of free power end" } + }, + "select": { + "hopselector": { + "name": "Hour of free power" + } } } } From 7b7b8689ef89617959f772c2a6d1e1da9cd8a415 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 3 Aug 2023 06:15:20 -0400 Subject: [PATCH 0145/1151] Bump python-roborock to 0.31.1 (#97632) bump to 0.31.1 --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index d26116a7818..eda6a5609a2 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.30.2"] + "requirements": ["python-roborock==0.31.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1da35f94dfc..79339211153 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.30.2 +python-roborock==0.31.1 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0567b57ca3e..1897f423514 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1582,7 +1582,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.30.2 +python-roborock==0.31.1 # homeassistant.components.smarttub python-smarttub==0.0.33 From 53703448eccbe5637549ca7d7e26a205b22c8056 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Aug 2023 00:20:35 -1000 Subject: [PATCH 0146/1151] Fix typo in tplink OUI (#97644) The last two were reversed for https://ouilookup.com/search/788cb5 --- homeassistant/components/tplink/manifest.json | 2 +- homeassistant/generated/dhcp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index c33106d13cc..b2fcc5c0161 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -154,7 +154,7 @@ }, { "hostname": "k[lps]*", - "macaddress": "788C5B*" + "macaddress": "788CB5*" } ], "documentation": "https://www.home-assistant.io/integrations/tplink", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 8b5dd91f64c..91a02ac3e06 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -782,7 +782,7 @@ DHCP: list[dict[str, str | bool]] = [ { "domain": "tplink", "hostname": "k[lps]*", - "macaddress": "788C5B*", + "macaddress": "788CB5*", }, { "domain": "tuya", From db5d1b10eaa7749c08a6833f06b301a4f1d68c68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Aug 2023 00:35:22 -1000 Subject: [PATCH 0147/1151] Fix tplink child plug state reporting (#97658) regressed in https://github.com/home-assistant/core/pull/96246 --- homeassistant/components/tplink/switch.py | 11 ++++-- tests/components/tplink/__init__.py | 2 + tests/components/tplink/test_switch.py | 45 +++++++++++++++-------- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 6c843246663..fb812abc293 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -116,7 +116,7 @@ class SmartPlugSwitchChild(SmartPlugSwitch): coordinator: TPLinkDataUpdateCoordinator, plug: SmartDevice, ) -> None: - """Initialize the switch.""" + """Initialize the child switch.""" super().__init__(device, coordinator) self._plug = plug self._attr_unique_id = legacy_device_id(plug) @@ -124,10 +124,15 @@ class SmartPlugSwitchChild(SmartPlugSwitch): @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the switch on.""" + """Turn the child switch on.""" await self._plug.turn_on() @async_refresh_after async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the switch off.""" + """Turn the child switch off.""" await self._plug.turn_off() + + @property + def is_on(self) -> bool: + """Return true if child switch is on.""" + return bool(self._plug.is_on) diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 4232d3e6909..816251ae3bb 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -180,12 +180,14 @@ def _mocked_strip() -> SmartStrip: plug0.alias = "Plug0" plug0.device_id = "bb:bb:cc:dd:ee:ff_PLUG0DEVICEID" plug0.mac = "bb:bb:cc:dd:ee:ff" + plug0.is_on = True plug0.protocol = _mock_protocol() plug1 = _mocked_plug() plug1.device_id = "cc:bb:cc:dd:ee:ff_PLUG1DEVICEID" plug1.mac = "cc:bb:cc:dd:ee:ff" plug1.alias = "Plug1" plug1.protocol = _mock_protocol() + plug1.is_on = False strip.children = [plug0, plug1] return strip diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index 1e5e03c0f37..05286e5ff48 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -9,7 +9,7 @@ import pytest from homeassistant.components import tplink from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.tplink.const import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -146,22 +146,37 @@ async def test_strip(hass: HomeAssistant) -> None: # since this is what the previous version did assert hass.states.get("switch.my_strip") is None - for plug_id in range(2): - entity_id = f"switch.my_strip_plug{plug_id}" - state = hass.states.get(entity_id) - assert state.state == STATE_ON + entity_id = "switch.my_strip_plug0" + state = hass.states.get(entity_id) + assert state.state == STATE_ON - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True - ) - strip.children[plug_id].turn_off.assert_called_once() - strip.children[plug_id].turn_off.reset_mock() + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + strip.children[0].turn_off.assert_called_once() + strip.children[0].turn_off.reset_mock() - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True - ) - strip.children[plug_id].turn_on.assert_called_once() - strip.children[plug_id].turn_on.reset_mock() + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + strip.children[0].turn_on.assert_called_once() + strip.children[0].turn_on.reset_mock() + + entity_id = "switch.my_strip_plug1" + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + strip.children[1].turn_off.assert_called_once() + strip.children[1].turn_off.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + strip.children[1].turn_on.assert_called_once() + strip.children[1].turn_on.reset_mock() async def test_strip_unique_ids(hass: HomeAssistant) -> None: From 1fa66953b4b5c9e75f6cf66246e6ec3b5f95ee73 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 3 Aug 2023 13:21:07 +0200 Subject: [PATCH 0148/1151] Use mirror to run `black` with pre-commit (#95605) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b4a6704717f..18cbb082145 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: ruff args: - --fix - - repo: https://github.com/psf/black + - repo: https://github.com/psf/black-pre-commit-mirror rev: 23.7.0 hooks: - id: black From 6e5baeec702a7b59039174ad987ec3db6ba09e56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Aug 2023 01:31:49 -1000 Subject: [PATCH 0149/1151] Bump pyatv to 0.13.3 (#97670) changelog: https://github.com/postlund/pyatv/compare/v0.13.2...v0.13.3 maybe fixes #80215 --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 4ead41e86e9..1d1c26b5fcd 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/apple_tv", "iot_class": "local_push", "loggers": ["pyatv", "srptools"], - "requirements": ["pyatv==0.13.2"], + "requirements": ["pyatv==0.13.3"], "zeroconf": [ "_mediaremotetv._tcp.local.", "_companion-link._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 79339211153..34078566c4f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1572,7 +1572,7 @@ pyatag==0.3.5.3 pyatmo==7.5.0 # homeassistant.components.apple_tv -pyatv==0.13.2 +pyatv==0.13.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1897f423514..ea9ccc2915d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1178,7 +1178,7 @@ pyatag==0.3.5.3 pyatmo==7.5.0 # homeassistant.components.apple_tv -pyatv==0.13.2 +pyatv==0.13.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 From 9980051c3a50b577f483e2b86214ec81e678bbf2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Aug 2023 01:33:05 -1000 Subject: [PATCH 0150/1151] Bump dbus-fast to 1.90.1 (#97619) * Bump dbus-fast to 1.88.0 - cython 3 fixes - performance improvements changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.87.5...v1.88.0 * one more * Bump dbus-fast to 1.90.0 * bump again for yet another round of cython3 fixes --- 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 bc07e2b94ae..67c27f014d1 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.6.1", - "dbus-fast==1.87.5" + "dbus-fast==1.90.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fd214bf68be..c4ad02afe79 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.6.1 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.2 -dbus-fast==1.87.5 +dbus-fast==1.90.1 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 34078566c4f..c597a235392 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.87.5 +dbus-fast==1.90.1 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea9ccc2915d..a28f8f48752 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.87.5 +dbus-fast==1.90.1 # homeassistant.components.debugpy debugpy==1.6.7 From d50b993f64f057cd856c995388c4adcb3d2cad44 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 3 Aug 2023 13:34:20 +0200 Subject: [PATCH 0151/1151] Bump pymodbus v3.4.1. (#97612) * Bump pymodbus v3.4.1. * Solve mypy problem. --- homeassistant/components/modbus/manifest.json | 2 +- homeassistant/components/modbus/modbus.py | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index c2e6b9ef467..d0d573227d8 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "gold", - "requirements": ["pymodbus==3.3.1"] + "requirements": ["pymodbus==3.4.1"] } diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index cb3501f3375..dd32e74cdbd 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -13,7 +13,6 @@ from pymodbus.client import ( ModbusTcpClient, ModbusUdpClient, ) -from pymodbus.constants import Defaults from pymodbus.exceptions import ModbusException from pymodbus.pdu import ModbusResponse from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer, ModbusSocketFramer @@ -301,7 +300,6 @@ class ModbusHub: else: self._pb_params["framer"] = ModbusSocketFramer - Defaults.Timeout = client_config[CONF_TIMEOUT] if CONF_MSG_WAIT in client_config: self._msg_wait = client_config[CONF_MSG_WAIT] / 1000 elif self._config_type == SERIAL: diff --git a/requirements_all.txt b/requirements_all.txt index c597a235392..0a09f288d2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1842,7 +1842,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.3.1 +pymodbus==3.4.1 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a28f8f48752..f0c4c1fece6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ pymeteoclimatic==0.0.6 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.3.1 +pymodbus==3.4.1 # homeassistant.components.monoprice pymonoprice==0.4 From 4c395c012477ca9004fcbde3460bc02ab0fe3522 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 3 Aug 2023 13:59:37 +0200 Subject: [PATCH 0152/1151] Fix date and timestamp device class in Command Line Sensor (#97663) * Fix date in Command Line sensor * prettier --- .../components/command_line/sensor.py | 24 ++++++--- tests/components/command_line/test_sensor.py | 51 +++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 1b865827e69..dd5ad2d5190 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -15,9 +15,11 @@ from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, + SensorDeviceClass, SensorEntity, SensorStateClass, ) +from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( CONF_COMMAND, CONF_DEVICE_CLASS, @@ -206,14 +208,24 @@ class CommandSensor(ManualTriggerEntity, SensorEntity): return if self._value_template is not None: - self._attr_native_value = ( - self._value_template.async_render_with_possible_json_value( - value, - None, - ) + value = self._value_template.async_render_with_possible_json_value( + value, + None, ) - else: + + if self.device_class not in { + SensorDeviceClass.DATE, + SensorDeviceClass.TIMESTAMP, + }: self._attr_native_value = value + self._process_manual_data(value) + return + + self._attr_native_value = None + if value is not None: + self._attr_native_value = async_parse_date_datetime( + value, self.entity_id, self.device_class + ) self._process_manual_data(value) self.async_write_ha_state() diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index a0f8f2cdf84..bc24ff5419f 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -646,3 +646,54 @@ async def test_updating_manually( ) await hass.async_block_till_done() assert called + + +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo 2022-12-22T13:15:30Z", + "device_class": "timestamp", + } + } + ] + } + ], +) +async def test_scrape_sensor_device_timestamp( + hass: HomeAssistant, load_yaml_integration: None +) -> None: + """Test Command Line sensor with a device of type TIMESTAMP.""" + entity_state = hass.states.get("sensor.test") + assert entity_state + assert entity_state.state == "2022-12-22T13:15:30+00:00" + + +@pytest.mark.parametrize( + "get_config", + [ + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo January 17, 2022", + "device_class": "date", + "value_template": "{{ strptime(value, '%B %d, %Y').strftime('%Y-%m-%d') }}", + } + } + ] + } + ], +) +async def test_scrape_sensor_device_date( + hass: HomeAssistant, load_yaml_integration: None +) -> None: + """Test Command Line sensor with a device of type DATE.""" + entity_state = hass.states.get("sensor.test") + assert entity_state + assert entity_state.state == "2022-01-17" From 6f8d666b57a629b3d07a30cfb9cd3ee891f275c1 Mon Sep 17 00:00:00 2001 From: Nick Iacullo Date: Thu, 3 Aug 2023 09:30:56 -0700 Subject: [PATCH 0153/1151] Enable the `PRESET_MODE` `FanEntityFeature` for VeSync air purifiers (#97657) --- homeassistant/components/vesync/fan.py | 4 ++-- .../vesync/snapshots/test_diagnostics.ambr | 2 +- tests/components/vesync/snapshots/test_fan.ambr | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index a3bf027c28f..e5347b204e6 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -86,10 +86,10 @@ def _setup_entities(devices, async_add_entities): class VeSyncFanHA(VeSyncDevice, FanEntity): """Representation of a VeSync fan.""" - _attr_supported_features = FanEntityFeature.SET_SPEED + _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE _attr_name = None - def __init__(self, fan): + def __init__(self, fan) -> None: """Initialize the VeSync fan device.""" super().__init__(fan) self.smartfan = fan diff --git a/tests/components/vesync/snapshots/test_diagnostics.ambr b/tests/components/vesync/snapshots/test_diagnostics.ambr index c463db179eb..10dfdd2ba14 100644 --- a/tests/components/vesync/snapshots/test_diagnostics.ambr +++ b/tests/components/vesync/snapshots/test_diagnostics.ambr @@ -200,7 +200,7 @@ 'auto', 'sleep', ]), - 'supported_features': 1, + 'supported_features': 9, }), 'entity_id': 'fan.fan', 'last_changed': str, diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 428f066e6cc..fa1a7a7b332 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -58,7 +58,7 @@ 'original_icon': None, 'original_name': None, 'platform': 'vesync', - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'air-purifier', 'unit_of_measurement': None, @@ -73,7 +73,7 @@ 'auto', 'sleep', ]), - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_131s', @@ -140,7 +140,7 @@ 'original_icon': None, 'original_name': None, 'platform': 'vesync', - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', 'unit_of_measurement': None, @@ -161,7 +161,7 @@ 'sleep', ]), 'screen_status': True, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_200s', @@ -229,7 +229,7 @@ 'original_icon': None, 'original_name': None, 'platform': 'vesync', - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '400s-purifier', 'unit_of_measurement': None, @@ -251,7 +251,7 @@ 'sleep', ]), 'screen_status': True, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_400s', @@ -319,7 +319,7 @@ 'original_icon': None, 'original_name': None, 'platform': 'vesync', - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '600s-purifier', 'unit_of_measurement': None, @@ -341,7 +341,7 @@ 'sleep', ]), 'screen_status': True, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'fan.air_purifier_600s', From 620525b2b4aeb730d373e5794792753efa7a4098 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Aug 2023 06:57:34 -1000 Subject: [PATCH 0154/1151] Bump zeroconf to 0.72.3 (#97668) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 73ebe15d0c7..bb0ec29271e 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.72.0"] + "requirements": ["zeroconf==0.72.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c4ad02afe79..b5d2f39c4a9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.72.0 +zeroconf==0.72.3 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 0a09f288d2c..76f144b2743 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2752,7 +2752,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.72.0 +zeroconf==0.72.3 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0c4c1fece6..b7b50c32660 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2025,7 +2025,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.72.0 +zeroconf==0.72.3 # homeassistant.components.zeversolar zeversolar==0.3.1 From 5e2d3b13b08153028213b853675e865c6d0bf0bb Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 3 Aug 2023 11:07:44 -0700 Subject: [PATCH 0155/1151] Bump opower to 0.0.19 (#97706) --- 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 c0eb319c10c..1b351d73011 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.18"] + "requirements": ["opower==0.0.19"] } diff --git a/requirements_all.txt b/requirements_all.txt index 76f144b2743..431c517dfd8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1368,7 +1368,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.18 +opower==0.0.19 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7b50c32660..bdbfe428731 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1037,7 +1037,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.18 +opower==0.0.19 # homeassistant.components.oralb oralb-ble==0.17.6 From c43e6d9bd1a760d8af14b4f8e49c84f10a1961a3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 3 Aug 2023 20:36:09 +0200 Subject: [PATCH 0156/1151] Fix detection of client wan-access rule in AVM Fritz!Box Tools (#97708) --- homeassistant/components/fritz/common.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index cdea8ebee54..8dfe5be9308 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -469,6 +469,11 @@ class FritzBoxTools( 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"], @@ -476,7 +481,7 @@ class FritzBoxTools( connection_type="", ip_address=host["IPAddress"], ssid=None, - wan_access="granted" in host["X_AVM-DE_WANAccess"], + wan_access=wan_access_result, ) if not self.fritz_status.device_has_mesh_support or ( From 6ed31840bca11440a2f2ece8a9417c8897174a2c Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 3 Aug 2023 21:06:45 +0200 Subject: [PATCH 0157/1151] Fix color mode attribute for both official and non official Hue lights (#97683) --- homeassistant/components/hue/v2/light.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 957aa4a7806..f42da406599 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -89,6 +89,7 @@ class HueLight(HueBaseEntity, LightEntity): self._supported_color_modes.add(ColorMode.BRIGHTNESS) # support transition if brightness control self._attr_supported_features |= LightEntityFeature.TRANSITION + self._color_temp_active: bool = False # get list of supported effects (combine effects and timed_effects) self._attr_effect_list = [] if effects := resource.effects: @@ -121,10 +122,8 @@ class HueLight(HueBaseEntity, LightEntity): @property def color_mode(self) -> ColorMode: """Return the color mode of the light.""" - if color_temp := self.resource.color_temperature: - # Hue lights return `mired_valid` to indicate CT is active - if color_temp.mirek is not None: - return ColorMode.COLOR_TEMP + if self.color_temp_active: + return ColorMode.COLOR_TEMP if self.resource.supports_color: return ColorMode.XY if self.resource.supports_dimming: @@ -132,6 +131,18 @@ class HueLight(HueBaseEntity, LightEntity): # fallback to on_off return ColorMode.ONOFF + @property + def color_temp_active(self) -> bool: + """Return if the light is in Color Temperature mode.""" + color_temp = self.resource.color_temperature + if color_temp is None or color_temp.mirek is None: + return False + # Official Hue lights return `mirek_valid` to indicate CT is active + # while non-official lights do not. + if self.device.product_data.certified: + return self.resource.color_temperature.mirek_valid + return self._color_temp_active + @property def xy_color(self) -> tuple[float, float] | None: """Return the xy color.""" @@ -193,6 +204,7 @@ class HueLight(HueBaseEntity, LightEntity): xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP)) brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) + self._color_temp_active = color_temp is not None flash = kwargs.get(ATTR_FLASH) effect = effect_str = kwargs.get(ATTR_EFFECT) if effect_str in (EFFECT_NONE, EFFECT_NONE.lower()): From e7e68907fa39cabebc8e8b05d46286988f7dc891 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 3 Aug 2023 21:11:15 +0200 Subject: [PATCH 0158/1151] Fix UniFi image platform not loading when passphrase is missing from WLAN (#97684) --- homeassistant/components/unifi/image.py | 4 ++-- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index dc4fb93eded..c3969c21bc4 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -41,7 +41,7 @@ class UnifiImageEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): """Validate and load entities from different UniFi handlers.""" image_fn: Callable[[UniFiController, ApiItemT], bytes] - value_fn: Callable[[ApiItemT], str] + value_fn: Callable[[ApiItemT], str | None] @dataclass @@ -99,7 +99,7 @@ class UnifiImageEntity(UnifiEntity[HandlerT, ApiItemT], ImageEntity): _attr_content_type = "image/png" current_image: bytes | None = None - previous_value = "" + previous_value: str | None = None def __init__( self, diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index c34d1035158..4cc45ddb6b8 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==50"], + "requirements": ["aiounifi==51"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 431c517dfd8..23482726019 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==50 +aiounifi==51 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdbfe428731..e5fd5484f9a 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==50 +aiounifi==51 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 6fd60024cce9c5cc00313f60aa1d433d0d710fa0 Mon Sep 17 00:00:00 2001 From: Meow Date: Thu, 3 Aug 2023 21:12:01 +0200 Subject: [PATCH 0159/1151] Refactored deprecated UNITS (#97368) --- .../specific_devices/test_arlo_baby.py | 4 +- .../specific_devices/test_ecobee3.py | 4 +- .../specific_devices/test_eve_degree.py | 9 +- .../specific_devices/test_mysa_living.py | 4 +- .../specific_devices/test_velux_gateway.py | 4 +- tests/helpers/test_temperature.py | 11 +- tests/helpers/test_template.py | 16 +- tests/util/test_distance.py | 179 ++++++++++-------- tests/util/test_temperature.py | 62 ++++-- 9 files changed, 165 insertions(+), 128 deletions(-) diff --git a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py index c90ff20c593..ae44f7f774f 100644 --- a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py +++ b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py @@ -1,6 +1,6 @@ """Make sure that an Arlo Baby can be setup.""" from homeassistant.components.sensor import SensorStateClass -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, EntityCategory +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature from homeassistant.core import HomeAssistant from ..common import ( @@ -64,7 +64,7 @@ async def test_arlo_baby_setup(hass: HomeAssistant) -> None: unique_id="00:00:00:00:00:00_1_1000", friendly_name="ArloBabyA0 Temperature", capabilities={"state_class": SensorStateClass.MEASUREMENT}, - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, state="24.0", ), EntityTestInfo( diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index d9af858de9f..1cdd4ccb907 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -15,7 +15,7 @@ from homeassistant.components.climate import ( ) from homeassistant.components.sensor import SensorStateClass from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -126,7 +126,7 @@ async def test_ecobee3_setup(hass: HomeAssistant) -> None: friendly_name="HomeW Current Temperature", unique_id="00:00:00:00:00:00_1_16_19", capabilities={"state_class": SensorStateClass.MEASUREMENT}, - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, state="21.8", ), EntityTestInfo( diff --git a/tests/components/homekit_controller/specific_devices/test_eve_degree.py b/tests/components/homekit_controller/specific_devices/test_eve_degree.py index e866069ef16..10fcd8ede8e 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_degree.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_degree.py @@ -1,7 +1,12 @@ """Make sure that Eve Degree (via Eve Extend) is enumerated properly.""" from homeassistant.components.number import NumberMode from homeassistant.components.sensor import SensorStateClass -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, EntityCategory, UnitOfPressure +from homeassistant.const import ( + PERCENTAGE, + EntityCategory, + UnitOfPressure, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant from ..common import ( @@ -36,7 +41,7 @@ async def test_eve_degree_setup(hass: HomeAssistant) -> None: unique_id="00:00:00:00:00:00_1_22", friendly_name="Eve Degree AA11 Temperature", capabilities={"state_class": SensorStateClass.MEASUREMENT}, - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, state="22.7719116210938", ), EntityTestInfo( diff --git a/tests/components/homekit_controller/specific_devices/test_mysa_living.py b/tests/components/homekit_controller/specific_devices/test_mysa_living.py index ca9c6cecde5..48828a2a6ad 100644 --- a/tests/components/homekit_controller/specific_devices/test_mysa_living.py +++ b/tests/components/homekit_controller/specific_devices/test_mysa_living.py @@ -1,7 +1,7 @@ """Make sure that Mysa Living is enumerated properly.""" from homeassistant.components.climate import ClimateEntityFeature from homeassistant.components.sensor import SensorStateClass -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant from ..common import ( @@ -55,7 +55,7 @@ async def test_mysa_living_setup(hass: HomeAssistant) -> None: entity_id="sensor.mysa_85dda9_current_temperature", friendly_name="Mysa-85dda9 Current Temperature", unique_id="00:00:00:00:00:00_1_20_25", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, capabilities={"state_class": SensorStateClass.MEASUREMENT}, state="24.1", ), diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py index 384b1d49d78..854de4b89d8 100644 --- a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -7,7 +7,7 @@ from homeassistant.components.sensor import SensorStateClass from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant @@ -73,7 +73,7 @@ async def test_velux_cover_setup(hass: HomeAssistant) -> None: friendly_name="VELUX Sensor Temperature sensor", capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="00:00:00:00:00:00_2_8", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, state="18.9", ), EntityTestInfo( diff --git a/tests/helpers/test_temperature.py b/tests/helpers/test_temperature.py index c4ab540f9d6..ceb4f7bdef2 100644 --- a/tests/helpers/test_temperature.py +++ b/tests/helpers/test_temperature.py @@ -5,8 +5,7 @@ from homeassistant.const import ( PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.temperature import display_temp @@ -18,21 +17,21 @@ def test_temperature_not_a_number(hass: HomeAssistant) -> None: """Test that temperature is a number.""" temp = "Temperature" with pytest.raises(Exception) as exception: - display_temp(hass, temp, TEMP_CELSIUS, PRECISION_HALVES) + display_temp(hass, temp, UnitOfTemperature.CELSIUS, PRECISION_HALVES) assert f"Temperature is not a number: {temp}" in str(exception.value) def test_celsius_halves(hass: HomeAssistant) -> None: """Test temperature to celsius rounding to halves.""" - assert display_temp(hass, TEMP, TEMP_CELSIUS, PRECISION_HALVES) == 24.5 + assert display_temp(hass, TEMP, UnitOfTemperature.CELSIUS, PRECISION_HALVES) == 24.5 def test_celsius_tenths(hass: HomeAssistant) -> None: """Test temperature to celsius rounding to tenths.""" - assert display_temp(hass, TEMP, TEMP_CELSIUS, PRECISION_TENTHS) == 24.6 + assert display_temp(hass, TEMP, UnitOfTemperature.CELSIUS, PRECISION_TENTHS) == 24.6 def test_fahrenheit_wholes(hass: HomeAssistant) -> None: """Test temperature to fahrenheit rounding to wholes.""" - assert display_temp(hass, TEMP, TEMP_FAHRENHEIT, PRECISION_WHOLE) == -4 + assert display_temp(hass, TEMP, UnitOfTemperature.FAHRENHEIT, PRECISION_WHOLE) == -4 diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 0c3f0e4469a..851ba7fb79b 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -19,15 +19,15 @@ from homeassistant.components import group from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - LENGTH_METERS, - LENGTH_MILLIMETERS, - MASS_GRAMS, STATE_ON, STATE_UNAVAILABLE, - TEMP_CELSIUS, VOLUME_LITERS, + UnitOfLength, + UnitOfMass, + UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError @@ -52,12 +52,12 @@ def _set_up_units(hass: HomeAssistant) -> None: """Set up the tests.""" hass.config.units = UnitSystem( "custom", - accumulated_precipitation=LENGTH_MILLIMETERS, + accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, conversions={}, - length=LENGTH_METERS, - mass=MASS_GRAMS, + length=UnitOfLength.METERS, + mass=UnitOfMass.GRAMS, pressure=UnitOfPressure.PA, - temperature=TEMP_CELSIUS, + temperature=UnitOfTemperature.CELSIUS, volume=VOLUME_LITERS, wind_speed=UnitOfSpeed.KILOMETERS_PER_HOUR, ) diff --git a/tests/util/test_distance.py b/tests/util/test_distance.py index d0b4bbc1ebe..90c2238bb63 100644 --- a/tests/util/test_distance.py +++ b/tests/util/test_distance.py @@ -3,38 +3,39 @@ import pytest from homeassistant.const import ( - LENGTH_CENTIMETERS, - LENGTH_FEET, - LENGTH_INCHES, - LENGTH_KILOMETERS, - LENGTH_METERS, - LENGTH_MILES, - LENGTH_MILLIMETERS, - LENGTH_YARD, + UnitOfLength, ) from homeassistant.exceptions import HomeAssistantError import homeassistant.util.distance as distance_util INVALID_SYMBOL = "bob" -VALID_SYMBOL = LENGTH_KILOMETERS +VALID_SYMBOL = UnitOfLength.KILOMETERS def test_raise_deprecation_warning(caplog: pytest.LogCaptureFixture) -> None: """Ensure that a warning is raised on use of convert.""" - assert distance_util.convert(2, LENGTH_METERS, LENGTH_METERS) == 2 + assert distance_util.convert(2, UnitOfLength.METERS, UnitOfLength.METERS) == 2 assert "use unit_conversion.DistanceConverter instead" in caplog.text def test_convert_same_unit() -> None: """Test conversion from any unit to same unit.""" - assert distance_util.convert(5, LENGTH_KILOMETERS, LENGTH_KILOMETERS) == 5 - assert distance_util.convert(2, LENGTH_METERS, LENGTH_METERS) == 2 - assert distance_util.convert(6, LENGTH_CENTIMETERS, LENGTH_CENTIMETERS) == 6 - assert distance_util.convert(3, LENGTH_MILLIMETERS, LENGTH_MILLIMETERS) == 3 - assert distance_util.convert(10, LENGTH_MILES, LENGTH_MILES) == 10 - assert distance_util.convert(9, LENGTH_YARD, LENGTH_YARD) == 9 - assert distance_util.convert(8, LENGTH_FEET, LENGTH_FEET) == 8 - assert distance_util.convert(7, LENGTH_INCHES, LENGTH_INCHES) == 7 + assert ( + distance_util.convert(5, UnitOfLength.KILOMETERS, UnitOfLength.KILOMETERS) == 5 + ) + assert distance_util.convert(2, UnitOfLength.METERS, UnitOfLength.METERS) == 2 + assert ( + distance_util.convert(6, UnitOfLength.CENTIMETERS, UnitOfLength.CENTIMETERS) + == 6 + ) + assert ( + distance_util.convert(3, UnitOfLength.MILLIMETERS, UnitOfLength.MILLIMETERS) + == 3 + ) + assert distance_util.convert(10, UnitOfLength.MILES, UnitOfLength.MILES) == 10 + assert distance_util.convert(9, UnitOfLength.YARDS, UnitOfLength.YARDS) == 9 + assert distance_util.convert(8, UnitOfLength.FEET, UnitOfLength.FEET) == 8 + assert distance_util.convert(7, UnitOfLength.INCHES, UnitOfLength.INCHES) == 7 def test_convert_invalid_unit() -> None: @@ -49,133 +50,145 @@ def test_convert_invalid_unit() -> None: def test_convert_nonnumeric_value() -> None: """Test exception is thrown for nonnumeric type.""" with pytest.raises(TypeError): - distance_util.convert("a", LENGTH_KILOMETERS, LENGTH_METERS) + distance_util.convert("a", UnitOfLength.KILOMETERS, UnitOfLength.METERS) @pytest.mark.parametrize( ("unit", "expected"), [ - (LENGTH_KILOMETERS, 8.04672), - (LENGTH_METERS, 8046.72), - (LENGTH_CENTIMETERS, 804672.0), - (LENGTH_MILLIMETERS, 8046720.0), - (LENGTH_YARD, 8800.0), - (LENGTH_FEET, 26400.0008448), - (LENGTH_INCHES, 316800.171072), + (UnitOfLength.KILOMETERS, 8.04672), + (UnitOfLength.METERS, 8046.72), + (UnitOfLength.CENTIMETERS, 804672.0), + (UnitOfLength.MILLIMETERS, 8046720.0), + (UnitOfLength.YARDS, 8800.0), + (UnitOfLength.FEET, 26400.0008448), + (UnitOfLength.INCHES, 316800.171072), ], ) def test_convert_from_miles(unit, expected) -> None: """Test conversion from miles to other units.""" miles = 5 - assert distance_util.convert(miles, LENGTH_MILES, unit) == pytest.approx(expected) + assert distance_util.convert(miles, UnitOfLength.MILES, unit) == pytest.approx( + expected + ) @pytest.mark.parametrize( ("unit", "expected"), [ - (LENGTH_KILOMETERS, 0.0045720000000000005), - (LENGTH_METERS, 4.572), - (LENGTH_CENTIMETERS, 457.2), - (LENGTH_MILLIMETERS, 4572), - (LENGTH_MILES, 0.002840908212), - (LENGTH_FEET, 15.00000048), - (LENGTH_INCHES, 180.0000972), + (UnitOfLength.KILOMETERS, 0.0045720000000000005), + (UnitOfLength.METERS, 4.572), + (UnitOfLength.CENTIMETERS, 457.2), + (UnitOfLength.MILLIMETERS, 4572), + (UnitOfLength.MILES, 0.002840908212), + (UnitOfLength.FEET, 15.00000048), + (UnitOfLength.INCHES, 180.0000972), ], ) def test_convert_from_yards(unit, expected) -> None: """Test conversion from yards to other units.""" yards = 5 - assert distance_util.convert(yards, LENGTH_YARD, unit) == pytest.approx(expected) + assert distance_util.convert(yards, UnitOfLength.YARDS, unit) == pytest.approx( + expected + ) @pytest.mark.parametrize( ("unit", "expected"), [ - (LENGTH_KILOMETERS, 1.524), - (LENGTH_METERS, 1524), - (LENGTH_CENTIMETERS, 152400.0), - (LENGTH_MILLIMETERS, 1524000.0), - (LENGTH_MILES, 0.9469694040000001), - (LENGTH_YARD, 1666.66667), - (LENGTH_INCHES, 60000.032400000004), + (UnitOfLength.KILOMETERS, 1.524), + (UnitOfLength.METERS, 1524), + (UnitOfLength.CENTIMETERS, 152400.0), + (UnitOfLength.MILLIMETERS, 1524000.0), + (UnitOfLength.MILES, 0.9469694040000001), + (UnitOfLength.YARDS, 1666.66667), + (UnitOfLength.INCHES, 60000.032400000004), ], ) def test_convert_from_feet(unit, expected) -> None: """Test conversion from feet to other units.""" feet = 5000 - assert distance_util.convert(feet, LENGTH_FEET, unit) == pytest.approx(expected) + assert distance_util.convert(feet, UnitOfLength.FEET, unit) == pytest.approx( + expected + ) @pytest.mark.parametrize( ("unit", "expected"), [ - (LENGTH_KILOMETERS, 0.127), - (LENGTH_METERS, 127.0), - (LENGTH_CENTIMETERS, 12700.0), - (LENGTH_MILLIMETERS, 127000.0), - (LENGTH_MILES, 0.078914117), - (LENGTH_YARD, 138.88889), - (LENGTH_FEET, 416.66668), + (UnitOfLength.KILOMETERS, 0.127), + (UnitOfLength.METERS, 127.0), + (UnitOfLength.CENTIMETERS, 12700.0), + (UnitOfLength.MILLIMETERS, 127000.0), + (UnitOfLength.MILES, 0.078914117), + (UnitOfLength.YARDS, 138.88889), + (UnitOfLength.FEET, 416.66668), ], ) def test_convert_from_inches(unit, expected) -> None: """Test conversion from inches to other units.""" inches = 5000 - assert distance_util.convert(inches, LENGTH_INCHES, unit) == pytest.approx(expected) + assert distance_util.convert(inches, UnitOfLength.INCHES, unit) == pytest.approx( + expected + ) @pytest.mark.parametrize( ("unit", "expected"), [ - (LENGTH_METERS, 5000), - (LENGTH_CENTIMETERS, 500000), - (LENGTH_MILLIMETERS, 5000000), - (LENGTH_MILES, 3.106855), - (LENGTH_YARD, 5468.066), - (LENGTH_FEET, 16404.2), - (LENGTH_INCHES, 196850.5), + (UnitOfLength.METERS, 5000), + (UnitOfLength.CENTIMETERS, 500000), + (UnitOfLength.MILLIMETERS, 5000000), + (UnitOfLength.MILES, 3.106855), + (UnitOfLength.YARDS, 5468.066), + (UnitOfLength.FEET, 16404.2), + (UnitOfLength.INCHES, 196850.5), ], ) def test_convert_from_kilometers(unit, expected) -> None: """Test conversion from kilometers to other units.""" km = 5 - assert distance_util.convert(km, LENGTH_KILOMETERS, unit) == pytest.approx(expected) + assert distance_util.convert(km, UnitOfLength.KILOMETERS, unit) == pytest.approx( + expected + ) @pytest.mark.parametrize( ("unit", "expected"), [ - (LENGTH_KILOMETERS, 5), - (LENGTH_CENTIMETERS, 500000), - (LENGTH_MILLIMETERS, 5000000), - (LENGTH_MILES, 3.106855), - (LENGTH_YARD, 5468.066), - (LENGTH_FEET, 16404.2), - (LENGTH_INCHES, 196850.5), + (UnitOfLength.KILOMETERS, 5), + (UnitOfLength.CENTIMETERS, 500000), + (UnitOfLength.MILLIMETERS, 5000000), + (UnitOfLength.MILES, 3.106855), + (UnitOfLength.YARDS, 5468.066), + (UnitOfLength.FEET, 16404.2), + (UnitOfLength.INCHES, 196850.5), ], ) def test_convert_from_meters(unit, expected) -> None: """Test conversion from meters to other units.""" m = 5000 - assert distance_util.convert(m, LENGTH_METERS, unit) == pytest.approx(expected) + assert distance_util.convert(m, UnitOfLength.METERS, unit) == pytest.approx( + expected + ) @pytest.mark.parametrize( ("unit", "expected"), [ - (LENGTH_KILOMETERS, 5), - (LENGTH_METERS, 5000), - (LENGTH_MILLIMETERS, 5000000), - (LENGTH_MILES, 3.106855), - (LENGTH_YARD, 5468.066), - (LENGTH_FEET, 16404.2), - (LENGTH_INCHES, 196850.5), + (UnitOfLength.KILOMETERS, 5), + (UnitOfLength.METERS, 5000), + (UnitOfLength.MILLIMETERS, 5000000), + (UnitOfLength.MILES, 3.106855), + (UnitOfLength.YARDS, 5468.066), + (UnitOfLength.FEET, 16404.2), + (UnitOfLength.INCHES, 196850.5), ], ) def test_convert_from_centimeters(unit, expected) -> None: """Test conversion from centimeters to other units.""" cm = 500000 - assert distance_util.convert(cm, LENGTH_CENTIMETERS, unit) == pytest.approx( + assert distance_util.convert(cm, UnitOfLength.CENTIMETERS, unit) == pytest.approx( expected ) @@ -183,18 +196,18 @@ def test_convert_from_centimeters(unit, expected) -> None: @pytest.mark.parametrize( ("unit", "expected"), [ - (LENGTH_KILOMETERS, 5), - (LENGTH_METERS, 5000), - (LENGTH_CENTIMETERS, 500000), - (LENGTH_MILES, 3.106855), - (LENGTH_YARD, 5468.066), - (LENGTH_FEET, 16404.2), - (LENGTH_INCHES, 196850.5), + (UnitOfLength.KILOMETERS, 5), + (UnitOfLength.METERS, 5000), + (UnitOfLength.CENTIMETERS, 500000), + (UnitOfLength.MILES, 3.106855), + (UnitOfLength.YARDS, 5468.066), + (UnitOfLength.FEET, 16404.2), + (UnitOfLength.INCHES, 196850.5), ], ) def test_convert_from_millimeters(unit, expected) -> None: """Test conversion from millimeters to other units.""" mm = 5000000 - assert distance_util.convert(mm, LENGTH_MILLIMETERS, unit) == pytest.approx( + assert distance_util.convert(mm, UnitOfLength.MILLIMETERS, unit) == pytest.approx( expected ) diff --git a/tests/util/test_temperature.py b/tests/util/test_temperature.py index d1b1a9fbb11..93edb8f7393 100644 --- a/tests/util/test_temperature.py +++ b/tests/util/test_temperature.py @@ -1,17 +1,22 @@ """Test Home Assistant temperature utility functions.""" import pytest -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN +from homeassistant.const import UnitOfTemperature from homeassistant.exceptions import HomeAssistantError import homeassistant.util.temperature as temperature_util INVALID_SYMBOL = "bob" -VALID_SYMBOL = TEMP_CELSIUS +VALID_SYMBOL = UnitOfTemperature.CELSIUS def test_raise_deprecation_warning(caplog: pytest.LogCaptureFixture) -> None: """Ensure that a warning is raised on use of convert.""" - assert temperature_util.convert(2, TEMP_CELSIUS, TEMP_CELSIUS) == 2 + assert ( + temperature_util.convert( + 2, UnitOfTemperature.CELSIUS, UnitOfTemperature.CELSIUS + ) + == 2 + ) assert "use unit_conversion.TemperatureConverter instead" in caplog.text @@ -34,9 +39,22 @@ def test_deprecated_functions( def test_convert_same_unit() -> None: """Test conversion from any unit to same unit.""" - assert temperature_util.convert(2, TEMP_CELSIUS, TEMP_CELSIUS) == 2 - assert temperature_util.convert(3, TEMP_FAHRENHEIT, TEMP_FAHRENHEIT) == 3 - assert temperature_util.convert(4, TEMP_KELVIN, TEMP_KELVIN) == 4 + assert ( + temperature_util.convert( + 2, UnitOfTemperature.CELSIUS, UnitOfTemperature.CELSIUS + ) + == 2 + ) + assert ( + temperature_util.convert( + 3, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.FAHRENHEIT + ) + == 3 + ) + assert ( + temperature_util.convert(4, UnitOfTemperature.KELVIN, UnitOfTemperature.KELVIN) + == 4 + ) def test_convert_invalid_unit() -> None: @@ -51,24 +69,26 @@ def test_convert_invalid_unit() -> None: def test_convert_nonnumeric_value() -> None: """Test exception is thrown for nonnumeric type.""" with pytest.raises(TypeError): - temperature_util.convert("a", TEMP_CELSIUS, TEMP_FAHRENHEIT) + temperature_util.convert( + "a", UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + ) def test_convert_from_celsius() -> None: """Test conversion from C to other units.""" celsius = 100 assert temperature_util.convert( - celsius, TEMP_CELSIUS, TEMP_FAHRENHEIT + celsius, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT ) == pytest.approx(212.0) assert temperature_util.convert( - celsius, TEMP_CELSIUS, TEMP_KELVIN + celsius, UnitOfTemperature.CELSIUS, UnitOfTemperature.KELVIN ) == pytest.approx(373.15) # Interval assert temperature_util.convert( - celsius, TEMP_CELSIUS, TEMP_FAHRENHEIT, True + celsius, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT, True ) == pytest.approx(180.0) assert temperature_util.convert( - celsius, TEMP_CELSIUS, TEMP_KELVIN, True + celsius, UnitOfTemperature.CELSIUS, UnitOfTemperature.KELVIN, True ) == pytest.approx(100) @@ -76,33 +96,33 @@ def test_convert_from_fahrenheit() -> None: """Test conversion from F to other units.""" fahrenheit = 100 assert temperature_util.convert( - fahrenheit, TEMP_FAHRENHEIT, TEMP_CELSIUS + fahrenheit, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS ) == pytest.approx(37.77777777777778) assert temperature_util.convert( - fahrenheit, TEMP_FAHRENHEIT, TEMP_KELVIN + fahrenheit, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.KELVIN ) == pytest.approx(310.92777777777775) # Interval assert temperature_util.convert( - fahrenheit, TEMP_FAHRENHEIT, TEMP_CELSIUS, True + fahrenheit, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS, True ) == pytest.approx(55.55555555555556) assert temperature_util.convert( - fahrenheit, TEMP_FAHRENHEIT, TEMP_KELVIN, True + fahrenheit, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.KELVIN, True ) == pytest.approx(55.55555555555556) def test_convert_from_kelvin() -> None: """Test conversion from K to other units.""" kelvin = 100 - assert temperature_util.convert(kelvin, TEMP_KELVIN, TEMP_CELSIUS) == pytest.approx( - -173.15 - ) assert temperature_util.convert( - kelvin, TEMP_KELVIN, TEMP_FAHRENHEIT + kelvin, UnitOfTemperature.KELVIN, UnitOfTemperature.CELSIUS + ) == pytest.approx(-173.15) + assert temperature_util.convert( + kelvin, UnitOfTemperature.KELVIN, UnitOfTemperature.FAHRENHEIT ) == pytest.approx(-279.66999999999996) # Interval assert temperature_util.convert( - kelvin, TEMP_KELVIN, TEMP_FAHRENHEIT, True + kelvin, UnitOfTemperature.KELVIN, UnitOfTemperature.FAHRENHEIT, True ) == pytest.approx(180.0) assert temperature_util.convert( - kelvin, TEMP_KELVIN, TEMP_KELVIN, True + kelvin, UnitOfTemperature.KELVIN, UnitOfTemperature.KELVIN, True ) == pytest.approx(100) From 9e7872e78b322bc910b2660cb1ec0672ecf68147 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:15:08 -0700 Subject: [PATCH 0160/1151] Fix NWS twice_daily forecast day/night detection (#97703) --- homeassistant/components/nws/const.py | 1 - homeassistant/components/nws/weather.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index 109af7a565b..e5718d5132f 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -26,7 +26,6 @@ CONF_STATION = "station" ATTRIBUTION = "Data from National Weather Service/NOAA" ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description" -ATTR_FORECAST_DAYTIME = "daytime" CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_EXCEPTIONAL: [ diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index e8a35ba66f1..8ddf842cd62 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -9,6 +9,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, ATTR_FORECAST_HUMIDITY, + ATTR_FORECAST_IS_DAYTIME, ATTR_FORECAST_NATIVE_DEW_POINT, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_WIND_SPEED, @@ -36,7 +37,6 @@ from homeassistant.util.unit_system import UnitSystem from . import base_unique_id, device_info from .const import ( - ATTR_FORECAST_DAYTIME, ATTR_FORECAST_DETAILED_DESCRIPTION, ATTRIBUTION, CONDITION_CLASSES, @@ -101,7 +101,6 @@ if TYPE_CHECKING: """Forecast with extra fields needed for NWS.""" detailed_description: str | None - daytime: bool | None class NWSWeather(WeatherEntity): @@ -268,7 +267,7 @@ class NWSWeather(WeatherEntity): data[ATTR_FORECAST_HUMIDITY] = forecast_entry.get("relativeHumidity") if self.mode == DAYNIGHT: - data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime") + data[ATTR_FORECAST_IS_DAYTIME] = forecast_entry.get("isDaytime") time = forecast_entry.get("iconTime") weather = forecast_entry.get("iconWeather") From 415e4b224364a55363aace3ee60ff252acb68569 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 3 Aug 2023 21:17:00 +0200 Subject: [PATCH 0161/1151] Bump python-opensky to 0.2.0 (#97687) --- homeassistant/components/opensky/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index f3fb13589bb..4d1047222ff 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/opensky", "iot_class": "cloud_polling", - "requirements": ["python-opensky==0.0.10"] + "requirements": ["python-opensky==0.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 23482726019..8843f861e29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2137,7 +2137,7 @@ python-mystrom==2.2.0 python-opendata-transport==0.3.0 # homeassistant.components.opensky -python-opensky==0.0.10 +python-opensky==0.2.0 # homeassistant.components.otbr # homeassistant.components.thread diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5fd5484f9a..9c90292fa71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1569,7 +1569,7 @@ python-miio==0.5.12 python-mystrom==2.2.0 # homeassistant.components.opensky -python-opensky==0.0.10 +python-opensky==0.2.0 # homeassistant.components.otbr # homeassistant.components.thread From bae5a3dbd641e39ed9dfc237ebdefa1937ecd896 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 3 Aug 2023 21:20:40 +0200 Subject: [PATCH 0162/1151] Fix ZHA `turn_on` issues with `transition=0`, improve tests (#97539) * Fix turn_on ignoring transition=0 and brightness=None, add test This fixes light.turn_on for ZHA lights ignoring a transition of 0 when no brightness is given at the same time. It also adds a test for that case. Fixes https://github.com/home-assistant/core/issues/93265 * Add test for "force on" lights This test checks that "force on" lights also get an "on" command (in addition to the "move to level" command) when turn_on is called with only transition=0. * Fix "on" command sent for transition=0 calls, fix FORCE_ON missing for transition=0 This fixes an issue where the "on" command is sent in addition to a "move_to_level_with_on_off" command, even though the latter one is enough (for non-FORCE_ON lights). It also fixes the test to not expect the unnecessary "on" command (in addition to the expected "move_to_level_with_on_off" command). The `brightness != 0` change is needed to fix an issue where FORCE_ON lights did not get the required "on" command (in addition to "move_to_level_with_on_off") if turn_on was called with only transition=0. (It could have been `brightness not None`, but that would also send an "on" command if turn_on is called with brightness=0 which HA somewhat "supports". The brightness != 0 check avoids that issue.) * Improve comments in ZHA light class --- homeassistant/components/zha/light.py | 12 ++-- tests/components/zha/test_light.py | 85 +++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 9e71691aaa5..73955614c07 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -329,7 +329,7 @@ class BaseLight(LogMixin, light.LightEntity): return if ( - (brightness is not None or transition) + (brightness is not None or transition is not None) and not new_color_provided_while_off and brightness_supported(self._attr_supported_color_modes) ): @@ -350,11 +350,11 @@ class BaseLight(LogMixin, light.LightEntity): self._attr_brightness = level if ( - brightness is None + (brightness is None and transition is None) and not new_color_provided_while_off - or (self._FORCE_ON and brightness) + or (self._FORCE_ON and brightness != 0) ): - # since some lights don't always turn on with move_to_level_with_on_off, + # since FORCE_ON lights don't turn on with move_to_level_with_on_off, # we should call the on command on the on_off cluster # if brightness is not 0. result = await self._on_off_cluster_handler.on() @@ -385,7 +385,7 @@ class BaseLight(LogMixin, light.LightEntity): return if new_color_provided_while_off: - # The light is has the correct color, so we can now transition + # The light has the correct color, so we can now transition # it to the correct brightness level. result = await self._level_cluster_handler.move_to_level( level=level, transition_time=int(10 * duration) @@ -1076,7 +1076,7 @@ class HueLight(Light): manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"}, ) class ForceOnLight(Light): - """Representation of a light which does not respect move_to_level_with_on_off.""" + """Representation of a light which does not respect on/off for move_to_level_with_on_off commands.""" _attr_name: str = "Light" _FORCE_ON = True diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 3abfd0e4f9c..c1f5cf04e35 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -530,7 +530,78 @@ async def test_transitions( light2_state = hass.states.get(device_2_entity_id) assert light2_state.state == STATE_OFF - # first test 0 length transition with no color provided + # first test 0 length transition with no color and no brightness provided + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_level.request.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": device_1_entity_id, "transition": 0}, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 1 + assert dev1_cluster_level.request.await_count == 1 + assert dev1_cluster_level.request.call_args == call( + False, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + level=254, # default "full on" brightness + transition_time=0, + expect_reply=True, + manufacturer=None, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 254 + + # test 0 length transition with no color and no brightness provided again, but for "force on" lights + eWeLink_cluster_on_off.request.reset_mock() + eWeLink_cluster_level.request.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": eWeLink_light_entity_id, "transition": 0}, + blocking=True, + ) + assert eWeLink_cluster_on_off.request.call_count == 1 + assert eWeLink_cluster_on_off.request.await_count == 1 + assert eWeLink_cluster_on_off.request.call_args_list[0] == call( + False, + eWeLink_cluster_on_off.commands_by_name["on"].id, + eWeLink_cluster_on_off.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tsn=None, + ) + assert eWeLink_cluster_color.request.call_count == 0 + assert eWeLink_cluster_color.request.await_count == 0 + assert eWeLink_cluster_level.request.call_count == 1 + assert eWeLink_cluster_level.request.await_count == 1 + assert eWeLink_cluster_level.request.call_args == call( + False, + eWeLink_cluster_level.commands_by_name["move_to_level_with_on_off"].id, + eWeLink_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + level=254, # default "full on" brightness + transition_time=0, + expect_reply=True, + manufacturer=None, + tsn=None, + ) + + eWeLink_state = hass.states.get(eWeLink_light_entity_id) + assert eWeLink_state.state == STATE_ON + assert eWeLink_state.attributes["brightness"] == 254 + + eWeLink_cluster_on_off.request.reset_mock() + eWeLink_cluster_level.request.reset_mock() + + # test 0 length transition with brightness, but no color provided dev1_cluster_on_off.request.reset_mock() dev1_cluster_level.request.reset_mock() await hass.services.async_call( @@ -1423,18 +1494,10 @@ async def async_test_level_on_off_from_hass( {"entity_id": entity_id, "transition": 10}, blocking=True, ) - assert on_off_cluster.request.call_count == 1 - assert on_off_cluster.request.await_count == 1 + assert on_off_cluster.request.call_count == 0 + assert on_off_cluster.request.await_count == 0 assert level_cluster.request.call_count == 1 assert level_cluster.request.await_count == 1 - assert on_off_cluster.request.call_args == call( - False, - on_off_cluster.commands_by_name["on"].id, - on_off_cluster.commands_by_name["on"].schema, - expect_reply=True, - manufacturer=None, - tsn=None, - ) assert level_cluster.request.call_args == call( False, level_cluster.commands_by_name["move_to_level_with_on_off"].id, From d33955c467721cf7e68c8b94735c0ac1118dea18 Mon Sep 17 00:00:00 2001 From: Blastoise186 <40033667+blastoise186@users.noreply.github.com> Date: Thu, 3 Aug 2023 20:36:12 +0100 Subject: [PATCH 0163/1151] Bump Cryptography to 41.0.3 for a second security fix (#97611) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- 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 b5d2f39c4a9..12e8afc3cf7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ bluetooth-auto-recovery==1.2.1 bluetooth-data-tools==1.6.1 certifi>=2021.5.30 ciso8601==2.3.0 -cryptography==41.0.2 +cryptography==41.0.3 dbus-fast==1.90.1 fnv-hash-fast==0.4.0 ha-av==10.1.1 diff --git a/pyproject.toml b/pyproject.toml index 497a4540dda..07d527eb77c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ "lru-dict==1.2.0", "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==41.0.2", + "cryptography==41.0.3", # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==23.2.0", "orjson==3.9.2", diff --git a/requirements.txt b/requirements.txt index 9f5023c9a1c..4106a8515d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ifaddr==0.2.0 Jinja2==3.1.2 lru-dict==1.2.0 PyJWT==2.8.0 -cryptography==41.0.2 +cryptography==41.0.3 pyOpenSSL==23.2.0 orjson==3.9.2 pip>=21.3.1 From 83af2f5b8b9a5d69372c612d4bde32d3618e78c4 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 3 Aug 2023 21:49:22 +0200 Subject: [PATCH 0164/1151] Allow to sort options in select selector (#97680) Co-authored-by: Franck Nijhof --- homeassistant/helpers/selector.py | 2 ++ tests/components/knx/test_device_trigger.py | 1 + tests/helpers/test_selector.py | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 08975c5c881..192777ae3be 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -988,6 +988,7 @@ class SelectSelectorConfig(TypedDict, total=False): custom_value: bool mode: SelectSelectorMode translation_key: str + sort: bool @SELECTORS.register("select") @@ -1005,6 +1006,7 @@ class SelectSelector(Selector[SelectSelectorConfig]): vol.Coerce(SelectSelectorMode), lambda val: val.value ), vol.Optional("translation_key"): cv.string, + vol.Optional("sort", default=False): cv.boolean, } ) diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index c3d3ed67b03..e901fd7f29e 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -201,6 +201,7 @@ async def test_get_trigger_capabilities_node_status( "mode": "dropdown", "multiple": True, "options": [], + "sort": False, }, }, } diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index c1d5f76ea78..590526cdb2b 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -655,6 +655,11 @@ def test_text_selector_schema(schema, valid_selections, invalid_selections) -> N (["red"], ["green", "blue"], []), (0, None, "red"), ), + ( + {"options": ["red", "green", "blue"], "sort": True}, + ("red", "blue"), + (0, None, ["red"]), + ), ), ) def test_select_selector_schema(schema, valid_selections, invalid_selections) -> None: From bfa394d399068d5282161c4ae59c087954cb6c68 Mon Sep 17 00:00:00 2001 From: Michael Arthur Date: Fri, 4 Aug 2023 09:09:14 +1200 Subject: [PATCH 0165/1151] address code comments / tidy ups (#97716) --- homeassistant/components/electric_kiwi/select.py | 16 +++++++--------- homeassistant/components/electric_kiwi/sensor.py | 6 +++--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/electric_kiwi/select.py b/homeassistant/components/electric_kiwi/select.py index a474c315258..9d883c72d1e 100644 --- a/homeassistant/components/electric_kiwi/select.py +++ b/homeassistant/components/electric_kiwi/select.py @@ -29,9 +29,8 @@ async def async_setup_entry( """Electric Kiwi select setup.""" hop_coordinator: ElectricKiwiHOPDataCoordinator = hass.data[DOMAIN][entry.entry_id] - _LOGGER.debug("Setting up HOP entity") - entities = [ElectricKiwiSelectHOPEntity(hop_coordinator, HOP_SELECT)] - async_add_entities(entities) + _LOGGER.debug("Setting up select entity") + async_add_entities([ElectricKiwiSelectHOPEntity(hop_coordinator, HOP_SELECT)]) class ElectricKiwiSelectHOPEntity( @@ -46,16 +45,15 @@ class ElectricKiwiSelectHOPEntity( def __init__( self, - hop_coordinator: ElectricKiwiHOPDataCoordinator, + coordinator: ElectricKiwiHOPDataCoordinator, description: SelectEntityDescription, ) -> None: """Initialise the HOP selection entity.""" - super().__init__(hop_coordinator) - self._attr_unique_id = f"{self.coordinator._ek_api.customer_number}_{self.coordinator._ek_api.connection_id}_{description.key}" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator._ek_api.customer_number}_{coordinator._ek_api.connection_id}_{description.key}" self.entity_description = description - self._state = None - self.values_dict = self.coordinator.get_hop_options() - self._attr_options = list(self.values_dict.keys()) + self.values_dict = coordinator.get_hop_options() + self._attr_options = list(self.values_dict) @property def current_option(self) -> str | None: diff --git a/homeassistant/components/electric_kiwi/sensor.py b/homeassistant/components/electric_kiwi/sensor.py index a657b768aa5..a3943437d4f 100644 --- a/homeassistant/components/electric_kiwi/sensor.py +++ b/homeassistant/components/electric_kiwi/sensor.py @@ -99,13 +99,13 @@ class ElectricKiwiHOPEntity( def __init__( self, - hop_coordinator: ElectricKiwiHOPDataCoordinator, + coordinator: ElectricKiwiHOPDataCoordinator, description: ElectricKiwiHOPSensorEntityDescription, ) -> None: """Entity object for Electric Kiwi sensor.""" - super().__init__(hop_coordinator) + super().__init__(coordinator) - self._attr_unique_id = f"{self.coordinator._ek_api.customer_number}_{self.coordinator._ek_api.connection_id}_{description.key}" + self._attr_unique_id = f"{coordinator._ek_api.customer_number}_{coordinator._ek_api.connection_id}_{description.key}" self.entity_description = description @property From fd26739bbf58303420bec02e1747d5937b11b9c2 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 3 Aug 2023 23:23:12 +0200 Subject: [PATCH 0166/1151] Fix unloading KNX integration without sensors (#97720) --- homeassistant/components/knx/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 1bb6d9bbdd2..3444e9b002a 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -342,10 +342,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms( entry, [ - platform - for platform in SUPPORTED_PLATFORMS - if platform in hass.data[DATA_KNX_CONFIG] - and platform is not Platform.NOTIFY + Platform.SENSOR, # always unload system entities (telegram counter, etc.) + *[ + platform + for platform in SUPPORTED_PLATFORMS + if platform in hass.data[DATA_KNX_CONFIG] + and platform not in (Platform.SENSOR, Platform.NOTIFY) + ], ], ) if unload_ok: From f7aec46b691922771ee34f4407374de93582e16b Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 4 Aug 2023 00:11:36 +0200 Subject: [PATCH 0167/1151] Bump zigpy to 0.56.4 (#97722) --- 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 5d0fdc646cf..041a93a8ead 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -25,7 +25,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.102", "zigpy-deconz==0.21.0", - "zigpy==0.56.3", + "zigpy==0.56.4", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", "zigpy-znp==0.11.4" diff --git a/requirements_all.txt b/requirements_all.txt index 8843f861e29..a682ca35542 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2779,7 +2779,7 @@ zigpy-zigate==0.11.0 zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.56.3 +zigpy==0.56.4 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c90292fa71..e1d2fdbc0d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2046,7 +2046,7 @@ zigpy-zigate==0.11.0 zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.56.3 +zigpy==0.56.4 # homeassistant.components.zwave_js zwave-js-server-python==0.49.0 From 52fc3c26d132a8a67604babd39eb113012e0a785 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 00:55:18 +0200 Subject: [PATCH 0168/1151] Fix yalex_ble test RuntimeWarning (#97732) --- tests/components/yalexs_ble/test_config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py index 2df37a72b70..593b9a7a9d0 100644 --- a/tests/components/yalexs_ble/test_config_flow.py +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Yale Access Bluetooth config flow.""" import asyncio -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch from bleak import BleakError import pytest @@ -32,6 +32,7 @@ def _get_mock_push_lock(): """Return a mock PushLock.""" mock_push_lock = Mock() mock_push_lock.start = AsyncMock() + mock_push_lock.start.return_value = MagicMock() mock_push_lock.wait_for_first_update = AsyncMock() mock_push_lock.stop = AsyncMock() mock_push_lock.lock_state = LockState( From 282ae80cc2f253e556230e755cd4451a741476fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Aug 2023 12:55:33 -1000 Subject: [PATCH 0169/1151] Fix hassfest check for schema (#97713) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- script/hassfest/config_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/hassfest/config_schema.py b/script/hassfest/config_schema.py index b794834161d..da2de9a6013 100644 --- a/script/hassfest/config_schema.py +++ b/script/hassfest/config_schema.py @@ -20,7 +20,7 @@ def _has_assignment(module: ast.Module, name: str) -> bool: continue if type(item) == ast.Assign: for target in item.targets: - if target.id == name: + if getattr(target, "id", None) == name: return True continue if item.target.id == name: From d1ad1c47e6d3c9c21089f5e2d4d58fe08a7ce865 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 01:07:11 +0200 Subject: [PATCH 0170/1151] Fix zha test RuntimeWarnings (#97733) --- tests/components/zha/test_config_flow.py | 3 +++ tests/components/zha/test_gateway.py | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 17665994806..8e071247872 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -80,6 +80,9 @@ def mock_app(): "can_rewrite_custom_eui64": False, } } + mock_app.add_listener = MagicMock() + mock_app.groups = MagicMock() + mock_app.devices = MagicMock() with patch( "zigpy.application.ControllerApplication.new", AsyncMock(return_value=mock_app) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index c58aaedcbbc..b9fcd4b6932 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -324,12 +324,15 @@ async def test_gateway_initialize_bellows_thread( zha_gateway._config.setdefault("zigpy_config", {}).update(config_override) with patch( - "bellows.zigbee.application.ControllerApplication.new", - new=AsyncMock(), - ) as mock_new: + "bellows.zigbee.application.ControllerApplication.new" + ) as controller_app_mock: + mock = AsyncMock() + mock.add_listener = MagicMock() + mock.groups = MagicMock() + controller_app_mock.return_value = mock await zha_gateway.async_initialize() - assert mock_new.mock_calls[0].args[0]["use_thread"] is thread_state + assert controller_app_mock.mock_calls[0].args[0]["use_thread"] is thread_state @pytest.mark.parametrize( From ddb384c2edbbc0844fa802fcf77f4ea0733253ab Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 01:23:12 +0200 Subject: [PATCH 0171/1151] Fix airvisual RuntimeWarning (#97739) --- .../components/airvisual/config_flow.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 27e79f2d40b..893726fc022 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -109,19 +109,21 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "airvisual_checked_api_keys_lock", asyncio.Lock() ) - if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS: - coro = cloud_api.air_quality.nearest_city() - error_schema = self.geography_coords_schema - error_step = "geography_by_coords" - else: - coro = cloud_api.air_quality.city( - user_input[CONF_CITY], user_input[CONF_STATE], user_input[CONF_COUNTRY] - ) - error_schema = GEOGRAPHY_NAME_SCHEMA - error_step = "geography_by_name" - async with valid_keys_lock: if user_input[CONF_API_KEY] not in valid_keys: + if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS: + coro = cloud_api.air_quality.nearest_city() + error_schema = self.geography_coords_schema + error_step = "geography_by_coords" + else: + coro = cloud_api.air_quality.city( + user_input[CONF_CITY], + user_input[CONF_STATE], + user_input[CONF_COUNTRY], + ) + error_schema = GEOGRAPHY_NAME_SCHEMA + error_step = "geography_by_name" + try: await coro except (InvalidKeyError, KeyExpiredError, UnauthorizedError): From d1f83094232280ff97cafff271641e0639c0be3d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Aug 2023 16:48:53 -1000 Subject: [PATCH 0172/1151] Bump zeroconf to 0.74.0 (#97745) * Bump zeroconf to 0.74.0 changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.72.3...0.74.0 - more cython build fixes - performance improvements (mostly for pyatv) * handle typing * handle typing * remove if TYPE_CHECKING, this doesnt get called that often * remove if TYPE_CHECKING, this doesnt get called that often --- homeassistant/components/thread/discovery.py | 33 +++++++++++++------ homeassistant/components/zeroconf/__init__.py | 8 ++++- .../components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 34 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/thread/discovery.py b/homeassistant/components/thread/discovery.py index 1006a44d5d3..d07469f36fb 100644 --- a/homeassistant/components/thread/discovery.py +++ b/homeassistant/components/thread/discovery.py @@ -57,25 +57,29 @@ def async_discovery_data_from_service( except UnicodeDecodeError: return None - ext_addr = service.properties.get(b"xa") - ext_pan_id = service.properties.get(b"xp") - network_name = try_decode(service.properties.get(b"nn")) - model_name = try_decode(service.properties.get(b"mn")) + # Service properties are always bytes if they are set from the network. + # For legacy backwards compatibility zeroconf allows properties to be set + # as strings but we never do that so we can safely cast here. + service_properties = cast(dict[bytes, bytes | None], service.properties) + ext_addr = service_properties.get(b"xa") + ext_pan_id = service_properties.get(b"xp") + network_name = try_decode(service_properties.get(b"nn")) + model_name = try_decode(service_properties.get(b"mn")) server = service.server - vendor_name = try_decode(service.properties.get(b"vn")) - thread_version = try_decode(service.properties.get(b"tv")) + vendor_name = try_decode(service_properties.get(b"vn")) + thread_version = try_decode(service_properties.get(b"tv")) unconfigured = None brand = KNOWN_BRANDS.get(vendor_name) if brand == "homeassistant": # Attempt to detect incomplete configuration - if (state_bitmap_b := service.properties.get(b"sb")) is not None: + if (state_bitmap_b := service_properties.get(b"sb")) is not None: try: state_bitmap = StateBitmap.from_bytes(state_bitmap_b) if not state_bitmap.is_active: unconfigured = True except ValueError: _LOGGER.debug("Failed to decode state bitmap in service %s", service) - if service.properties.get(b"at") is None: + if service_properties.get(b"at") is None: unconfigured = True return ThreadRouterDiscoveryData( @@ -168,10 +172,19 @@ class ThreadRouterDiscovery: return _LOGGER.debug("_add_update_service %s %s", name, service) + # Service properties are always bytes if they are set from the network. + # For legacy backwards compatibility zeroconf allows properties to be set + # as strings but we never do that so we can safely cast here. + service_properties = cast(dict[bytes, bytes | None], service.properties) + + if not (xa := service_properties.get(b"xa")): + _LOGGER.debug("_add_update_service failed to find xa in %s", service) + return + # We use the extended mac address as key, bail out if it's missing try: - extended_mac_address = service.properties[b"xa"].hex() - except (KeyError, UnicodeDecodeError) as err: + extended_mac_address = xa.hex() + except UnicodeDecodeError as err: _LOGGER.debug("_add_update_service failed to parse service %s", err) return diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index f77909b1bdd..b85f9f0fd83 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -553,11 +553,17 @@ def info_from_service(service: AsyncServiceInfo) -> ZeroconfServiceInfo | None: break if not host: return None + + # Service properties are always bytes if they are set from the network. + # For legacy backwards compatibility zeroconf allows properties to be set + # as strings but we never do that so we can safely cast here. + service_properties = cast(dict[bytes, bytes | None], service.properties) + properties: dict[str, Any] = { k.decode("ascii", "replace"): None if v is None else v.decode("utf-8", "replace") - for k, v in service.properties.items() + for k, v in service_properties.items() } assert service.server is not None, "server cannot be none if there are addresses" diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index bb0ec29271e..cd7b9e95e75 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.72.3"] + "requirements": ["zeroconf==0.74.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 12e8afc3cf7..fa5b52478d0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -52,7 +52,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.72.3 +zeroconf==0.74.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index a682ca35542..f5476b8647d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2752,7 +2752,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.72.3 +zeroconf==0.74.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1d2fdbc0d4..e77638d777e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2025,7 +2025,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.72.3 +zeroconf==0.74.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 0cc80a9d29962341e45910e1edc7a594704e513a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Aug 2023 16:49:55 -1000 Subject: [PATCH 0173/1151] Add OUI to tplink diagnostics (#97646) * Add OUI to tplink diagnostics The main reason discovery does not work for new devices is we are missing the OUI. Since we redact the whole mac address in the diagnostics, this makes it difficult to fix. We now include the OUI in the diagnostics * fix: use cached mac * fix: tests --- homeassistant/components/tplink/diagnostics.py | 5 ++++- tests/components/tplink/test_diagnostics.py | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tplink/diagnostics.py b/homeassistant/components/tplink/diagnostics.py index 5121def2e47..c81356ee658 100644 --- a/homeassistant/components/tplink/diagnostics.py +++ b/homeassistant/components/tplink/diagnostics.py @@ -6,6 +6,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import format_mac from .const import DOMAIN from .coordinator import TPLinkDataUpdateCoordinator @@ -36,6 +37,8 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + oui = format_mac(coordinator.device.mac)[:8].upper() return async_redact_data( - {"device_last_response": coordinator.device.internal_state}, TO_REDACT + {"device_last_response": coordinator.device.internal_state, "oui": oui}, + TO_REDACT, ) diff --git a/tests/components/tplink/test_diagnostics.py b/tests/components/tplink/test_diagnostics.py index 5b3fe4803a4..3ef42c48b2f 100644 --- a/tests/components/tplink/test_diagnostics.py +++ b/tests/components/tplink/test_diagnostics.py @@ -14,17 +14,19 @@ from tests.typing import ClientSessionGenerator @pytest.mark.parametrize( - ("mocked_dev", "fixture_file", "sysinfo_vars"), + ("mocked_dev", "fixture_file", "sysinfo_vars", "expected_oui"), [ ( _mocked_bulb(), "tplink-diagnostics-data-bulb-kl130.json", ["mic_mac", "deviceId", "oemId", "hwId", "alias"], + "AA:BB:CC", ), ( _mocked_plug(), "tplink-diagnostics-data-plug-hs110.json", ["mac", "deviceId", "oemId", "hwId", "alias", "longitude_i", "latitude_i"], + "AA:BB:CC", ), ], ) @@ -34,6 +36,7 @@ async def test_diagnostics( mocked_dev: SmartDevice, fixture_file: str, sysinfo_vars: list[str], + expected_oui: str | None, ): """Test diagnostics for config entry.""" diagnostics_data = json.loads(load_fixture(fixture_file, "tplink")) @@ -58,3 +61,5 @@ async def test_diagnostics( sysinfo = last_response["system"]["get_sysinfo"] for var in sysinfo_vars: assert sysinfo[var] == "**REDACTED**" + + assert result["oui"] == expected_oui From 615d7f0da76925f784574aff9ccfd6d384869770 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 4 Aug 2023 00:30:03 -0600 Subject: [PATCH 0174/1151] Add ability to remove Litter-Robot if no longer provided by integration (#97702) --- .../components/litterrobot/__init__.py | 15 ++++++ tests/components/litterrobot/common.py | 14 ++++++ tests/components/litterrobot/test_init.py | 46 +++++++++++++++++-- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index c7eda2f118b..daf71fe8a6e 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -6,6 +6,7 @@ from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, LitterRobot4, Ro from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry from .const import DOMAIN from .hub import LitterRobotHub @@ -58,3 +59,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + hub: LitterRobotHub = hass.data[DOMAIN][config_entry.entry_id] + return not any( + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN + for robot in hub.account.robots + if robot.serial == identifier[1] + ) diff --git a/tests/components/litterrobot/common.py b/tests/components/litterrobot/common.py index f5b4e32a1e1..5bf6fb7cce6 100644 --- a/tests/components/litterrobot/common.py +++ b/tests/components/litterrobot/common.py @@ -142,3 +142,17 @@ FEEDER_ROBOT_DATA = { } VACUUM_ENTITY_ID = "vacuum.test_litter_box" + + +async def remove_device(ws_client, device_id, config_entry_id): + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index 8d340f40515..170d6313029 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -1,5 +1,5 @@ """Test Litter-Robot setup process.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException import pytest @@ -13,14 +13,18 @@ from homeassistant.components.vacuum import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.setup import async_setup_component -from .common import CONFIG, VACUUM_ENTITY_ID +from .common import CONFIG, VACUUM_ENTITY_ID, remove_device from .conftest import setup_integration from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator -async def test_unload_entry(hass: HomeAssistant, mock_account) -> None: +async def test_unload_entry(hass: HomeAssistant, mock_account: MagicMock) -> None: """Test being able to unload an entry.""" entry = await setup_integration(hass, mock_account, VACUUM_DOMAIN) @@ -49,7 +53,9 @@ async def test_unload_entry(hass: HomeAssistant, mock_account) -> None: ), ) async def test_entry_not_setup( - hass: HomeAssistant, side_effect, expected_state + hass: HomeAssistant, + side_effect: LitterRobotException, + expected_state: ConfigEntryState, ) -> None: """Test being able to handle config entry not setup.""" entry = MockConfigEntry( @@ -64,3 +70,35 @@ async def test_entry_not_setup( ): await hass.config_entries.async_setup(entry.entry_id) assert entry.state is expected_state + + +async def test_device_remove_devices( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_account: MagicMock +) -> None: + """Test we can only remove a device that no longer exists.""" + assert await async_setup_component(hass, "config", {}) + config_entry = await setup_integration(hass, mock_account, VACUUM_DOMAIN) + + registry: EntityRegistry = er.async_get(hass) + entity = registry.entities[VACUUM_ENTITY_ID] + assert entity.unique_id == "LR3C012345-litter_box" + + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get(entity.device_id) + assert ( + await remove_device( + await hass_ws_client(hass), device_entry.id, config_entry.entry_id + ) + is False + ) + + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(litterrobot.DOMAIN, "test-serial", "remove-serial")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), dead_device_entry.id, config_entry.entry_id + ) + is True + ) From 1587ac2137081653ac59039583d86d6892326fab Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 4 Aug 2023 08:45:36 +0200 Subject: [PATCH 0175/1151] Waqi State unknown if value is string (#97617) --- homeassistant/components/waqi/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index e91e3da5aa5..ae4e46c2a0b 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from contextlib import suppress from datetime import timedelta import logging @@ -141,8 +142,9 @@ class WaqiSensor(SensorEntity): @property def native_value(self): """Return the state of the device.""" - if self._data is not None: - return self._data.get("aqi") + if value := self._data.get("aqi"): + with suppress(ValueError): + return float(value) return None @property From 37885400c97152cb5988d7a9952c3b0f905ecb5f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 09:21:36 +0200 Subject: [PATCH 0176/1151] Fix mqtt test DeprecationWarnings (#97734) --- tests/components/mqtt/test_config_flow.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 2ebc4a50ef0..f0681a537da 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1,5 +1,6 @@ """Test config flow.""" -from collections.abc import Generator +from collections.abc import Generator, Iterator +from contextlib import contextmanager from pathlib import Path from random import getrandbits from ssl import SSLError @@ -136,19 +137,22 @@ def mock_process_uploaded_file(tmp_path: Path) -> Generator[MagicMock, None, Non file_id_cert = str(uuid4()) file_id_key = str(uuid4()) - def _mock_process_uploaded_file(hass: HomeAssistant, file_id) -> None: + @contextmanager + def _mock_process_uploaded_file( + hass: HomeAssistant, file_id: str + ) -> Iterator[Path | None]: if file_id == file_id_ca: with open(tmp_path / "ca.crt", "wb") as cafile: cafile.write(b"## mock CA certificate file ##") - return tmp_path / "ca.crt" + yield tmp_path / "ca.crt" elif file_id == file_id_cert: with open(tmp_path / "client.crt", "wb") as certfile: certfile.write(b"## mock client certificate file ##") - return tmp_path / "client.crt" + yield tmp_path / "client.crt" elif file_id == file_id_key: with open(tmp_path / "client.key", "wb") as keyfile: keyfile.write(b"## mock key file ##") - return tmp_path / "client.key" + yield tmp_path / "client.key" else: pytest.fail(f"Unexpected file_id: {file_id}") From 3ac2106eb8dd3a1a2dc2c687cd7855d8f888f4c6 Mon Sep 17 00:00:00 2001 From: Cyr-ius <1258123+cyr-ius@users.noreply.github.com> Date: Fri, 4 Aug 2023 09:25:51 +0200 Subject: [PATCH 0177/1151] Fix freebox enumerate raid disks (#97696) --- homeassistant/components/freebox/router.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 4a9c22847ae..122242f1959 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -161,10 +161,13 @@ class FreeboxRouter: async def _update_raids_sensors(self) -> None: """Update Freebox raids.""" # None at first request - fbx_raids: list[dict[str, Any]] = await self._api.storage.get_raids() or [] - - for fbx_raid in fbx_raids: - self.raids[fbx_raid["id"]] = fbx_raid + try: + fbx_raids: list[dict[str, Any]] = await self._api.storage.get_raids() or [] + except HttpRequestError: + _LOGGER.warning("Unable to enumerate raid disks") + else: + for fbx_raid in fbx_raids: + self.raids[fbx_raid["id"]] = fbx_raid async def update_home_devices(self) -> None: """Update Home devices (alarm, light, sensor, switch, remote ...).""" From df45e52dc5dbfd51ce8a939e8cf7a68555546eda Mon Sep 17 00:00:00 2001 From: Luke Date: Fri, 4 Aug 2023 03:28:48 -0400 Subject: [PATCH 0178/1151] Add battery sensor to Roborock (#97715) * add battery sensor * Remove translation for battery Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/roborock/sensor.py | 14 +++++++++++++- tests/components/roborock/test_sensor.py | 3 ++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 818fd338ffb..0629839f01b 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -13,7 +13,12 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import AREA_SQUARE_METERS, EntityCategory, UnitOfTime +from homeassistant.const import ( + AREA_SQUARE_METERS, + PERCENTAGE, + EntityCategory, + UnitOfTime, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -122,6 +127,13 @@ SENSOR_DESCRIPTIONS = [ entity_category=EntityCategory.DIAGNOSTIC, options=RoborockErrorCode.keys(), ), + RoborockSensorDescription( + key="battery", + value_fn=lambda data: data.status.battery, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + ), ] diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index f9f3d327d29..19648343bb4 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -14,7 +14,7 @@ from tests.common import MockConfigEntry async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> None: """Test sensors and check test values are correctly set.""" - assert len(hass.states.async_all("sensor")) == 10 + assert len(hass.states.async_all("sensor")) == 11 assert hass.states.get("sensor.roborock_s7_maxv_main_brush_time_left").state == str( MAIN_BRUSH_REPLACE_TIME - 74382 ) @@ -37,3 +37,4 @@ async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> Non ) assert hass.states.get("sensor.roborock_s7_maxv_cleaning_area").state == "21.0" assert hass.states.get("sensor.roborock_s7_maxv_vacuum_error").state == "none" + assert hass.states.get("sensor.roborock_s7_maxv_battery").state == "100" From b16254a0de73d475b1702e315fba2d89afda5871 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 4 Aug 2023 00:32:59 -0700 Subject: [PATCH 0179/1151] Bump opower to 0.0.20 (#97752) --- 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 1b351d73011..94758720722 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.19"] + "requirements": ["opower==0.0.20"] } diff --git a/requirements_all.txt b/requirements_all.txt index f5476b8647d..ef680477da3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1368,7 +1368,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.19 +opower==0.0.20 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e77638d777e..be55a636232 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1037,7 +1037,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.19 +opower==0.0.20 # homeassistant.components.oralb oralb-ble==0.17.6 From 99145d46d2738a405f49118c50d449a0f097a539 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 09:34:04 +0200 Subject: [PATCH 0180/1151] Fix keymitt_ble RuntimeWarning (#97729) --- homeassistant/components/keymitt_ble/config_flow.py | 2 +- tests/components/keymitt_ble/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/keymitt_ble/config_flow.py b/homeassistant/components/keymitt_ble/config_flow.py index 49ab04163dc..e8176b152a6 100644 --- a/homeassistant/components/keymitt_ble/config_flow.py +++ b/homeassistant/components/keymitt_ble/config_flow.py @@ -138,7 +138,7 @@ class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN): await self._client.connect(init=True) return self.async_show_form(step_id="link") - if not self._client.is_connected(): + if not await self._client.is_connected(): errors["base"] = "linking" else: await self._client.disconnect() diff --git a/tests/components/keymitt_ble/__init__.py b/tests/components/keymitt_ble/__init__.py index 2938e22c924..c6e56739d76 100644 --- a/tests/components/keymitt_ble/__init__.py +++ b/tests/components/keymitt_ble/__init__.py @@ -77,6 +77,6 @@ class MockMicroBotApiClientFail: async def disconnect(self): """Mock disconnect.""" - def is_connected(self): + async def is_connected(self): """Mock disconnected.""" return False From e905ac173f3db2bf314eb100d990ca5db3c6c271 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 09:34:55 +0200 Subject: [PATCH 0181/1151] Fix command_line tests RuntimeWarnings (#97731) --- tests/components/command_line/test_binary_sensor.py | 3 ++- tests/components/command_line/test_sensor.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index 50971219f48..7d5db4603fe 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -246,7 +246,7 @@ async def test_updating_to_often( assert called async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=15)) wait_till_event.set() - asyncio.wait(0) + await asyncio.sleep(0) assert ( "Updating Command Line Binary Sensor Test took longer than the scheduled update interval" not in caplog.text @@ -258,6 +258,7 @@ async def test_updating_to_often( await asyncio.sleep(0) async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) wait_till_event.set() + await asyncio.sleep(0) assert ( "Updating Command Line Binary Sensor Test took longer than the scheduled update interval" diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index bc24ff5419f..0fb47109ab1 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -580,7 +580,7 @@ async def test_updating_to_often( assert called async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=15)) wait_till_event.set() - asyncio.wait(0) + await asyncio.sleep(0) assert ( "Updating Command Line Sensor Test took longer than the scheduled update interval" @@ -593,6 +593,7 @@ async def test_updating_to_often( await asyncio.sleep(0) async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) wait_till_event.set() + await asyncio.sleep(0) assert ( "Updating Command Line Sensor Test took longer than the scheduled update interval" From c33e3ce212a119c73f2f20cc2d782278a877492f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:21:57 +0200 Subject: [PATCH 0182/1151] Fix core test RuntimeWarnings (#97730) --- tests/test_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 7e0766c8ac5..488975ef02f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1247,7 +1247,7 @@ async def test_serviceregistry_async_service_raise_exception( await hass.services.async_call("test_domain", "REGISTER_CALLS", blocking=True) # Non-blocking service call never throw exception - hass.services.async_call("test_domain", "REGISTER_CALLS", blocking=False) + await hass.services.async_call("test_domain", "REGISTER_CALLS", blocking=False) await hass.async_block_till_done() @@ -1267,7 +1267,7 @@ async def test_serviceregistry_callback_service_raise_exception( await hass.services.async_call("test_domain", "REGISTER_CALLS", blocking=True) # Non-blocking service call never throw exception - hass.services.async_call("test_domain", "REGISTER_CALLS", blocking=False) + await hass.services.async_call("test_domain", "REGISTER_CALLS", blocking=False) await hass.async_block_till_done() From f39a35c4ef258ab4a0b10443a240f87fa6599e22 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:25:08 +0200 Subject: [PATCH 0183/1151] Fix jinja2 DeprecationWarnings (#97728) --- tests/helpers/test_event.py | 2 +- tests/helpers/test_template.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 9436226b335..b88f716a8ec 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1903,7 +1903,7 @@ async def test_track_template_result_with_wildcard(hass: HomeAssistant) -> None: template_complex_str = r""" {% for state in states %} - {% if state.entity_id | regex_match('.*\.office_') %} + {% if state.entity_id | regex_match('.*\\.office_') %} {{ state.entity_id }}={{ state.state }} {% endif %} {% endfor %} diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 851ba7fb79b..9994f0cadc1 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -3437,7 +3437,7 @@ async def test_async_render_to_info_with_wildcard_matching_entity_id( template_complex_str = r""" {% for state in states.cover %} - {% if state.entity_id | regex_match('.*\.office_') %} + {% if state.entity_id | regex_match('.*\\.office_') %} {{ state.entity_id }}={{ state.state }} {% endif %} {% endfor %} From f68f9d4e01e603dd3a32c4d8611c2e78a0faf03c Mon Sep 17 00:00:00 2001 From: Nerdix <70015952+N3rdix@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:31:54 +0200 Subject: [PATCH 0184/1151] Fix Kostal_Plenticore SELECT entities using device_info correctly (#97690) --- homeassistant/components/kostal_plenticore/select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 2118d4b47c6..1a89e5617cc 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -111,7 +111,7 @@ class PlenticoreDataSelect( self.platform_name = platform_name self.module_id = description.module_id self.data_id = description.key - self._device_info = device_info + self._attr_device_info = device_info self._attr_unique_id = f"{entry_id}_{description.module_id}" @property From b23b2ac2e8f9203530571eecee32acba04c2bd61 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:32:23 +0200 Subject: [PATCH 0185/1151] Fix http test DeprecationWarnings (#97737) --- tests/components/http/test_security_filter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/http/test_security_filter.py b/tests/components/http/test_security_filter.py index 5469b7ebfa7..9e4353d7e61 100644 --- a/tests/components/http/test_security_filter.py +++ b/tests/components/http/test_security_filter.py @@ -75,7 +75,7 @@ async def test_bad_requests( fail_on_query_string, aiohttp_client: ClientSessionGenerator, caplog: pytest.LogCaptureFixture, - loop, + event_loop, ) -> None: """Test request paths that should be filtered.""" app = web.Application() @@ -93,7 +93,7 @@ async def test_bad_requests( man_params = "" http = urllib3.PoolManager() - resp = await loop.run_in_executor( + resp = await event_loop.run_in_executor( None, http.request, "GET", @@ -126,7 +126,7 @@ async def test_bad_requests_with_unsafe_bytes( fail_on_query_string, aiohttp_client: ClientSessionGenerator, caplog: pytest.LogCaptureFixture, - loop, + event_loop, ) -> None: """Test request with unsafe bytes in their URLs.""" app = web.Application() @@ -144,7 +144,7 @@ async def test_bad_requests_with_unsafe_bytes( man_params = "" http = urllib3.PoolManager() - resp = await loop.run_in_executor( + resp = await event_loop.run_in_executor( None, http.request, "GET", From cd8d6ecd8140fae9d65ebd640a2bd7ea381729e0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:32:51 +0200 Subject: [PATCH 0186/1151] Fix recorder DeprecationWarnings (#97738) --- tests/components/recorder/db_schema_16.py | 3 +-- tests/components/recorder/db_schema_18.py | 3 +-- tests/components/recorder/test_init.py | 2 +- tests/components/recorder/test_pool.py | 4 ++-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/components/recorder/db_schema_16.py b/tests/components/recorder/db_schema_16.py index 23b0ec1f921..36641ada625 100644 --- a/tests/components/recorder/db_schema_16.py +++ b/tests/components/recorder/db_schema_16.py @@ -23,8 +23,7 @@ from sqlalchemy import ( distinct, ) from sqlalchemy.dialects import mysql -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship +from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm.session import Session from homeassistant.const import ( diff --git a/tests/components/recorder/db_schema_18.py b/tests/components/recorder/db_schema_18.py index 3eeebc8e649..ba1cfa09cd4 100644 --- a/tests/components/recorder/db_schema_18.py +++ b/tests/components/recorder/db_schema_18.py @@ -23,8 +23,7 @@ from sqlalchemy import ( distinct, ) from sqlalchemy.dialects import mysql -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship +from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm.session import Session from homeassistant.const import ( diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 4e9a0261ec2..bdeecf14c57 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -2224,7 +2224,7 @@ async def test_connect_args_priority(hass: HomeAssistant, config_url) -> None: return "mysql" @classmethod - def dbapi(cls): + def import_dbapi(cls): ... def engine_created(*args): diff --git a/tests/components/recorder/test_pool.py b/tests/components/recorder/test_pool.py index f8442761200..3a6ff50af2b 100644 --- a/tests/components/recorder/test_pool.py +++ b/tests/components/recorder/test_pool.py @@ -26,14 +26,14 @@ def test_recorder_pool(caplog: pytest.LogCaptureFixture) -> None: def _get_connection_twice(): session = get_session() - connections.append(session.connection().connection.connection) + connections.append(session.connection().connection.driver_connection) session.close() if shutdown: engine.pool.shutdown() session = get_session() - connections.append(session.connection().connection.connection) + connections.append(session.connection().connection.driver_connection) session.close() caplog.clear() From 95ca609124d896d6d0ce1ef83ba540855faa2eb0 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Fri, 4 Aug 2023 11:33:03 +0200 Subject: [PATCH 0187/1151] Bump pyduotecno to 2023.8.3 (#97759) --- homeassistant/components/duotecno/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index c0bd29547c5..69490b6b5aa 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/duotecno", "iot_class": "local_push", - "requirements": ["pyduotecno==2023.8.1"] + "requirements": ["pyduotecno==2023.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index ef680477da3..bab601fbc0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1650,7 +1650,7 @@ pydrawise==2023.7.1 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.8.1 +pyduotecno==2023.8.3 # homeassistant.components.ebox pyebox==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be55a636232..5dede8d7daf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1223,7 +1223,7 @@ pydiscovergy==2.0.3 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.8.1 +pyduotecno==2023.8.3 # homeassistant.components.econet pyeconet==0.1.20 From 8719aa76ca9cc660b6915c02b5585e566ff0d1e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Aug 2023 23:46:19 -1000 Subject: [PATCH 0188/1151] Avoid calling the http access logging when logging is disabled in emulated_hue (#97750) --- homeassistant/components/emulated_hue/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 1ba93da716c..a98d2c08a48 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -6,6 +6,7 @@ import logging from aiohttp import web import voluptuous as vol +from homeassistant.components.http import HomeAssistantAccessLogger from homeassistant.components.network import async_get_source_ip from homeassistant.const import ( CONF_ENTITIES, @@ -100,7 +101,7 @@ async def start_emulated_hue_bridge( config.advertise_port or config.listen_port, ) - runner = web.AppRunner(app) + runner = web.AppRunner(app, access_log_class=HomeAssistantAccessLogger) await runner.setup() site = web.TCPSite(runner, config.host_ip_addr, config.listen_port) From 6adb06956b23adaa10af0f3490bf9d9792002c21 Mon Sep 17 00:00:00 2001 From: Matthieu Barthelemy Date: Fri, 4 Aug 2023 11:50:03 +0200 Subject: [PATCH 0189/1151] Add overkiz battery sensor level medium (#97472) --- homeassistant/components/overkiz/sensor.py | 2 +- homeassistant/components/overkiz/strings.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index c841e3b0e36..b5296d772df 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -67,7 +67,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:battery", device_class=SensorDeviceClass.ENUM, - options=["full", "normal", "low", "verylow"], + options=["full", "normal", "medium", "low", "verylow"], translation_key="battery", ), OverkizSensorDescription( diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index c4daf32499a..bcf1e121f6f 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -77,6 +77,7 @@ "full": "Full", "low": "Low", "normal": "Normal", + "medium": "Medium", "verylow": "Very low" } }, From 447479d0a0075f0adcd91f2e0c6ad15ab8ff223e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 12:29:18 +0200 Subject: [PATCH 0190/1151] Add packaging as default requirement (#97712) --- homeassistant/package_constraints.txt | 1 + homeassistant/runner.py | 8 +++++--- pyproject.toml | 1 + requirements.txt | 1 + tests/test_runner.py | 19 ++++++++++++------- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fa5b52478d0..2fdbd60dd46 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,6 +31,7 @@ Jinja2==3.1.2 lru-dict==1.2.0 mutagen==1.46.0 orjson==3.9.2 +packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.0.0 pip>=21.3.1 diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 67ec232db9c..4bbf1a7dada 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -11,6 +11,8 @@ import threading import traceback from typing import Any +import packaging.tags + from . import bootstrap from .core import callback from .helpers.frame import warn_use @@ -29,7 +31,6 @@ from .util.thread import deadlock_safe_shutdown # MAX_EXECUTOR_WORKERS = 64 TASK_CANCELATION_TIMEOUT = 5 -ALPINE_RELEASE_FILE = "/etc/alpine-release" _LOGGER = logging.getLogger(__name__) @@ -164,8 +165,9 @@ def _enable_posix_spawn() -> None: # The subprocess module does not know about Alpine Linux/musl # and will use fork() instead of posix_spawn() which significantly # less efficient. This is a workaround to force posix_spawn() - # on Alpine Linux which is supported by musl. - subprocess._USE_POSIX_SPAWN = os.path.exists(ALPINE_RELEASE_FILE) + # when using musl since cpython is not aware its supported. + tag = next(packaging.tags.sys_tags()) + subprocess._USE_POSIX_SPAWN = "musllinux" in tag.platform def run(runtime_config: RuntimeConfig) -> int: diff --git a/pyproject.toml b/pyproject.toml index 07d527eb77c..9a526187999 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ dependencies = [ # pyOpenSSL 23.2.0 is required to work with cryptography 41+ "pyOpenSSL==23.2.0", "orjson==3.9.2", + "packaging>=23.1", "pip>=21.3.1", "python-slugify==4.0.1", "PyYAML==6.0.1", diff --git a/requirements.txt b/requirements.txt index 4106a8515d1..debdc7dbcb3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ PyJWT==2.8.0 cryptography==41.0.3 pyOpenSSL==23.2.0 orjson==3.9.2 +packaging>=23.1 pip>=21.3.1 python-slugify==4.0.1 PyYAML==6.0.1 diff --git a/tests/test_runner.py b/tests/test_runner.py index f32321c578c..5fe5c2881ff 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -1,8 +1,10 @@ """Test the runner.""" import asyncio +from collections.abc import Iterator import threading from unittest.mock import patch +import packaging.tags import py import pytest @@ -147,19 +149,22 @@ async def test_unhandled_exception_traceback( def test__enable_posix_spawn() -> None: - """Test that we can enable posix_spawn on Alpine.""" + """Test that we can enable posix_spawn on musllinux.""" - def _mock_alpine_exists(path): - return path == "/etc/alpine-release" + def _mock_sys_tags_any() -> Iterator[packaging.tags.Tag]: + yield from packaging.tags.parse_tag("py3-none-any") - with patch.object(runner.subprocess, "_USE_POSIX_SPAWN", False), patch.object( - runner.os.path, "exists", _mock_alpine_exists + def _mock_sys_tags_musl() -> Iterator[packaging.tags.Tag]: + yield from packaging.tags.parse_tag("cp311-cp311-musllinux_1_1_x86_64") + + with patch.object(runner.subprocess, "_USE_POSIX_SPAWN", False), patch( + "homeassistant.runner.packaging.tags.sys_tags", side_effect=_mock_sys_tags_musl ): runner._enable_posix_spawn() assert runner.subprocess._USE_POSIX_SPAWN is True - with patch.object(runner.subprocess, "_USE_POSIX_SPAWN", False), patch.object( - runner.os.path, "exists", return_value=False + with patch.object(runner.subprocess, "_USE_POSIX_SPAWN", False), patch( + "homeassistant.runner.packaging.tags.sys_tags", side_effect=_mock_sys_tags_any ): runner._enable_posix_spawn() assert runner.subprocess._USE_POSIX_SPAWN is False From 3f5a21dce41907616a82e3c36449e8992cf48636 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 Aug 2023 12:31:32 +0200 Subject: [PATCH 0191/1151] Fix mailbox PytestCollectionWarning (#97740) --- tests/components/mailbox/test_init.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index e0cae290e2b..5a5333a42d5 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -40,6 +40,9 @@ def _create_message(idx: int) -> dict[str, Any]: class TestMailbox(mailbox.Mailbox): """Test Mailbox, with 10 sample messages.""" + # This class doesn't contain any tests! Skip pytest test collection. + __test__ = False + def __init__(self, hass: HomeAssistant, name: str) -> None: """Initialize Test mailbox.""" super().__init__(hass, name) From 0971449b94ed3bd3463542bd5b32035ec36235c2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 4 Aug 2023 12:42:31 +0200 Subject: [PATCH 0192/1151] Remove unused translation key from OpenSky (#97699) --- homeassistant/components/opensky/strings.json | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/opensky/strings.json b/homeassistant/components/opensky/strings.json index 768ffde155f..c5746ffdb46 100644 --- a/homeassistant/components/opensky/strings.json +++ b/homeassistant/components/opensky/strings.json @@ -4,7 +4,6 @@ "user": { "description": "Fill in the location to track.", "data": { - "name": "[%key:common::config_flow::data::api_key%]", "radius": "Radius", "latitude": "[%key:common::config_flow::data::latitude%]", "longitude": "[%key:common::config_flow::data::longitude%]", From 2820514c3300cf08917d7ccb49f0b1565954d4b6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 4 Aug 2023 12:43:06 +0200 Subject: [PATCH 0193/1151] Break long strings in Axis integration (#97254) --- homeassistant/components/axis/camera.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 53e2c3c9fe5..0b3a93f24fc 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -71,18 +71,30 @@ class AxisCamera(AxisEntity, MjpegCamera): Additionally used when device change IP address. """ image_options = self.generate_options(skip_stream_profile=True) - self._still_image_url = f"http://{self.device.host}:{self.device.port}/axis-cgi/jpg/image.cgi{image_options}" + self._still_image_url = ( + f"http://{self.device.host}:{self.device.port}/axis-cgi" + f"/jpg/image.cgi{image_options}" + ) mjpeg_options = self.generate_options() - self._mjpeg_url = f"http://{self.device.host}:{self.device.port}/axis-cgi/mjpg/video.cgi{mjpeg_options}" + self._mjpeg_url = ( + f"http://{self.device.host}:{self.device.port}/axis-cgi" + f"/mjpg/video.cgi{mjpeg_options}" + ) stream_options = self.generate_options(add_video_codec_h264=True) - self._stream_source = f"rtsp://{self.device.username}:{self.device.password}@{self.device.host}/axis-media/media.amp{stream_options}" + self._stream_source = ( + f"rtsp://{self.device.username}:{self.device.password}" + f"@{self.device.host}/axis-media/media.amp{stream_options}" + ) self.device.additional_diagnostics["camera_sources"] = { "Image": self._still_image_url, "MJPEG": self._mjpeg_url, - "Stream": f"rtsp://user:pass@{self.device.host}/axis-media/media.amp{stream_options}", + "Stream": ( + f"rtsp://user:pass@{self.device.host}/axis-media" + f"/media.amp{stream_options}" + ), } @property From ecce601d3ff9f9220c36cb38fe8bef61df550059 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 4 Aug 2023 12:46:23 +0200 Subject: [PATCH 0194/1151] Fix WAQI being zero (#97767) --- homeassistant/components/waqi/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index ae4e46c2a0b..71ec703df3f 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -142,7 +142,7 @@ class WaqiSensor(SensorEntity): @property def native_value(self): """Return the state of the device.""" - if value := self._data.get("aqi"): + if (value := self._data.get("aqi")) is not None: with suppress(ValueError): return float(value) return None From 80d0f3223741ce6cf7cb6b12ad003556662ce36b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 4 Aug 2023 12:46:53 +0200 Subject: [PATCH 0195/1151] Add has entity name to Solarlog (#97764) --- homeassistant/components/solarlog/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index a69d2a4c382..936dc998c86 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -218,6 +218,8 @@ async def async_setup_entry( class SolarlogSensor(CoordinatorEntity[SolarlogData], SensorEntity): """Representation of a Sensor.""" + _attr_has_entity_name = True + entity_description: SolarLogSensorEntityDescription def __init__( @@ -228,7 +230,6 @@ class SolarlogSensor(CoordinatorEntity[SolarlogData], SensorEntity): """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{coordinator.name} {description.name}" self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, coordinator.unique_id)}, From d78e39d5683d63c94c490817e37147e89e780519 Mon Sep 17 00:00:00 2001 From: amitfin Date: Fri, 4 Aug 2023 13:51:04 +0300 Subject: [PATCH 0196/1151] Fix allow_name_translation logic (#97701) --- script/hassfest/translations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 1754c166ef7..22c3e927703 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -61,8 +61,9 @@ def allow_name_translation(integration: Integration) -> bool: """Validate that the translation name is not the same as the integration name.""" # Only enforce for core because custom integrations can't be # added to allow list. - return integration.core and ( - integration.domain in ALLOW_NAME_TRANSLATION + return ( + not integration.core + or integration.domain in ALLOW_NAME_TRANSLATION or integration.quality_scale == "internal" ) From 9282cb21ab317c788523405f7f0c0c20220e9b7e Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 4 Aug 2023 18:54:54 +0800 Subject: [PATCH 0197/1151] Raise PlatformNotReady on initial OwnTone connection failure (#97257) --- .../components/forked_daapd/__init__.py | 5 +++-- .../components/forked_daapd/media_player.py | 6 ++++-- .../forked_daapd/test_config_flow.py | 19 ++++++++++++++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index 14f40db2057..9dfb92c60c8 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -18,9 +18,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Remove forked-daapd component.""" status = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if status and hass.data.get(DOMAIN) and hass.data[DOMAIN].get(entry.entry_id): - hass.data[DOMAIN][entry.entry_id][ + if websocket_handler := hass.data[DOMAIN][entry.entry_id][ HASS_DATA_UPDATER_KEY - ].websocket_handler.cancel() + ].websocket_handler: + websocket_handler.cancel() for remove_listener in hass.data[DOMAIN][entry.entry_id][ HASS_DATA_REMOVE_LISTENERS_KEY ]: diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index e1f1ece055b..868ec8e1f9e 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -31,6 +31,7 @@ from homeassistant.components.spotify import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -127,10 +128,10 @@ async def async_setup_entry( forked_daapd_updater = ForkedDaapdUpdater( hass, forked_daapd_api, config_entry.entry_id ) - await forked_daapd_updater.async_init() hass.data[DOMAIN][config_entry.entry_id][ HASS_DATA_UPDATER_KEY ] = forked_daapd_updater + await forked_daapd_updater.async_init() async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -914,7 +915,8 @@ class ForkedDaapdUpdater: async def async_init(self): """Perform async portion of class initialization.""" - server_config = await self._api.get_request("config") + if not (server_config := await self._api.get_request("config")): + raise PlatformNotReady if websocket_port := server_config.get("websocket_port"): self.websocket_handler = asyncio.create_task( self._api.start_websocket_handler( diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index 81357b6f3eb..fc02cdb4123 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -1,5 +1,5 @@ """The config flow tests for the forked_daapd media player platform.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -12,9 +12,11 @@ from homeassistant.components.forked_daapd.const import ( CONF_TTS_VOLUME, DOMAIN, ) +from homeassistant.components.forked_daapd.media_player import async_setup_entry from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from tests.common import MockConfigEntry @@ -242,3 +244,18 @@ async def test_options_flow(hass: HomeAssistant, config_entry) -> None: }, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + + +async def test_async_setup_entry_not_ready(hass: HomeAssistant, config_entry) -> None: + """Test that a PlatformNotReady exception is thrown during platform setup.""" + + with patch( + "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", + autospec=True, + ) as mock_api: + mock_api.return_value.get_request.return_value = None + config_entry.add_to_hass(hass) + with pytest.raises(PlatformNotReady): + await async_setup_entry(hass, config_entry, MagicMock()) + await hass.async_block_till_done() + mock_api.return_value.get_request.assert_called_once() From 41eca416389a7b4dc819a22cf555545dbfc534c9 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 4 Aug 2023 05:08:49 -0700 Subject: [PATCH 0198/1151] Handle Alert exception on notification failure (#93632) --- homeassistant/components/alert/__init__.py | 13 ++++++++++--- tests/components/alert/test_init.py | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 9b3fb0f29c8..721ed0d0c21 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -26,6 +26,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HassJob, HomeAssistant +from homeassistant.exceptions import ServiceNotFound import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -293,9 +294,15 @@ class Alert(Entity): LOGGER.debug(msg_payload) for target in self._notifiers: - await self.hass.services.async_call( - DOMAIN_NOTIFY, target, msg_payload, context=self._context - ) + try: + await self.hass.services.async_call( + DOMAIN_NOTIFY, target, msg_payload, context=self._context + ) + except ServiceNotFound: + LOGGER.error( + "Failed to call notify.%s, retrying at next notification interval", + target, + ) async def async_turn_on(self, **kwargs: Any) -> None: """Async Unacknowledge alert.""" diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index 550727a2a22..8dfbb437646 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -36,6 +36,7 @@ from tests.common import async_mock_service NAME = "alert_test" DONE_MESSAGE = "alert_gone" NOTIFIER = "test" +BAD_NOTIFIER = "bad_notifier" TEMPLATE = "{{ states.sensor.test.entity_id }}" TEST_ENTITY = "sensor.test" TITLE = "{{ states.sensor.test.entity_id }}" @@ -199,6 +200,26 @@ async def test_notification( assert len(mock_notifier) == 2 +async def test_bad_notifier( + hass: HomeAssistant, mock_notifier: list[ServiceCall] +) -> None: + """Test a broken notifier does not break the alert.""" + config = deepcopy(TEST_CONFIG) + config[DOMAIN][NAME][CONF_NOTIFIERS] = [BAD_NOTIFIER, NOTIFIER] + assert await async_setup_component(hass, DOMAIN, config) + assert len(mock_notifier) == 0 + + hass.states.async_set("sensor.test", STATE_ON) + await hass.async_block_till_done() + assert len(mock_notifier) == 1 + assert hass.states.get(ENTITY_ID).state == STATE_ON + + hass.states.async_set("sensor.test", STATE_OFF) + await hass.async_block_till_done() + assert len(mock_notifier) == 2 + assert hass.states.get(ENTITY_ID).state == STATE_IDLE + + async def test_no_notifiers( hass: HomeAssistant, mock_notifier: list[ServiceCall] ) -> None: From b5e23ee6509ee3c141505ae365411784af8b148c Mon Sep 17 00:00:00 2001 From: Ian Harcombe Date: Fri, 4 Aug 2023 16:04:18 +0100 Subject: [PATCH 0199/1151] Fix metoffice visibility range sensor device class (#97763) * Fix for issue #97694 Issue #97694 points out that the visibility range is a string with a band of distances, not a single value, which is causing issues for front end components. Removed the device class to leave as an informational string value. * Removed *all* empty device_class statements * Missed an empty device_class :( --- homeassistant/components/metoffice/sensor.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 3bf50525ca9..fcb8e5b134e 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -51,14 +51,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="name", name="Station name", - device_class=None, icon="mdi:label-outline", entity_registry_enabled_default=False, ), SensorEntityDescription( key="weather", name="Weather", - device_class=None, icon="mdi:weather-sunny", # but will adapt to current conditions entity_registry_enabled_default=True, ), @@ -107,7 +105,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="visibility", name="Visibility", - device_class=None, icon="mdi:eye", entity_registry_enabled_default=False, ), @@ -115,14 +112,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="visibility_distance", name="Visibility distance", native_unit_of_measurement=UnitOfLength.KILOMETERS, - device_class=SensorDeviceClass.DISTANCE, icon="mdi:eye", entity_registry_enabled_default=False, ), SensorEntityDescription( key="uv", name="UV index", - device_class=None, native_unit_of_measurement=UV_INDEX, icon="mdi:weather-sunny-alert", entity_registry_enabled_default=True, @@ -130,7 +125,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="precipitation", name="Probability of precipitation", - device_class=None, native_unit_of_measurement=PERCENTAGE, icon="mdi:weather-rainy", entity_registry_enabled_default=True, From 66cb407e4f9135d05e3251d1bd7ed3d474749923 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 4 Aug 2023 19:22:46 +0200 Subject: [PATCH 0200/1151] Improve counting of UniFi WLAN Clients sensor (#97785) --- homeassistant/components/unifi/sensor.py | 2 ++ tests/components/unifi/test_sensor.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 7dc086878e3..bb9365d486b 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -77,6 +77,8 @@ def async_wlan_client_value_fn(controller: UniFiController, wlan: Wlan) -> int: client.mac for client in controller.api.clients.values() if client.essid == wlan.name + and dt_util.utcnow() - dt_util.utc_from_timestamp(client.last_seen or 0) + < controller.option_detection_time ] ) diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index d619cd4c3c9..3d50df8ada9 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -470,6 +470,7 @@ async def test_wlan_client_sensors( wireless_client_1 = { "essid": "SSID 1", "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), "mac": "00:00:00:00:00:01", "name": "Wireless client", "oui": "Producer", @@ -479,6 +480,7 @@ async def test_wlan_client_sensors( wireless_client_2 = { "essid": "SSID 2", "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), "mac": "00:00:00:00:00:02", "name": "Wireless client2", "oui": "Producer2", @@ -526,9 +528,17 @@ async def test_wlan_client_sensors( # Verify state update - decreasing number wireless_client_1["essid"] = "SSID" - wireless_client_2["essid"] = "SSID" - mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_1) + + async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + ssid_1 = hass.states.get("sensor.ssid_1") + assert ssid_1.state == "1" + + # Verify state update - decreasing number + + wireless_client_2["last_seen"] = 0 mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_2) async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) From b286da211a376696d8dc2ab1dcec6e81d00d9bdf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Aug 2023 19:25:01 +0200 Subject: [PATCH 0201/1151] Add is_admin check to check configuration API (#97788) --- homeassistant/components/config/core.py | 4 ++++ tests/components/config/test_core.py | 15 +++++++++++++++ tests/conftest.py | 4 ++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index 999e9433cbb..5a825e5676a 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -9,6 +9,7 @@ from homeassistant.components.http import HomeAssistantView 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 @@ -30,6 +31,9 @@ class CheckConfigView(HomeAssistantView): 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" diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 9612609c1c5..fa7f33858a6 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -60,6 +60,21 @@ async def test_validate_config_ok( assert result["errors"] == "beer" +async def test_validate_config_requires_admin( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_read_only_access_token: str, +) -> None: + """Test checking configuration does not work as a normal user.""" + with patch.object(config, "SECTIONS", ["core"]): + await async_setup_component(hass, "config", {}) + + client = await hass_client(hass_read_only_access_token) + resp = await client.post("/api/config/core/check_config") + + assert resp.status == HTTPStatus.UNAUTHORIZED + + async def test_websocket_core_update(hass: HomeAssistant, client) -> None: """Test core config update websocket command.""" assert hass.config.latitude != 60 diff --git a/tests/conftest.py b/tests/conftest.py index 40fd1c2eef0..0b63ddec6af 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -744,10 +744,10 @@ def hass_client( ) -> ClientSessionGenerator: """Return an authenticated HTTP client.""" - async def auth_client() -> TestClient: + async def auth_client(access_token: str | None = hass_access_token) -> TestClient: """Return an authenticated client.""" return await aiohttp_client( - hass.http.app, headers={"Authorization": f"Bearer {hass_access_token}"} + hass.http.app, headers={"Authorization": f"Bearer {access_token}"} ) return auth_client From bbc34bae87d2c5fac4e1bb89831dd427ccc8c2c1 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 4 Aug 2023 20:14:32 +0200 Subject: [PATCH 0202/1151] modbus: use pb not pymodbus consistently as name. (#97780) Use pb not pymodbus consistently as name. --- .../components/modbus/base_platform.py | 8 ++--- .../components/modbus/binary_sensor.py | 2 +- homeassistant/components/modbus/climate.py | 16 +++++----- homeassistant/components/modbus/cover.py | 6 ++-- homeassistant/components/modbus/modbus.py | 32 +++++++++---------- homeassistant/components/modbus/sensor.py | 2 +- 6 files changed, 31 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 337919c81f7..c936773bea7 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -73,10 +73,6 @@ class BasePlatform(Entity): def __init__(self, hub: ModbusHub, entry: dict[str, Any]) -> None: """Initialize the Modbus binary sensor.""" self._hub = hub - # temporary fix, - # make sure slave is always defined to avoid an error in pymodbus - # attr(in_waiting) not defined. - # see issue #657 and PR #660 in riptideio/pymodbus self._slave = entry.get(CONF_SLAVE, 0) self._address = int(entry[CONF_ADDRESS]) self._input_type = entry[CONF_INPUT_TYPE] @@ -287,7 +283,7 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): async def async_turn(self, command: int) -> None: """Evaluate switch result.""" - result = await self._hub.async_pymodbus_call( + result = await self._hub.async_pb_call( self._slave, self._address, command, self._write_type ) if result is None: @@ -323,7 +319,7 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): if self._call_active: return self._call_active = True - result = await self._hub.async_pymodbus_call( + result = await self._hub.async_pb_call( self._slave, self._verify_address, 1, self._verify_type ) self._call_active = False diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 43f43585775..05668bac0a9 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -97,7 +97,7 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): if self._call_active: return self._call_active = True - result = await self._hub.async_pymodbus_call( + result = await self._hub.async_pb_call( self._slave, self._address, self._count, self._input_type ) self._call_active = False diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 27a82c7f53b..95f8bee0bc9 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -155,14 +155,14 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): if self._hvac_onoff_register is not None: # Turn HVAC Off by writing 0 to the On/Off register, or 1 otherwise. if self._hvac_onoff_write_registers: - await self._hub.async_pymodbus_call( + await self._hub.async_pb_call( self._slave, self._hvac_onoff_register, [0 if hvac_mode == HVACMode.OFF else 1], CALL_TYPE_WRITE_REGISTERS, ) else: - await self._hub.async_pymodbus_call( + await self._hub.async_pb_call( self._slave, self._hvac_onoff_register, 0 if hvac_mode == HVACMode.OFF else 1, @@ -174,14 +174,14 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): for value, mode in self._hvac_mode_mapping: if mode == hvac_mode: if self._hvac_mode_write_registers: - await self._hub.async_pymodbus_call( + await self._hub.async_pb_call( self._slave, self._hvac_mode_register, [value], CALL_TYPE_WRITE_REGISTERS, ) else: - await self._hub.async_pymodbus_call( + await self._hub.async_pb_call( self._slave, self._hvac_mode_register, value, @@ -217,21 +217,21 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): DataType.UINT16, ): if self._target_temperature_write_registers: - result = await self._hub.async_pymodbus_call( + result = await self._hub.async_pb_call( self._slave, self._target_temperature_register, [int(float(registers[0]))], CALL_TYPE_WRITE_REGISTERS, ) else: - result = await self._hub.async_pymodbus_call( + result = await self._hub.async_pb_call( self._slave, self._target_temperature_register, int(float(registers[0])), CALL_TYPE_WRITE_REGISTER, ) else: - result = await self._hub.async_pymodbus_call( + result = await self._hub.async_pb_call( self._slave, self._target_temperature_register, [int(float(i)) for i in registers], @@ -287,7 +287,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self, register_type: str, register: int, raw: bool | None = False ) -> float | None: """Read register using the Modbus hub slave.""" - result = await self._hub.async_pymodbus_call( + result = await self._hub.async_pb_call( self._slave, register, self._count, register_type ) if result is None: diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 62344f470c4..3c4247c61fb 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -120,7 +120,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" - result = await self._hub.async_pymodbus_call( + result = await self._hub.async_pb_call( self._slave, self._write_address, self._state_open, self._write_type ) self._attr_available = result is not None @@ -128,7 +128,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" - result = await self._hub.async_pymodbus_call( + result = await self._hub.async_pb_call( self._slave, self._write_address, self._state_closed, self._write_type ) self._attr_available = result is not None @@ -142,7 +142,7 @@ class ModbusCover(BasePlatform, CoverEntity, RestoreEntity): if self._call_active: return self._call_active = True - result = await self._hub.async_pymodbus_call( + result = await self._hub.async_pb_call( self._slave, self._address, 1, self._input_type ) self._call_active = False diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index dd32e74cdbd..fdb7be3d3cf 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -79,7 +79,7 @@ _LOGGER = logging.getLogger(__name__) ConfEntry = namedtuple("ConfEntry", "call_type attr func_name") RunEntry = namedtuple("RunEntry", "attr func") -PYMODBUS_CALL = [ +PB_CALL = [ ConfEntry( CALL_TYPE_COIL, "bits", @@ -178,11 +178,11 @@ async def async_modbus_setup( service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB ] if isinstance(value, list): - await hub.async_pymodbus_call( + await hub.async_pb_call( unit, address, [int(float(i)) for i in value], CALL_TYPE_WRITE_REGISTERS ) else: - await hub.async_pymodbus_call( + await hub.async_pb_call( unit, address, int(float(value)), CALL_TYPE_WRITE_REGISTER ) @@ -199,9 +199,9 @@ async def async_modbus_setup( service.data[ATTR_HUB] if ATTR_HUB in service.data else DEFAULT_HUB ] if isinstance(state, list): - await hub.async_pymodbus_call(unit, address, state, CALL_TYPE_WRITE_COILS) + await hub.async_pb_call(unit, address, state, CALL_TYPE_WRITE_COILS) else: - await hub.async_pymodbus_call(unit, address, state, CALL_TYPE_WRITE_COIL) + await hub.async_pb_call(unit, address, state, CALL_TYPE_WRITE_COIL) for x_write in ( (SERVICE_WRITE_REGISTER, async_write_register, ATTR_VALUE, cv.positive_int), @@ -264,7 +264,7 @@ class ModbusHub: self.name = client_config[CONF_NAME] self._config_type = client_config[CONF_TYPE] self._config_delay = client_config[CONF_DELAY] - self._pb_call: dict[str, RunEntry] = {} + self._pb_request: dict[str, RunEntry] = {} self._pb_class = { SERIAL: ModbusSerialClient, TCP: ModbusTcpClient, @@ -315,10 +315,10 @@ class ModbusHub: _LOGGER.error(log_text) self._in_error = error_state - async def async_pymodbus_connect(self) -> None: + async def async_pb_connect(self) -> None: """Connect to device, async.""" async with self._lock: - if not await self.hass.async_add_executor_job(self._pymodbus_connect): + if not await self.hass.async_add_executor_job(self.pb_connect): err = f"{self.name} connect failed, retry in pymodbus" self._log_error(err, error_state=False) @@ -330,12 +330,12 @@ class ModbusHub: self._log_error(str(exception_error), error_state=False) return False - for entry in PYMODBUS_CALL: + for entry in PB_CALL: func = getattr(self._client, entry.func_name) - self._pb_call[entry.call_type] = RunEntry(entry.attr, func) + self._pb_request[entry.call_type] = RunEntry(entry.attr, func) self.hass.async_create_background_task( - self.async_pymodbus_connect(), "modbus-connect" + self.async_pb_connect(), "modbus-connect" ) # Start counting down to allow modbus requests. @@ -374,7 +374,7 @@ class ModbusHub: message = f"modbus {self.name} communication closed" _LOGGER.warning(message) - def _pymodbus_connect(self) -> bool: + def pb_connect(self) -> bool: """Connect client.""" try: self._client.connect() # type: ignore[union-attr] @@ -386,12 +386,12 @@ class ModbusHub: _LOGGER.info(message) return True - def _pymodbus_call( + def pb_call( self, unit: int | None, address: int, value: int | list[int], use_call: str ) -> ModbusResponse | None: """Call sync. pymodbus.""" kwargs = {"slave": unit} if unit else {} - entry = self._pb_call[use_call] + entry = self._pb_request[use_call] try: result: ModbusResponse = entry.func(address, value, **kwargs) except ModbusException as exception_error: @@ -403,7 +403,7 @@ class ModbusHub: self._in_error = False return result - async def async_pymodbus_call( + async def async_pb_call( self, unit: int | None, address: int, @@ -417,7 +417,7 @@ class ModbusHub: if not self._client: return None result = await self.hass.async_add_executor_job( - self._pymodbus_call, unit, address, value, use_call + self.pb_call, unit, address, value, use_call ) if self._msg_wait: # small delay until next request/response diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index ca8246577fd..1edd732efeb 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -101,7 +101,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): """Update the state of the sensor.""" # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval - raw_result = await self._hub.async_pymodbus_call( + raw_result = await self._hub.async_pb_call( self._slave, self._address, self._count, self._input_type ) if raw_result is None: From 32c1ffcde1d6e9ff43ef46379077fd78cec855fe Mon Sep 17 00:00:00 2001 From: Jason Cook Date: Fri, 4 Aug 2023 15:23:33 -0400 Subject: [PATCH 0203/1151] Update strings.json to correct grammer. (#97790) Change of to off. --- homeassistant/components/mqtt/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 55677798a08..516672c88ab 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -14,7 +14,7 @@ }, "entity_name_startswith_device_name_yaml": { "title": "Manual configured MQTT entities with a name that starts with the device name", - "description": "Some MQTT entities have an entity name that starts with the device name. This is not expected. To avoid a duplicate name the device name prefix is stripped of the entity name as a work-a-round. Please update your configuration and restart Home Assistant to fix this issue. \n\nList of affected entities:\n\n{config}" + "description": "Some MQTT entities have an entity name that starts with the device name. This is not expected. To avoid a duplicate name the device name prefix is stripped off the entity name as a work-a-round. Please update your configuration and restart Home Assistant to fix this issue. \n\nList of affected entities:\n\n{config}" }, "entity_name_is_device_name_discovery": { "title": "Discovered MQTT entities with a name that is equal to the device name", @@ -22,7 +22,7 @@ }, "entity_name_startswith_device_name_discovery": { "title": "Discovered entities with a name that starts with the device name", - "description": "Some MQTT entities have an entity name that starts with the device name. This is not expected. To avoid a duplicate name the device name prefix is stripped of the entity name as a work-a-round. Please inform the maintainer of the software application that supplies the affected entities to fix this issue. \n\nList of affected entities:\n\n{config}" + "description": "Some MQTT entities have an entity name that starts with the device name. This is not expected. To avoid a duplicate name the device name prefix is stripped off the entity name as a work-a-round. Please inform the maintainer of the software application that supplies the affected entities to fix this issue. \n\nList of affected entities:\n\n{config}" } }, "config": { From 50da5c3fae65b311faa2c7993e974985b30016d7 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 4 Aug 2023 21:35:35 +0200 Subject: [PATCH 0204/1151] Fix typo in telegram_bot translations (#97793) --- homeassistant/components/telegram_bot/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/strings.json b/homeassistant/components/telegram_bot/strings.json index eeca235ab44..4dfe0a28d01 100644 --- a/homeassistant/components/telegram_bot/strings.json +++ b/homeassistant/components/telegram_bot/strings.json @@ -168,7 +168,7 @@ }, "send_animation": { "name": "Send animation", - "description": "Sends an anmiation.", + "description": "Sends an animation.", "fields": { "url": { "name": "[%key:common::config_flow::data::url%]", From f0abea48a6aef5c8f35770f36df5a47488ed3df6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 4 Aug 2023 21:38:32 +0200 Subject: [PATCH 0205/1151] Fix Flexit mypy error in pymodbus (#97799) --- homeassistant/components/flexit/climate.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 838d2c934f9..dbe1e060a12 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -177,9 +177,7 @@ class Flexit(ClimateEntity): self, register_type: str, register: int ) -> int: """Read register using the Modbus hub slave.""" - result = await self._hub.async_pymodbus_call( - self._slave, register, 1, register_type - ) + result = await self._hub.async_pb_call(self._slave, register, 1, register_type) if result is None: _LOGGER.error("Error reading value from Flexit modbus adapter") return -1 @@ -197,7 +195,7 @@ class Flexit(ClimateEntity): return result / 10.0 async def _async_write_int16_to_register(self, register: int, value: int) -> bool: - result = await self._hub.async_pymodbus_call( + result = await self._hub.async_pb_call( self._slave, register, value, CALL_TYPE_WRITE_REGISTER ) if not result: From c2023936c1a95b715c883cd46e03d0773582f9e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Aug 2023 22:01:08 -1000 Subject: [PATCH 0206/1151] Bump aiohomekit to 2.6.13 (#97820) --- 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 8cc80ef864e..01b85ef6bbb 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.12"], + "requirements": ["aiohomekit==2.6.13"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index bab601fbc0b..376ca53b8ab 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.12 +aiohomekit==2.6.13 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5dede8d7daf..f15d8ad9fb8 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.12 +aiohomekit==2.6.13 # homeassistant.components.emulated_hue # homeassistant.components.http From d611b169acff9b1908fec2e15253b10ac8cade88 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 5 Aug 2023 11:05:15 +0000 Subject: [PATCH 0207/1151] Don't assume that `battery_level` value is always present in Tractive `hw_info` (#97766) Don't assume that battery_level value is always present in hw_info --- homeassistant/components/tractive/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index e9739819734..a97ea963362 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -43,7 +43,7 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): """Initialize tracker entity.""" super().__init__(user_id, item.trackable, item.tracker_details) - self._battery_level: int = item.hw_info["battery_level"] + self._battery_level: int | None = item.hw_info.get("battery_level") self._latitude: float = item.pos_report["latlong"][0] self._longitude: float = item.pos_report["latlong"][1] self._accuracy: int = item.pos_report["pos_uncertainty"] @@ -75,7 +75,7 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): return self._accuracy @property - def battery_level(self) -> int: + def battery_level(self) -> int | None: """Return the battery level of the device.""" return self._battery_level From c5e55679124baa4636604568e9388a750fe47f24 Mon Sep 17 00:00:00 2001 From: G-Two <7310260+G-Two@users.noreply.github.com> Date: Sat, 5 Aug 2023 09:52:20 -0400 Subject: [PATCH 0208/1151] Add device tracker to Subaru integration (#79492) * Add device tracker to subaru integration * Fix timestamp in device tracker * Add test for device tracker * Incorporate PR review comments * Apply suggestions from code review Co-authored-by: G Johansson * Incorporate code review comments * Add tests for bad device tracker data * Check device tracker data is available in entity --------- Co-authored-by: G Johansson --- homeassistant/components/subaru/const.py | 1 + .../components/subaru/device_tracker.py | 91 +++++++++++++++++++ .../components/subaru/test_device_tracker.py | 60 ++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 homeassistant/components/subaru/device_tracker.py create mode 100644 tests/components/subaru/test_device_tracker.py diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py index 42badfc0185..9c94ed35361 100644 --- a/homeassistant/components/subaru/const.py +++ b/homeassistant/components/subaru/const.py @@ -37,6 +37,7 @@ API_GEN_3 = "g3" MANUFACTURER = "Subaru" PLATFORMS = [ + Platform.DEVICE_TRACKER, Platform.LOCK, Platform.SENSOR, ] diff --git a/homeassistant/components/subaru/device_tracker.py b/homeassistant/components/subaru/device_tracker.py new file mode 100644 index 00000000000..4a8cb8ad5ee --- /dev/null +++ b/homeassistant/components/subaru/device_tracker.py @@ -0,0 +1,91 @@ +"""Support for Subaru device tracker.""" +from __future__ import annotations + +from typing import Any + +from subarulink.const import LATITUDE, LONGITUDE, TIMESTAMP + +from homeassistant.components.device_tracker import SourceType +from homeassistant.components.device_tracker.config_entry import TrackerEntity +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 . import get_device_info +from .const import ( + DOMAIN, + ENTRY_COORDINATOR, + ENTRY_VEHICLES, + VEHICLE_HAS_REMOTE_SERVICE, + VEHICLE_STATUS, + VEHICLE_VIN, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Subaru device tracker by config_entry.""" + entry: dict = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator = entry[ENTRY_COORDINATOR] + vehicle_info: dict = entry[ENTRY_VEHICLES] + entities: list[SubaruDeviceTracker] = [] + for vehicle in vehicle_info.values(): + if vehicle[VEHICLE_HAS_REMOTE_SERVICE]: + entities.append(SubaruDeviceTracker(vehicle, coordinator)) + async_add_entities(entities) + + +class SubaruDeviceTracker( + CoordinatorEntity[DataUpdateCoordinator[dict[str, Any]]], TrackerEntity +): + """Class for Subaru device tracker.""" + + _attr_icon = "mdi:car" + _attr_has_entity_name = True + _attr_name = None + + def __init__(self, vehicle_info: dict, coordinator: DataUpdateCoordinator) -> None: + """Initialize the device tracker.""" + super().__init__(coordinator) + self.vin = vehicle_info[VEHICLE_VIN] + self._attr_device_info = get_device_info(vehicle_info) + self._attr_unique_id = f"{self.vin}_location" + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return entity specific state attributes.""" + return { + "Position timestamp": self.coordinator.data[self.vin][VEHICLE_STATUS].get( + TIMESTAMP + ) + } + + @property + def latitude(self) -> float | None: + """Return latitude value of the vehicle.""" + return self.coordinator.data[self.vin][VEHICLE_STATUS].get(LATITUDE) + + @property + def longitude(self) -> float | None: + """Return longitude value of the vehicle.""" + return self.coordinator.data[self.vin][VEHICLE_STATUS].get(LONGITUDE) + + @property + def source_type(self) -> SourceType: + """Return the source type of the vehicle.""" + return SourceType.GPS + + @property + def available(self) -> bool: + """Return if entity is available.""" + if vehicle_data := self.coordinator.data.get(self.vin): + if status := vehicle_data.get(VEHICLE_STATUS): + return status.keys() & {LATITUDE, LONGITUDE, TIMESTAMP} + return False diff --git a/tests/components/subaru/test_device_tracker.py b/tests/components/subaru/test_device_tracker.py new file mode 100644 index 00000000000..6bef5dc1c2c --- /dev/null +++ b/tests/components/subaru/test_device_tracker.py @@ -0,0 +1,60 @@ +"""Test Subaru device tracker.""" +from copy import deepcopy +from unittest.mock import patch + +from subarulink.const import LATITUDE, LONGITUDE, TIMESTAMP, VEHICLE_STATUS + +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +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, +) + +DEVICE_ID = "device_tracker.test_vehicle_2" + + +async def test_device_tracker(hass: HomeAssistant, ev_entry) -> None: + """Test subaru device tracker entity exists and has correct info.""" + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(DEVICE_ID) + assert entry + actual = hass.states.get(DEVICE_ID) + assert ( + actual.attributes.get(ATTR_LONGITUDE) == EXPECTED_STATE_EV_IMPERIAL[LONGITUDE] + ) + assert actual.attributes.get(ATTR_LATITUDE) == EXPECTED_STATE_EV_IMPERIAL[LATITUDE] + + +async def test_device_tracker_none_data(hass: HomeAssistant, ev_entry) -> None: + """Test when location information contains None.""" + bad_status = deepcopy(VEHICLE_STATUS_EV) + bad_status[VEHICLE_STATUS][LATITUDE] = None + bad_status[VEHICLE_STATUS][LONGITUDE] = None + bad_status[VEHICLE_STATUS][TIMESTAMP] = None + with patch(MOCK_API_FETCH), patch(MOCK_API_GET_DATA, return_value=bad_status): + advance_time_to_next_fetch(hass) + await hass.async_block_till_done() + + actual = hass.states.get(DEVICE_ID) + assert not actual.attributes.get(ATTR_LATITUDE) + assert not actual.attributes.get(ATTR_LONGITUDE) + + +async def test_device_tracker_missing_data(hass: HomeAssistant, ev_entry) -> None: + """Test when location keys are missing from vehicle status.""" + bad_status = deepcopy(VEHICLE_STATUS_EV) + bad_status[VEHICLE_STATUS].pop(LATITUDE) + bad_status[VEHICLE_STATUS].pop(LONGITUDE) + bad_status[VEHICLE_STATUS].pop(TIMESTAMP) + with patch(MOCK_API_FETCH), patch(MOCK_API_GET_DATA, return_value=bad_status): + advance_time_to_next_fetch(hass) + await hass.async_block_till_done() + + actual = hass.states.get(DEVICE_ID) + assert not actual.attributes.get(ATTR_LATITUDE) + assert not actual.attributes.get(ATTR_LONGITUDE) From 2e263560ec86df58eee9ba90189e6827b723c13c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 5 Aug 2023 16:21:39 +0200 Subject: [PATCH 0209/1151] Fix Command Line template error when data is None (#97845) Command Line template error --- .../components/command_line/sensor.py | 4 +- tests/components/command_line/test_sensor.py | 40 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index dd5ad2d5190..2ccbdbc4785 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -207,7 +207,8 @@ class CommandSensor(ManualTriggerEntity, SensorEntity): self._process_manual_data(value) return - if self._value_template is not None: + self._attr_native_value = None + if self._value_template is not None and value is not None: value = self._value_template.async_render_with_possible_json_value( value, None, @@ -221,7 +222,6 @@ class CommandSensor(ManualTriggerEntity, SensorEntity): self._process_manual_data(value) return - self._attr_native_value = None if value is not None: self._attr_native_value = async_parse_date_datetime( value, self.entity_id, self.device_class diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 0fb47109ab1..da2bf1f6dd9 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from datetime import timedelta +import subprocess from typing import Any from unittest.mock import patch @@ -16,7 +17,7 @@ from homeassistant.components.homeassistant import ( SERVICE_UPDATE_ENTITY, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.helpers.issue_registry as ir @@ -698,3 +699,40 @@ async def test_scrape_sensor_device_date( entity_state = hass.states.get("sensor.test") assert entity_state assert entity_state.state == "2022-01-17" + + +async def test_template_not_error_when_data_is_none( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test command sensor with template not logging error when data is None.""" + + with patch( + "homeassistant.components.command_line.utils.subprocess.check_output", + side_effect=subprocess.CalledProcessError, + ): + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "failed command", + "unit_of_measurement": "MB", + "value_template": "{{ (value.split('\t')[0]|int(0)/1000)|round(3) }}", + } + } + ] + }, + ) + await hass.async_block_till_done() + + entity_state = hass.states.get("sensor.test") + assert entity_state + assert entity_state.state == STATE_UNKNOWN + + assert ( + "Template variable error: 'None' has no attribute 'split' when rendering" + not in caplog.text + ) From a22aa285d338425406f1526779d7135a8b0c701c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 5 Aug 2023 17:00:33 +0200 Subject: [PATCH 0210/1151] Fix Melcloud import issue (#97673) * Fix Melcloud import issue * remove old issue * Simplify * Update homeassistant/components/melcloud/strings.json Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/melcloud/strings.json * Update homeassistant/components/melcloud/strings.json * Apply suggestions from code review --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/melcloud/__init__.py | 17 +-- .../components/melcloud/config_flow.py | 61 ++++++++-- .../components/melcloud/strings.json | 10 ++ tests/components/melcloud/test_config_flow.py | 111 +++++++++++++++++- 4 files changed, 174 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index ddadfa74266..eea169c3591 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -13,13 +13,12 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_USERNAME, Platform -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle @@ -62,20 +61,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: data={CONF_USERNAME: username, CONF_TOKEN: token}, ) ) - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.2.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "MELCloud", - }, - ) return True diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index 6b3eeaa19ae..3d6d42c8b7a 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -11,11 +11,47 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import AbortFlow, FlowResultType from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DOMAIN +async def async_create_import_issue( + hass: HomeAssistant, source: str, issue: str, success: bool = False +) -> None: + """Create issue from import.""" + if source != config_entries.SOURCE_IMPORT: + return + if not success: + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_{issue}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key=f"deprecated_yaml_import_issue_{issue}", + ) + return + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "MELCloud", + }, + ) + + class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -24,7 +60,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _create_entry(self, username: str, token: str): """Register new entry.""" await self.async_set_unique_id(username) - self._abort_if_unique_id_configured({CONF_TOKEN: token}) + try: + self._abort_if_unique_id_configured({CONF_TOKEN: token}) + except AbortFlow: + await async_create_import_issue(self.hass, self.context["source"], "", True) + raise return self.async_create_entry( title=username, data={CONF_USERNAME: username, CONF_TOKEN: token} ) @@ -37,11 +77,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): token: str | None = None, ): """Create client.""" - if password is None and token is None: - raise ValueError( - "Invalid internal state. Called without either password or token" - ) - try: async with timeout(10): if (acquired_token := token) is None: @@ -56,9 +91,18 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) except ClientResponseError as err: if err.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): + await async_create_import_issue( + self.hass, self.context["source"], "invalid_auth" + ) return self.async_abort(reason="invalid_auth") + await async_create_import_issue( + self.hass, self.context["source"], "cannot_connect" + ) return self.async_abort(reason="cannot_connect") except (asyncio.TimeoutError, ClientError): + await async_create_import_issue( + self.hass, self.context["source"], "cannot_connect" + ) return self.async_abort(reason="cannot_connect") return await self._create_entry(username, acquired_token) @@ -77,6 +121,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input): """Import a config entry.""" - return await self._create_client( + result = await self._create_client( user_input[CONF_USERNAME], token=user_input[CONF_TOKEN] ) + if result["type"] == FlowResultType.CREATE_ENTRY: + await async_create_import_issue(self.hass, self.context["source"], "", True) + return result diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json index bef65e28880..13827e4e5b5 100644 --- a/homeassistant/components/melcloud/strings.json +++ b/homeassistant/components/melcloud/strings.json @@ -40,5 +40,15 @@ } } } + }, + "issues": { + "deprecated_yaml_import_issue_invalid_auth": { + "title": "The MELCloud YAML configuration import failed", + "description": "Configuring MELCloud using YAML is being removed but there was an authentication error importing your YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the MELCloud YAML configuration from your configuration.yaml file and continue to [set up the integration](/config/integrations/dashboard/add?domain=melcoud) manually." + }, + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The MELCloud YAML configuration import failed", + "description": "Configuring MELCloud using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to MELCloud works and restart Home Assistant to try again or remove the MELCloud YAML configuration from your configuration.yaml file and continue to [set up the integration](/config/integrations/dashboard/add?domain=melcoud) manually." + } } } diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index b81aacf0040..8f877eb1eca 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -7,9 +7,10 @@ from aiohttp import ClientError, ClientResponseError import pymelcloud import pytest -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.melcloud.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +import homeassistant.helpers.issue_registry as ir from tests.common import MockConfigEntry @@ -119,6 +120,106 @@ async def test_form_response_errors( assert result["reason"] == message +@pytest.mark.parametrize( + ("error", "message", "issue"), + [ + ( + HTTPStatus.UNAUTHORIZED, + "invalid_auth", + "deprecated_yaml_import_issue_invalid_auth", + ), + ( + HTTPStatus.FORBIDDEN, + "invalid_auth", + "deprecated_yaml_import_issue_invalid_auth", + ), + ( + HTTPStatus.INTERNAL_SERVER_ERROR, + "cannot_connect", + "deprecated_yaml_import_issue_cannot_connect", + ), + ], +) +async def test_step_import_fails( + hass: HomeAssistant, + mock_login, + mock_get_devices, + mock_request_info, + error: Exception, + message: str, + issue: str, +) -> None: + """Test raising issues on import.""" + mock_get_devices.side_effect = ClientResponseError( + mock_request_info(), (), status=error + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"username": "test-email@test-domain.com", "token": "test-token"}, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == message + + issue_registry = ir.async_get(hass) + assert issue_registry.async_get_issue(DOMAIN, issue) + + +async def test_step_import_fails_ClientError( + hass: HomeAssistant, + mock_login, + mock_get_devices, + mock_request_info, +) -> None: + """Test raising issues on import for ClientError.""" + mock_get_devices.side_effect = ClientError() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"username": "test-email@test-domain.com", "token": "test-token"}, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + issue_registry = ir.async_get(hass) + assert issue_registry.async_get_issue( + DOMAIN, "deprecated_yaml_import_issue_cannot_connect" + ) + + +async def test_step_import_already_exist( + hass: HomeAssistant, + mock_login, + mock_get_devices, + mock_request_info, +) -> None: + """Test that errors are shown when duplicates are added.""" + conf = {"username": "test-email@test-domain.com", "token": "test-token"} + config_entry = MockConfigEntry( + domain=DOMAIN, + data=conf, + title=conf["username"], + unique_id=conf["username"], + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_yaml_melcloud" + ) + assert issue.translation_key == "deprecated_yaml" + + async def test_import_with_token( hass: HomeAssistant, mock_login, mock_get_devices ) -> None: @@ -144,6 +245,12 @@ async def test_import_with_token( assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_yaml_melcloud" + ) + assert issue.translation_key == "deprecated_yaml" + async def test_token_refresh(hass: HomeAssistant, mock_login, mock_get_devices) -> None: """Re-configuration with existing username should refresh token.""" From 65705173305df2e169b6bd70712eb472558878bc Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sat, 5 Aug 2023 18:29:46 +0200 Subject: [PATCH 0211/1151] Add lightplatform to Duotecno (#97582) * add light platform * Add light to coveragerc * Update homeassistant/components/duotecno/light.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/duotecno/light.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/duotecno/light.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/duotecno/light.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/duotecno/light.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/duotecno/light.py Co-authored-by: Joost Lekkerkerker * comments * revert * re-implement comments --------- Co-authored-by: Joost Lekkerkerker --- .coveragerc | 1 + homeassistant/components/duotecno/__init__.py | 2 +- homeassistant/components/duotecno/light.py | 69 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/duotecno/light.py diff --git a/.coveragerc b/.coveragerc index 9c6e7a1a223..564b3203ac9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -234,6 +234,7 @@ omit = homeassistant/components/duotecno/entity.py homeassistant/components/duotecno/switch.py homeassistant/components/duotecno/cover.py + homeassistant/components/duotecno/light.py homeassistant/components/dwd_weather_warnings/const.py homeassistant/components/dwd_weather_warnings/coordinator.py homeassistant/components/dwd_weather_warnings/sensor.py diff --git a/homeassistant/components/duotecno/__init__.py b/homeassistant/components/duotecno/__init__.py index 98003c3e8c4..4c8060b468d 100644 --- a/homeassistant/components/duotecno/__init__.py +++ b/homeassistant/components/duotecno/__init__.py @@ -11,7 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN -PLATFORMS: list[Platform] = [Platform.SWITCH, Platform.COVER] +PLATFORMS: list[Platform] = [Platform.SWITCH, Platform.COVER, Platform.LIGHT] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/duotecno/light.py b/homeassistant/components/duotecno/light.py new file mode 100644 index 00000000000..01d3bf488f1 --- /dev/null +++ b/homeassistant/components/duotecno/light.py @@ -0,0 +1,69 @@ +"""Support for Duotecno lights.""" +from typing import Any + +from duotecno.unit import DimUnit + +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 +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import DuotecnoEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Duotecno light based on config_entry.""" + cntrl = hass.data[DOMAIN][entry.entry_id] + async_add_entities(DuotecnoLight(channel) for channel in cntrl.get_units("DimUnit")) + + +class DuotecnoLight(DuotecnoEntity, LightEntity): + """Representation of a light.""" + + _unit: DimUnit + _attr_color_mode = ColorMode.BRIGHTNESS + _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + + @property + def is_on(self) -> bool: + """Return true if the light is on.""" + return self._unit.is_on() + + @property + def brightness(self) -> int: + """Return the brightness of the light.""" + return int((self._unit.get_dimmer_state() * 255) / 100) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the light to turn on.""" + if (val := kwargs.get(ATTR_BRIGHTNESS)) is not None: + # set to a value + val = max(int((val * 100) / 255), 1) + else: + # restore state + val = None + try: + await self._unit.set_dimmer_state(val) + except OSError as err: + raise HomeAssistantError( + "Transmit for the set_dimmer_state packet failed" + ) from err + + async def async_turn_off(self, **kwargs: Any) -> None: + """Instruct the light to turn off.""" + try: + await self._unit.set_dimmer_state(0) + except OSError as err: + raise HomeAssistantError( + "Transmit for the set_dimmer_state packet failed" + ) from err From 03335ae1dca28257774c6608676a7b0cf2cba833 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 5 Aug 2023 18:48:13 +0200 Subject: [PATCH 0212/1151] Add missing translation key to Gardena Bluetooth (#97855) --- homeassistant/components/gardena_bluetooth/strings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/gardena_bluetooth/strings.json b/homeassistant/components/gardena_bluetooth/strings.json index 1d9a281fdbc..1fc6e10b5a6 100644 --- a/homeassistant/components/gardena_bluetooth/strings.json +++ b/homeassistant/components/gardena_bluetooth/strings.json @@ -15,7 +15,8 @@ "cannot_connect": "Failed to connect: {error}" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } }, "entity": { From 249fa513c979fe24f89b526af1f8ba21173ab326 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 5 Aug 2023 19:02:44 +0200 Subject: [PATCH 0213/1151] Bump pysensibo to 1.0.33 (#97853) * Bump pysensibo to 1.0.33 * for loop --- .../components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensibo/fixtures/data.json | 157 ++++++++++++++++++ tests/components/sensibo/test_climate.py | 19 ++- 5 files changed, 178 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 26182102442..42964ddce8f 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -15,5 +15,5 @@ "iot_class": "cloud_polling", "loggers": ["pysensibo"], "quality_scale": "platinum", - "requirements": ["pysensibo==1.0.32"] + "requirements": ["pysensibo==1.0.33"] } diff --git a/requirements_all.txt b/requirements_all.txt index 376ca53b8ab..e62b8189436 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1985,7 +1985,7 @@ pysaj==0.0.16 pyschlage==2023.7.0 # homeassistant.components.sensibo -pysensibo==1.0.32 +pysensibo==1.0.33 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f15d8ad9fb8..b9f8a268107 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1477,7 +1477,7 @@ pysabnzbd==1.1.1 pyschlage==2023.7.0 # homeassistant.components.sensibo -pysensibo==1.0.32 +pysensibo==1.0.33 # homeassistant.components.serial # homeassistant.components.zha diff --git a/tests/components/sensibo/fixtures/data.json b/tests/components/sensibo/fixtures/data.json index 7db7b4a7c4a..8be6d1e173a 100644 --- a/tests/components/sensibo/fixtures/data.json +++ b/tests/components/sensibo/fixtures/data.json @@ -547,6 +547,163 @@ "autoOffEnabled": false, "antiMoldTimer": null, "antiMoldConfig": null + }, + { + "isGeofenceOnEnterEnabledForThisUser": false, + "isClimateReactGeofenceOnEnterEnabledForThisUser": false, + "isMotionGeofenceOnEnterEnabled": false, + "isOwner": true, + "id": "BBZZBBZZ", + "qrId": "AAAAAAAACC", + "temperatureUnit": "C", + "room": { + "uid": "99YY99YY", + "name": "Bedroom", + "icon": "Diningroom" + }, + "acState": { + "timestamp": { + "time": "2022-04-30T11:23:30.067312Z", + "secondsAgo": -1 + }, + "on": false + }, + "lastStateChange": { + "time": "2022-04-30T11:21:41Z", + "secondsAgo": 108 + }, + "lastStateChangeToOn": { + "time": "2022-04-30T09:43:26Z", + "secondsAgo": 6003 + }, + "lastStateChangeToOff": { + "time": "2022-04-30T11:21:37Z", + "secondsAgo": 112 + }, + "location": { + "id": "ZZZZZZZZZZYY", + "name": "Home", + "latLon": [58.9806976, 20.5864297], + "address": ["Sealand 99", "Some county"], + "country": "United Country", + "createTime": { + "time": "2020-03-21T15:44:15Z", + "secondsAgo": 66543240 + } + }, + "connectionStatus": { + "isAlive": true, + "lastSeen": { + "time": "2022-04-30T11:23:20.642798Z", + "secondsAgo": 9 + } + }, + "firmwareVersion": "PUR00111", + "firmwareType": "pure-esp32", + "productModel": "pure", + "configGroup": "stable", + "currentlyAvailableFirmwareVersion": "PUR00111", + "cleanFiltersNotificationEnabled": false, + "shouldShowFilterCleaningNotification": false, + "isGeofenceOnExitEnabled": false, + "isClimateReactGeofenceOnExitEnabled": false, + "isMotionGeofenceOnExitEnabled": false, + "serial": "0987654321", + "sensorsCalibration": { + "temperature": 0.0, + "humidity": 0.0 + }, + "motionSensors": [], + "tags": [], + "timer": null, + "schedules": [], + "motionConfig": null, + "filtersCleaning": { + "acOnSecondsSinceLastFiltersClean": 415560, + "filtersCleanSecondsThreshold": 14256000, + "lastFiltersCleanTime": { + "time": "2022-04-23T15:58:45Z", + "secondsAgo": 588284 + }, + "shouldCleanFilters": false + }, + "serviceSubscriptions": [], + "roomIsOccupied": null, + "mainMeasurementsSensor": null, + "pureBoostConfig": null, + "warrantyEligible": "no", + "warrantyEligibleUntil": { + "time": "2022-04-10T09:58:58Z", + "secondsAgo": 1733071 + }, + "features": ["optimusTrial", "softShowPlus"], + "runningHealthcheck": null, + "lastHealthcheck": null, + "lastACStateChange": { + "id": "AA22", + "time": { + "time": "2022-04-30T11:21:37Z", + "secondsAgo": 112 + }, + "status": "Success", + "acState": { + "timestamp": { + "time": "2022-04-30T11:23:30.090144Z", + "secondsAgo": -1 + }, + "on": false, + "mode": "fan", + "fanLevel": "low", + "light": "on" + }, + "resultingAcState": { + "timestamp": { + "time": "2022-04-30T11:23:30.090185Z", + "secondsAgo": -1 + }, + "on": false, + "mode": "fan", + "fanLevel": "low", + "light": "on" + }, + "changedProperties": ["on"], + "reason": "UserRequest", + "failureReason": null, + "resolveTime": { + "time": "2022-04-30T11:21:37Z", + "secondsAgo": 112 + }, + "causedByScheduleId": null, + "causedByScheduleType": null + }, + "homekitSupported": true, + "remoteCapabilities": null, + "remote": { + "toggle": false, + "window": false + }, + "remoteFlavor": "Eccentric Eagle", + "remoteAlternatives": [], + "smartMode": null, + "measurements": { + "time": { + "time": "2022-04-30T11:23:20.642798Z", + "secondsAgo": 9 + }, + "rssi": -58, + "pm25": 1, + "motion": false, + "roomIsOccupied": null + }, + "accessPoint": { + "ssid": "Sensibo-09876", + "password": null + }, + "macAddress": "00:01:00:01:00:01", + "autoOffMinutes": null, + "autoOffEnabled": false, + "antiMoldTimer": null, + "antiMoldConfig": null } ] } diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 4e856d396c1..56a7a8c902c 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -76,12 +76,16 @@ async def test_climate_find_valid_targets() -> None: async def test_climate( - hass: HomeAssistant, load_int: ConfigEntry, get_data: SensiboData + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + get_data: SensiboData, + load_int: ConfigEntry, ) -> None: """Test the Sensibo climate.""" state1 = hass.states.get("climate.hallway") state2 = hass.states.get("climate.kitchen") + state3 = hass.states.get("climate.bedroom") assert state1.state == "heat" assert state1.attributes == { @@ -113,6 +117,19 @@ async def test_climate( assert state2.state == "off" + assert not state3 + found_log = False + logs = caplog.get_records("setup") + for log in logs: + if ( + log.message + == "Device Bedroom not correctly registered with Sensibo cloud. Skipping device" + ): + found_log = True + break + + assert found_log + async def test_climate_fan( hass: HomeAssistant, From 24e4f0169c9ce5bb52a2a0e3c4c9de8a7f4c0b33 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 5 Aug 2023 19:43:22 +0200 Subject: [PATCH 0214/1151] Fix Samsung syncthru device info (#97843) Fix Samsung device info --- homeassistant/components/syncthru/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 52d54e5e58d..db546266328 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config_entry_id=entry.entry_id, configuration_url=printer.url, connections=device_connections(printer), - default_manufacturer="Samsung", + manufacturer="Samsung", identifiers=device_identifiers(printer), model=printer.model(), name=printer.hostname(), From 74360543529930e9cb46ca9f26f3ca3bdc443a88 Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Sun, 6 Aug 2023 05:54:54 +1200 Subject: [PATCH 0215/1151] Update starlink-grpc-tools to 1.1.2 (#97824) --- homeassistant/components/starlink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/starlink/manifest.json b/homeassistant/components/starlink/manifest.json index 2230259dcc4..c719afa968d 100644 --- a/homeassistant/components/starlink/manifest.json +++ b/homeassistant/components/starlink/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/starlink", "iot_class": "local_polling", "quality_scale": "silver", - "requirements": ["starlink-grpc-core==1.1.1"] + "requirements": ["starlink-grpc-core==1.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index e62b8189436..310ad785eac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2452,7 +2452,7 @@ starline==0.1.5 starlingbank==3.2 # homeassistant.components.starlink -starlink-grpc-core==1.1.1 +starlink-grpc-core==1.1.2 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9f8a268107..aa13b4b2713 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1800,7 +1800,7 @@ srpenergy==1.3.6 starline==0.1.5 # homeassistant.components.starlink -starlink-grpc-core==1.1.1 +starlink-grpc-core==1.1.2 # homeassistant.components.statsd statsd==3.2.1 From 5fcac42a0fc4dbc7ee26ba57f38796121c6fcd66 Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Sun, 6 Aug 2023 05:55:35 +1200 Subject: [PATCH 0216/1151] Add untested Starlink components to .coveragerc (#97825) --- .coveragerc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.coveragerc b/.coveragerc index 564b3203ac9..32704ffce8b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1169,7 +1169,12 @@ omit = homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py + homeassistant/components/starlink/__init__.py + homeassistant/components/starlink/binary_sensor.py + homeassistant/components/starlink/button.py homeassistant/components/starlink/coordinator.py + homeassistant/components/starlink/sensor.py + homeassistant/components/starlink/switch.py homeassistant/components/starline/__init__.py homeassistant/components/starline/account.py homeassistant/components/starline/binary_sensor.py From 76c443777dff66f5e44a52ac7e4176974308157d Mon Sep 17 00:00:00 2001 From: MarkGodwin <10632972+MarkGodwin@users.noreply.github.com> Date: Sat, 5 Aug 2023 18:59:03 +0100 Subject: [PATCH 0217/1151] Bump Omada API version to fix #96193 (#97848) --- homeassistant/components/tplink_omada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink_omada/manifest.json b/homeassistant/components/tplink_omada/manifest.json index 795e6adf5b7..9c303b24661 100644 --- a/homeassistant/components/tplink_omada/manifest.json +++ b/homeassistant/components/tplink_omada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tplink_omada", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["tplink_omada_client==1.2.4"] + "requirements": ["tplink_omada_client==1.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 310ad785eac..0f37d4d77f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2566,7 +2566,7 @@ total-connect-client==2023.2 tp-connected==0.0.4 # homeassistant.components.tplink_omada -tplink_omada_client==1.2.4 +tplink_omada_client==1.3.2 # homeassistant.components.transmission transmission-rpc==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa13b4b2713..2e67cb7bdc8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1872,7 +1872,7 @@ toonapi==0.2.1 total-connect-client==2023.2 # homeassistant.components.tplink_omada -tplink_omada_client==1.2.4 +tplink_omada_client==1.3.2 # homeassistant.components.transmission transmission-rpc==4.1.5 From e43ad1c6a0d0f5eede5f784e4df8baf64214df88 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 5 Aug 2023 20:07:20 +0200 Subject: [PATCH 0218/1151] Add restart device to UniFi button platform (#97642) * Add restart device to UniFi Button platform * Add tests for button platform * Small corrections --- homeassistant/components/unifi/button.py | 111 ++++++++++++++++++++++ homeassistant/components/unifi/const.py | 1 + tests/components/unifi/test_button.py | 92 ++++++++++++++++++ tests/components/unifi/test_controller.py | 10 +- 4 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/unifi/button.py create mode 100644 tests/components/unifi/test_button.py diff --git a/homeassistant/components/unifi/button.py b/homeassistant/components/unifi/button.py new file mode 100644 index 00000000000..6b0660325f0 --- /dev/null +++ b/homeassistant/components/unifi/button.py @@ -0,0 +1,111 @@ +"""Button platform for UniFi Network integration. + +Support for restarting UniFi devices. +""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any, Generic + +import aiounifi +from aiounifi.interfaces.api_handlers import ItemEvent +from aiounifi.interfaces.devices import Devices +from aiounifi.models.api import ApiItemT +from aiounifi.models.device import Device, DeviceRestartRequest + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN as UNIFI_DOMAIN +from .controller import UniFiController +from .entity import ( + HandlerT, + UnifiEntity, + UnifiEntityDescription, + async_device_available_fn, + async_device_device_info_fn, +) + + +@callback +async def async_restart_device_control_fn( + api: aiounifi.Controller, obj_id: str +) -> None: + """Restart device.""" + await api.request(DeviceRestartRequest.create(obj_id)) + + +@dataclass +class UnifiButtonEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): + """Validate and load entities from different UniFi handlers.""" + + control_fn: Callable[[aiounifi.Controller, str], Coroutine[Any, Any, None]] + + +@dataclass +class UnifiButtonEntityDescription( + ButtonEntityDescription, + UnifiEntityDescription[HandlerT, ApiItemT], + UnifiButtonEntityDescriptionMixin[HandlerT, ApiItemT], +): + """Class describing UniFi button entity.""" + + +ENTITY_DESCRIPTIONS: tuple[UnifiButtonEntityDescription, ...] = ( + UnifiButtonEntityDescription[Devices, Device]( + key="Device restart", + entity_category=EntityCategory.CONFIG, + has_entity_name=True, + device_class=ButtonDeviceClass.RESTART, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + control_fn=async_restart_device_control_fn, + device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + name_fn=lambda _: "Restart", + object_fn=lambda api, obj_id: api.devices[obj_id], + should_poll=False, + supported_fn=lambda controller, obj_id: True, + unique_id_fn=lambda controller, obj_id: f"device_restart-{obj_id}", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up button platform for UniFi Network integration.""" + controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + + if controller.site_role != "admin": + return + + controller.register_platform_add_entities( + UnifiButtonEntity, ENTITY_DESCRIPTIONS, async_add_entities + ) + + +class UnifiButtonEntity(UnifiEntity[HandlerT, ApiItemT], ButtonEntity): + """Base representation of a UniFi image.""" + + entity_description: UnifiButtonEntityDescription[HandlerT, ApiItemT] + + async def async_press(self) -> None: + """Press the button.""" + await self.entity_description.control_fn(self.controller.api, self._obj_id) + + @callback + def async_update_state(self, event: ItemEvent, obj_id: str) -> None: + """Update entity state.""" diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index e03bd50d483..176511645aa 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -8,6 +8,7 @@ LOGGER = logging.getLogger(__package__) DOMAIN = "unifi" PLATFORMS = [ + Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.IMAGE, Platform.SENSOR, diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py new file mode 100644 index 00000000000..89b65b1f981 --- /dev/null +++ b/tests/components/unifi/test_button.py @@ -0,0 +1,92 @@ +"""UniFi Network button platform tests.""" + +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.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .test_controller import ( + setup_unifi_integration, +) + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_restart_device_button( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket +) -> None: + """Test restarting device button.""" + config_entry = await setup_unifi_integration( + hass, + aioclient_mock, + devices_response=[ + { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.0.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "switch", + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + } + ], + ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 + + ent_reg = er.async_get(hass) + ent_reg_entry = ent_reg.async_get("button.switch_restart") + assert ent_reg_entry.unique_id == "device_restart-00:00:00:00:01:01" + assert ent_reg_entry.entity_category is EntityCategory.CONFIG + + # Validate state object + button = hass.states.get("button.switch_restart") + assert button is not None + assert button.attributes.get(ATTR_DEVICE_CLASS) == ButtonDeviceClass.RESTART + + # Send restart device command + aioclient_mock.clear_requests() + aioclient_mock.post( + f"https://{controller.host}:1234/api/s/{controller.site}/cmd/devmgr", + ) + + await hass.services.async_call( + BUTTON_DOMAIN, + "press", + {"entity_id": "button.switch_restart"}, + blocking=True, + ) + assert aioclient_mock.call_count == 1 + assert aioclient_mock.mock_calls[0][2] == { + "cmd": "restart", + "mac": "00:00:00:00:01:01", + "reboot_type": "soft", + } + + # Availability signalling + + # Controller disconnects + mock_unifi_websocket(state=WebsocketState.DISCONNECTED) + await hass.async_block_till_done() + assert hass.states.get("button.switch_restart").state == STATE_UNAVAILABLE + + # Controller reconnects + mock_unifi_websocket(state=WebsocketState.RUNNING) + await hass.async_block_till_done() + assert hass.states.get("button.switch_restart").state != STATE_UNAVAILABLE diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 5f1b5d33dcd..2d28240a90d 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -9,6 +9,7 @@ import aiounifi from aiounifi.websocket import WebsocketState import pytest +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -222,10 +223,11 @@ async def test_controller_setup( entry = controller.config_entry assert len(forward_entry_setup.mock_calls) == len(PLATFORMS) - assert forward_entry_setup.mock_calls[0][1] == (entry, TRACKER_DOMAIN) - assert forward_entry_setup.mock_calls[1][1] == (entry, IMAGE_DOMAIN) - assert forward_entry_setup.mock_calls[2][1] == (entry, SENSOR_DOMAIN) - assert forward_entry_setup.mock_calls[3][1] == (entry, SWITCH_DOMAIN) + assert forward_entry_setup.mock_calls[0][1] == (entry, BUTTON_DOMAIN) + assert forward_entry_setup.mock_calls[1][1] == (entry, TRACKER_DOMAIN) + assert forward_entry_setup.mock_calls[2][1] == (entry, IMAGE_DOMAIN) + assert forward_entry_setup.mock_calls[3][1] == (entry, SENSOR_DOMAIN) + assert forward_entry_setup.mock_calls[4][1] == (entry, SWITCH_DOMAIN) assert controller.host == ENTRY_CONFIG[CONF_HOST] assert controller.site == ENTRY_CONFIG[CONF_SITE_ID] From 4b6048b5e0c6bf9645b6b623f7cfc7ba175a26ec Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sat, 5 Aug 2023 20:08:14 +0200 Subject: [PATCH 0219/1151] Bump bimmer_connected to 0.13.9, fix notify (#97860) Bump bimmer_connected to 0.13.9 Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 82426fbce08..ff2804a8c04 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected==0.13.8"] + "requirements": ["bimmer-connected==0.13.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0f37d4d77f5..39947d3fc2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -503,7 +503,7 @@ beautifulsoup4==4.11.1 bellows==0.35.8 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.13.8 +bimmer-connected==0.13.9 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e67cb7bdc8..e8655855bf8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -427,7 +427,7 @@ beautifulsoup4==4.11.1 bellows==0.35.8 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.13.8 +bimmer-connected==0.13.9 # homeassistant.components.bluetooth bleak-retry-connector==3.1.1 From 6c8971f18ad7cd1906b294cf4a37f746bb9d8e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Lindh=C3=A9?= Date: Sat, 5 Aug 2023 20:44:26 +0200 Subject: [PATCH 0220/1151] Improve code quality of CalDav (#97570) * Use keyword arguments when constructing `WebDavCalendarData` * Use keyword arguments when constructing `WebDavCalendarEntity` * Remove random newlines --- homeassistant/components/caldav/calendar.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index e4892ae0383..712873e51ce 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -105,7 +105,12 @@ def setup_platform( entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass) calendar_devices.append( WebDavCalendarEntity( - name, calendar, entity_id, days, True, cust_calendar[CONF_SEARCH] + name=name, + calendar=calendar, + entity_id=entity_id, + days=days, + all_day=True, + search=cust_calendar[CONF_SEARCH], ) ) @@ -126,7 +131,12 @@ class WebDavCalendarEntity(CalendarEntity): def __init__(self, name, calendar, entity_id, days, all_day=False, search=None): """Create the WebDav Calendar Event Device.""" - self.data = WebDavCalendarData(calendar, days, all_day, search) + self.data = WebDavCalendarData( + calendar=calendar, + days=days, + include_all_day=all_day, + search=search, + ) self.entity_id = entity_id self._event: CalendarEvent | None = None self._attr_name = name @@ -347,10 +357,8 @@ class WebDavCalendarData: """Return the end datetime as determined by dtend or duration.""" if hasattr(obj, "dtend"): enddate = obj.dtend.value - elif hasattr(obj, "duration"): enddate = obj.dtstart.value + obj.duration.value - else: enddate = obj.dtstart.value + timedelta(days=1) From 2e8e5aabaef9ee0624d8cf0dec6946d1a9b2dba1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 5 Aug 2023 21:32:53 +0200 Subject: [PATCH 0221/1151] Refactor alexa modules to avoid circular deps (#97618) * Refactor alexa modules to avoid circula deps * Add test http api auth and AlexaConfig * Update test * Improve test --- homeassistant/components/alexa/__init__.py | 4 +- homeassistant/components/alexa/handlers.py | 3 +- homeassistant/components/alexa/messages.py | 195 ----------------- homeassistant/components/alexa/smart_home.py | 145 ++++++++++++- .../components/alexa/smart_home_http.py | 137 ------------ .../components/alexa/state_report.py | 196 +++++++++++++++++- tests/components/alexa/test_common.py | 4 +- tests/components/alexa/test_smart_home.py | 33 ++- .../components/alexa/test_smart_home_http.py | 25 ++- 9 files changed, 388 insertions(+), 354 deletions(-) delete mode 100644 homeassistant/components/alexa/messages.py delete mode 100644 homeassistant/components/alexa/smart_home_http.py diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 0008ba26f8a..219553b3563 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entityfilter from homeassistant.helpers.typing import ConfigType -from . import flash_briefings, intent, smart_home_http +from . import flash_briefings, intent, smart_home from .const import ( CONF_AUDIO, CONF_DISPLAY_CATEGORIES, @@ -100,6 +100,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if CONF_SMART_HOME in config: smart_home_config: dict[str, Any] | None = config[CONF_SMART_HOME] smart_home_config = smart_home_config or SMART_HOME_SCHEMA({}) - await smart_home_http.async_setup(hass, smart_home_config) + await smart_home.async_setup(hass, smart_home_config) return True diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index c1b99b017e5..4235d739d22 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -75,8 +75,7 @@ from .errors import ( AlexaUnsupportedThermostatModeError, AlexaVideoActionNotPermittedForContentError, ) -from .messages import AlexaDirective, AlexaResponse -from .state_report import async_enable_proactive_mode +from .state_report import AlexaDirective, AlexaResponse, async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive" diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py deleted file mode 100644 index 4dd154ea11f..00000000000 --- a/homeassistant/components/alexa/messages.py +++ /dev/null @@ -1,195 +0,0 @@ -"""Alexa models.""" -import logging -from uuid import uuid4 - -from .const import ( - API_CONTEXT, - API_DIRECTIVE, - API_ENDPOINT, - API_EVENT, - API_HEADER, - API_PAYLOAD, - API_SCOPE, -) -from .entities import ENTITY_ADAPTERS -from .errors import AlexaInvalidEndpointError - -_LOGGER = logging.getLogger(__name__) - - -class AlexaDirective: - """An incoming Alexa directive.""" - - def __init__(self, request): - """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.entity = self.entity_id = self.endpoint = self.instance = None - - def load_entity(self, hass, config): - """Set attributes related to the entity for this request. - - Sets these attributes when self.has_endpoint is True: - - - entity - - entity_id - - endpoint - - instance (when header includes instance property) - - Behavior when self.has_endpoint is False is undefined. - - Will raise AlexaInvalidEndpointError if the endpoint in the request is - malformed or nonexistent. - """ - _endpoint_id = 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): - raise AlexaInvalidEndpointError(_endpoint_id) - - 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): - """Create an API formatted response. - - Async friendly. - """ - response = AlexaResponse(name, namespace, payload) - - token = self._directive[API_HEADER].get("correlationToken") - if token: - response.set_correlation_token(token) - - if self.has_endpoint: - response.set_endpoint(self._directive[API_ENDPOINT].copy()) - - return response - - def error( - self, - namespace="Alexa", - error_type="INTERNAL_ERROR", - error_message="", - payload=None, - ): - """Create a API formatted error response. - - Async friendly. - """ - payload = payload or {} - payload["type"] = error_type - payload["message"] = error_message - - _LOGGER.info( - "Request %s/%s error %s: %s", - self._directive[API_HEADER]["namespace"], - self._directive[API_HEADER]["name"], - error_type, - error_message, - ) - - return self.response(name="ErrorResponse", namespace=namespace, payload=payload) - - -class AlexaResponse: - """Class to hold a response.""" - - def __init__(self, name, namespace, payload=None): - """Initialize the response.""" - payload = payload or {} - self._response = { - API_EVENT: { - API_HEADER: { - "namespace": namespace, - "name": name, - "messageId": str(uuid4()), - "payloadVersion": "3", - }, - API_PAYLOAD: payload, - } - } - - @property - def name(self): - """Return the name of this response.""" - return self._response[API_EVENT][API_HEADER]["name"] - - @property - def namespace(self): - """Return the namespace of this response.""" - return self._response[API_EVENT][API_HEADER]["namespace"] - - def set_correlation_token(self, token): - """Set the correlationToken. - - This should normally mirror the value from a request, and is set by - AlexaDirective.response() usually. - """ - self._response[API_EVENT][API_HEADER]["correlationToken"] = token - - def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None): - """Set the endpoint dictionary. - - This is used to send proactive messages to Alexa. - """ - self._response[API_EVENT][API_ENDPOINT] = { - API_SCOPE: {"type": "BearerToken", "token": bearer_token} - } - - 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): - """Set the endpoint. - - This should normally mirror the value from a request, and is set by - AlexaDirective.response() usually. - """ - self._response[API_EVENT][API_ENDPOINT] = endpoint - - def _properties(self): - context = self._response.setdefault(API_CONTEXT, {}) - return context.setdefault("properties", []) - - def add_context_property(self, prop): - """Add a property to the response context. - - The Alexa response includes a list of properties which provides - feedback on how states have changed. For example if a user asks, - "Alexa, set thermostat to 20 degrees", the API expects a response with - the new value of the property, and Alexa will respond to the user - "Thermostat set to 20 degrees". - - async_handle_message() will call .merge_context_properties() for every - request automatically, however often handlers will call services to - change state but the effects of those changes are applied - asynchronously. Thus, handlers should call this method to confirm - changes before returning. - """ - self._properties().append(prop) - - def merge_context_properties(self, endpoint): - """Add all properties from given endpoint if not already set. - - Handlers should be using .add_context_property(). - """ - properties = self._properties() - already_set = {(p["namespace"], p["name"]) for p in properties} - - for prop in endpoint.serialize_properties(): - if (prop["namespace"], prop["name"]) not in already_set: - self.add_context_property(prop) - - def serialize(self): - """Return response as a JSON-able data structure.""" - return self._response diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 24229507877..3f8932a48bc 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1,16 +1,153 @@ """Support for alexa Smart Home Skill API.""" import logging -import homeassistant.core as ha +from homeassistant import core +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.typing import ConfigType -from .const import API_DIRECTIVE, API_HEADER, EVENT_ALEXA_SMART_HOME +from .auth import Auth +from .config import AbstractConfig +from .const import ( + API_DIRECTIVE, + API_HEADER, + CONF_ENDPOINT, + CONF_ENTITY_CONFIG, + CONF_FILTER, + CONF_LOCALE, + EVENT_ALEXA_SMART_HOME, +) from .errors import AlexaBridgeUnreachableError, AlexaError from .handlers import HANDLERS -from .messages import AlexaDirective +from .state_report import AlexaDirective + +SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" _LOGGER = logging.getLogger(__name__) +class AlexaConfig(AbstractConfig): + """Alexa config.""" + + def __init__(self, hass, config): + """Initialize Alexa config.""" + super().__init__(hass) + self._config = config + + if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET): + self._auth = Auth(hass, config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET]) + else: + self._auth = None + + @property + def supports_auth(self): + """Return if config supports auth.""" + return self._auth is not None + + @property + def should_report_state(self): + """Return if we should proactively report states.""" + return self._auth is not None and self.authorized + + @property + def endpoint(self): + """Endpoint for report state.""" + return self._config.get(CONF_ENDPOINT) + + @property + def entity_config(self): + """Return entity config.""" + return self._config.get(CONF_ENTITY_CONFIG) or {} + + @property + def locale(self): + """Return config locale.""" + return self._config.get(CONF_LOCALE) + + @core.callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return "" + + @core.callback + def should_expose(self, entity_id): + """If an entity should be exposed.""" + if not self._config[CONF_FILTER].empty_filter: + return self._config[CONF_FILTER](entity_id) + + entity_registry = er.async_get(self.hass) + if registry_entry := entity_registry.async_get(entity_id): + auxiliary_entity = ( + registry_entry.entity_category is not None + or registry_entry.hidden_by is not None + ) + else: + auxiliary_entity = False + return not auxiliary_entity + + @core.callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + self._auth.async_invalidate_access_token() + + async def async_get_access_token(self): + """Get an access token.""" + return await self._auth.async_get_access_token() + + async def async_accept_grant(self, code): + """Accept a grant.""" + return await self._auth.async_do_auth(code) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> None: + """Activate Smart Home functionality of Alexa component. + + This is optional, triggered by having a `smart_home:` sub-section in the + alexa configuration. + + Even if that's disabled, the functionality in this module may still be used + by the cloud component which will call async_handle_message directly. + """ + smart_home_config = AlexaConfig(hass, config) + await smart_home_config.async_initialize() + hass.http.register_view(SmartHomeView(smart_home_config)) + + if smart_home_config.should_report_state: + await smart_home_config.async_enable_proactive_mode() + + +class SmartHomeView(HomeAssistantView): + """Expose Smart Home v3 payload interface via HTTP POST.""" + + url = SMART_HOME_HTTP_ENDPOINT + name = "api:alexa:smart_home" + + def __init__(self, smart_home_config): + """Initialize.""" + self.smart_home_config = smart_home_config + + async def post(self, request): + """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() + + _LOGGER.debug("Received Alexa Smart Home request: %s", message) + + response = await async_handle_message( + hass, self.smart_home_config, message, context=core.Context(user_id=user.id) + ) + _LOGGER.debug("Sending Alexa Smart Home response: %s", response) + return b"" if response is None else self.json(response) + + async def async_handle_message(hass, config, request, context=None, enabled=True): """Handle incoming API messages. @@ -21,7 +158,7 @@ async def async_handle_message(hass, config, request, context=None, enabled=True assert request[API_DIRECTIVE][API_HEADER]["payloadVersion"] == "3" if context is None: - context = ha.Context() + context = Context() directive = AlexaDirective(request) diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py deleted file mode 100644 index 3a702421d94..00000000000 --- a/homeassistant/components/alexa/smart_home_http.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Alexa HTTP interface.""" -import logging - -from homeassistant import core -from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.typing import ConfigType - -from .auth import Auth -from .config import AbstractConfig -from .const import CONF_ENDPOINT, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_LOCALE -from .smart_home import async_handle_message - -_LOGGER = logging.getLogger(__name__) -SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" - - -class AlexaConfig(AbstractConfig): - """Alexa config.""" - - def __init__(self, hass, config): - """Initialize Alexa config.""" - super().__init__(hass) - self._config = config - - if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET): - self._auth = Auth(hass, config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET]) - else: - self._auth = None - - @property - def supports_auth(self): - """Return if config supports auth.""" - return self._auth is not None - - @property - def should_report_state(self): - """Return if we should proactively report states.""" - return self._auth is not None and self.authorized - - @property - def endpoint(self): - """Endpoint for report state.""" - return self._config.get(CONF_ENDPOINT) - - @property - def entity_config(self): - """Return entity config.""" - return self._config.get(CONF_ENTITY_CONFIG) or {} - - @property - def locale(self): - """Return config locale.""" - return self._config.get(CONF_LOCALE) - - @core.callback - def user_identifier(self): - """Return an identifier for the user that represents this config.""" - return "" - - @core.callback - def should_expose(self, entity_id): - """If an entity should be exposed.""" - if not self._config[CONF_FILTER].empty_filter: - return self._config[CONF_FILTER](entity_id) - - entity_registry = er.async_get(self.hass) - if registry_entry := entity_registry.async_get(entity_id): - auxiliary_entity = ( - registry_entry.entity_category is not None - or registry_entry.hidden_by is not None - ) - else: - auxiliary_entity = False - return not auxiliary_entity - - @core.callback - def async_invalidate_access_token(self): - """Invalidate access token.""" - self._auth.async_invalidate_access_token() - - async def async_get_access_token(self): - """Get an access token.""" - return await self._auth.async_get_access_token() - - async def async_accept_grant(self, code): - """Accept a grant.""" - return await self._auth.async_do_auth(code) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> None: - """Activate Smart Home functionality of Alexa component. - - This is optional, triggered by having a `smart_home:` sub-section in the - alexa configuration. - - Even if that's disabled, the functionality in this module may still be used - by the cloud component which will call async_handle_message directly. - """ - smart_home_config = AlexaConfig(hass, config) - await smart_home_config.async_initialize() - hass.http.register_view(SmartHomeView(smart_home_config)) - - if smart_home_config.should_report_state: - await smart_home_config.async_enable_proactive_mode() - - -class SmartHomeView(HomeAssistantView): - """Expose Smart Home v3 payload interface via HTTP POST.""" - - url = SMART_HOME_HTTP_ENDPOINT - name = "api:alexa:smart_home" - - def __init__(self, smart_home_config): - """Initialize.""" - self.smart_home_config = smart_home_config - - async def post(self, request): - """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() - - _LOGGER.debug("Received Alexa Smart Home request: %s", message) - - response = await async_handle_message( - hass, self.smart_home_config, message, context=core.Context(user_id=user.id) - ) - _LOGGER.debug("Sending Alexa Smart Home response: %s", response) - return b"" if response is None else self.json(response) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 04bb561560f..808e0eac482 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -6,6 +6,7 @@ from http import HTTPStatus import json import logging from typing import TYPE_CHECKING, cast +from uuid import uuid4 import aiohttp import async_timeout @@ -19,10 +20,21 @@ from homeassistant.helpers.significant_change import create_checker import homeassistant.util.dt as dt_util from homeassistant.util.json import JsonObjectType, json_loads_object -from .const import API_CHANGE, DATE_FORMAT, DOMAIN, Cause +from .const import ( + API_CHANGE, + API_CONTEXT, + API_DIRECTIVE, + API_ENDPOINT, + API_EVENT, + API_HEADER, + API_PAYLOAD, + API_SCOPE, + DATE_FORMAT, + DOMAIN, + Cause, +) from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id -from .errors import NoTokenAvailable, RequireRelink -from .messages import AlexaResponse +from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink if TYPE_CHECKING: from .config import AbstractConfig @@ -31,6 +43,184 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_TIMEOUT = 10 +class AlexaDirective: + """An incoming Alexa directive.""" + + def __init__(self, request): + """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.entity = self.entity_id = self.endpoint = self.instance = None + + def load_entity(self, hass, config): + """Set attributes related to the entity for this request. + + Sets these attributes when self.has_endpoint is True: + + - entity + - entity_id + - endpoint + - instance (when header includes instance property) + + Behavior when self.has_endpoint is False is undefined. + + Will raise AlexaInvalidEndpointError if the endpoint in the request is + malformed or nonexistent. + """ + _endpoint_id = 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): + raise AlexaInvalidEndpointError(_endpoint_id) + + 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): + """Create an API formatted response. + + Async friendly. + """ + response = AlexaResponse(name, namespace, payload) + + token = self._directive[API_HEADER].get("correlationToken") + if token: + response.set_correlation_token(token) + + if self.has_endpoint: + response.set_endpoint(self._directive[API_ENDPOINT].copy()) + + return response + + def error( + self, + namespace="Alexa", + error_type="INTERNAL_ERROR", + error_message="", + payload=None, + ): + """Create a API formatted error response. + + Async friendly. + """ + payload = payload or {} + payload["type"] = error_type + payload["message"] = error_message + + _LOGGER.info( + "Request %s/%s error %s: %s", + self._directive[API_HEADER]["namespace"], + self._directive[API_HEADER]["name"], + error_type, + error_message, + ) + + return self.response(name="ErrorResponse", namespace=namespace, payload=payload) + + +class AlexaResponse: + """Class to hold a response.""" + + def __init__(self, name, namespace, payload=None): + """Initialize the response.""" + payload = payload or {} + self._response = { + API_EVENT: { + API_HEADER: { + "namespace": namespace, + "name": name, + "messageId": str(uuid4()), + "payloadVersion": "3", + }, + API_PAYLOAD: payload, + } + } + + @property + def name(self): + """Return the name of this response.""" + return self._response[API_EVENT][API_HEADER]["name"] + + @property + def namespace(self): + """Return the namespace of this response.""" + return self._response[API_EVENT][API_HEADER]["namespace"] + + def set_correlation_token(self, token): + """Set the correlationToken. + + This should normally mirror the value from a request, and is set by + AlexaDirective.response() usually. + """ + self._response[API_EVENT][API_HEADER]["correlationToken"] = token + + def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None): + """Set the endpoint dictionary. + + This is used to send proactive messages to Alexa. + """ + self._response[API_EVENT][API_ENDPOINT] = { + API_SCOPE: {"type": "BearerToken", "token": bearer_token} + } + + 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): + """Set the endpoint. + + This should normally mirror the value from a request, and is set by + AlexaDirective.response() usually. + """ + self._response[API_EVENT][API_ENDPOINT] = endpoint + + def _properties(self): + context = self._response.setdefault(API_CONTEXT, {}) + return context.setdefault("properties", []) + + def add_context_property(self, prop): + """Add a property to the response context. + + The Alexa response includes a list of properties which provides + feedback on how states have changed. For example if a user asks, + "Alexa, set thermostat to 20 degrees", the API expects a response with + the new value of the property, and Alexa will respond to the user + "Thermostat set to 20 degrees". + + async_handle_message() will call .merge_context_properties() for every + request automatically, however often handlers will call services to + change state but the effects of those changes are applied + asynchronously. Thus, handlers should call this method to confirm + changes before returning. + """ + self._properties().append(prop) + + def merge_context_properties(self, endpoint): + """Add all properties from given endpoint if not already set. + + Handlers should be using .add_context_property(). + """ + properties = self._properties() + already_set = {(p["namespace"], p["name"]) for p in properties} + + for prop in endpoint.serialize_properties(): + if (prop["namespace"], prop["name"]) not in already_set: + self.add_context_property(prop) + + def serialize(self): + """Return response as a JSON-able data structure.""" + return self._response + + async def async_enable_proactive_mode(hass, smart_home_config): """Enable the proactive mode. diff --git a/tests/components/alexa/test_common.py b/tests/components/alexa/test_common.py index 53a836f97f3..4cbe112af49 100644 --- a/tests/components/alexa/test_common.py +++ b/tests/components/alexa/test_common.py @@ -4,7 +4,7 @@ from uuid import uuid4 import pytest -from homeassistant.components.alexa import config, smart_home, smart_home_http +from homeassistant.components.alexa import config, smart_home from homeassistant.components.alexa.const import CONF_ENDPOINT, CONF_FILTER, CONF_LOCALE from homeassistant.core import Context, callback from homeassistant.helpers import entityfilter @@ -16,7 +16,7 @@ TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token" TEST_LOCALE = "en-US" -class MockConfig(smart_home_http.AlexaConfig): +class MockConfig(smart_home.AlexaConfig): """Mock Alexa config.""" entity_config = { diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 477e7884e4f..317febcfdd1 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,10 +1,10 @@ """Test for smart home alexa support.""" from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest -from homeassistant.components.alexa import messages, smart_home +from homeassistant.components.alexa import smart_home, state_report import homeassistant.components.camera as camera from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.media_player import MediaPlayerEntityFeature @@ -29,6 +29,7 @@ from .test_common import ( ) from tests.common import async_capture_events, async_mock_service +from tests.typing import ClientSessionGenerator @pytest.fixture @@ -58,7 +59,7 @@ def test_create_api_message_defaults(hass: HomeAssistant) -> None: """Create an API message response of a request with defaults.""" request = get_new_request("Alexa.PowerController", "TurnOn", "switch#xy") directive_header = request["directive"]["header"] - directive = messages.AlexaDirective(request) + directive = state_report.AlexaDirective(request) msg = directive.response(payload={"test": 3})._response @@ -84,7 +85,7 @@ def test_create_api_message_special() -> None: request = get_new_request("Alexa.PowerController", "TurnOn") directive_header = request["directive"]["header"] directive_header.pop("correlationToken") - directive = messages.AlexaDirective(request) + directive = state_report.AlexaDirective(request) msg = directive.response("testName", "testNameSpace")._response @@ -4372,3 +4373,27 @@ async def test_api_message_sets_authorized(hass: HomeAssistant) -> None: config._store.set_authorized.assert_not_called() await smart_home.async_handle_message(hass, config, msg) config._store.set_authorized.assert_called_once_with(True) + + +async def test_alexa_config( + hass: HomeAssistant, hass_client: ClientSessionGenerator +) -> None: + """Test all methods of the AlexaConfig class.""" + config = { + "filter": entityfilter.FILTER_SCHEMA({"include_domains": ["sensor"]}), + } + test_config = smart_home.AlexaConfig(hass, config) + await test_config.async_initialize() + assert not test_config.supports_auth + assert not test_config.should_report_state + assert test_config.endpoint is None + assert test_config.entity_config == {} + assert test_config.user_identifier() == "" + assert test_config.locale is None + assert test_config.should_expose("sensor.test") + assert not test_config.should_expose("switch.test") + with patch.object(test_config, "_auth", AsyncMock()): + 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") + test_config._auth.async_do_auth.assert_called_once_with("grant_code") diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py index b279e75b634..b0f78e958d7 100644 --- a/tests/components/alexa/test_smart_home_http.py +++ b/tests/components/alexa/test_smart_home_http.py @@ -1,8 +1,11 @@ """Test Smart Home HTTP endpoints.""" from http import HTTPStatus import json +from typing import Any -from homeassistant.components.alexa import DOMAIN, smart_home_http +import pytest + +from homeassistant.components.alexa import DOMAIN, smart_home from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -19,19 +22,31 @@ async def do_http_discovery(config, hass, hass_client): request = get_new_request("Alexa.Discovery", "Discover") response = await http_client.post( - smart_home_http.SMART_HOME_HTTP_ENDPOINT, + smart_home.SMART_HOME_HTTP_ENDPOINT, data=json.dumps(request), headers={"content-type": CONTENT_TYPE_JSON}, ) return response +@pytest.mark.parametrize( + "config", + [ + {"alexa": {"smart_home": None}}, + { + "alexa": { + "smart_home": { + "client_id": "someclientid", + "client_secret": "verysecret", + } + } + }, + ], +) async def test_http_api( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, hass_client: ClientSessionGenerator, config: dict[str, Any] ) -> None: """With `smart_home:` HTTP API is exposed.""" - config = {"alexa": {"smart_home": None}} - response = await do_http_discovery(config, hass, hass_client) response_data = await response.json() From e5261fe2a3f98ef48b6ad29d27742113c05f376e Mon Sep 17 00:00:00 2001 From: Alberto Geniola Date: Sat, 5 Aug 2023 22:03:51 +0200 Subject: [PATCH 0222/1151] Implement Elmax cover platform (#79409) * Implement Elmax cover platform. * Reduce the number of code lines by leveraging the := operator * Move _COMMAND_BY_MOTION_STATUS declaration at the top * Remove redundant null-check * Move conditional platform setup logic into the platform itself * Remove redundant log * Change log severity for stop request on IDLE cover state --------- Co-authored-by: G Johansson --- .coveragerc | 3 +- homeassistant/components/elmax/common.py | 7 ++ homeassistant/components/elmax/const.py | 1 + homeassistant/components/elmax/cover.py | 136 +++++++++++++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/elmax/cover.py diff --git a/.coveragerc b/.coveragerc index 32704ffce8b..2f1b83bc8b0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -284,7 +284,8 @@ omit = homeassistant/components/elmax/alarm_control_panel.py homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/common.py - homeassistant/components/elmax/binary_sensor.py + homeassistant/components/elmax/const.py + homeassistant/components/elmax/cover.py homeassistant/components/elmax/switch.py homeassistant/components/elv/* homeassistant/components/emby/media_player.py diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index b0f51740b04..797121b6e46 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -16,6 +16,7 @@ from elmax_api.exceptions import ( from elmax_api.http import Elmax from elmax_api.model.actuator import Actuator from elmax_api.model.area import Area +from elmax_api.model.cover import Cover from elmax_api.model.endpoint import DeviceEndpoint from elmax_api.model.panel import PanelEntry, PanelStatus @@ -80,6 +81,12 @@ class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): return self._state_by_endpoint[area_id] raise HomeAssistantError("Unknown area") + def get_cover_state(self, cover_id: str) -> Cover: + """Return state of a specific cover.""" + if self._state_by_endpoint is not None: + return self._state_by_endpoint[cover_id] + raise HomeAssistantError("Unknown cover") + @property def http_client(self): """Return the current http client being used by this instance.""" diff --git a/homeassistant/components/elmax/const.py b/homeassistant/components/elmax/const.py index cd35211e592..cd2c73002a4 100644 --- a/homeassistant/components/elmax/const.py +++ b/homeassistant/components/elmax/const.py @@ -15,6 +15,7 @@ ELMAX_PLATFORMS = [ Platform.SWITCH, Platform.BINARY_SENSOR, Platform.ALARM_CONTROL_PANEL, + Platform.COVER, ] POLLING_SECONDS = 30 diff --git a/homeassistant/components/elmax/cover.py b/homeassistant/components/elmax/cover.py new file mode 100644 index 00000000000..8a6acb154aa --- /dev/null +++ b/homeassistant/components/elmax/cover.py @@ -0,0 +1,136 @@ +"""Elmax cover platform.""" +from __future__ import annotations + +import logging +from typing import Any + +from elmax_api.model.command import CoverCommand +from elmax_api.model.cover_status import CoverStatus + +from homeassistant.components.cover import CoverEntity, CoverEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ElmaxCoordinator +from .common import ElmaxEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +_COMMAND_BY_MOTION_STATUS = ( + { # Maps the stop command to use for every cover motion status + CoverStatus.DOWN: CoverCommand.DOWN, + CoverStatus.UP: CoverCommand.UP, + CoverStatus.IDLE: None, + } +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Elmax cover platform.""" + coordinator: ElmaxCoordinator = hass.data[DOMAIN][config_entry.entry_id] + # Add the cover feature only if supported by the current panel. + if coordinator.data is None or not coordinator.data.cover_feature: + return + + known_devices = set() + + def _discover_new_devices(): + if (panel_status := coordinator.data) is None: + return # In case the panel is offline, its status will be None. In that case, simply do nothing + + # Otherwise, add all the entities we found + entities = [] + for cover in panel_status.covers: + # Skip already handled devices + if cover.endpoint_id in known_devices: + continue + entity = ElmaxCover( + panel=coordinator.panel_entry, + elmax_device=cover, + panel_version=panel_status.release, + coordinator=coordinator, + ) + entities.append(entity) + + if entities: + async_add_entities(entities) + known_devices.update([e.unique_id for e in entities]) + + # Register a listener for the discovery of new devices + config_entry.async_on_unload(coordinator.async_add_listener(_discover_new_devices)) + + # Immediately run a discovery, so we don't need to wait for the next update + _discover_new_devices() + + +class ElmaxCover(ElmaxEntity, CoverEntity): + """Elmax Cover entity implementation.""" + + _attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP + ) + + def __check_cover_status(self, status_to_check: CoverStatus) -> bool | None: + """Check if the current cover entity is in a specific state.""" + if ( + state := self.coordinator.get_cover_state(self._device.endpoint_id).status + ) is None: + return None + return state == status_to_check + + @property + def is_closed(self) -> bool | None: + """Tells if the cover is closed or not.""" + return self.coordinator.get_cover_state(self._device.endpoint_id).position == 0 + + @property + def current_cover_position(self) -> int | None: + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self.coordinator.get_cover_state(self._device.endpoint_id).position + + @property + def is_opening(self) -> bool | None: + """Tells if the cover is opening or not.""" + return self.__check_cover_status(CoverStatus.UP) + + @property + def is_closing(self) -> bool | None: + """Return if the cover is closing or not.""" + return self.__check_cover_status(CoverStatus.DOWN) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + # To stop the cover, Elmax requires us to re-issue the same command once again. + # To detect the current motion status, we request an immediate refresh to the coordinator + await self.coordinator.async_request_refresh() + motion_status = self.coordinator.get_cover_state( + self._device.endpoint_id + ).status + command = _COMMAND_BY_MOTION_STATUS[motion_status] + if command: + await self.coordinator.http_client.execute_command( + endpoint_id=self._device.endpoint_id, command=command + ) + else: + _LOGGER.debug("Ignoring stop request as the cover is IDLE") + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self.coordinator.http_client.execute_command( + endpoint_id=self._device.endpoint_id, command=CoverCommand.UP + ) + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + await self.coordinator.http_client.execute_command( + endpoint_id=self._device.endpoint_id, command=CoverCommand.DOWN + ) From c478a81deb866cdfad8ef24b9cab80bf44128945 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Aug 2023 10:21:46 -1000 Subject: [PATCH 0223/1151] Bump bluetooth-data-tools to 1.7.0 (#97821) --- homeassistant/components/bluetooth/manifest.json | 2 +- 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 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 67c27f014d1..147d38203aa 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.6.1", + "bluetooth-data-tools==1.7.0", "dbus-fast==1.90.1" ] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d35cf90c60f..303e773bbd3 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.6.1", + "bluetooth-data-tools==1.7.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 1a613a82098..60d2efe6536 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.6.1", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.7.0", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 5a1eef40001..9ebbe07703a 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.6.1", "led-ble==1.0.0"] + "requirements": ["bluetooth-data-tools==1.7.0", "led-ble==1.0.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2fdbd60dd46..e8557a3b922 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.6.1 +bluetooth-data-tools==1.7.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 39947d3fc2d..965b0912d7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ bluetooth-auto-recovery==1.2.1 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.6.1 +bluetooth-data-tools==1.7.0 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8655855bf8..bf3054f9db0 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.6.1 +bluetooth-data-tools==1.7.0 # homeassistant.components.bond bond-async==0.2.1 From 74d02a15748377f95cc8350efcfc7f63dff4e599 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sat, 5 Aug 2023 22:28:24 +0200 Subject: [PATCH 0224/1151] BMW: Remove deprecated refresh from cloud button (#97864) * Remove deprecated refresh from cloud button * Clean up strings.json --- .../components/bmw_connected_drive/button.py | 41 ++++++------------- .../bmw_connected_drive/strings.json | 3 -- .../snapshots/test_button.ambr | 24 ----------- .../bmw_connected_drive/test_button.py | 33 ++++++++++----- 4 files changed, 35 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/button.py b/homeassistant/components/bmw_connected_drive/button.py index 6edb1a3f2ac..c3f066610a9 100644 --- a/homeassistant/components/bmw_connected_drive/button.py +++ b/homeassistant/components/bmw_connected_drive/button.py @@ -26,14 +26,17 @@ _LOGGER = logging.getLogger(__name__) @dataclass -class BMWButtonEntityDescription(ButtonEntityDescription): +class BMWRequiredKeysMixin: + """Mixin for required keys.""" + + remote_function: Callable[[MyBMWVehicle], Coroutine[Any, Any, RemoteServiceStatus]] + + +@dataclass +class BMWButtonEntityDescription(ButtonEntityDescription, BMWRequiredKeysMixin): """Class describing BMW button entities.""" enabled_when_read_only: bool = False - remote_function: Callable[ - [MyBMWVehicle], Coroutine[Any, Any, RemoteServiceStatus] - ] | None = None - account_function: Callable[[BMWDataUpdateCoordinator], Coroutine] | None = None is_available: Callable[[MyBMWVehicle], bool] = lambda _: True @@ -69,13 +72,6 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = ( icon="mdi:crosshairs-question", remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_vehicle_finder(), ), - BMWButtonEntityDescription( - key="refresh", - translation_key="refresh", - icon="mdi:refresh", - account_function=lambda coordinator: coordinator.async_request_refresh(), - enabled_when_read_only=True, - ), ) @@ -120,22 +116,9 @@ class BMWButton(BMWBaseEntity, ButtonEntity): async def async_press(self) -> None: """Press the button.""" - if self.entity_description.remote_function: - try: - await self.entity_description.remote_function(self.vehicle) - except MyBMWAPIError as ex: - raise HomeAssistantError(ex) from ex - elif self.entity_description.account_function: - _LOGGER.warning( - "The 'Refresh from cloud' button is deprecated. Use the" - " 'homeassistant.update_entity' service with any BMW entity for a full" - " reload. See" - " https://www.home-assistant.io/integrations/bmw_connected_drive/#update-the-state--refresh-from-api" - " for details" - ) - try: - await self.entity_description.account_function(self.coordinator) - except MyBMWAPIError as ex: - raise HomeAssistantError(ex) from ex + try: + await self.entity_description.remote_function(self.vehicle) + except MyBMWAPIError as ex: + raise HomeAssistantError(ex) from ex self.coordinator.async_update_listeners() diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json index af73417b1a9..69abd97ddfe 100644 --- a/homeassistant/components/bmw_connected_drive/strings.json +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -66,9 +66,6 @@ }, "find_vehicle": { "name": "Find vehicle" - }, - "refresh": { - "name": "Refresh from cloud" } }, "lock": { diff --git a/tests/components/bmw_connected_drive/snapshots/test_button.ambr b/tests/components/bmw_connected_drive/snapshots/test_button.ambr index a7520a6bce0..af43f118a77 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_button.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_button.ambr @@ -61,18 +61,6 @@ 'last_updated': , 'state': 'unknown', }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Refresh from cloud', - 'icon': 'mdi:refresh', - }), - 'context': , - 'entity_id': 'button.i4_edrive40_refresh_from_cloud', - 'last_changed': , - 'last_updated': , - 'state': 'unknown', - }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -121,17 +109,5 @@ 'last_updated': , 'state': 'unknown', }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Refresh from cloud', - 'icon': 'mdi:refresh', - }), - 'context': , - 'entity_id': 'button.i3_rex_refresh_from_cloud', - 'last_changed': , - 'last_updated': , - 'state': 'unknown', - }), ]) # --- diff --git a/tests/components/bmw_connected_drive/test_button.py b/tests/components/bmw_connected_drive/test_button.py index 236dd76ce9f..16803756702 100644 --- a/tests/components/bmw_connected_drive/test_button.py +++ b/tests/components/bmw_connected_drive/test_button.py @@ -1,4 +1,7 @@ """Test BMW buttons.""" +from unittest.mock import AsyncMock + +from bimmer_connected.models import MyBMWRemoteServiceError from bimmer_connected.vehicle.remote_services import RemoteServices import pytest import respx @@ -8,6 +11,7 @@ from homeassistant.components.bmw_connected_drive.coordinator import ( BMWDataUpdateCoordinator, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from . import setup_mocked_integration @@ -58,22 +62,31 @@ async def test_update_triggers_success( assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 1 -async def test_refresh_from_cloud( +async def test_update_failed( hass: HomeAssistant, bmw_fixture: respx.Router, + monkeypatch: pytest.MonkeyPatch, ) -> None: - """Test button press for deprecated service.""" + """Test button press.""" # Setup component assert await setup_mocked_integration(hass) BMWDataUpdateCoordinator.async_update_listeners.reset_mock() - # Test - await hass.services.async_call( - "button", - "press", - blocking=True, - target={"entity_id": "button.i4_edrive40_refresh_from_cloud"}, + # Setup exception + monkeypatch.setattr( + RemoteServices, + "trigger_remote_service", + AsyncMock(side_effect=MyBMWRemoteServiceError), ) - assert RemoteServices.trigger_remote_service.call_count == 0 - assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 2 + + # Test + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "button", + "press", + blocking=True, + target={"entity_id": "button.i4_edrive40_flash_lights"}, + ) + assert RemoteServices.trigger_remote_service.call_count == 1 + assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 0 From c7b7ca876997518e104105ac002d17eb3f754deb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 5 Aug 2023 22:36:45 +0200 Subject: [PATCH 0225/1151] Add yeelight class to fix superclass issue (#97649) * Add device naming to Yeelight * Add extra light entity to fix superclass * Add extra light entity to fix superclass --- homeassistant/components/yeelight/light.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 35739b0f596..f5f39e9997d 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -297,7 +297,7 @@ async def async_setup_entry( _lights_setup_helper(YeelightColorLightWithNightlightSwitch) _lights_setup_helper(YeelightNightLightModeWithoutBrightnessControl) else: - _lights_setup_helper(YeelightColorLightWithoutNightlightSwitch) + _lights_setup_helper(YeelightColorLightWithoutNightlightSwitchLight) elif device_type == BulbType.WhiteTemp: if nl_switch_light and device.is_nightlight_supported: _lights_setup_helper(YeelightWithNightLight) @@ -931,6 +931,14 @@ class YeelightColorLightWithoutNightlightSwitch( """Representation of a Color Yeelight light.""" +class YeelightColorLightWithoutNightlightSwitchLight( + YeelightColorLightWithoutNightlightSwitch +): + """Representation of a Color Yeelight light.""" + + _attr_name = None + + class YeelightColorLightWithNightlightSwitch( YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight ): From 41c684f36e2570b52d71afe2585df6597ef20904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 5 Aug 2023 22:46:03 +0200 Subject: [PATCH 0226/1151] Use new Mill api (#97497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api. Wip Signed-off-by: Daniel Hjelseth Høyer * Mill, new api Signed-off-by: Daniel Hjelseth Høyer * Mill, new api Signed-off-by: Daniel Hjelseth Høyer * Mill, new api Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/mill/sensor.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/mill/sensor.py Co-authored-by: Joost Lekkerkerker * Mill, new api Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/mill/climate.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/mill/climate.py Co-authored-by: Joost Lekkerkerker --------- Signed-off-by: Daniel Hjelseth Høyer Co-authored-by: Joost Lekkerkerker --- homeassistant/components/mill/climate.py | 52 ++++++--------------- homeassistant/components/mill/manifest.json | 2 +- homeassistant/components/mill/sensor.py | 7 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 18 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index f1487ed59f1..1e578087b73 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -5,8 +5,6 @@ import mill import voluptuous as vol from homeassistant.components.climate import ( - FAN_OFF, - FAN_ON, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -18,7 +16,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_USERNAME, PRECISION_HALVES, - PRECISION_WHOLE, + PRECISION_TENTHS, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -90,11 +88,13 @@ async def async_setup_entry( class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity): """Representation of a Mill Thermostat device.""" - _attr_fan_modes = [FAN_ON, FAN_OFF] _attr_has_entity_name = True + _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP _attr_name = None + _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_target_temperature_step = PRECISION_TENTHS _attr_temperature_unit = UnitOfTemperature.CELSIUS def __init__( @@ -111,22 +111,9 @@ class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, heater.device_id)}, manufacturer=MANUFACTURER, - model=f"Generation {heater.generation}", + model=heater.model, name=heater.name, ) - if heater.is_gen1: - self._attr_hvac_modes = [HVACMode.HEAT] - else: - self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] - - if heater.generation < 3: - self._attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE - ) - self._attr_target_temperature_step = PRECISION_WHOLE - else: - self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - self._attr_target_temperature_step = PRECISION_HALVES self._update_attr(heater) @@ -139,26 +126,16 @@ class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity): ) await self.coordinator.async_request_refresh() - async def async_set_fan_mode(self, fan_mode: str) -> None: - """Set new target fan mode.""" - fan_status = 1 if fan_mode == FAN_ON else 0 - await self.coordinator.mill_data_connection.heater_control( - self._id, fan_status=fan_status - ) - await self.coordinator.async_request_refresh() - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" - heater = self.coordinator.data[self._id] - if hvac_mode == HVACMode.HEAT: await self.coordinator.mill_data_connection.heater_control( - self._id, power_status=1 + self._id, power_status=True ) await self.coordinator.async_request_refresh() - elif hvac_mode == HVACMode.OFF and not heater.is_gen1: + elif hvac_mode == HVACMode.OFF: await self.coordinator.mill_data_connection.heater_control( - self._id, power_status=0 + self._id, power_status=False ) await self.coordinator.async_request_refresh() @@ -178,23 +155,20 @@ class MillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntity): self._available = heater.available self._attr_extra_state_attributes = { "open_window": heater.open_window, - "heating": heater.is_heating, "controlled_by_tibber": heater.tibber_control, - "heater_generation": heater.generation, } - if heater.room: - self._attr_extra_state_attributes["room"] = heater.room.name - self._attr_extra_state_attributes["avg_room_temp"] = heater.room.avg_temp + if heater.room_name: + self._attr_extra_state_attributes["room"] = heater.room_name + self._attr_extra_state_attributes["avg_room_temp"] = heater.room_avg_temp else: self._attr_extra_state_attributes["room"] = "Independent device" self._attr_target_temperature = heater.set_temp self._attr_current_temperature = heater.current_temp - self._attr_fan_mode = FAN_ON if heater.fan_status == 1 else HVACMode.OFF - if heater.is_heating == 1: + if heater.is_heating: self._attr_hvac_action = HVACAction.HEATING else: self._attr_hvac_action = HVACAction.IDLE - if heater.is_gen1 or heater.power_status == 1: + if heater.power_status: self._attr_hvac_mode = HVACMode.HEAT else: self._attr_hvac_mode = HVACMode.OFF diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 0666c1107ca..66a358648fd 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.10.0", "mill-local==0.2.0"] + "requirements": ["millheater==0.11.0", "mill-local==0.2.0"] } diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index a915418bb93..843e8b6570e 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -172,13 +172,10 @@ class MillSensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{mill_device.device_id}_{entity_description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, mill_device.device_id)}, - name=self.name, + name=mill_device.name, manufacturer=MANUFACTURER, + model=mill_device.model, ) - if isinstance(mill_device, mill.Heater): - self._attr_device_info["model"] = f"Generation {mill_device.generation}" - elif isinstance(mill_device, mill.Sensor): - self._attr_device_info["model"] = "Mill Sense Air" self._update_attr(mill_device) @callback diff --git a/requirements_all.txt b/requirements_all.txt index 965b0912d7a..645dcf3fde0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1207,7 +1207,7 @@ micloud==0.5 mill-local==0.2.0 # homeassistant.components.mill -millheater==0.10.0 +millheater==0.11.0 # homeassistant.components.minio minio==7.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf3054f9db0..c8e3c0e76e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -924,7 +924,7 @@ micloud==0.5 mill-local==0.2.0 # homeassistant.components.mill -millheater==0.10.0 +millheater==0.11.0 # homeassistant.components.minio minio==7.1.12 From 71a81e1f5d6861b968e6236941b17d5bfcb9c4a4 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 5 Aug 2023 22:47:04 +0200 Subject: [PATCH 0227/1151] Change discovergy integration type (#97391) --- homeassistant/components/discovergy/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovergy/manifest.json b/homeassistant/components/discovergy/manifest.json index d5bdc018eda..4223318ed93 100644 --- a/homeassistant/components/discovergy/manifest.json +++ b/homeassistant/components/discovergy/manifest.json @@ -4,7 +4,7 @@ "codeowners": ["@jpbede"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/discovergy", - "integration_type": "hub", + "integration_type": "service", "iot_class": "cloud_polling", "requirements": ["pydiscovergy==2.0.3"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0d0e1ca2d71..ed51bcc7dbf 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1113,7 +1113,7 @@ }, "discovergy": { "name": "Discovergy", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling" }, From e4d9daf7463936587afb687beb41497f2a53bd97 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 5 Aug 2023 22:53:14 +0200 Subject: [PATCH 0228/1151] Migrate to SensorEntityDescriptions for Trafikverket Train (#97318) * tvt migrate to sensor entity description * spelling * revert spelling --- .../components/trafikverket_train/__init__.py | 9 ++ .../components/trafikverket_train/sensor.py | 88 +++++++++++++------ .../trafikverket_train/strings.json | 38 ++++++++ 3 files changed, 106 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/trafikverket_train/__init__.py b/homeassistant/components/trafikverket_train/__init__.py index dd35d058ed5..8f11590c487 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant 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 @@ -39,6 +40,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entity_reg = er.async_get(hass) + entries = er.async_entries_for_config_entry(entity_reg, entry.entry_id) + for entity in entries: + if not entity.unique_id.startswith(entry.entry_id): + entity_reg.async_update_entity( + entity.entity_id, new_unique_id=f"{entry.entry_id}-departure_time" + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index f57850e51b8..47f31e35c63 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -1,24 +1,29 @@ """Train information for departures and delays, provided by Trafikverket.""" from __future__ import annotations -from datetime import time, timedelta -from typing import TYPE_CHECKING +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, time, timedelta from pytrafikverket.trafikverket_train import StationInfo -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_WEEKDAY 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 +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from .const import CONF_TIME, DOMAIN -from .coordinator import TVDataUpdateCoordinator -from .util import create_unique_id +from .coordinator import TrainData, TVDataUpdateCoordinator ATTR_DEPARTURE_STATE = "departure_state" ATTR_CANCELED = "canceled" @@ -33,6 +38,42 @@ ICON = "mdi:train" SCAN_INTERVAL = timedelta(minutes=5) +@dataclass +class TrafikverketRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[TrainData], StateType | datetime] + extra_fn: Callable[[TrainData], dict[str, StateType | datetime]] + + +@dataclass +class TrafikverketSensorEntityDescription( + SensorEntityDescription, TrafikverketRequiredKeysMixin +): + """Describes Trafikverket sensor entity.""" + + +SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( + TrafikverketSensorEntityDescription( + key="departure_time", + translation_key="departure_time", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: data.departure_time, + extra_fn=lambda data: { + ATTR_DEPARTURE_STATE: data.departure_state, + ATTR_CANCELED: data.cancelled, + ATTR_DELAY_TIME: data.delayed_time, + ATTR_PLANNED_TIME: data.planned_time, + ATTR_ESTIMATED_TIME: data.estimated_time, + ATTR_ACTUAL_TIME: data.actual_time, + ATTR_OTHER_INFORMATION: data.other_info, + ATTR_DEVIATIONS: data.deviation, + }, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -55,7 +96,9 @@ async def async_setup_entry( entry.data[CONF_WEEKDAY], train_time, entry.entry_id, + description, ) + for description in SENSOR_TYPES ], True, ) @@ -64,10 +107,8 @@ async def async_setup_entry( class TrainSensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): """Contains data about a train depature.""" - _attr_icon = ICON - _attr_device_class = SensorDeviceClass.TIMESTAMP _attr_has_entity_name = True - _attr_name = None + entity_description: TrafikverketSensorEntityDescription def __init__( self, @@ -78,9 +119,11 @@ class TrainSensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): weekday: list, departuretime: time | None, entry_id: str, + entity_description: TrafikverketSensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) + self.entity_description = entity_description self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, @@ -89,11 +132,7 @@ class TrainSensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): name=name, configuration_url="https://api.trafikinfo.trafikverket.se/", ) - if TYPE_CHECKING: - assert from_station.name and to_station.name - self._attr_unique_id = create_unique_id( - from_station.name, to_station.name, departuretime, weekday - ) + self._attr_unique_id = f"{entry_id}-{entity_description.key}" self._update_attr() @callback @@ -103,19 +142,10 @@ class TrainSensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): @callback def _update_attr(self) -> None: - """Retrieve latest state.""" - - data = self.coordinator.data - - self._attr_native_value = data.departure_time - - self._attr_extra_state_attributes = { - ATTR_DEPARTURE_STATE: data.departure_state, - ATTR_CANCELED: data.cancelled, - ATTR_DELAY_TIME: data.delayed_time, - ATTR_PLANNED_TIME: data.planned_time, - ATTR_ESTIMATED_TIME: data.estimated_time, - ATTR_ACTUAL_TIME: data.actual_time, - ATTR_OTHER_INFORMATION: data.other_info, - ATTR_DEVIATIONS: data.deviation, - } + """Retrieve latest states.""" + self._attr_native_value = self.entity_description.value_fn( + self.coordinator.data + ) + self._attr_extra_state_attributes = self.entity_description.extra_fn( + self.coordinator.data + ) diff --git a/homeassistant/components/trafikverket_train/strings.json b/homeassistant/components/trafikverket_train/strings.json index 0089f6db8fc..05032027b97 100644 --- a/homeassistant/components/trafikverket_train/strings.json +++ b/homeassistant/components/trafikverket_train/strings.json @@ -41,5 +41,43 @@ "sun": "[%key:common::time::sunday%]" } } + }, + "entity": { + "sensor": { + "departure_time": { + "name": "Departure time", + "state_attributes": { + "departure_state": { + "name": "Departure state", + "state": { + "on_time": "On time", + "delayed": "Delayed", + "canceled": "Cancelled" + } + }, + "canceled": { + "name": "Cancelled" + }, + "number_of_minutes_delayed": { + "name": "Minutes delayed" + }, + "planned_time": { + "name": "Planned time" + }, + "estimated_time": { + "name": "Estimated time" + }, + "actual_time": { + "name": "Actual time" + }, + "other_information": { + "name": "Other information" + }, + "deviations": { + "name": "Deviations" + } + } + } + } } } From 0caeac1a82a6205deae963e1ca9878161a570619 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Sat, 5 Aug 2023 23:13:21 +0200 Subject: [PATCH 0229/1151] Add support for toothbrushes to xiaomi-ble (#97276) * Add support for toothbrushes to xiaomi-ble * use str for string --- .../components/xiaomi_ble/binary_sensor.py | 3 ++ .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 22 +++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/test_sensor.py | 34 +++++++++++++++++++ 6 files changed, 57 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 5ff418fe831..5490676ad1a 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -72,6 +72,9 @@ BINARY_SENSOR_DESCRIPTIONS = { key=ExtendedBinarySensorDeviceClass.PRY_THE_DOOR, device_class=BinarySensorDeviceClass.TAMPER, ), + ExtendedBinarySensorDeviceClass.TOOTHBRUSH: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.TOOTHBRUSH, + ), } diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index e2b327c6823..a03e3f388ed 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.20.0"] + "requirements": ["xiaomi-ble==0.21.1"] } diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 86ffdedafd1..56bfbb1b020 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from xiaomi_ble import DeviceClass, SensorUpdate, Units +from xiaomi_ble.parser import ExtendedSensorDeviceClass from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -25,6 +26,7 @@ from homeassistant.const import ( UnitOfMass, UnitOfPressure, UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -129,13 +131,23 @@ SENSOR_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), - # Used for e.g. consumable sensor on WX08ZM - (None, Units.PERCENTAGE): SensorEntityDescription( - key=str(Units.PERCENTAGE), - device_class=None, + # Used for e.g. consumable sensor on WX08ZM and M1S-T500 + (ExtendedSensorDeviceClass.CONSUMABLE, Units.PERCENTAGE): SensorEntityDescription( + key=str(ExtendedSensorDeviceClass.CONSUMABLE), native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), + # Used for score after brushing with a toothbrush + (ExtendedSensorDeviceClass.SCORE, None): SensorEntityDescription( + key=str(ExtendedSensorDeviceClass.SCORE), + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for counting during brushing + (ExtendedSensorDeviceClass.COUNTER, Units.TIME_SECONDS): SensorEntityDescription( + key=str(ExtendedSensorDeviceClass.COUNTER), + native_unit_of_measurement=UnitOfTime.SECONDS, + state_class=SensorStateClass.MEASUREMENT, + ), } @@ -153,7 +165,7 @@ def sensor_update_to_bluetooth_data_update( (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() - if description.native_unit_of_measurement + if description.device_class }, entity_data={ device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value diff --git a/requirements_all.txt b/requirements_all.txt index 645dcf3fde0..bcfa77e219d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2698,7 +2698,7 @@ wyoming==1.1.0 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.20.0 +xiaomi-ble==0.21.1 # homeassistant.components.knx xknx==2.11.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8e3c0e76e8..f8e922977e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1983,7 +1983,7 @@ wyoming==1.1.0 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.20.0 +xiaomi-ble==0.21.1 # homeassistant.components.knx xknx==2.11.2 diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 7f39228a012..a2b0e62821a 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -132,6 +132,40 @@ async def test_xiaomi_consumable(hass: HomeAssistant) -> None: await hass.async_block_till_done() +async def test_xiaomi_score(hass: HomeAssistant) -> None: + """Make sure that score sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="ED:DE:34:3F:48:0C", + data={"bindkey": "1330b99cded13258acc391627e9771f7"}, + ) + 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( + "ED:DE:34:3F:48:0C", + b"\x48\x58\x06\x08\xc9H\x0e\xf1\x12\x81\x07\x973\xfc\x14\x00\x00VD\xdbA", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + sensor = hass.states.get("sensor.smart_toothbrush_480c_score") + + sensor_attr = sensor.attributes + assert sensor.state == "83" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Toothbrush 480C Score" + assert sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_xiaomi_battery_voltage(hass: HomeAssistant) -> None: """Make sure that battery voltage sensors are correctly mapped.""" entry = MockConfigEntry( From 8195c9d1a78ad8b06384d8ec720180b9f6af2787 Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Sat, 5 Aug 2023 23:35:54 +0200 Subject: [PATCH 0230/1151] Use constants for translation keys and rename latency time to latency (#97866) Use constants for translation keys, rename latency time to latency and some small cleanups --- .../components/minecraft_server/__init__.py | 6 +-- .../minecraft_server/binary_sensor.py | 4 +- .../minecraft_server/config_flow.py | 2 +- .../components/minecraft_server/const.py | 12 ++++-- .../components/minecraft_server/sensor.py | 38 +++++++++++-------- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index aef6c94767f..6457f19a335 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -86,7 +86,7 @@ class MinecraftServer: # Data provided by 3rd party library self.version: str | None = None self.protocol_version: int | None = None - self.latency_time: float | None = None + self.latency: float | None = None self.players_online: int | None = None self.players_max: int | None = None self.players_list: list[str] | None = None @@ -174,7 +174,7 @@ class MinecraftServer: self.protocol_version = status_response.version.protocol self.players_online = status_response.players.online self.players_max = status_response.players.max - self.latency_time = status_response.latency + self.latency = status_response.latency self.motd = status_response.motd.to_plain() self.players_list = [] @@ -197,7 +197,7 @@ class MinecraftServer: self.protocol_version = None self.players_online = None self.players_max = None - self.latency_time = None + self.latency = None self.players_list = None self.motd = None diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py index 5c9cb5f42e1..3589bfab3e2 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MinecraftServer -from .const import DOMAIN, ICON_STATUS, NAME_STATUS +from .const import DOMAIN, ICON_STATUS, KEY_STATUS, NAME_STATUS from .entity import MinecraftServerEntity @@ -30,7 +30,7 @@ async def async_setup_entry( class MinecraftServerStatusBinarySensor(MinecraftServerEntity, BinarySensorEntity): """Representation of a Minecraft Server status binary sensor.""" - _attr_translation_key = "status" + _attr_translation_key = KEY_STATUS def __init__(self, server: MinecraftServer) -> None: """Initialize status binary sensor.""" diff --git a/homeassistant/components/minecraft_server/config_flow.py b/homeassistant/components/minecraft_server/config_flow.py index b402b7cfff0..c8429284cd8 100644 --- a/homeassistant/components/minecraft_server/config_flow.py +++ b/homeassistant/components/minecraft_server/config_flow.py @@ -30,7 +30,7 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): address_left, separator, address_right = user_input[CONF_HOST].rpartition( ":" ) - # If no separator is found, 'rpartition' return ('', '', original_string). + # If no separator is found, 'rpartition' returns ('', '', original_string). if separator == "": host = address_right else: diff --git a/homeassistant/components/minecraft_server/const.py b/homeassistant/components/minecraft_server/const.py index 8fe7c9b2791..72a891138c4 100644 --- a/homeassistant/components/minecraft_server/const.py +++ b/homeassistant/components/minecraft_server/const.py @@ -8,7 +8,7 @@ DEFAULT_PORT = 25565 DOMAIN = "minecraft_server" -ICON_LATENCY_TIME = "mdi:signal" +ICON_LATENCY = "mdi:signal" ICON_PLAYERS_MAX = "mdi:account-multiple" ICON_PLAYERS_ONLINE = "mdi:account-multiple" ICON_PROTOCOL_VERSION = "mdi:numeric" @@ -16,11 +16,17 @@ ICON_STATUS = "mdi:lan" ICON_VERSION = "mdi:numeric" ICON_MOTD = "mdi:minecraft" -KEY_SERVERS = "servers" +KEY_LATENCY = "latency" +KEY_PLAYERS_MAX = "players_max" +KEY_PLAYERS_ONLINE = "players_online" +KEY_PROTOCOL_VERSION = "protocol_version" +KEY_STATUS = "status" +KEY_VERSION = "version" +KEY_MOTD = "motd" MANUFACTURER = "Mojang AB" -NAME_LATENCY_TIME = "Latency Time" +NAME_LATENCY = "Latency Time" NAME_PLAYERS_MAX = "Players Max" NAME_PLAYERS_ONLINE = "Players Online" NAME_PROTOCOL_VERSION = "Protocol Version" diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 3a9e4b8f0a0..045aa3cec4e 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -11,13 +11,19 @@ from . import MinecraftServer from .const import ( ATTR_PLAYERS_LIST, DOMAIN, - ICON_LATENCY_TIME, + ICON_LATENCY, ICON_MOTD, ICON_PLAYERS_MAX, ICON_PLAYERS_ONLINE, ICON_PROTOCOL_VERSION, ICON_VERSION, - NAME_LATENCY_TIME, + KEY_LATENCY, + KEY_MOTD, + KEY_PLAYERS_MAX, + KEY_PLAYERS_ONLINE, + KEY_PROTOCOL_VERSION, + KEY_VERSION, + NAME_LATENCY, NAME_MOTD, NAME_PLAYERS_MAX, NAME_PLAYERS_ONLINE, @@ -41,7 +47,7 @@ async def async_setup_entry( entities = [ MinecraftServerVersionSensor(server), MinecraftServerProtocolVersionSensor(server), - MinecraftServerLatencyTimeSensor(server), + MinecraftServerLatencySensor(server), MinecraftServerPlayersOnlineSensor(server), MinecraftServerPlayersMaxSensor(server), MinecraftServerMOTDSensor(server), @@ -75,7 +81,7 @@ class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity): class MinecraftServerVersionSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server version sensor.""" - _attr_translation_key = "version" + _attr_translation_key = KEY_VERSION def __init__(self, server: MinecraftServer) -> None: """Initialize version sensor.""" @@ -89,7 +95,7 @@ class MinecraftServerVersionSensor(MinecraftServerSensorEntity): class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server protocol version sensor.""" - _attr_translation_key = "protocol_version" + _attr_translation_key = KEY_PROTOCOL_VERSION def __init__(self, server: MinecraftServer) -> None: """Initialize protocol version sensor.""" @@ -104,29 +110,29 @@ class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): self._attr_native_value = self._server.protocol_version -class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity): - """Representation of a Minecraft Server latency time sensor.""" +class MinecraftServerLatencySensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server latency sensor.""" - _attr_translation_key = "latency" + _attr_translation_key = KEY_LATENCY def __init__(self, server: MinecraftServer) -> None: - """Initialize latency time sensor.""" + """Initialize latency sensor.""" super().__init__( server=server, - type_name=NAME_LATENCY_TIME, - icon=ICON_LATENCY_TIME, + type_name=NAME_LATENCY, + icon=ICON_LATENCY, unit=UnitOfTime.MILLISECONDS, ) async def async_update(self) -> None: - """Update latency time.""" - self._attr_native_value = self._server.latency_time + """Update latency.""" + self._attr_native_value = self._server.latency class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server online players sensor.""" - _attr_translation_key = "players_online" + _attr_translation_key = KEY_PLAYERS_ONLINE def __init__(self, server: MinecraftServer) -> None: """Initialize online players sensor.""" @@ -153,7 +159,7 @@ class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server maximum number of players sensor.""" - _attr_translation_key = "players_max" + _attr_translation_key = KEY_PLAYERS_MAX def __init__(self, server: MinecraftServer) -> None: """Initialize maximum number of players sensor.""" @@ -172,7 +178,7 @@ class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server MOTD sensor.""" - _attr_translation_key = "motd" + _attr_translation_key = KEY_MOTD def __init__(self, server: MinecraftServer) -> None: """Initialize MOTD sensor.""" From 966784877f5bcd81790f94d7fbc608756e358496 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 5 Aug 2023 23:37:01 +0200 Subject: [PATCH 0231/1151] Remove long overdue deprecated service boost_heating from Hive (#97444) * Hive heating_boost deprecation * Remove strings * Remove service * services * Remove strings --- homeassistant/components/hive/climate.py | 21 --------------------- homeassistant/components/hive/services.yaml | 19 ------------------- homeassistant/components/hive/strings.json | 16 +--------------- 3 files changed, 1 insertion(+), 55 deletions(-) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 95304371e79..99de8b99675 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -66,19 +66,6 @@ async def async_setup_entry( platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - "boost_heating", - { - vol.Required(ATTR_TIME_PERIOD): vol.All( - cv.time_period, - cv.positive_timedelta, - lambda td: td.total_seconds() // 60, - ), - vol.Optional(ATTR_TEMPERATURE, default="25.0"): vol.Coerce(float), - }, - "async_heating_boost", - ) - platform.async_register_entity_service( SERVICE_BOOST_HEATING_ON, { @@ -137,14 +124,6 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): temperature = curtemp + 0.5 await self.hive.heating.setBoostOn(self.device, 30, temperature) - async def async_heating_boost(self, time_period, temperature): - """Handle boost heating service call.""" - _LOGGER.warning( - "Hive Service heating_boost will be removed in 2021.7.0, please update to" - " heating_boost_on" - ) - await self.async_heating_boost_on(time_period, temperature) - @refresh_system async def async_heating_boost_on(self, time_period, temperature): """Handle boost heating service call.""" diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml index 96066246230..e2ad59852a3 100644 --- a/homeassistant/components/hive/services.yaml +++ b/homeassistant/components/hive/services.yaml @@ -1,22 +1,3 @@ -boost_heating: - target: - entity: - integration: hive - domain: climate - fields: - time_period: - required: true - example: 01:30:00 - selector: - time: - temperature: - default: 25.0 - selector: - number: - min: 7 - max: 35 - step: 0.5 - unit_of_measurement: ° boost_heating_on: target: entity: diff --git a/homeassistant/components/hive/strings.json b/homeassistant/components/hive/strings.json index e2a3e9dc7e1..277a1aac754 100644 --- a/homeassistant/components/hive/strings.json +++ b/homeassistant/components/hive/strings.json @@ -58,20 +58,6 @@ } }, "services": { - "boost_heating": { - "name": "Boost heating (to be deprecated)", - "description": "To be deprecated please use boost_heating_on.", - "fields": { - "time_period": { - "name": "Time period", - "description": "Set the time period for the boost." - }, - "temperature": { - "name": "Temperature", - "description": "Set the target temperature for the boost period." - } - } - }, "boost_heating_on": { "name": "Boost heating on", "description": "Sets the boost mode ON defining the period of time and the desired target temperature for the boost.", @@ -82,7 +68,7 @@ }, "temperature": { "name": "Temperature", - "description": "[%key:component::hive::services::boost_heating::fields::temperature::description%]" + "description": "Set the target temperature for the boost period." } } }, From a09090bf99263399858c07a6177cd2cf5aaacea5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Aug 2023 12:31:50 -1000 Subject: [PATCH 0232/1151] Do not fire homekit_controller events from IP polling (#97869) * Fix homekit_controller triggers when value is None * fixes * cover --- .../homekit_controller/device_trigger.py | 25 ++++++++++++------- .../homekit_controller/test_device_trigger.py | 8 ++++++ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index bbc56ddd4a4..4f44cea34f4 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -80,11 +80,11 @@ class TriggerSource: self._iid_trigger_keys.setdefault(iid, set()).add(trigger_key) await connection.add_watchable_characteristics([(aid, iid)]) - def fire(self, iid: int, value: dict[str, Any]) -> None: + def fire(self, iid: int, ev: dict[str, Any]) -> None: """Process events that have been received from a HomeKit accessory.""" for trigger_key in self._iid_trigger_keys.get(iid, set()): for event_handler in self._callbacks.get(trigger_key, []): - event_handler(value) + event_handler(ev) def async_get_triggers(self) -> Generator[tuple[str, str], None, None]: """List device triggers for HomeKit devices.""" @@ -99,20 +99,23 @@ class TriggerSource: ) -> CALLBACK_TYPE: """Attach a trigger.""" trigger_data = trigger_info["trigger_data"] - trigger_key = (config[CONF_TYPE], config[CONF_SUBTYPE]) + type_: str = config[CONF_TYPE] + sub_type: str = config[CONF_SUBTYPE] + trigger_key = (type_, sub_type) job = HassJob(action) + trigger_callbacks = self._callbacks.setdefault(trigger_key, []) + hass = self._hass @callback - def event_handler(char: dict[str, Any]) -> None: - if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]: + def event_handler(ev: dict[str, Any]) -> None: + if sub_type != HK_TO_HA_INPUT_EVENT_VALUES[ev["value"]]: return - self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}}) + hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}}) - self._callbacks.setdefault(trigger_key, []).append(event_handler) + trigger_callbacks.append(event_handler) def async_remove_handler(): - if trigger_key in self._callbacks: - self._callbacks[trigger_key].remove(event_handler) + trigger_callbacks.remove(event_handler) return async_remove_handler @@ -259,6 +262,10 @@ def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], dict[str, if not trigger_sources: return for (aid, iid), ev in events.items(): + # If the value is None, we received the event via polling + # and we don't want to trigger on that + if ev["value"] is None: + continue if aid in conn.devices: device_id = conn.devices[aid] if source := trigger_sources.get(device_id): diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index 757823aba9b..c7e5005446f 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -425,6 +425,14 @@ async def test_handle_events_late_setup(hass: HomeAssistant, utcnow, calls) -> N assert len(calls) == 1 assert calls[0].data["some"] == "device - button1 - single_press - 0" + # Make sure automation doesn't trigger for a polled None + helper.pairing.testing.update_named_service( + "Button 1", {CharacteristicsTypes.INPUT_EVENT: None} + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + # Make sure automation doesn't trigger for long press helper.pairing.testing.update_named_service( "Button 1", {CharacteristicsTypes.INPUT_EVENT: 1} From 34013ac3e9d5ee6d96eda48702e337b13bd80acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Sun, 6 Aug 2023 01:25:17 +0200 Subject: [PATCH 0233/1151] Use PRECISION_TENTHS for Mill local integration (#97874) --- homeassistant/components/mill/climate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 1e578087b73..975bb2ff2c7 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -15,7 +15,6 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_IP_ADDRESS, CONF_USERNAME, - PRECISION_HALVES, PRECISION_TENTHS, UnitOfTemperature, ) @@ -184,7 +183,7 @@ class LocalMillHeater(CoordinatorEntity[MillDataUpdateCoordinator], ClimateEntit _attr_min_temp = MIN_TEMP _attr_name = None _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - _attr_target_temperature_step = PRECISION_HALVES + _attr_target_temperature_step = PRECISION_TENTHS _attr_temperature_unit = UnitOfTemperature.CELSIUS def __init__(self, coordinator: MillDataUpdateCoordinator) -> None: From 02e546e3ef73c961a623d124fca45885f419e478 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Aug 2023 13:33:16 -1000 Subject: [PATCH 0234/1151] Refactor enphase_envoy to use pyenphase library (#97862) --- .coveragerc | 1 + .../components/enphase_envoy/__init__.py | 90 ++----- .../components/enphase_envoy/config_flow.py | 146 ++++++----- .../components/enphase_envoy/const.py | 4 - .../components/enphase_envoy/coordinator.py | 76 ++++++ .../components/enphase_envoy/diagnostics.py | 6 +- .../components/enphase_envoy/manifest.json | 4 +- .../components/enphase_envoy/sensor.py | 236 +++++++++++------- .../components/enphase_envoy/strings.json | 6 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/enphase_envoy/conftest.py | 111 ++++---- .../enphase_envoy/fixtures/__init__.py | 1 - .../enphase_envoy/fixtures/data.json | 10 - .../fixtures/inverters_production.json | 18 -- .../enphase_envoy/test_config_flow.py | 88 +++++-- .../enphase_envoy/test_diagnostics.py | 29 +-- 17 files changed, 460 insertions(+), 378 deletions(-) create mode 100644 homeassistant/components/enphase_envoy/coordinator.py delete mode 100644 tests/components/enphase_envoy/fixtures/__init__.py delete mode 100644 tests/components/enphase_envoy/fixtures/data.json delete mode 100644 tests/components/enphase_envoy/fixtures/inverters_production.json diff --git a/.coveragerc b/.coveragerc index 2f1b83bc8b0..7e0c71ec9fb 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/coordinator.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/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index e94cb9c47d8..daa6a2f4492 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -1,27 +1,16 @@ """The Enphase Envoy integration.""" from __future__ import annotations -from datetime import timedelta -import logging - -import async_timeout -from envoy_reader.envoy_reader import EnvoyReader -import httpx +from pyenphase import Envoy from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import COORDINATOR, DOMAIN, NAME, PLATFORMS -from .sensor import SENSORS - -SCAN_INTERVAL = timedelta(seconds=60) - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, PLATFORMS +from .coordinator import EnphaseUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -29,64 +18,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config = entry.data name = config[CONF_NAME] + host = config[CONF_HOST] + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] - envoy_reader = EnvoyReader( - config[CONF_HOST], - config[CONF_USERNAME], - config[CONF_PASSWORD], - inverters=True, - async_client=get_async_client(hass), - ) + envoy = Envoy(host, get_async_client(hass, verify_ssl=False)) - async def async_update_data(): - """Fetch data from API endpoint.""" - async with async_timeout.timeout(30): - try: - await envoy_reader.getData() - except httpx.HTTPStatusError as err: - raise ConfigEntryAuthFailed from err - except httpx.HTTPError as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err - - data = { - description.key: await getattr(envoy_reader, description.key)() - for description in SENSORS - } - data["inverters_production"] = await envoy_reader.inverters_production() - - _LOGGER.debug("Retrieved data from API: %s", data) - - return data - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"envoy {name}", - update_method=async_update_data, - update_interval=SCAN_INTERVAL, - always_update=False, - ) - - try: - await coordinator.async_config_entry_first_refresh() - except ConfigEntryAuthFailed: - envoy_reader.get_inverters = False - await coordinator.async_config_entry_first_refresh() + coordinator = EnphaseUpdateCoordinator(hass, envoy, name, username, password) + await coordinator.async_config_entry_first_refresh() if not entry.unique_id: - try: - serial = await envoy_reader.get_full_serial_number() - except httpx.HTTPError as ex: - raise ConfigEntryNotReady( - f"Could not obtain serial number from envoy: {ex}" - ) from ex + hass.config_entries.async_update_entry(entry, unique_id=envoy.serial_number) - hass.config_entries.async_update_entry(entry, unique_id=serial) - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - COORDINATOR: coordinator, - NAME: name, - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -106,13 +50,13 @@ async def async_remove_config_entry_device( ) -> bool: """Remove an enphase_envoy config entry from a device.""" dev_ids = {dev_id[1] for dev_id in device_entry.identifiers if dev_id[0] == DOMAIN} - data: dict = hass.data[DOMAIN][config_entry.entry_id] - coordinator: DataUpdateCoordinator = data[COORDINATOR] - envoy_data: dict = coordinator.data + coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + envoy_data = coordinator.envoy.data envoy_serial_num = config_entry.unique_id if envoy_serial_num in dev_ids: return False - for inverter in envoy_data.get("inverters_production", []): - if str(inverter) in dev_ids: - return False + if envoy_data and envoy_data.inverters: + for inverter in envoy_data.inverters: + if str(inverter) in dev_ids: + return False return True diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 3707733b1af..93eaa9514e9 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -2,12 +2,17 @@ from __future__ import annotations from collections.abc import Mapping -import contextlib import logging from typing import Any -from envoy_reader.envoy_reader import EnvoyReader -import httpx +from awesomeversion import AwesomeVersion +from pyenphase import ( + AUTH_TOKEN_MIN_VERSION, + Envoy, + EnvoyAuthenticationError, + EnvoyAuthenticationRequired, + EnvoyError, +) import voluptuous as vol from homeassistant import config_entries @@ -15,7 +20,6 @@ from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.httpx_client import get_async_client from homeassistant.util.network import is_ipv4_address @@ -27,25 +31,19 @@ ENVOY = "Envoy" CONF_SERIAL = "serial" +INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired) -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> EnvoyReader: +INSTALLER_AUTH_USERNAME = "installer" + + +async def validate_input( + hass: HomeAssistant, host: str, username: str, password: str +) -> Envoy: """Validate the user input allows us to connect.""" - envoy_reader = EnvoyReader( - data[CONF_HOST], - data[CONF_USERNAME], - data[CONF_PASSWORD], - inverters=False, - async_client=get_async_client(hass), - ) - - try: - await envoy_reader.getData() - except httpx.HTTPStatusError as err: - raise InvalidAuth from err - except (RuntimeError, httpx.HTTPError) as err: - raise CannotConnect from err - - return envoy_reader + envoy = Envoy(host, get_async_client(hass, verify_ssl=False)) + await envoy.setup() + await envoy.authenticate(username=username, password=password) + return envoy class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -57,10 +55,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize an envoy flow.""" self.ip_address = None self.username = None + self.protovers: str | None = None self._reauth_entry = None @callback - def _async_generate_schema(self): + def _async_generate_schema(self) -> vol.Schema: """Generate schema.""" schema = {} @@ -68,15 +67,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): schema[vol.Required(CONF_HOST, default=self.ip_address)] = vol.In( [self.ip_address] ) - else: + elif not self._reauth_entry: schema[vol.Required(CONF_HOST)] = str - schema[vol.Optional(CONF_USERNAME, default=self.username or "envoy")] = str + default_username = "" + if ( + not self.username + and self.protovers + and AwesomeVersion(self.protovers) < AUTH_TOKEN_MIN_VERSION + ): + default_username = INSTALLER_AUTH_USERNAME + + schema[ + vol.Optional(CONF_USERNAME, default=self.username or default_username) + ] = str schema[vol.Optional(CONF_PASSWORD, default="")] = str + return vol.Schema(schema) @callback - def _async_current_hosts(self): + def _async_current_hosts(self) -> set[str]: """Return a set of hosts.""" return { entry.data[CONF_HOST] @@ -91,6 +101,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not is_ipv4_address(discovery_info.host): return self.async_abort(reason="not_ipv4_address") serial = discovery_info.properties["serialnum"] + self.protovers = discovery_info.properties.get("protovers") await self.async_set_unique_id(serial) self.ip_address = discovery_info.host self._abort_if_unique_id_configured({CONF_HOST: self.ip_address}) @@ -116,81 +127,84 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) + assert self._reauth_entry is not None + if unique_id := self._reauth_entry.unique_id: + await self.async_set_unique_id(unique_id, raise_on_progress=False) return await self.async_step_user() def _async_envoy_name(self) -> str: """Return the name of the envoy.""" - if self.unique_id: - return f"{ENVOY} {self.unique_id}" - return ENVOY - - async def _async_set_unique_id_from_envoy(self, envoy_reader: EnvoyReader) -> bool: - """Set the unique id by fetching it from the envoy.""" - serial = None - with contextlib.suppress(httpx.HTTPError): - serial = await envoy_reader.get_full_serial_number() - if serial: - await self.async_set_unique_id(serial) - return True - return False + return f"{ENVOY} {self.unique_id}" if self.unique_id else ENVOY async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} + description_placeholders: dict[str, str] = {} + + if self._reauth_entry: + host = self._reauth_entry.data[CONF_HOST] + else: + host = (user_input or {}).get(CONF_HOST) or self.ip_address or "" if user_input is not None: - if ( - not self._reauth_entry - and user_input[CONF_HOST] in self._async_current_hosts() - ): - return self.async_abort(reason="already_configured") + if not self._reauth_entry: + if host in self._async_current_hosts(): + return self.async_abort(reason="already_configured") + try: - envoy_reader = await validate_input(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: + envoy = await validate_input( + self.hass, + host, + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + except INVALID_AUTH_ERRORS as e: errors["base"] = "invalid_auth" + description_placeholders = {"reason": str(e)} + except EnvoyError as e: + errors["base"] = "cannot_connect" + description_placeholders = {"reason": str(e)} except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - data = user_input.copy() - data[CONF_NAME] = self._async_envoy_name() + name = self._async_envoy_name() if self._reauth_entry: self.hass.config_entries.async_update_entry( self._reauth_entry, - data=data, + data=self._reauth_entry.data | user_input, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload( + self._reauth_entry.entry_id + ) ) return self.async_abort(reason="reauth_successful") - if not self.unique_id and await self._async_set_unique_id_from_envoy( - envoy_reader - ): - data[CONF_NAME] = self._async_envoy_name() + if not self.unique_id: + await self.async_set_unique_id(envoy.serial_number) + name = self._async_envoy_name() if self.unique_id: - self._abort_if_unique_id_configured({CONF_HOST: data[CONF_HOST]}) + self._abort_if_unique_id_configured({CONF_HOST: host}) - return self.async_create_entry(title=data[CONF_NAME], data=data) + # CONF_NAME is still set for legacy backwards compatibility + return self.async_create_entry( + title=name, data={CONF_HOST: host, CONF_NAME: name} | user_input + ) if self.unique_id: self.context["title_placeholders"] = { CONF_SERIAL: self.unique_id, - CONF_HOST: self.ip_address, + CONF_HOST: host, } + return self.async_show_form( step_id="user", data_schema=self._async_generate_schema(), + description_placeholders=description_placeholders, errors=errors, ) - - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(HomeAssistantError): - """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index e7c0b7f2a5e..63af27f3ee2 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -4,7 +4,3 @@ from homeassistant.const import Platform DOMAIN = "enphase_envoy" PLATFORMS = [Platform.SENSOR] - - -COORDINATOR = "coordinator" -NAME = "name" diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py new file mode 100644 index 00000000000..0ba89ee8087 --- /dev/null +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -0,0 +1,76 @@ +"""The enphase_envoy component.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any + +from pyenphase import ( + Envoy, + EnvoyAuthenticationError, + EnvoyAuthenticationRequired, + EnvoyError, +) + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +SCAN_INTERVAL = timedelta(seconds=60) +_LOGGER = logging.getLogger(__name__) + + +class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """DataUpdateCoordinator to gather data from any envoy.""" + + envoy_serial_number: str + + def __init__( + self, + hass: HomeAssistant, + envoy: Envoy, + name: str, + username: str, + password: str, + ) -> None: + """Initialize DataUpdateCoordinator for the envoy.""" + self.envoy = envoy + self.username = username + self.password = password + self.name = name + self._setup_complete = False + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=SCAN_INTERVAL, + always_update=False, + ) + + async def _async_setup_and_authenticate(self) -> None: + """Set up and authenticate with the envoy.""" + envoy = self.envoy + await envoy.setup() + assert envoy.serial_number is not None + self.envoy_serial_number = envoy.serial_number + await envoy.authenticate(username=self.username, password=self.password) + self._setup_complete = True + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch all device and sensor data from api.""" + envoy = self.envoy + for tries in range(2): + try: + if not self._setup_complete: + await self._async_setup_and_authenticate() + return (await envoy.update()).raw + except (EnvoyAuthenticationError, EnvoyAuthenticationRequired) as err: + if self._setup_complete and tries == 0: + # token likely expired or firmware changed, try to re-authenticate + self._setup_complete = False + continue + raise ConfigEntryAuthFailed from err + except EnvoyError as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + raise RuntimeError("Unreachable code in _async_update_data") # pragma: no cover diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index daba57e9488..792f681bb53 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -7,9 +7,9 @@ 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.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import COORDINATOR, DOMAIN +from .const import DOMAIN +from .coordinator import EnphaseUpdateCoordinator CONF_TITLE = "title" @@ -27,7 +27,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] + coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] return async_redact_data( { diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 28a8d0ba28a..8fcd1667852 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -5,8 +5,8 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", - "loggers": ["envoy_reader"], - "requirements": ["envoy-reader==0.20.1"], + "loggers": ["pyenphase"], + "requirements": ["pyenphase==0.8.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index f42c8d94ea2..ae7f1849641 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -5,7 +5,8 @@ from collections.abc import Callable from dataclasses import dataclass import datetime import logging -from typing import cast + +from pyenphase import EnvoyInverter, EnvoySystemConsumption, EnvoySystemProduction from homeassistant.components.sensor import ( SensorDeviceClass, @@ -16,16 +17,15 @@ 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.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import UNDEFINED from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, - DataUpdateCoordinator, ) from homeassistant.util import dt as dt_util -from .const import COORDINATOR, DOMAIN, NAME +from .const import DOMAIN +from .coordinator import EnphaseUpdateCoordinator ICON = "mdi:flash" _LOGGER = logging.getLogger(__name__) @@ -35,100 +35,147 @@ LAST_REPORTED_KEY = "last_reported" @dataclass -class EnvoyRequiredKeysMixin: +class EnvoyInverterRequiredKeysMixin: """Mixin for required keys.""" - value_fn: Callable[[tuple[float, str]], datetime.datetime | float | None] + value_fn: Callable[[EnvoyInverter], datetime.datetime | float] @dataclass -class EnvoySensorEntityDescription(SensorEntityDescription, EnvoyRequiredKeysMixin): +class EnvoyInverterSensorEntityDescription( + SensorEntityDescription, EnvoyInverterRequiredKeysMixin +): """Describes an Envoy inverter sensor entity.""" -def _inverter_last_report_time( - watt_report_time: tuple[float, str] -) -> datetime.datetime | None: - if (report_time := watt_report_time[1]) is None: - return None - if (last_reported_dt := dt_util.parse_datetime(report_time)) is None: - return None - if last_reported_dt.tzinfo is None: - return last_reported_dt.replace(tzinfo=dt_util.UTC) - return last_reported_dt - - INVERTER_SENSORS = ( - EnvoySensorEntityDescription( + EnvoyInverterSensorEntityDescription( key=INVERTERS_KEY, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, - value_fn=lambda watt_report_time: watt_report_time[0], + value_fn=lambda inverter: inverter.last_report_watts, ), - EnvoySensorEntityDescription( + EnvoyInverterSensorEntityDescription( key=LAST_REPORTED_KEY, name="Last Reported", device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, - value_fn=_inverter_last_report_time, + value_fn=lambda inverter: dt_util.utc_from_timestamp(inverter.last_report_date), ), ) -SENSORS = ( - SensorEntityDescription( + +@dataclass +class EnvoyProductionRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoySystemProduction], int] + + +@dataclass +class EnvoyProductionSensorEntityDescription( + SensorEntityDescription, EnvoyProductionRequiredKeysMixin +): + """Describes an Envoy production sensor entity.""" + + +PRODUCTION_SENSORS = ( + EnvoyProductionSensorEntityDescription( key="production", name="Current Power Production", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=3, + value_fn=lambda production: production.watts_now, ), - SensorEntityDescription( + EnvoyProductionSensorEntityDescription( key="daily_production", name="Today's Energy Production", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + value_fn=lambda production: production.watt_hours_today, ), - SensorEntityDescription( + EnvoyProductionSensorEntityDescription( key="seven_days_production", name="Last Seven Days Energy Production", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=1, + value_fn=lambda production: production.watt_hours_last_7_days, ), - SensorEntityDescription( + EnvoyProductionSensorEntityDescription( key="lifetime_production", name="Lifetime Energy Production", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, + suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, + suggested_display_precision=3, + value_fn=lambda production: production.watt_hours_lifetime, ), - SensorEntityDescription( +) + + +@dataclass +class EnvoyConsumptionRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoySystemConsumption], int] + + +@dataclass +class EnvoyConsumptionSensorEntityDescription( + SensorEntityDescription, EnvoyConsumptionRequiredKeysMixin +): + """Describes an Envoy consumption sensor entity.""" + + +CONSUMPTION_SENSORS = ( + EnvoyConsumptionSensorEntityDescription( key="consumption", name="Current Power Consumption", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=3, + value_fn=lambda consumption: consumption.watts_now, ), - SensorEntityDescription( + EnvoyConsumptionSensorEntityDescription( key="daily_consumption", name="Today's Energy Consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + value_fn=lambda consumption: consumption.watt_hours_today, ), - SensorEntityDescription( + EnvoyConsumptionSensorEntityDescription( key="seven_days_consumption", name="Last Seven Days Energy Consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=1, + value_fn=lambda consumption: consumption.watt_hours_last_7_days, ), - SensorEntityDescription( + EnvoyConsumptionSensorEntityDescription( key="lifetime_consumption", name="Lifetime Energy Consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, + suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, + suggested_display_precision=3, + value_fn=lambda consumption: consumption.watt_hours_lifetime, ), ) @@ -139,58 +186,47 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up envoy sensor platform.""" - data: dict = hass.data[DOMAIN][config_entry.entry_id] - coordinator: DataUpdateCoordinator = data[COORDINATOR] - envoy_data: dict = coordinator.data - envoy_name: str = data[NAME] + 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 _LOGGER.debug("Envoy data: %s", envoy_data) - entities: list[Envoy | EnvoyInverter] = [] - for description in SENSORS: - sensor_data = envoy_data.get(description.key) - if isinstance(sensor_data, str) and "not available" in sensor_data: - continue - entities.append( - Envoy( - coordinator, - description, - envoy_name, - envoy_serial_num, - ) - ) - - if production := envoy_data.get("inverters_production"): + entities: list[Entity] = [ + EnvoyProductionEntity(coordinator, description) + for description in PRODUCTION_SENSORS + ] + if envoy_data.system_consumption: entities.extend( - EnvoyInverter( - coordinator, - description, - envoy_name, - envoy_serial_num, - str(inverter), - ) + EnvoyConsumptionEntity(coordinator, description) + for description in CONSUMPTION_SENSORS + ) + if envoy_data.inverters: + entities.extend( + EnvoyInverterEntity(coordinator, description, inverter) for description in INVERTER_SENSORS - for inverter in production + for inverter in envoy_data.inverters ) async_add_entities(entities) -class Envoy(CoordinatorEntity, SensorEntity): +class EnvoyEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): """Envoy inverter entity.""" _attr_icon = ICON def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: EnphaseUpdateCoordinator, description: SensorEntityDescription, - envoy_name: str, - envoy_serial_num: str, ) -> 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_name = f"{envoy_name} {description.name}" self._attr_unique_id = f"{envoy_serial_num}_{description.key}" self._attr_device_info = DeviceInfo( @@ -198,44 +234,71 @@ class Envoy(CoordinatorEntity, SensorEntity): manufacturer="Enphase", model="Envoy", name=envoy_name, + sw_version=str(coordinator.envoy.firmware), ) super().__init__(coordinator) + +class EnvoyProductionEntity(EnvoyEntity): + """Envoy production entity.""" + + entity_description: EnvoyProductionSensorEntityDescription + @property - def native_value(self) -> float | None: + def native_value(self) -> int | None: """Return the state of the sensor.""" - if (value := self.coordinator.data.get(self.entity_description.key)) is None: - return None - return cast(float, value) + 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) -class EnvoyInverter(CoordinatorEntity, SensorEntity): +class EnvoyConsumptionEntity(EnvoyEntity): + """Envoy consumption entity.""" + + entity_description: EnvoyConsumptionSensorEntityDescription + + @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) + + +class EnvoyInverterEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): """Envoy inverter entity.""" _attr_icon = ICON - entity_description: EnvoySensorEntityDescription + entity_description: EnvoyInverterSensorEntityDescription def __init__( self, - coordinator: DataUpdateCoordinator, - description: EnvoySensorEntityDescription, - envoy_name: str, - envoy_serial_num: str, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyInverterSensorEntityDescription, serial_number: str, ) -> None: """Initialize Envoy inverter entity.""" self.entity_description = description + envoy_name = coordinator.name self._serial_number = serial_number - if description.name is not UNDEFINED: - self._attr_name = ( - f"{envoy_name} Inverter {serial_number} {description.name}" - ) - else: + name = description.name + 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 name or unique_id. self._attr_name = f"{envoy_name} Inverter {serial_number}" - if description.key == INVERTERS_KEY: self._attr_unique_id = serial_number else: - self._attr_unique_id = f"{serial_number}_{description.key}" + # Additional sensors have a name and unique_id that includes the + # sensor key. + self._attr_name = f"{envoy_name} Inverter {serial_number} {name}" + 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}", @@ -246,9 +309,10 @@ class EnvoyInverter(CoordinatorEntity, SensorEntity): super().__init__(coordinator) @property - def native_value(self) -> datetime.datetime | float | None: + def native_value(self) -> datetime.datetime | float: """Return the state of the sensor.""" - watt_report_time: tuple[float, str] = self.coordinator.data[ - "inverters_production" - ][self._serial_number] - return self.entity_description.value_fn(watt_report_time) + 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) diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 822ee14fc9e..1614813393c 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -3,7 +3,7 @@ "flow_title": "{serial} ({host})", "step": { "user": { - "description": "For newer models, enter username `envoy` without a password. For older models, enter username `installer` without a password. For all other models, enter a valid username and password.", + "description": "For firmware version 7.0 and later, enter the Enphase cloud credentials, for older models models, enter username `installer` without a password.", "data": { "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", @@ -12,8 +12,8 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "Cannot connect: {reason}", + "invalid_auth": "Invalid authentication: {reason}", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/requirements_all.txt b/requirements_all.txt index bcfa77e219d..b27484b0f01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,9 +738,6 @@ enturclient==0.2.4 # homeassistant.components.environment_canada env-canada==0.5.36 -# homeassistant.components.enphase_envoy -envoy-reader==0.20.1 - # homeassistant.components.season ephem==4.1.2 @@ -1664,6 +1661,9 @@ pyedimax==0.2.1 # homeassistant.components.efergy pyefergy==22.1.1 +# homeassistant.components.enphase_envoy +pyenphase==0.8.0 + # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8e922977e9..3902f13c2e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -594,9 +594,6 @@ enocean==0.50 # homeassistant.components.environment_canada env-canada==0.5.36 -# homeassistant.components.enphase_envoy -envoy-reader==0.20.1 - # homeassistant.components.season ephem==4.1.2 @@ -1231,6 +1228,9 @@ pyeconet==0.1.20 # homeassistant.components.efergy pyefergy==22.1.1 +# homeassistant.components.enphase_envoy +pyenphase==0.8.0 + # homeassistant.components.everlights pyeverlights==0.1.0 diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index 93a76bdd510..b5ea878ae42 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -1,7 +1,13 @@ """Define test fixtures for Enphase Envoy.""" -import json from unittest.mock import AsyncMock, Mock, patch +from pyenphase import ( + Envoy, + EnvoyData, + EnvoyInverter, + EnvoySystemConsumption, + EnvoySystemProduction, +) import pytest from homeassistant.components.enphase_envoy import DOMAIN @@ -9,7 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNA from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry @pytest.fixture(name="config_entry") @@ -36,66 +42,49 @@ def config_fixture(): } -@pytest.fixture(name="gateway_data", scope="package") -def gateway_data_fixture(): - """Define a fixture to return gateway data.""" - return json.loads(load_fixture("data.json", "enphase_envoy")) - - -@pytest.fixture(name="inverters_production_data", scope="package") -def inverters_production_data_fixture(): - """Define a fixture to return inverter production data.""" - return json.loads(load_fixture("inverters_production.json", "enphase_envoy")) - - -@pytest.fixture(name="mock_envoy_reader") -def mock_envoy_reader_fixture( - gateway_data, - mock_get_data, - mock_get_full_serial_number, - mock_inverters_production, - serial_number, -): - """Define a mocked EnvoyReader fixture.""" - mock_envoy_reader = Mock( - getData=mock_get_data, - get_full_serial_number=mock_get_full_serial_number, - inverters_production=mock_inverters_production, +@pytest.fixture(name="mock_envoy") +def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup): + """Define a mocked Envoy fixture.""" + mock_envoy = Mock(spec=Envoy) + mock_envoy.serial_number = serial_number + mock_envoy.authenticate = mock_authenticate + mock_envoy.setup = mock_setup + mock_envoy.data = EnvoyData( + system_consumption=EnvoySystemConsumption( + watt_hours_last_7_days=1234, + watt_hours_lifetime=1234, + watt_hours_today=1234, + watts_now=1234, + ), + system_production=EnvoySystemProduction( + watt_hours_last_7_days=1234, + watt_hours_lifetime=1234, + watt_hours_today=1234, + watts_now=1234, + ), + inverters={ + "1": EnvoyInverter( + serial_number="1", + last_report_date=1, + last_report_watts=1, + max_report_watts=1, + ) + }, + raw={"varies_by": "firmware_version"}, ) - - for key, value in gateway_data.items(): - setattr(mock_envoy_reader, key, AsyncMock(return_value=value)) - - return mock_envoy_reader - - -@pytest.fixture(name="mock_get_full_serial_number") -def mock_get_full_serial_number_fixture(serial_number): - """Define a mocked EnvoyReader.get_full_serial_number fixture.""" - return AsyncMock(return_value=serial_number) - - -@pytest.fixture(name="mock_get_data") -def mock_get_data_fixture(): - """Define a mocked EnvoyReader.getData fixture.""" - return AsyncMock() - - -@pytest.fixture(name="mock_inverters_production") -def mock_inverters_production_fixture(inverters_production_data): - """Define a mocked EnvoyReader.inverters_production fixture.""" - return AsyncMock(return_value=inverters_production_data) + mock_envoy.update = AsyncMock(return_value=mock_envoy.data) + return mock_envoy @pytest.fixture(name="setup_enphase_envoy") -async def setup_enphase_envoy_fixture(hass, config, mock_envoy_reader): +async def setup_enphase_envoy_fixture(hass, config, mock_envoy): """Define a fixture to set up Enphase Envoy.""" with patch( - "homeassistant.components.enphase_envoy.config_flow.EnvoyReader", - return_value=mock_envoy_reader, + "homeassistant.components.enphase_envoy.config_flow.Envoy", + return_value=mock_envoy, ), patch( - "homeassistant.components.enphase_envoy.EnvoyReader", - return_value=mock_envoy_reader, + "homeassistant.components.enphase_envoy.Envoy", + return_value=mock_envoy, ), patch( "homeassistant.components.enphase_envoy.PLATFORMS", [] ): @@ -104,6 +93,18 @@ async def setup_enphase_envoy_fixture(hass, config, mock_envoy_reader): yield +@pytest.fixture(name="mock_authenticate") +def mock_authenticate(): + """Define a mocked Envoy.authenticate fixture.""" + return AsyncMock() + + +@pytest.fixture(name="mock_setup") +def mock_setup(): + """Define a mocked Envoy.setup fixture.""" + return AsyncMock() + + @pytest.fixture(name="serial_number") def serial_number_fixture(): """Define a serial number fixture.""" diff --git a/tests/components/enphase_envoy/fixtures/__init__.py b/tests/components/enphase_envoy/fixtures/__init__.py deleted file mode 100644 index b3ef7db17a3..00000000000 --- a/tests/components/enphase_envoy/fixtures/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Define data fixtures for Enphase Envoy.""" diff --git a/tests/components/enphase_envoy/fixtures/data.json b/tests/components/enphase_envoy/fixtures/data.json deleted file mode 100644 index d6868a6dbf7..00000000000 --- a/tests/components/enphase_envoy/fixtures/data.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "production": 1840, - "daily_production": 28223, - "seven_days_production": 174482, - "lifetime_production": 5924391, - "consumption": 1840, - "daily_consumption": 5923857, - "seven_days_consumption": 5923857, - "lifetime_consumption": 5923857 -} diff --git a/tests/components/enphase_envoy/fixtures/inverters_production.json b/tests/components/enphase_envoy/fixtures/inverters_production.json deleted file mode 100644 index 14891f2d278..00000000000 --- a/tests/components/enphase_envoy/fixtures/inverters_production.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "202140024014": [136, "2022-10-08 16:43:36"], - "202140023294": [163, "2022-10-08 16:43:41"], - "202140013819": [130, "2022-10-08 16:43:31"], - "202140023794": [139, "2022-10-08 16:43:38"], - "202140023381": [130, "2022-10-08 16:43:47"], - "202140024176": [54, "2022-10-08 16:43:59"], - "202140003284": [132, "2022-10-08 16:43:55"], - "202140019854": [129, "2022-10-08 16:43:58"], - "202140020743": [131, "2022-10-08 16:43:49"], - "202140023531": [28, "2022-10-08 16:43:53"], - "202140024241": [164, "2022-10-08 16:43:33"], - "202140022963": [164, "2022-10-08 16:43:41"], - "202140023149": [118, "2022-10-08 16:43:47"], - "202140024828": [129, "2022-10-08 16:43:36"], - "202140023269": [133, "2022-10-08 16:43:43"], - "202140024157": [112, "2022-10-08 16:43:52"] -} diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index fac5b01c60e..a4481f4ed51 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Enphase Envoy config flow.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock -import httpx +from pyenphase import EnvoyAuthenticationError, EnvoyError import pytest from homeassistant import config_entries @@ -65,16 +65,7 @@ async def test_user_no_serial_number( } -@pytest.mark.parametrize( - "mock_get_full_serial_number", - [ - AsyncMock( - side_effect=httpx.HTTPStatusError( - "any", request=MagicMock(), response=MagicMock() - ) - ) - ], -) +@pytest.mark.parametrize("serial_number", [None]) async def test_user_fetching_serial_fails( hass: HomeAssistant, setup_enphase_envoy ) -> None: @@ -104,13 +95,9 @@ async def test_user_fetching_serial_fails( @pytest.mark.parametrize( - "mock_get_data", + "mock_authenticate", [ - AsyncMock( - side_effect=httpx.HTTPStatusError( - "any", request=MagicMock(), response=MagicMock() - ) - ) + AsyncMock(side_effect=EnvoyAuthenticationError("test")), ], ) async def test_form_invalid_auth(hass: HomeAssistant, setup_enphase_envoy) -> None: @@ -131,7 +118,8 @@ async def test_form_invalid_auth(hass: HomeAssistant, setup_enphase_envoy) -> No @pytest.mark.parametrize( - "mock_get_data", [AsyncMock(side_effect=httpx.HTTPError("any"))] + "mock_setup", + [AsyncMock(side_effect=EnvoyError)], ) async def test_form_cannot_connect(hass: HomeAssistant, setup_enphase_envoy) -> None: """Test we handle cannot connect error.""" @@ -150,7 +138,10 @@ async def test_form_cannot_connect(hass: HomeAssistant, setup_enphase_envoy) -> assert result2["errors"] == {"base": "cannot_connect"} -@pytest.mark.parametrize("mock_get_data", [AsyncMock(side_effect=ValueError)]) +@pytest.mark.parametrize( + "mock_setup", + [AsyncMock(side_effect=ValueError)], +) async def test_form_unknown_error(hass: HomeAssistant, setup_enphase_envoy) -> None: """Test we handle unknown error.""" result = await hass.config_entries.flow.async_init( @@ -168,7 +159,17 @@ async def test_form_unknown_error(hass: HomeAssistant, setup_enphase_envoy) -> N assert result2["errors"] == {"base": "unknown"} -async def test_zeroconf(hass: HomeAssistant, setup_enphase_envoy) -> None: +def _get_schema_default(schema, key_name): + """Iterate schema to find a key.""" + for schema_key in schema: + if schema_key == key_name: + return schema_key.default() + raise KeyError(f"{key_name} not found in schema") + + +async def test_zeroconf_pre_token_firmware( + hass: HomeAssistant, setup_enphase_envoy +) -> None: """Test we can setup from zeroconf.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -179,13 +180,55 @@ async def test_zeroconf(hass: HomeAssistant, setup_enphase_envoy) -> None: hostname="mock_hostname", name="mock_name", port=None, - properties={"serialnum": "1234"}, + properties={"serialnum": "1234", "protovers": "3.0.0"}, type="mock_type", ), ) assert result["type"] == "form" assert result["step_id"] == "user" + assert _get_schema_default(result["data_schema"].schema, "username") == "installer" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + assert result2["type"] == "create_entry" + assert result2["title"] == "Envoy 1234" + assert result2["result"].unique_id == "1234" + assert result2["data"] == { + "host": "1.1.1.1", + "name": "Envoy 1234", + "username": "test-username", + "password": "test-password", + } + + +async def test_zeroconf_token_firmware( + hass: HomeAssistant, setup_enphase_envoy +) -> None: + """Test we can setup from zeroconf.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="1.1.1.1", + addresses=["1.1.1.1"], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234", "protovers": "7.0.0"}, + type="mock_type", + ), + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert _get_schema_default(result["data_schema"].schema, "username") == "" + result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -311,7 +354,6 @@ async def test_reauth(hass: HomeAssistant, config_entry, setup_enphase_envoy) -> result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "host": "1.1.1.1", "username": "test-username", "password": "test-password", }, diff --git a/tests/components/enphase_envoy/test_diagnostics.py b/tests/components/enphase_envoy/test_diagnostics.py index 5fd69d7bfb9..aa5f08567ae 100644 --- a/tests/components/enphase_envoy/test_diagnostics.py +++ b/tests/components/enphase_envoy/test_diagnostics.py @@ -32,32 +32,5 @@ async def test_entry_diagnostics( "unique_id": REDACTED, "disabled_by": None, }, - "data": { - "production": 1840, - "daily_production": 28223, - "seven_days_production": 174482, - "lifetime_production": 5924391, - "consumption": 1840, - "daily_consumption": 5923857, - "seven_days_consumption": 5923857, - "lifetime_consumption": 5923857, - "inverters_production": { - "202140024014": [136, "2022-10-08 16:43:36"], - "202140023294": [163, "2022-10-08 16:43:41"], - "202140013819": [130, "2022-10-08 16:43:31"], - "202140023794": [139, "2022-10-08 16:43:38"], - "202140023381": [130, "2022-10-08 16:43:47"], - "202140024176": [54, "2022-10-08 16:43:59"], - "202140003284": [132, "2022-10-08 16:43:55"], - "202140019854": [129, "2022-10-08 16:43:58"], - "202140020743": [131, "2022-10-08 16:43:49"], - "202140023531": [28, "2022-10-08 16:43:53"], - "202140024241": [164, "2022-10-08 16:43:33"], - "202140022963": [164, "2022-10-08 16:43:41"], - "202140023149": [118, "2022-10-08 16:43:47"], - "202140024828": [129, "2022-10-08 16:43:36"], - "202140023269": [133, "2022-10-08 16:43:43"], - "202140024157": [112, "2022-10-08 16:43:52"], - }, - }, + "data": {"varies_by": "firmware_version"}, } From 3bed32f16e0b9f0575102a8d9c0abc450ec8dc7f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 6 Aug 2023 02:32:35 +0200 Subject: [PATCH 0235/1151] Add entity translations for Enphase Envoy (#97876) Co-authored-by: J. Nick Koston --- .../components/enphase_envoy/sensor.py | 30 +++++++++--------- .../components/enphase_envoy/strings.json | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index ae7f1849641..7d89ba94499 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -51,6 +51,7 @@ class EnvoyInverterSensorEntityDescription( INVERTER_SENSORS = ( EnvoyInverterSensorEntityDescription( key=INVERTERS_KEY, + name=None, native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, @@ -58,7 +59,7 @@ INVERTER_SENSORS = ( ), EnvoyInverterSensorEntityDescription( key=LAST_REPORTED_KEY, - name="Last Reported", + translation_key=LAST_REPORTED_KEY, device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, value_fn=lambda inverter: dt_util.utc_from_timestamp(inverter.last_report_date), @@ -83,7 +84,7 @@ class EnvoyProductionSensorEntityDescription( PRODUCTION_SENSORS = ( EnvoyProductionSensorEntityDescription( key="production", - name="Current Power Production", + translation_key="current_power_production", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, @@ -93,7 +94,7 @@ PRODUCTION_SENSORS = ( ), EnvoyProductionSensorEntityDescription( key="daily_production", - name="Today's Energy Production", + translation_key="daily_production", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, @@ -103,7 +104,7 @@ PRODUCTION_SENSORS = ( ), EnvoyProductionSensorEntityDescription( key="seven_days_production", - name="Last Seven Days Energy Production", + translation_key="seven_days_production", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -112,7 +113,7 @@ PRODUCTION_SENSORS = ( ), EnvoyProductionSensorEntityDescription( key="lifetime_production", - name="Lifetime Energy Production", + translation_key="lifetime_production", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, @@ -140,7 +141,7 @@ class EnvoyConsumptionSensorEntityDescription( CONSUMPTION_SENSORS = ( EnvoyConsumptionSensorEntityDescription( key="consumption", - name="Current Power Consumption", + translation_key="current_power_consumption", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, @@ -150,7 +151,7 @@ CONSUMPTION_SENSORS = ( ), EnvoyConsumptionSensorEntityDescription( key="daily_consumption", - name="Today's Energy Consumption", + translation_key="daily_consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, @@ -160,7 +161,7 @@ CONSUMPTION_SENSORS = ( ), EnvoyConsumptionSensorEntityDescription( key="seven_days_consumption", - name="Last Seven Days Energy Consumption", + translation_key="seven_days_consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -169,7 +170,7 @@ CONSUMPTION_SENSORS = ( ), EnvoyConsumptionSensorEntityDescription( key="lifetime_consumption", - name="Lifetime Energy Consumption", + translation_key="lifetime_consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, @@ -216,6 +217,7 @@ class EnvoyEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): """Envoy inverter entity.""" _attr_icon = ICON + _attr_has_entity_name = True def __init__( self, @@ -227,7 +229,6 @@ class EnvoyEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): envoy_name = coordinator.name envoy_serial_num = coordinator.envoy.serial_number assert envoy_serial_num is not None - self._attr_name = f"{envoy_name} {description.name}" self._attr_unique_id = f"{envoy_serial_num}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, envoy_serial_num)}, @@ -271,6 +272,7 @@ class EnvoyInverterEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEnt """Envoy inverter entity.""" _attr_icon = ICON + _attr_has_entity_name = True entity_description: EnvoyInverterSensorEntityDescription def __init__( @@ -281,20 +283,16 @@ class EnvoyInverterEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEnt ) -> None: """Initialize Envoy inverter entity.""" self.entity_description = description - envoy_name = coordinator.name self._serial_number = serial_number - name = description.name 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 name or unique_id. - self._attr_name = f"{envoy_name} Inverter {serial_number}" + # break existing installations by changing the unique_id. self._attr_unique_id = serial_number else: - # Additional sensors have a name and unique_id that includes the + # Additional sensors have a unique_id that includes the # sensor key. - self._attr_name = f"{envoy_name} Inverter {serial_number} {name}" self._attr_unique_id = f"{serial_number}_{key}" envoy_serial_num = coordinator.envoy.serial_number diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 1614813393c..d503dacb2d8 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -20,5 +20,36 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "sensor": { + "last_reported": { + "name": "Last reported" + }, + "current_power_production": { + "name": "Current power production" + }, + "daily_production": { + "name": "Energy production today" + }, + "seven_days_production": { + "name": "Energy production last seven days" + }, + "lifetime_production": { + "name": "Lifetime energy production" + }, + "current_power_consumption": { + "name": "Current power consumption" + }, + "daily_consumption": { + "name": "Energy consumption today" + }, + "seven_days_consumption": { + "name": "Energy consumption last seven days" + }, + "lifetime_consumption": { + "name": "Lifetime energy consumption" + } + } } } From 6a65a9771513a106ee52ba9e31773fcb2d38b67d Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sat, 5 Aug 2023 20:33:01 -0400 Subject: [PATCH 0236/1151] Bump pyschlage to 2023.8.0 (#97875) --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index e8b1443358d..9a84a3446c7 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2023.7.0"] + "requirements": ["pyschlage==2023.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index b27484b0f01..1be1e301ba5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1982,7 +1982,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2023.7.0 +pyschlage==2023.8.0 # homeassistant.components.sensibo pysensibo==1.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3902f13c2e2..9c109faa990 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1474,7 +1474,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2023.7.0 +pyschlage==2023.8.0 # homeassistant.components.sensibo pysensibo==1.0.33 From 00e78fbf192c8c9b197cbdfa5638160935eff385 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Aug 2023 14:51:19 -1000 Subject: [PATCH 0237/1151] Cache envoy auth tokens to ensure integration works if cloud is offline (#97872) --- .../components/enphase_envoy/__init__.py | 12 ++--- .../components/enphase_envoy/config_flow.py | 6 +-- .../components/enphase_envoy/const.py | 9 ++++ .../components/enphase_envoy/coordinator.py | 52 +++++++++++++------ .../components/enphase_envoy/diagnostics.py | 3 +- .../components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/enphase_envoy/conftest.py | 10 +++- .../enphase_envoy/test_diagnostics.py | 1 + 10 files changed, 65 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index daa6a2f4492..7f06a032128 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from pyenphase import Envoy from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.httpx_client import get_async_client @@ -16,15 +16,9 @@ from .coordinator import EnphaseUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Enphase Envoy from a config entry.""" - config = entry.data - name = config[CONF_NAME] - host = config[CONF_HOST] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - + host = entry.data[CONF_HOST] envoy = Envoy(host, get_async_client(hass, verify_ssl=False)) - - coordinator = EnphaseUpdateCoordinator(hass, envoy, name, username, password) + coordinator = EnphaseUpdateCoordinator(hass, envoy, entry) await coordinator.async_config_entry_first_refresh() if not entry.unique_id: diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 93eaa9514e9..3ec39739ed7 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -9,8 +9,6 @@ from awesomeversion import AwesomeVersion from pyenphase import ( AUTH_TOKEN_MIN_VERSION, Envoy, - EnvoyAuthenticationError, - EnvoyAuthenticationRequired, EnvoyError, ) import voluptuous as vol @@ -23,7 +21,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client from homeassistant.util.network import is_ipv4_address -from .const import DOMAIN +from .const import DOMAIN, INVALID_AUTH_ERRORS _LOGGER = logging.getLogger(__name__) @@ -31,8 +29,6 @@ ENVOY = "Envoy" CONF_SERIAL = "serial" -INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired) - INSTALLER_AUTH_USERNAME = "installer" diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 63af27f3ee2..ed829817bf8 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -1,6 +1,15 @@ """The enphase_envoy component.""" +from pyenphase import ( + EnvoyAuthenticationError, + EnvoyAuthenticationRequired, +) + from homeassistant.const import Platform 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 0ba89ee8087..85a7dc4c2f8 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -7,15 +7,18 @@ from typing import Any from pyenphase import ( Envoy, - EnvoyAuthenticationError, - EnvoyAuthenticationRequired, EnvoyError, + EnvoyTokenAuth, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .const import CONF_TOKEN, INVALID_AUTH_ERRORS + SCAN_INTERVAL = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) @@ -25,24 +28,18 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): envoy_serial_number: str - def __init__( - self, - hass: HomeAssistant, - envoy: Envoy, - name: str, - username: str, - password: str, - ) -> None: + def __init__(self, hass: HomeAssistant, envoy: Envoy, entry: ConfigEntry) -> None: """Initialize DataUpdateCoordinator for the envoy.""" self.envoy = envoy - self.username = username - self.password = password - self.name = name + entry_data = entry.data + self.entry = entry + self.username = entry_data[CONF_USERNAME] + self.password = entry_data[CONF_PASSWORD] self._setup_complete = False super().__init__( hass, _LOGGER, - name=name, + name=entry_data[CONF_NAME], update_interval=SCAN_INTERVAL, always_update=False, ) @@ -53,7 +50,32 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): await envoy.setup() assert envoy.serial_number is not None self.envoy_serial_number = envoy.serial_number + + if token := self.entry.data.get(CONF_TOKEN): + try: + await envoy.authenticate(token=token) + except INVALID_AUTH_ERRORS: + # token likely expired or firmware changed + # so we fall through to authenticate with username/password + pass + else: + self._setup_complete = True + return + await envoy.authenticate(username=self.username, password=self.password) + assert envoy.auth is not None + + if isinstance(envoy.auth, EnvoyTokenAuth): + # update token in config entry so we can + # startup without hitting the Cloud API + # as long as the token is valid + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_TOKEN: envoy.auth.token, + }, + ) self._setup_complete = True async def _async_update_data(self) -> dict[str, Any]: @@ -64,7 +86,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): if not self._setup_complete: await self._async_setup_and_authenticate() return (await envoy.update()).raw - except (EnvoyAuthenticationError, EnvoyAuthenticationRequired) as err: + except INVALID_AUTH_ERRORS as err: if self._setup_complete and tries == 0: # token likely expired or firmware changed, try to re-authenticate self._setup_complete = False diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index 792f681bb53..a6ce86c4857 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import CONF_TOKEN, DOMAIN from .coordinator import EnphaseUpdateCoordinator CONF_TITLE = "title" @@ -20,6 +20,7 @@ TO_REDACT = { CONF_TITLE, CONF_UNIQUE_ID, CONF_USERNAME, + CONF_TOKEN, } diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 8fcd1667852..40e58348768 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.8.0"], + "requirements": ["pyenphase==0.9.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 1be1e301ba5..8f8ed2852ff 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.8.0 +pyenphase==0.9.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c109faa990..2fdafad096c 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.8.0 +pyenphase==0.9.0 # homeassistant.components.everlights pyeverlights==0.1.0 diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index b5ea878ae42..355c247b182 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -7,6 +7,7 @@ from pyenphase import ( EnvoyInverter, EnvoySystemConsumption, EnvoySystemProduction, + EnvoyTokenAuth, ) import pytest @@ -43,12 +44,13 @@ def config_fixture(): @pytest.fixture(name="mock_envoy") -def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup): +def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth): """Define a mocked Envoy fixture.""" mock_envoy = Mock(spec=Envoy) mock_envoy.serial_number = serial_number mock_envoy.authenticate = mock_authenticate mock_envoy.setup = mock_setup + mock_envoy.auth = mock_auth mock_envoy.data = EnvoyData( system_consumption=EnvoySystemConsumption( watt_hours_last_7_days=1234, @@ -99,6 +101,12 @@ def mock_authenticate(): return AsyncMock() +@pytest.fixture(name="mock_auth") +def mock_auth(serial_number): + """Define a mocked EnvoyAuth fixture.""" + return EnvoyTokenAuth("127.0.0.1", token="abc", envoy_serial=serial_number) + + @pytest.fixture(name="mock_setup") def mock_setup(): """Define a mocked Envoy.setup fixture.""" diff --git a/tests/components/enphase_envoy/test_diagnostics.py b/tests/components/enphase_envoy/test_diagnostics.py index aa5f08567ae..fb1a54dc522 100644 --- a/tests/components/enphase_envoy/test_diagnostics.py +++ b/tests/components/enphase_envoy/test_diagnostics.py @@ -24,6 +24,7 @@ async def test_entry_diagnostics( "name": REDACTED, "username": REDACTED, "password": REDACTED, + "token": REDACTED, }, "options": {}, "pref_disable_new_entities": False, From 91b308b4ad1b9e462ab69b257b8ca343917544c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Aug 2023 19:14:18 -1000 Subject: [PATCH 0238/1151] Fix handling HomeKit events when the char is in error state (#97884) --- .../components/homekit_controller/device_trigger.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 4f44cea34f4..9eab0fbb098 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -262,14 +262,13 @@ def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], dict[str, if not trigger_sources: return for (aid, iid), ev in events.items(): - # If the value is None, we received the event via polling - # and we don't want to trigger on that - if ev["value"] is None: - continue if aid in conn.devices: device_id = conn.devices[aid] if source := trigger_sources.get(device_id): - source.fire(iid, ev) + # If the value is None, we received the event via polling + # and we don't want to trigger on that + if ev.get("value") is not None: + source.fire(iid, ev) async def async_get_triggers( From 0511071757b81616dddfca4b4de5692e358e56ba Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sun, 6 Aug 2023 04:26:16 -0400 Subject: [PATCH 0239/1151] Schlage: Set the battery sensor state_class to measurement (#97879) Set state_class to measurement --- homeassistant/components/schlage/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/schlage/sensor.py b/homeassistant/components/schlage/sensor.py index aa2ff87e5bf..2cf1694e111 100644 --- a/homeassistant/components/schlage/sensor.py +++ b/homeassistant/components/schlage/sensor.py @@ -6,6 +6,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory @@ -22,6 +23,7 @@ _SENSOR_DESCRIPTIONS: list[SensorEntityDescription] = [ device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, ), ] From c4a5373976e9df201801a6da7ce76274225f0113 Mon Sep 17 00:00:00 2001 From: Johannes Wagner <7691102+joanwa@users.noreply.github.com> Date: Sun, 6 Aug 2023 13:47:54 +0200 Subject: [PATCH 0240/1151] Handle explicit Modbus NaN values (#90800) Co-authored-by: jan iversen --- homeassistant/components/modbus/__init__.py | 3 +++ .../components/modbus/base_platform.py | 13 +++++++++++-- homeassistant/components/modbus/const.py | 1 + homeassistant/components/modbus/validators.py | 14 ++++++++++++++ tests/components/modbus/test_init.py | 18 ++++++++++++++++++ tests/components/modbus/test_sensor.py | 10 ++++++++++ 6 files changed, 57 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index d9e81b74ce9..0108c37a10b 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -82,6 +82,7 @@ from .const import ( # noqa: F401 CONF_MIN_TEMP, CONF_MIN_VALUE, CONF_MSG_WAIT, + CONF_NAN_VALUE, CONF_PARITY, CONF_PRECISION, CONF_RETRIES, @@ -123,6 +124,7 @@ from .modbus import ModbusHub, async_modbus_setup from .validators import ( duplicate_entity_validator, duplicate_modbus_validator, + nan_validator, number_validator, scan_interval_validator, struct_validator, @@ -298,6 +300,7 @@ SENSOR_SCHEMA = vol.All( vol.Optional(CONF_SLAVE_COUNT, default=0): cv.positive_int, vol.Optional(CONF_MIN_VALUE): number_validator, vol.Optional(CONF_MAX_VALUE): number_validator, + vol.Optional(CONF_NAN_VALUE): nan_validator, vol.Optional(CONF_ZERO_SUPPRESS): number_validator, } ), diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index c936773bea7..343d5a36b26 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_UNIQUE_ID, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -46,6 +47,7 @@ from .const import ( CONF_LAZY_ERROR, CONF_MAX_VALUE, CONF_MIN_VALUE, + CONF_NAN_VALUE, CONF_PRECISION, CONF_SCALE, CONF_STATE_OFF, @@ -101,6 +103,7 @@ class BasePlatform(Entity): self._min_value = get_optional_numeric_config(CONF_MIN_VALUE) self._max_value = get_optional_numeric_config(CONF_MAX_VALUE) + self._nan_value = entry.get(CONF_NAN_VALUE, None) self._zero_suppress = get_optional_numeric_config(CONF_ZERO_SUPPRESS) @abstractmethod @@ -173,8 +176,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): registers.reverse() return registers - def __process_raw_value(self, entry: float | int) -> float | int: - """Process value from sensor with scaling, offset, min/max etc.""" + def __process_raw_value(self, entry: float | int | str) -> float | int | str: + """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" + if self._nan_value and entry in (self._nan_value, -self._nan_value): + return STATE_UNAVAILABLE val: float | int = self._scale * entry + self._offset if self._min_value is not None and val < self._min_value: return self._min_value @@ -225,6 +230,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): # the conversion only when it's absolutely necessary. if isinstance(val_result, int) and self._precision == 0: return str(val_result) + if isinstance(val_result, str): + if val_result == "nan": + val_result = STATE_UNAVAILABLE # pragma: no cover + return val_result return f"{float(val_result):.{self._precision}f}" diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 264268f323e..3b565e91f92 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -30,6 +30,7 @@ CONF_MAX_VALUE = "max_value" CONF_MIN_TEMP = "min_temp" 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" diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index a583b93ea80..ee9d40dd874 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -139,6 +139,20 @@ def number_validator(value: Any) -> int | float: raise vol.Invalid(f"invalid number {value}") from err +def nan_validator(value: Any) -> int: + """Convert nan string to number (can be hex string or int).""" + if isinstance(value, int): + return value + try: + return int(value) + except (TypeError, ValueError): + pass + try: + return int(value, 16) + except (TypeError, ValueError) as err: + raise vol.Invalid(f"invalid number {value}") from err + + def scan_interval_validator(config: dict) -> dict: """Control scan_interval.""" for hub in config: diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 2daf722bb05..d9d3b035c94 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -64,6 +64,7 @@ from homeassistant.components.modbus.const import ( from homeassistant.components.modbus.validators import ( duplicate_entity_validator, duplicate_modbus_validator, + nan_validator, number_validator, struct_validator, ) @@ -141,6 +142,23 @@ async def test_number_validator() -> None: pytest.fail("Number_validator not throwing exception") +async def test_nan_validator() -> None: + """Test number validator.""" + + for value, value_type in ( + (15, int), + ("15", int), + ("abcdef", int), + ("0xabcdef", int), + ): + assert isinstance(nan_validator(value), value_type) + + with pytest.raises(vol.Invalid): + nan_validator("x15") + with pytest.raises(vol.Invalid): + nan_validator("not a hex string") + + @pytest.mark.parametrize( "do_config", [ diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index be9ea95d86a..48a081ef637 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.modbus.const import ( CONF_LAZY_ERROR, CONF_MAX_VALUE, CONF_MIN_VALUE, + CONF_NAN_VALUE, CONF_PRECISION, CONF_SCALE, CONF_SLAVE_COUNT, @@ -558,6 +559,15 @@ async def test_config_wrong_struct_sensor( False, str(int(0x02010404)), ), + ( + { + CONF_DATA_TYPE: DataType.INT32, + CONF_NAN_VALUE: "0x80000000", + }, + [0x8000, 0x0000], + False, + STATE_UNAVAILABLE, + ), ( { CONF_DATA_TYPE: DataType.INT32, From 56dd88db1742d7345e350bec608a7a1d33d98a5d Mon Sep 17 00:00:00 2001 From: String-656 Date: Sun, 6 Aug 2023 20:26:56 +0800 Subject: [PATCH 0241/1151] Replace Float 'nan' with None for modbus floats (#93832) Co-authored-by: jan iversen --- homeassistant/components/modbus/base_platform.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 343d5a36b26..e4c657a6c54 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -218,6 +218,9 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): # the conversion only when it's absolutely necessary. if isinstance(v_temp, int) and self._precision == 0: v_result.append(str(v_temp)) + elif v_temp != v_temp: # noqa: PLR0124 + # NaN float detection replace with None + v_result.append("nan") # pragma: no cover else: v_result.append(f"{float(v_temp):.{self._precision}f}") return ",".join(map(str, v_result)) @@ -228,6 +231,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): # We could convert int to float, and the code would still work; however # we lose some precision, and unit tests will fail. Therefore, we do # the conversion only when it's absolutely necessary. + + # NaN float detection replace with None + if val_result != val_result: # noqa: PLR0124 + return None # pragma: no cover if isinstance(val_result, int) and self._precision == 0: return str(val_result) if isinstance(val_result, str): From 74d1c30f7e76ff0e6ac51a061281e6847373f995 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 6 Aug 2023 18:08:07 +0200 Subject: [PATCH 0242/1151] Trafikverket Train sensor and attributes to new sensors (#71432) * Initial commit * Finalize tvt sensors * Translations * Fix sensor translation * migration * migration fix 2 * Fix name * Fix translation * remove translation * Fixes * Fix isort and mypy * translations * logging * Remove logging * departure time * Review comments * Mod update entity unique id * Fix uom * not async * UnitOfTime * cleanup from rebase * Remove call self * cleanup extra attributes * Fix rebase again * string state --- .../components/trafikverket_train/sensor.py | 137 ++++++++++-------- .../trafikverket_train/strings.json | 60 ++++---- 2 files changed, 102 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 47f31e35c63..58cf62dd505 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -3,9 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from datetime import datetime, time, timedelta - -from pytrafikverket.trafikverket_train import StationInfo +from datetime import datetime from homeassistant.components.sensor import ( SensorDeviceClass, @@ -13,37 +11,23 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_WEEKDAY +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.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import dt as dt_util -from .const import CONF_TIME, DOMAIN +from .const import ATTRIBUTION, DOMAIN from .coordinator import TrainData, TVDataUpdateCoordinator -ATTR_DEPARTURE_STATE = "departure_state" -ATTR_CANCELED = "canceled" -ATTR_DELAY_TIME = "number_of_minutes_delayed" -ATTR_PLANNED_TIME = "planned_time" -ATTR_ESTIMATED_TIME = "estimated_time" -ATTR_ACTUAL_TIME = "actual_time" -ATTR_OTHER_INFORMATION = "other_information" -ATTR_DEVIATIONS = "deviations" - -ICON = "mdi:train" -SCAN_INTERVAL = timedelta(minutes=5) - @dataclass class TrafikverketRequiredKeysMixin: """Mixin for required keys.""" value_fn: Callable[[TrainData], StateType | datetime] - extra_fn: Callable[[TrainData], dict[str, StateType | datetime]] @dataclass @@ -60,16 +44,64 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.departure_time, - extra_fn=lambda data: { - ATTR_DEPARTURE_STATE: data.departure_state, - ATTR_CANCELED: data.cancelled, - ATTR_DELAY_TIME: data.delayed_time, - ATTR_PLANNED_TIME: data.planned_time, - ATTR_ESTIMATED_TIME: data.estimated_time, - ATTR_ACTUAL_TIME: data.actual_time, - ATTR_OTHER_INFORMATION: data.other_info, - ATTR_DEVIATIONS: data.deviation, - }, + ), + TrafikverketSensorEntityDescription( + key="departure_state", + translation_key="departure_state", + icon="mdi:clock", + value_fn=lambda data: data.departure_time, + device_class=SensorDeviceClass.ENUM, + options=["on_time", "delayed", "canceled"], + ), + TrafikverketSensorEntityDescription( + key="cancelled", + translation_key="cancelled", + icon="mdi:alert", + value_fn=lambda data: data.cancelled, + ), + TrafikverketSensorEntityDescription( + key="delayed_time", + translation_key="delayed_time", + icon="mdi:clock", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + value_fn=lambda data: data.delayed_time, + ), + TrafikverketSensorEntityDescription( + key="planned_time", + translation_key="planned_time", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: data.planned_time, + entity_registry_enabled_default=False, + ), + TrafikverketSensorEntityDescription( + key="estimated_time", + translation_key="estimated_time", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: data.estimated_time, + entity_registry_enabled_default=False, + ), + TrafikverketSensorEntityDescription( + key="actual_time", + translation_key="actual_time", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: data.actual_time, + entity_registry_enabled_default=False, + ), + TrafikverketSensorEntityDescription( + key="other_info", + translation_key="other_info", + icon="mdi:information-variant", + value_fn=lambda data: data.other_info, + ), + TrafikverketSensorEntityDescription( + key="deviation", + translation_key="deviation", + icon="mdi:alert", + value_fn=lambda data: data.deviation, ), ) @@ -81,71 +113,48 @@ async def async_setup_entry( coordinator: TVDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - to_station = coordinator.to_station - from_station = coordinator.from_station - get_time: str | None = entry.data.get(CONF_TIME) - train_time = dt_util.parse_time(get_time) if get_time else None - async_add_entities( [ - TrainSensor( - coordinator, - entry.data[CONF_NAME], - from_station, - to_station, - entry.data[CONF_WEEKDAY], - train_time, - entry.entry_id, - description, - ) + TrainSensor(coordinator, entry.data[CONF_NAME], entry.entry_id, description) for description in SENSOR_TYPES - ], - True, + ] ) class TrainSensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): """Contains data about a train depature.""" - _attr_has_entity_name = True entity_description: TrafikverketSensorEntityDescription + _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, coordinator: TVDataUpdateCoordinator, name: str, - from_station: StationInfo, - to_station: StationInfo, - weekday: list, - departuretime: time | None, entry_id: str, entity_description: TrafikverketSensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) + self._attr_unique_id = f"{entry_id}-{entity_description.key}" self.entity_description = entity_description self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, - manufacturer="Trafikverket", - model="v2.0", name=name, configuration_url="https://api.trafikinfo.trafikverket.se/", ) - self._attr_unique_id = f"{entry_id}-{entity_description.key}" self._update_attr() + @callback + def _update_attr(self) -> None: + """Update _attr.""" + self._attr_native_value = self.entity_description.value_fn( + self.coordinator.data + ) + @callback def _handle_coordinator_update(self) -> None: self._update_attr() return super()._handle_coordinator_update() - - @callback - def _update_attr(self) -> None: - """Retrieve latest states.""" - self._attr_native_value = self.entity_description.value_fn( - self.coordinator.data - ) - self._attr_extra_state_attributes = self.entity_description.extra_fn( - self.coordinator.data - ) diff --git a/homeassistant/components/trafikverket_train/strings.json b/homeassistant/components/trafikverket_train/strings.json index 05032027b97..59431107ae2 100644 --- a/homeassistant/components/trafikverket_train/strings.json +++ b/homeassistant/components/trafikverket_train/strings.json @@ -45,38 +45,36 @@ "entity": { "sensor": { "departure_time": { - "name": "Departure time", - "state_attributes": { - "departure_state": { - "name": "Departure state", - "state": { - "on_time": "On time", - "delayed": "Delayed", - "canceled": "Cancelled" - } - }, - "canceled": { - "name": "Cancelled" - }, - "number_of_minutes_delayed": { - "name": "Minutes delayed" - }, - "planned_time": { - "name": "Planned time" - }, - "estimated_time": { - "name": "Estimated time" - }, - "actual_time": { - "name": "Actual time" - }, - "other_information": { - "name": "Other information" - }, - "deviations": { - "name": "Deviations" - } + "name": "Departure time" + }, + "departure_state": { + "name": "Departure state", + "state": { + "on_time": "On time", + "delayed": "Delayed", + "canceled": "Cancelled" } + }, + "cancelled": { + "name": "Cancelled" + }, + "delayed_time": { + "name": "Delayed time" + }, + "planned_time": { + "name": "Planned time" + }, + "estimated_time": { + "name": "Estimated time" + }, + "actual_time": { + "name": "Actual time" + }, + "other_info": { + "name": "Other information" + }, + "deviation": { + "name": "Deviation" } } } From 163bbe2c5d0474770a5af6923b21adcc643b4fb9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 6 Aug 2023 18:50:00 +0200 Subject: [PATCH 0243/1151] Fix Trafikverket Train departure state (#97917) Fix tvt departure state --- homeassistant/components/trafikverket_train/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 58cf62dd505..97d7a6b34fa 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -49,7 +49,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( key="departure_state", translation_key="departure_state", icon="mdi:clock", - value_fn=lambda data: data.departure_time, + value_fn=lambda data: data.departure_state, device_class=SensorDeviceClass.ENUM, options=["on_time", "delayed", "canceled"], ), From 6bc5b8989dc41d34b95c32c90878fd0f877ea361 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 6 Aug 2023 19:07:18 +0200 Subject: [PATCH 0244/1151] Fix Trafivkerket Train coordinator exceptions (#97919) Fix tvt coordinator exceptions --- .../components/trafikverket_train/coordinator.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/trafikverket_train/coordinator.py b/homeassistant/components/trafikverket_train/coordinator.py index fba6eb93dd9..3125fea8e39 100644 --- a/homeassistant/components/trafikverket_train/coordinator.py +++ b/homeassistant/components/trafikverket_train/coordinator.py @@ -6,6 +6,12 @@ from datetime import date, datetime, time, timedelta import logging from pytrafikverket import TrafikverketTrain +from pytrafikverket.exceptions import ( + InvalidAuthentication, + MultipleTrainAnnouncementFound, + NoTrainAnnouncementFound, + UnknownError, +) from pytrafikverket.trafikverket_train import StationInfo, TrainStop from homeassistant.config_entries import ConfigEntry @@ -119,9 +125,13 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[TrainData]): state = await self._train_api.async_get_next_train_stop( self.from_station, self.to_station, when ) - except ValueError as error: - if "Invalid authentication" in error.args[0]: - raise ConfigEntryAuthFailed from error + except InvalidAuthentication as error: + raise ConfigEntryAuthFailed from error + except ( + NoTrainAnnouncementFound, + MultipleTrainAnnouncementFound, + UnknownError, + ) as error: raise UpdateFailed( f"Train departure {when} encountered a problem: {error}" ) from error From 0535578440275d7b7b44734d76a9a180f6f6f95b Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sun, 6 Aug 2023 19:12:19 +0200 Subject: [PATCH 0245/1151] Velbus code cleanup (#97584) * Some cleanup and code improvements for the velbus integration * Comments * More comments * Update homeassistant/components/velbus/entity.py Co-authored-by: G Johansson * More comments --------- Co-authored-by: G Johansson --- .../components/velbus/binary_sensor.py | 7 +++-- homeassistant/components/velbus/button.py | 8 +++--- homeassistant/components/velbus/climate.py | 9 +++---- homeassistant/components/velbus/cover.py | 6 ++++- homeassistant/components/velbus/entity.py | 27 +++++++++++++++++++ homeassistant/components/velbus/light.py | 6 ++++- homeassistant/components/velbus/select.py | 3 ++- homeassistant/components/velbus/switch.py | 4 ++- 8 files changed, 52 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/velbus/binary_sensor.py b/homeassistant/components/velbus/binary_sensor.py index ef0cef938b1..25591cc1cb0 100644 --- a/homeassistant/components/velbus/binary_sensor.py +++ b/homeassistant/components/velbus/binary_sensor.py @@ -18,10 +18,9 @@ async def async_setup_entry( """Set up Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] - entities = [] - for channel in cntrl.get_all("binary_sensor"): - entities.append(VelbusBinarySensor(channel)) - async_add_entities(entities) + async_add_entities( + VelbusBinarySensor(channel) for channel in cntrl.get_all("binary_sensor") + ) class VelbusBinarySensor(VelbusEntity, BinarySensorEntity): diff --git a/homeassistant/components/velbus/button.py b/homeassistant/components/velbus/button.py index d75486bab7a..2a0392c48cb 100644 --- a/homeassistant/components/velbus/button.py +++ b/homeassistant/components/velbus/button.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import VelbusEntity +from .entity import VelbusEntity, api_call async def async_setup_entry( @@ -24,10 +24,7 @@ async def async_setup_entry( """Set up Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] - entities = [] - for channel in cntrl.get_all("button"): - entities.append(VelbusButton(channel)) - async_add_entities(entities) + async_add_entities(VelbusButton(channel) for channel in cntrl.get_all("button")) class VelbusButton(VelbusEntity, ButtonEntity): @@ -37,6 +34,7 @@ class VelbusButton(VelbusEntity, ButtonEntity): _attr_entity_registry_enabled_default = False _attr_entity_category = EntityCategory.CONFIG + @api_call async def async_press(self) -> None: """Handle the button press.""" await self._channel.press() diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index ccdfb3b073b..ecdddd19289 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, PRESET_MODES -from .entity import VelbusEntity +from .entity import VelbusEntity, api_call async def async_setup_entry( @@ -27,10 +27,7 @@ async def async_setup_entry( """Set up Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] - entities = [] - for channel in cntrl.get_all("climate"): - entities.append(VelbusClimate(channel)) - async_add_entities(entities) + async_add_entities(VelbusClimate(channel) for channel in cntrl.get_all("climate")) class VelbusClimate(VelbusEntity, ClimateEntity): @@ -67,6 +64,7 @@ class VelbusClimate(VelbusEntity, ClimateEntity): """Return the current temperature.""" return self._channel.get_state() + @api_call async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: @@ -74,6 +72,7 @@ class VelbusClimate(VelbusEntity, ClimateEntity): await self._channel.set_temp(temp) self.async_write_ha_state() + @api_call async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the new preset mode.""" await self._channel.set_preset(PRESET_MODES[preset_mode]) diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 009c4fadfb9..46881fcdcaf 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import VelbusEntity +from .entity import VelbusEntity, api_call async def async_setup_entry( @@ -81,18 +81,22 @@ class VelbusCover(VelbusEntity, CoverEntity): return 100 - pos return None + @api_call async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._channel.open() + @api_call async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._channel.close() + @api_call async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._channel.stop() + @api_call async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" await self._channel.set_position(100 - kwargs[ATTR_POSITION]) diff --git a/homeassistant/components/velbus/entity.py b/homeassistant/components/velbus/entity.py index 13ecb7febab..46d9f03b4fb 100644 --- a/homeassistant/components/velbus/entity.py +++ b/homeassistant/components/velbus/entity.py @@ -1,8 +1,13 @@ """Support for Velbus devices.""" from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps +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 .const import DOMAIN @@ -35,3 +40,25 @@ class VelbusEntity(Entity): async def _on_update(self) -> None: self.async_write_ha_state() + + +_T = TypeVar("_T", bound="VelbusEntity") +_P = ParamSpec("_P") + + +def api_call( + func: Callable[Concatenate[_T, _P], Awaitable[None]] +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: + """Catch command exceptions.""" + + @wraps(func) + async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Wrap all command methods.""" + try: + await func(self, *args, **kwargs) + except OSError as exc: + raise HomeAssistantError( + f"Could not execute {func.__name__} service for {self.name}" + ) from exc + + return cmd_wrapper diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index ca00a3134ce..1806c2905e9 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import VelbusEntity +from .entity import VelbusEntity, api_call async def async_setup_entry( @@ -63,6 +63,7 @@ class VelbusLight(VelbusEntity, LightEntity): """Return the brightness of the light.""" return int((self._channel.get_dimmer_state() * 255) / 100) + @api_call async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the Velbus light to turn on.""" if ATTR_BRIGHTNESS in kwargs: @@ -83,6 +84,7 @@ class VelbusLight(VelbusEntity, LightEntity): ) await getattr(self._channel, attr)(*args) + @api_call async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the velbus light to turn off.""" attr, *args = ( @@ -113,6 +115,7 @@ class VelbusButtonLight(VelbusEntity, LightEntity): """Return true if the light is on.""" return self._channel.is_on() + @api_call async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the Velbus light to turn on.""" if ATTR_FLASH in kwargs: @@ -126,6 +129,7 @@ class VelbusButtonLight(VelbusEntity, LightEntity): attr, *args = "set_led_state", "on" await getattr(self._channel, attr)(*args) + @api_call async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the velbus light to turn off.""" attr, *args = "set_led_state", "off" diff --git a/homeassistant/components/velbus/select.py b/homeassistant/components/velbus/select.py index af79b5d1276..6e2b4d1a746 100644 --- a/homeassistant/components/velbus/select.py +++ b/homeassistant/components/velbus/select.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import VelbusEntity +from .entity import VelbusEntity, api_call async def async_setup_entry( @@ -37,6 +37,7 @@ class VelbusSelect(VelbusEntity, SelectEntity): self._attr_options = self._channel.get_options() self._attr_unique_id = f"{self._attr_unique_id}-program_select" + @api_call async def async_select_option(self, option: str) -> None: """Update the program on the module.""" await self._channel.set_selected_program(option) diff --git a/homeassistant/components/velbus/switch.py b/homeassistant/components/velbus/switch.py index 6de8373d3fc..db7c165840e 100644 --- a/homeassistant/components/velbus/switch.py +++ b/homeassistant/components/velbus/switch.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import VelbusEntity +from .entity import VelbusEntity, api_call async def async_setup_entry( @@ -36,10 +36,12 @@ class VelbusSwitch(VelbusEntity, SwitchEntity): """Return true if the switch is on.""" return self._channel.is_on() + @api_call async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the switch to turn on.""" await self._channel.turn_on() + @api_call async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the switch to turn off.""" await self._channel.turn_off() From 42bca0f94a393ed841f03f7d60d99d2881851b47 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 6 Aug 2023 19:39:24 +0200 Subject: [PATCH 0246/1151] Complete test coverage for OpenSky (#97863) * Use mockobject for OpenSky testing * Complete test coverage for OpenSky * Complete test coverage for OpenSky * Use method patching --- .coveragerc | 1 - tests/components/opensky/conftest.py | 23 +++- tests/components/opensky/fixtures/states.json | 105 ++++++++++++++++ .../components/opensky/fixtures/states_1.json | 45 +++++++ .../opensky/snapshots/test_sensor.ambr | 42 +++++++ tests/components/opensky/test_sensor.py | 113 ++++++++++++++++-- 6 files changed, 317 insertions(+), 12 deletions(-) create mode 100644 tests/components/opensky/fixtures/states.json create mode 100644 tests/components/opensky/fixtures/states_1.json create mode 100644 tests/components/opensky/snapshots/test_sensor.ambr diff --git a/.coveragerc b/.coveragerc index 7e0c71ec9fb..d895b1adf0a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -863,7 +863,6 @@ omit = homeassistant/components/openhome/const.py homeassistant/components/openhome/media_player.py homeassistant/components/opensensemap/air_quality.py - homeassistant/components/opensky/sensor.py homeassistant/components/opentherm_gw/__init__.py homeassistant/components/opentherm_gw/binary_sensor.py homeassistant/components/opentherm_gw/climate.py diff --git a/tests/components/opensky/conftest.py b/tests/components/opensky/conftest.py index 63e514d0d8f..7cf3074a2a3 100644 --- a/tests/components/opensky/conftest.py +++ b/tests/components/opensky/conftest.py @@ -1,5 +1,6 @@ """Configure tests for the OpenSky integration.""" from collections.abc import Awaitable, Callable +import json from unittest.mock import patch import pytest @@ -10,7 +11,7 @@ from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture ComponentSetup = Callable[[MockConfigEntry], Awaitable[None]] @@ -32,6 +33,23 @@ def mock_config_entry() -> MockConfigEntry: ) +@pytest.fixture(name="config_entry_altitude") +def mock_config_entry_altitude() -> MockConfigEntry: + """Create Opensky entry with altitude in Home Assistant.""" + return MockConfigEntry( + domain=DOMAIN, + title="OpenSky", + data={ + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + }, + options={ + CONF_RADIUS: 10.0, + CONF_ALTITUDE: 12500.0, + }, + ) + + @pytest.fixture(name="setup_integration") async def mock_setup_integration( hass: HomeAssistant, @@ -40,9 +58,10 @@ async def mock_setup_integration( async def func(mock_config_entry: MockConfigEntry) -> None: mock_config_entry.add_to_hass(hass) + json_fixture = load_fixture("opensky/states.json") with patch( "python_opensky.OpenSky.get_states", - return_value=StatesResponse(states=[], time=0), + return_value=StatesResponse.parse_obj(json.loads(json_fixture)), ): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/opensky/fixtures/states.json b/tests/components/opensky/fixtures/states.json new file mode 100644 index 00000000000..7fee53157c8 --- /dev/null +++ b/tests/components/opensky/fixtures/states.json @@ -0,0 +1,105 @@ +{ + "time": 1691244533, + "states": [ + { + "icao24": "3c6708", + "callsign": "DLH459 ", + "origin_country": "Germany", + "time_position": 1691244522, + "last_contact": 1691244522, + "longitude": 5.4445, + "latitude": 52.2991, + "baro_altitude": 12496.8, + "on_ground": false, + "velocity": 259.73, + "true_track": 134.84, + "vertical_rate": 0, + "sensors": null, + "geo_altitude": 12710.16, + "squawk": "1151", + "spi": false, + "position_source": 0, + "category": 6 + }, + { + "icao24": "3c6708", + "callsign": " ", + "origin_country": "Germany", + "time_position": 1691244522, + "last_contact": 1691244522, + "longitude": 5.4445, + "latitude": 52.2991, + "baro_altitude": 12496.8, + "on_ground": false, + "velocity": 259.73, + "true_track": 134.84, + "vertical_rate": 0, + "sensors": null, + "geo_altitude": 12710.16, + "squawk": "1151", + "spi": false, + "position_source": 0, + "category": 6 + }, + { + "icao24": "4846df", + "callsign": "", + "origin_country": "Kingdom of the Netherlands", + "time_position": 1691244404, + "last_contact": 1691244404, + "longitude": 4.7441, + "latitude": 52.3076, + "baro_altitude": null, + "on_ground": true, + "velocity": 8.75, + "true_track": 272.81, + "vertical_rate": null, + "sensors": null, + "geo_altitude": null, + "squawk": null, + "spi": false, + "position_source": 0, + "category": 17 + }, + { + "icao24": "4846df", + "callsign": "DLH420 ", + "origin_country": "Kingdom of the Netherlands", + "time_position": 1691244404, + "last_contact": 1691244404, + "longitude": 4.7441, + "latitude": 52.3076, + "baro_altitude": null, + "on_ground": true, + "velocity": 8.75, + "true_track": 272.81, + "vertical_rate": null, + "sensors": null, + "geo_altitude": null, + "squawk": null, + "spi": false, + "position_source": 0, + "category": 17 + }, + { + "icao24": "3e3d01", + "callsign": "ECA2HL ", + "origin_country": "Germany", + "time_position": 1691244533, + "last_contact": 1691244533, + "longitude": 5.5217, + "latitude": 52.4561, + "baro_altitude": 12500.8, + "on_ground": false, + "velocity": 201.9, + "true_track": 82.39, + "vertical_rate": 0, + "sensors": null, + "geo_altitude": 12733.02, + "squawk": "1071", + "spi": false, + "position_source": 0, + "category": 1 + } + ] +} diff --git a/tests/components/opensky/fixtures/states_1.json b/tests/components/opensky/fixtures/states_1.json new file mode 100644 index 00000000000..bd76428627e --- /dev/null +++ b/tests/components/opensky/fixtures/states_1.json @@ -0,0 +1,45 @@ +{ + "time": 1691244533, + "states": [ + { + "icao24": "4846df", + "callsign": "", + "origin_country": "Kingdom of the Netherlands", + "time_position": 1691244404, + "last_contact": 1691244404, + "longitude": 4.7441, + "latitude": 52.3076, + "baro_altitude": null, + "on_ground": true, + "velocity": 8.75, + "true_track": 272.81, + "vertical_rate": null, + "sensors": null, + "geo_altitude": null, + "squawk": null, + "spi": false, + "position_source": 0, + "category": 17 + }, + { + "icao24": "3e3d01", + "callsign": "ECA2HL ", + "origin_country": "Germany", + "time_position": 1691244533, + "last_contact": 1691244533, + "longitude": 5.5217, + "latitude": 52.4561, + "baro_altitude": 12500.8, + "on_ground": false, + "velocity": 201.9, + "true_track": 82.39, + "vertical_rate": 0, + "sensors": null, + "geo_altitude": 12733.02, + "squawk": "1071", + "spi": false, + "position_source": 0, + "category": 1 + } + ] +} diff --git a/tests/components/opensky/snapshots/test_sensor.ambr b/tests/components/opensky/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..1bd85d23400 --- /dev/null +++ b/tests/components/opensky/snapshots/test_sensor.ambr @@ -0,0 +1,42 @@ +# serializer version: 1 +# name: test_sensor + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Information provided by the OpenSky Network (https://opensky-network.org)', + 'friendly_name': 'OpenSky', + 'icon': 'mdi:airplane', + 'unit_of_measurement': 'flights', + }), + 'context': , + 'entity_id': 'sensor.opensky', + 'last_changed': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensor_altitude + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Information provided by the OpenSky Network (https://opensky-network.org)', + 'friendly_name': 'OpenSky', + 'icon': 'mdi:airplane', + 'unit_of_measurement': 'flights', + }), + 'context': , + 'entity_id': 'sensor.opensky', + 'last_changed': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_sensor_updating + list([ + , + ]) +# --- +# name: test_sensor_updating.1 + list([ + , + , + ]) +# --- diff --git a/tests/components/opensky/test_sensor.py b/tests/components/opensky/test_sensor.py index 1768efebc78..eb17721929c 100644 --- a/tests/components/opensky/test_sensor.py +++ b/tests/components/opensky/test_sensor.py @@ -1,20 +1,115 @@ """OpenSky sensor tests.""" -from homeassistant.components.opensky.const import DOMAIN +from datetime import timedelta +import json +from unittest.mock import patch + +from python_opensky import StatesResponse +from syrupy import SnapshotAssertion + +from homeassistant.components.opensky.const import ( + DOMAIN, + EVENT_OPENSKY_ENTRY, + EVENT_OPENSKY_EXIT, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PLATFORM, CONF_RADIUS, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from .conftest import ComponentSetup + +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture LEGACY_CONFIG = {Platform.SENSOR: [{CONF_PLATFORM: DOMAIN, CONF_RADIUS: 10.0}]} async def test_legacy_migration(hass: HomeAssistant) -> None: """Test migration from yaml to config flow.""" - assert await async_setup_component(hass, Platform.SENSOR, LEGACY_CONFIG) - await hass.async_block_till_done() - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.LOADED - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 + json_fixture = load_fixture("opensky/states.json") + with patch( + "python_opensky.OpenSky.get_states", + return_value=StatesResponse.parse_obj(json.loads(json_fixture)), + ): + assert await async_setup_component(hass, Platform.SENSOR, LEGACY_CONFIG) + await hass.async_block_till_done() + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + issue_registry = ir.async_get(hass) + assert len(issue_registry.issues) == 1 + + +async def test_sensor( + hass: HomeAssistant, + config_entry: MockConfigEntry, + setup_integration: ComponentSetup, + snapshot: SnapshotAssertion, +): + """Test setup sensor.""" + await setup_integration(config_entry) + + state = hass.states.get("sensor.opensky") + assert state == snapshot + events = [] + + async def event_listener(event: Event) -> None: + events.append(event) + + hass.bus.async_listen(EVENT_OPENSKY_ENTRY, event_listener) + hass.bus.async_listen(EVENT_OPENSKY_EXIT, event_listener) + assert events == [] + + +async def test_sensor_altitude( + hass: HomeAssistant, + config_entry_altitude: MockConfigEntry, + setup_integration: ComponentSetup, + snapshot: SnapshotAssertion, +): + """Test setup sensor with a set altitude.""" + await setup_integration(config_entry_altitude) + + state = hass.states.get("sensor.opensky") + assert state == snapshot + + +async def test_sensor_updating( + hass: HomeAssistant, + config_entry: MockConfigEntry, + setup_integration: ComponentSetup, + snapshot: SnapshotAssertion, +): + """Test updating sensor.""" + await setup_integration(config_entry) + + def get_states_response_fixture(fixture: str) -> StatesResponse: + json_fixture = load_fixture(fixture) + return StatesResponse.parse_obj(json.loads(json_fixture)) + + events = [] + + async def event_listener(event: Event) -> None: + events.append(event) + + hass.bus.async_listen(EVENT_OPENSKY_ENTRY, event_listener) + hass.bus.async_listen(EVENT_OPENSKY_EXIT, event_listener) + + async def skip_time_and_check_events() -> None: + future = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert events == snapshot + + with patch( + "python_opensky.OpenSky.get_states", + return_value=get_states_response_fixture("opensky/states_1.json"), + ): + await skip_time_and_check_events() + with patch( + "python_opensky.OpenSky.get_states", + return_value=get_states_response_fixture("opensky/states.json"), + ): + await skip_time_and_check_events() From a59793df4cb7420bbd884da8633252d8a0e802cb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 6 Aug 2023 20:01:01 +0200 Subject: [PATCH 0247/1151] Bump pytrafikverket to 0.3.4 (#97921) --- homeassistant/components/trafikverket_ferry/manifest.json | 2 +- homeassistant/components/trafikverket_train/manifest.json | 2 +- .../components/trafikverket_weatherstation/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/trafikverket_ferry/manifest.json b/homeassistant/components/trafikverket_ferry/manifest.json index 5822566505b..960108fca03 100644 --- a/homeassistant/components/trafikverket_ferry/manifest.json +++ b/homeassistant/components/trafikverket_ferry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_ferry", "iot_class": "cloud_polling", "loggers": ["pytrafikverket"], - "requirements": ["pytrafikverket==0.3.3"] + "requirements": ["pytrafikverket==0.3.4"] } diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 7b8369cec17..88dfc0c4311 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", "iot_class": "cloud_polling", "loggers": ["pytrafikverket"], - "requirements": ["pytrafikverket==0.3.3"] + "requirements": ["pytrafikverket==0.3.4"] } diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 014637b99f6..d59fe44e118 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", "iot_class": "cloud_polling", "loggers": ["pytrafikverket"], - "requirements": ["pytrafikverket==0.3.3"] + "requirements": ["pytrafikverket==0.3.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8f8ed2852ff..bb199e1a9c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2191,7 +2191,7 @@ pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.3.3 +pytrafikverket==0.3.4 # homeassistant.components.usb pyudev==0.23.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2fdafad096c..056b354f451 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1611,7 +1611,7 @@ pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.3.3 +pytrafikverket==0.3.4 # homeassistant.components.usb pyudev==0.23.2 From ceac5f8d5a2c26af4e982d759d4248c783a86bb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Aug 2023 08:31:45 -1000 Subject: [PATCH 0248/1151] Proactively refresh the enphase envoy token to handle cloud service downtime (#97880) --- .../components/enphase_envoy/coordinator.py | 113 ++++++++++++++---- .../components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 91 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index 85a7dc4c2f8..f3ad1705080 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -1,6 +1,8 @@ """The enphase_envoy component.""" from __future__ import annotations +import contextlib +import datetime from datetime import timedelta import logging from typing import Any @@ -13,13 +15,19 @@ from pyenphase import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant +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 SCAN_INTERVAL = timedelta(seconds=60) + +TOKEN_REFRESH_CHECK_INTERVAL = timedelta(days=1) +STALE_TOKEN_THRESHOLD = timedelta(days=30).total_seconds() + _LOGGER = logging.getLogger(__name__) @@ -36,6 +44,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.username = entry_data[CONF_USERNAME] self.password = entry_data[CONF_PASSWORD] self._setup_complete = False + self._cancel_token_refresh: CALLBACK_TYPE | None = None super().__init__( hass, _LOGGER, @@ -44,39 +53,92 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): always_update=False, ) + @callback + def _async_refresh_token_if_needed(self, now: datetime.datetime) -> None: + """Proactively refresh token if its stale in case cloud services goes down.""" + assert isinstance(self.envoy.auth, EnvoyTokenAuth) + expire_time = self.envoy.auth.expire_timestamp + remain = expire_time - now.timestamp() + fresh = remain > STALE_TOKEN_THRESHOLD + name = self.name + _LOGGER.debug("%s: %s seconds remaining on token fresh=%s", name, remain, fresh) + if not fresh: + self.hass.async_create_background_task( + self._async_try_refresh_token(), "{name} token refresh" + ) + + async def _async_try_refresh_token(self) -> None: + """Try to refresh token.""" + assert isinstance(self.envoy.auth, EnvoyTokenAuth) + _LOGGER.debug("%s: Trying to refresh token", self.name) + try: + await self.envoy.auth.refresh() + except EnvoyError as err: + # If we can't refresh the token, we try again later + # If the token actually ends up expiring, we'll + # re-authenticate with username/password and get a new token + # or log an error if that fails + _LOGGER.debug("%s: Error refreshing token: %s", err, self.name) + return + self._async_update_saved_token() + + @callback + def _async_mark_setup_complete(self) -> None: + """Mark setup as complete and setup token refresh if needed.""" + self._setup_complete = True + if self._cancel_token_refresh: + self._cancel_token_refresh() + self._cancel_token_refresh = None + if not isinstance(self.envoy.auth, EnvoyTokenAuth): + return + self._cancel_token_refresh = async_track_time_interval( + self.hass, + self._async_refresh_token_if_needed, + TOKEN_REFRESH_CHECK_INTERVAL, + cancel_on_shutdown=True, + ) + async def _async_setup_and_authenticate(self) -> None: """Set up and authenticate with the envoy.""" envoy = self.envoy await envoy.setup() assert envoy.serial_number is not None self.envoy_serial_number = envoy.serial_number - if token := self.entry.data.get(CONF_TOKEN): - try: - await envoy.authenticate(token=token) - except INVALID_AUTH_ERRORS: - # token likely expired or firmware changed - # so we fall through to authenticate with username/password - pass - else: - self._setup_complete = True + with contextlib.suppress(*INVALID_AUTH_ERRORS): + # Always set the username and password + # so we can refresh the token if needed + await envoy.authenticate( + username=self.username, password=self.password, token=token + ) + # The token is valid, but we still want + # to refresh it if it's stale right away + self._async_refresh_token_if_needed(dt_util.utcnow()) return + # token likely expired or firmware changed + # so we fall through to authenticate with + # username/password + await self.envoy.authenticate(username=self.username, password=self.password) + # Password auth succeeded, so we can update the token + # if we are using EnvoyTokenAuth + self._async_update_saved_token() - await envoy.authenticate(username=self.username, password=self.password) - assert envoy.auth is not None - - if isinstance(envoy.auth, EnvoyTokenAuth): - # update token in config entry so we can - # startup without hitting the Cloud API - # as long as the token is valid - self.hass.config_entries.async_update_entry( - self.entry, - data={ - **self.entry.data, - CONF_TOKEN: envoy.auth.token, - }, - ) - self._setup_complete = True + def _async_update_saved_token(self) -> None: + """Update saved token in config entry.""" + envoy = self.envoy + if not isinstance(envoy.auth, EnvoyTokenAuth): + return + # update token in config entry so we can + # startup without hitting the Cloud API + # as long as the token is valid + _LOGGER.debug("%s: Updating token in config entry from auth", self.name) + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_TOKEN: envoy.auth.token, + }, + ) async def _async_update_data(self) -> dict[str, Any]: """Fetch all device and sensor data from api.""" @@ -85,6 +147,7 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): try: if not self._setup_complete: await self._async_setup_and_authenticate() + self._async_mark_setup_complete() return (await envoy.update()).raw except INVALID_AUTH_ERRORS as err: if self._setup_complete and tries == 0: diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 40e58348768..b3afbdd29c3 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.9.0"], + "requirements": ["pyenphase==0.10.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index bb199e1a9c2..5a3cf580803 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.9.0 +pyenphase==0.10.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 056b354f451..7dda254b5f2 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.9.0 +pyenphase==0.10.0 # homeassistant.components.everlights pyeverlights==0.1.0 From 4d24a3ffaa65710287514153b4c7ab3342006e1e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 6 Aug 2023 20:58:16 +0200 Subject: [PATCH 0249/1151] Bump pytrafikverket to 0.3.5 (#97923) --- homeassistant/components/trafikverket_ferry/manifest.json | 2 +- homeassistant/components/trafikverket_train/manifest.json | 2 +- .../components/trafikverket_weatherstation/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/trafikverket_ferry/manifest.json b/homeassistant/components/trafikverket_ferry/manifest.json index 960108fca03..47f1e62be00 100644 --- a/homeassistant/components/trafikverket_ferry/manifest.json +++ b/homeassistant/components/trafikverket_ferry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_ferry", "iot_class": "cloud_polling", "loggers": ["pytrafikverket"], - "requirements": ["pytrafikverket==0.3.4"] + "requirements": ["pytrafikverket==0.3.5"] } diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 88dfc0c4311..47b4c21c867 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", "iot_class": "cloud_polling", "loggers": ["pytrafikverket"], - "requirements": ["pytrafikverket==0.3.4"] + "requirements": ["pytrafikverket==0.3.5"] } diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index d59fe44e118..8c46afa5972 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", "iot_class": "cloud_polling", "loggers": ["pytrafikverket"], - "requirements": ["pytrafikverket==0.3.4"] + "requirements": ["pytrafikverket==0.3.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5a3cf580803..3ac7eed6278 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2191,7 +2191,7 @@ pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.3.4 +pytrafikverket==0.3.5 # homeassistant.components.usb pyudev==0.23.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7dda254b5f2..da5bdc38c7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1611,7 +1611,7 @@ pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.3.4 +pytrafikverket==0.3.5 # homeassistant.components.usb pyudev==0.23.2 From 99c3ca030d9acfa5390b9ac4cc7d497e8670a6c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Aug 2023 09:31:37 -1000 Subject: [PATCH 0250/1151] Bump pyenphase to 0.11.0 (#97926) --- 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 b3afbdd29c3..1738af3c225 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.10.0"], + "requirements": ["pyenphase==0.11.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 3ac7eed6278..b1fed9cd852 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.10.0 +pyenphase==0.11.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da5bdc38c7a..5a7cb959dfc 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.10.0 +pyenphase==0.11.0 # homeassistant.components.everlights pyeverlights==0.1.0 From 50ccd68de156ba9cc3dd11a8d7bea16f0c8ea951 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sun, 6 Aug 2023 16:20:16 -0400 Subject: [PATCH 0251/1151] Bump pyschlage to 2023.8.1 (#97927) --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index 9a84a3446c7..25316004c58 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2023.8.0"] + "requirements": ["pyschlage==2023.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index b1fed9cd852..093e1c8e5ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1982,7 +1982,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2023.8.0 +pyschlage==2023.8.1 # homeassistant.components.sensibo pysensibo==1.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a7cb959dfc..647527c0df1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1474,7 +1474,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2023.8.0 +pyschlage==2023.8.1 # homeassistant.components.sensibo pysensibo==1.0.33 From c9edc973f03956ce5718929eafb58c1d749d3944 Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 6 Aug 2023 16:34:14 -0400 Subject: [PATCH 0252/1151] Bump python-roborock to 0.32.2 (#97907) * bump to 0.32.2 * fix test --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roborock/conftest.py | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index eda6a5609a2..05fff332c67 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.31.1"] + "requirements": ["python-roborock==0.32.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 093e1c8e5ea..1d256bddb6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.31.1 +python-roborock==0.32.2 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 647527c0df1..7dfae3209d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1582,7 +1582,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.31.1 +python-roborock==0.32.2 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index eb281076825..ef841769f8d 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -67,6 +67,10 @@ async def setup_entry( return_value=PROP, ), patch( "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + ), patch( + "homeassistant.components.roborock.RoborockMqttClient._wait_response" + ), patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient._wait_response" ): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() From d993aa59ea097b25084a5fde2730a576eb13b7b5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 6 Aug 2023 22:59:44 +0200 Subject: [PATCH 0253/1151] Update orjson to 3.9.3 (#97930) --- 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 e8557a3b922..61ed6ab3dcd 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.2 +orjson==3.9.3 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.0.0 diff --git a/pyproject.toml b/pyproject.toml index 9a526187999..3ee1bf33477 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.2", + "orjson==3.9.3", "packaging>=23.1", "pip>=21.3.1", "python-slugify==4.0.1", diff --git a/requirements.txt b/requirements.txt index debdc7dbcb3..9cca2393a0d 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.2 +orjson==3.9.3 packaging>=23.1 pip>=21.3.1 python-slugify==4.0.1 From 4230465fcdd14247f75d9829f87835bf84054d90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Aug 2023 11:11:03 -1000 Subject: [PATCH 0254/1151] Avoid polling event characteristic in homekit_controller (#97877) --- homeassistant/components/homekit_controller/entity.py | 6 +++++- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index 046dc9f17ec..6ebe777d5f8 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -5,6 +5,7 @@ from typing import Any from aiohomekit.model import Accessory from aiohomekit.model.characteristics import ( + EVENT_CHARACTERISTICS, Characteristic, CharacteristicPermissions, CharacteristicsTypes, @@ -111,7 +112,10 @@ class HomeKitEntity(Entity): def _setup_characteristic(self, char: Characteristic) -> None: """Configure an entity based on a HomeKit characteristics metadata.""" # Build up a list of (aid, iid) tuples to poll on update() - if CharacteristicPermissions.paired_read in char.perms: + if ( + CharacteristicPermissions.paired_read in char.perms + and char.type not in EVENT_CHARACTERISTICS + ): self.pollable_characteristics.append((self._aid, char.iid)) # Build up a list of (aid, iid) tuples to subscribe to diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 01b85ef6bbb..d26b15bdc7a 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.13"], + "requirements": ["aiohomekit==2.6.14"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 1d256bddb6b..7c2a09a9ab6 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.13 +aiohomekit==2.6.14 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7dfae3209d9..301663f104f 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.13 +aiohomekit==2.6.14 # homeassistant.components.emulated_hue # homeassistant.components.http From 157f7a3079d7d95fc353039a1d7eef17f6064b4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Aug 2023 12:20:28 -1000 Subject: [PATCH 0255/1151] Bump pyatv to 0.13.4 (#97932) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 1d1c26b5fcd..a22687c0fb5 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/apple_tv", "iot_class": "local_push", "loggers": ["pyatv", "srptools"], - "requirements": ["pyatv==0.13.3"], + "requirements": ["pyatv==0.13.4"], "zeroconf": [ "_mediaremotetv._tcp.local.", "_companion-link._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 7c2a09a9ab6..35939641468 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1569,7 +1569,7 @@ pyatag==0.3.5.3 pyatmo==7.5.0 # homeassistant.components.apple_tv -pyatv==0.13.3 +pyatv==0.13.4 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 301663f104f..3b9bdcff35e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1175,7 +1175,7 @@ pyatag==0.3.5.3 pyatmo==7.5.0 # homeassistant.components.apple_tv -pyatv==0.13.3 +pyatv==0.13.4 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 From df8f1328e91f2588ab95b8ceac2c21b0aff6b01c Mon Sep 17 00:00:00 2001 From: Alex Yao <33379584+alexyao2015@users.noreply.github.com> Date: Sun, 6 Aug 2023 17:20:48 -0500 Subject: [PATCH 0256/1151] Bump yeelight to v0.7.13 (#97933) Co-authored-by: alexyao2015 --- homeassistant/components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 7f5a67f4220..766ac0700e5 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.12", "async-upnp-client==0.34.1"], + "requirements": ["yeelight==0.7.13", "async-upnp-client==0.34.1"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 35939641468..8abd8adf790 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2728,7 +2728,7 @@ yalexs-ble==2.2.3 yalexs==1.5.1 # homeassistant.components.yeelight -yeelight==0.7.12 +yeelight==0.7.13 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b9bdcff35e..b978d5e7010 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2010,7 +2010,7 @@ yalexs-ble==2.2.3 yalexs==1.5.1 # homeassistant.components.yeelight -yeelight==0.7.12 +yeelight==0.7.13 # homeassistant.components.yolink yolink-api==0.3.0 From b2d2de990d707cb64e7a41ba790aa3294c484d05 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 7 Aug 2023 00:36:19 +0200 Subject: [PATCH 0257/1151] Remove DWD code owner (#97938) --- CODEOWNERS | 4 ++-- homeassistant/components/dwd_weather_warnings/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 5ba36fb30c1..012f256f372 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -297,8 +297,8 @@ build.json @home-assistant/supervisor /tests/components/dunehd/ @bieniu /homeassistant/components/duotecno/ @cereal2nd /tests/components/duotecno/ @cereal2nd -/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo -/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo +/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo +/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo /homeassistant/components/dynalite/ @ziv1234 /tests/components/dynalite/ @ziv1234 /homeassistant/components/eafm/ @Jc2k diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index a383e33eab2..dab3a39c10f 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -1,7 +1,7 @@ { "domain": "dwd_weather_warnings", "name": "Deutscher Wetterdienst (DWD) Weather Warnings", - "codeowners": ["@runningman84", "@stephan192", "@Hummel95", "@andarotajo"], + "codeowners": ["@runningman84", "@stephan192", "@andarotajo"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "iot_class": "cloud_polling", From 3969de6c76a5d70dc624167273d23fc388aa2228 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 7 Aug 2023 00:37:06 +0200 Subject: [PATCH 0258/1151] Freeze time for whirlpool test to avoid fail (#97935) --- tests/components/whirlpool/test_sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index 4e451f46e9b..8e8c5513097 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -2,6 +2,7 @@ from datetime import datetime, timezone from unittest.mock import MagicMock +import pytest from whirlpool.washerdryer import MachineState from homeassistant.components.whirlpool.sensor import SCAN_INTERVAL @@ -325,6 +326,7 @@ async def test_no_restore_state( assert state.state != "unknown" +@pytest.mark.freeze_time("2022-11-30 00:00:00") async def test_callback( hass: HomeAssistant, mock_sensor_api_instances: MagicMock, @@ -377,8 +379,8 @@ async def test_callback( assert state.state == time # Test timestamp change for > 60 seconds. - mock_sensor1_api.get_attribute.return_value = "120" + mock_sensor1_api.get_attribute.return_value = "125" callback() state = hass.states.get("sensor.washer_end_time") - newtime = utc_from_timestamp(as_timestamp(time) + 60) + newtime = utc_from_timestamp(as_timestamp(time) + 65) assert state.state == newtime.isoformat() From 05e131452d3890804a143ff04feb0f88b82b8b07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Aug 2023 16:55:34 -1000 Subject: [PATCH 0259/1151] Add model/part number data enphase_envoy (#97942) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- homeassistant/components/enphase_envoy/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 1738af3c225..831553bd312 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.11.0"], + "requirements": ["pyenphase==0.14.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 7d89ba94499..71efba899d2 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -233,7 +233,7 @@ class EnvoyEntity(CoordinatorEntity[EnphaseUpdateCoordinator], SensorEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, envoy_serial_num)}, manufacturer="Enphase", - model="Envoy", + model=coordinator.envoy.part_number or "Envoy", name=envoy_name, sw_version=str(coordinator.envoy.firmware), ) diff --git a/requirements_all.txt b/requirements_all.txt index 8abd8adf790..696a931634f 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.11.0 +pyenphase==0.14.1 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b978d5e7010..8fb487aea9e 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.11.0 +pyenphase==0.14.1 # homeassistant.components.everlights pyeverlights==0.1.0 From 3df71eca4538d32a55d7e3ef2a21fd3f5d7a0545 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Aug 2023 05:23:52 +0200 Subject: [PATCH 0260/1151] Ensure webhooks take HA cloud into account (#97801) * Ensure webhooks take HA cloud into account * Avoid circular import --- homeassistant/components/webhook/__init__.py | 28 +++++++++++++------- tests/components/webhook/test_init.py | 14 +++++++++- tests/components/webhook/test_trigger.py | 20 +++++++++++--- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 9711c30b19e..5f82ca54283 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -145,16 +145,26 @@ async def async_handle_webhook( return Response(status=HTTPStatus.METHOD_NOT_ALLOWED) if webhook["local_only"] in (True, None) and not isinstance(request, MockRequest): - if TYPE_CHECKING: - assert isinstance(request, Request) - assert request.remote is not None - try: - remote = ip_address(request.remote) - except ValueError: - _LOGGER.debug("Unable to parse remote ip %s", request.remote) - return Response(status=HTTPStatus.OK) + if has_cloud := "cloud" in hass.config.components: + from hass_nabucasa import remote # pylint: disable=import-outside-toplevel - if not network.is_local(remote): + is_local = True + if has_cloud and remote.is_cloud_request.get(): + is_local = False + else: + if TYPE_CHECKING: + assert isinstance(request, Request) + assert request.remote is not None + + try: + request_remote = ip_address(request.remote) + except ValueError: + _LOGGER.debug("Unable to parse remote ip %s", request.remote) + return Response(status=HTTPStatus.OK) + + is_local = network.is_local(request_remote) + + if not is_local: _LOGGER.warning("Received remote request for local webhook %s", webhook_id) if webhook["local_only"]: return Response(status=HTTPStatus.OK) diff --git a/tests/components/webhook/test_init.py b/tests/components/webhook/test_init.py index ff0346a3d8b..fbe0da15853 100644 --- a/tests/components/webhook/test_init.py +++ b/tests/components/webhook/test_init.py @@ -1,7 +1,7 @@ """Test the webhook component.""" from http import HTTPStatus from ipaddress import ip_address -from unittest.mock import patch +from unittest.mock import Mock, patch from aiohttp import web import pytest @@ -206,6 +206,8 @@ async def test_webhook_not_allowed_method(hass: HomeAssistant) -> None: async def test_webhook_local_only(hass: HomeAssistant, mock_client) -> None: """Test posting a webhook with local only.""" + hass.config.components.add("cloud") + hooks = [] webhook_id = webhook.async_generate_id() @@ -234,6 +236,16 @@ async def test_webhook_local_only(hass: HomeAssistant, mock_client) -> None: # No hook received assert len(hooks) == 1 + # Request from Home Assistant Cloud remote UI + with patch( + "hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True)) + ): + resp = await mock_client.post(f"/api/webhook/{webhook_id}", json={"data": True}) + + # No hook received + assert resp.status == HTTPStatus.OK + assert len(hooks) == 1 + async def test_listing_webhook( hass: HomeAssistant, diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index 392ab58a30f..990482c500e 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -1,6 +1,6 @@ """The tests for the webhook automation trigger.""" from ipaddress import ip_address -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest @@ -68,6 +68,9 @@ async def test_webhook_post( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator ) -> None: """Test triggering with a POST webhook.""" + # Set up fake cloud + hass.config.components.add("cloud") + events = [] @callback @@ -114,6 +117,16 @@ async def test_webhook_post( await hass.async_block_till_done() assert len(events) == 1 + # Request from Home Assistant Cloud remote UI + with patch( + "hass_nabucasa.remote.is_cloud_request", Mock(get=Mock(return_value=True)) + ): + await client.post("/api/webhook/post_webhook", data={"hello": "world"}) + + # No hook received + await hass.async_block_till_done() + assert len(events) == 1 + async def test_webhook_allowed_methods_internet( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator @@ -141,7 +154,6 @@ async def test_webhook_allowed_methods_internet( }, "action": { "event": "test_success", - "event_data_template": {"hello": "yo {{ trigger.data.hello }}"}, }, } }, @@ -150,7 +162,7 @@ async def test_webhook_allowed_methods_internet( client = await hass_client_no_auth() - await client.post("/api/webhook/post_webhook", data={"hello": "world"}) + await client.post("/api/webhook/post_webhook") await hass.async_block_till_done() assert len(events) == 0 @@ -160,7 +172,7 @@ async def test_webhook_allowed_methods_internet( "homeassistant.components.webhook.ip_address", return_value=ip_address("123.123.123.123"), ): - await client.put("/api/webhook/post_webhook", data={"hello": "world"}) + await client.put("/api/webhook/post_webhook") await hass.async_block_till_done() assert len(events) == 1 From 369a484a786a6734c7b6bf59151304fc6db03fef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Aug 2023 05:25:13 +0200 Subject: [PATCH 0261/1151] Add default headers to webserver responses (#97784) * Add default headers to webserver responses * Set default server header * Fix other tests --- homeassistant/components/http/__init__.py | 8 +++++ homeassistant/components/http/headers.py | 32 +++++++++++++++++ tests/components/http/test_headers.py | 44 +++++++++++++++++++++++ tests/scripts/test_check_config.py | 1 + 4 files changed, 85 insertions(+) create mode 100644 homeassistant/components/http/headers.py create mode 100644 tests/components/http/test_headers.py diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 68602e34d3e..48ad0cb8752 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -53,6 +53,7 @@ from .const import ( # noqa: F401 ) from .cors import setup_cors from .forwarded import async_setup_forwarded +from .headers import setup_headers from .request_context import current_request, setup_request_context from .security_filter import setup_security_filter from .static import CACHE_HEADERS, CachingStaticResource @@ -69,6 +70,7 @@ CONF_SSL_PEER_CERTIFICATE: Final = "ssl_peer_certificate" CONF_SSL_KEY: Final = "ssl_key" CONF_CORS_ORIGINS: Final = "cors_allowed_origins" CONF_USE_X_FORWARDED_FOR: Final = "use_x_forwarded_for" +CONF_USE_X_FRAME_OPTIONS: Final = "use_x_frame_options" CONF_TRUSTED_PROXIES: Final = "trusted_proxies" CONF_LOGIN_ATTEMPTS_THRESHOLD: Final = "login_attempts_threshold" CONF_IP_BAN_ENABLED: Final = "ip_ban_enabled" @@ -118,6 +120,7 @@ HTTP_SCHEMA: Final = vol.All( vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In( [SSL_INTERMEDIATE, SSL_MODERN] ), + vol.Optional(CONF_USE_X_FRAME_OPTIONS, default=True): cv.boolean, } ), ) @@ -136,6 +139,7 @@ class ConfData(TypedDict, total=False): ssl_key: str cors_allowed_origins: list[str] use_x_forwarded_for: bool + use_x_frame_options: bool trusted_proxies: list[IPv4Network | IPv6Network] login_attempts_threshold: int ip_ban_enabled: bool @@ -180,6 +184,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ssl_key = conf.get(CONF_SSL_KEY) cors_origins = conf[CONF_CORS_ORIGINS] use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False) + use_x_frame_options = conf[CONF_USE_X_FRAME_OPTIONS] trusted_proxies = conf.get(CONF_TRUSTED_PROXIES) or [] is_ban_enabled = conf[CONF_IP_BAN_ENABLED] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] @@ -200,6 +205,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: use_x_forwarded_for=use_x_forwarded_for, login_threshold=login_threshold, is_ban_enabled=is_ban_enabled, + use_x_frame_options=use_x_frame_options, ) async def stop_server(event: Event) -> None: @@ -331,6 +337,7 @@ class HomeAssistantHTTP: use_x_forwarded_for: bool, login_threshold: int, is_ban_enabled: bool, + use_x_frame_options: bool, ) -> None: """Initialize the server.""" self.app[KEY_HASS] = self.hass @@ -348,6 +355,7 @@ class HomeAssistantHTTP: await async_setup_auth(self.hass, self.app) + setup_headers(self.app, use_x_frame_options) setup_cors(self.app, cors_origins) if self.ssl_certificate: diff --git a/homeassistant/components/http/headers.py b/homeassistant/components/http/headers.py new file mode 100644 index 00000000000..b53f354b144 --- /dev/null +++ b/homeassistant/components/http/headers.py @@ -0,0 +1,32 @@ +"""Middleware that helps with the control of headers in our responses.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable + +from aiohttp.web import Application, Request, StreamResponse, middleware + +from homeassistant.core import callback + + +@callback +def setup_headers(app: Application, use_x_frame_options: bool) -> None: + """Create headers middleware for the app.""" + + @middleware + async def headers_middleware( + request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] + ) -> StreamResponse: + """Process request and add headers to the responses.""" + response = await handler(request) + response.headers["Referrer-Policy"] = "no-referrer" + response.headers["X-Content-Type-Options"] = "nosniff" + + # Set an empty server header, to prevent aiohttp of setting one. + response.headers["Server"] = "" + + if use_x_frame_options: + response.headers["X-Frame-Options"] = "SAMEORIGIN" + + return response + + app.middlewares.append(headers_middleware) diff --git a/tests/components/http/test_headers.py b/tests/components/http/test_headers.py new file mode 100644 index 00000000000..6d7dbad68f6 --- /dev/null +++ b/tests/components/http/test_headers.py @@ -0,0 +1,44 @@ +"""Test headers middleware.""" +from http import HTTPStatus + +from aiohttp import web + +from homeassistant.components.http.headers import setup_headers + +from tests.typing import ClientSessionGenerator + + +async def mock_handler(request): + """Return OK.""" + return web.Response(text="OK") + + +async def test_headers_added(aiohttp_client: ClientSessionGenerator) -> None: + """Test that headers are being added on each request.""" + app = web.Application() + app.router.add_get("/", mock_handler) + + setup_headers(app, use_x_frame_options=True) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/") + + assert resp.status == HTTPStatus.OK + assert resp.headers["Referrer-Policy"] == "no-referrer" + assert resp.headers["Server"] == "" + assert resp.headers["X-Content-Type-Options"] == "nosniff" + assert resp.headers["X-Frame-Options"] == "SAMEORIGIN" + + +async def test_allow_framing(aiohttp_client: ClientSessionGenerator) -> None: + """Test that we allow framing when disabled.""" + app = web.Application() + app.router.add_get("/", mock_handler) + + setup_headers(app, use_x_frame_options=False) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/") + + assert resp.status == HTTPStatus.OK + assert "X-Frame-Options" not in resp.headers diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 44a4d55d545..e410dd672ce 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -117,6 +117,7 @@ def test_secrets(mock_is_file, event_loop, mock_hass_config_yaml: None) -> None: "login_attempts_threshold": -1, "server_port": 8123, "ssl_profile": "modern", + "use_x_frame_options": True, } assert res["secret_cache"] == { get_test_config_dir("secrets.yaml"): {"http_pw": "http://google.com"} From 1adfa6bbeb4e61714065d99a1169fb457d4b7d13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Aug 2023 20:25:03 -1000 Subject: [PATCH 0262/1151] Reduce overhead to start a config entry flow by optimizing fetching the handler (#97883) --- homeassistant/config_entries.py | 30 ++++++++++++++++++------------ tests/test_config_entries.py | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 15fcb9a50de..1e3af81395a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -964,10 +964,9 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): Handler key is the domain of the component that we want to set up. """ - await _load_integration(self.hass, handler_key, self._hass_config) - if (handler := HANDLERS.get(handler_key)) is None: - raise data_entry_flow.UnknownHandler - + handler = await _async_get_flow_handler( + self.hass, handler_key, self._hass_config + ) if not context or "source" not in context: raise KeyError("Context not set or doesn't have a source set") @@ -1830,12 +1829,8 @@ class OptionsFlowManager(data_entry_flow.FlowManager): if entry is None: raise UnknownEntry(handler_key) - await _load_integration(self.hass, entry.domain, {}) - - if entry.domain not in HANDLERS: - raise data_entry_flow.UnknownHandler - - return HANDLERS[entry.domain].async_get_options_flow(entry) + handler = await _async_get_flow_handler(self.hass, entry.domain, {}) + return handler.async_get_options_flow(entry) async def async_finish_flow( self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult @@ -2021,9 +2016,15 @@ async def support_remove_from_device(hass: HomeAssistant, domain: str) -> bool: return hasattr(component, "async_remove_config_entry_device") -async def _load_integration( +async def _async_get_flow_handler( hass: HomeAssistant, domain: str, hass_config: ConfigType -) -> None: +) -> type[ConfigFlow]: + """Get a flow handler for specified domain.""" + + # First check if there is a handler registered for the domain + if domain in hass.config.components and (handler := HANDLERS.get(domain)): + return handler + try: integration = await loader.async_get_integration(hass, domain) except loader.IntegrationNotFound as err: @@ -2042,3 +2043,8 @@ async def _load_integration( err, ) raise data_entry_flow.UnknownHandler + + if handler := HANDLERS.get(domain): + return handler + + raise data_entry_flow.UnknownHandler diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 68e6fc59987..3485162cbb3 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1544,6 +1544,28 @@ async def test_init_custom_integration(hass: HomeAssistant) -> None: await hass.config_entries.flow.async_init("bla", context={"source": "user"}) +async def test_init_custom_integration_with_missing_handler( + hass: HomeAssistant, +) -> None: + """Test initializing flow for custom integration with a missing handler.""" + integration = loader.Integration( + hass, + "custom_components.hue", + None, + {"name": "Hue", "dependencies": [], "requirements": [], "domain": "hue"}, + ) + mock_integration( + hass, + MockModule("hue"), + ) + mock_entity_platform(hass, "config_flow.hue", None) + with pytest.raises(data_entry_flow.UnknownHandler), patch( + "homeassistant.loader.async_get_integration", + return_value=integration, + ): + await hass.config_entries.flow.async_init("bla", context={"source": "user"}) + + async def test_support_entry_unload(hass: HomeAssistant) -> None: """Test unloading entry.""" assert await config_entries.support_entry_unload(hass, "light") From 56257b7a3864697ed157c60f40295da5d7728428 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Aug 2023 20:25:18 -1000 Subject: [PATCH 0263/1151] Restore passive bluetooth entity data at startup (#97462) --- .../components/bluetooth/__init__.py | 3 +- .../bluetooth/passive_update_processor.py | 250 +++++++++++++- .../test_passive_update_processor.py | 324 +++++++++++++++++- 3 files changed, 560 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index bf4dbf81f01..2e0e62440ab 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -38,7 +38,7 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.issue_registry import async_delete_issue from homeassistant.loader import async_get_bluetooth -from . import models +from . import models, passive_update_processor from .api import ( _get_manager, async_address_present, @@ -125,6 +125,7 @@ async def _async_get_adapter_from_address( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" + await passive_update_processor.async_setup(hass) integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) integration_matcher.async_setup() bluetooth_adapters = get_adapters() diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 29634c9a18c..78965ae5cde 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -2,12 +2,31 @@ from __future__ import annotations import dataclasses +from datetime import timedelta +from functools import cache import logging -from typing import TYPE_CHECKING, Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypedDict, TypeVar, cast -from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant import config_entries +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_NAME, + CONF_ENTITY_CATEGORY, + EVENT_HOMEASSISTANT_STOP, + 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.event import async_track_time_interval +from homeassistant.helpers.storage import Store +from homeassistant.util.enum import try_parse_enum from .const import DOMAIN from .update_coordinator import BasePassiveBluetoothCoordinator @@ -23,6 +42,12 @@ if TYPE_CHECKING: BluetoothServiceInfoBleak, ) +STORAGE_KEY = "bluetooth.passive_update_processor" +STORAGE_VERSION = 1 +STORAGE_SAVE_INTERVAL = timedelta(minutes=15) +PASSIVE_UPDATE_PROCESSOR = "passive_update_processor" +_T = TypeVar("_T") + @dataclasses.dataclass(slots=True, frozen=True) class PassiveBluetoothEntityKey: @@ -36,8 +61,67 @@ class PassiveBluetoothEntityKey: key: str device_id: str | None + def to_string(self) -> str: + """Convert the key to a string which can be used as JSON key.""" + return f"{self.key}___{self.device_id or ''}" -_T = TypeVar("_T") + @classmethod + def from_string(cls, key: str) -> PassiveBluetoothEntityKey: + """Convert a string (from JSON) to a key.""" + key, device_id = key.split("___") + return cls(key, device_id or None) + + +@dataclasses.dataclass(slots=True, frozen=False) +class PassiveBluetoothProcessorData: + """Data for the passive bluetooth processor.""" + + coordinators: set[PassiveBluetoothProcessorCoordinator] + all_restore_data: dict[str, dict[str, RestoredPassiveBluetoothDataUpdate]] + + +class RestoredPassiveBluetoothDataUpdate(TypedDict): + """Restored PassiveBluetoothDataUpdate.""" + + devices: dict[str, DeviceInfo] + entity_descriptions: dict[str, dict[str, Any]] + entity_names: dict[str, str | None] + entity_data: dict[str, Any] + + +# Fields do not change so we can cache the result +# of calling fields() on the dataclass +cached_fields = cache(dataclasses.fields) + + +def deserialize_entity_description( + descriptions_class: type[EntityDescription], data: dict[str, Any] +) -> EntityDescription: + """Deserialize an entity description.""" + result: dict[str, Any] = {} + for field in cached_fields(descriptions_class): # type: ignore[arg-type] + field_name = field.name + # It would be nice if field.type returned the actual + # type instead of a str so we could avoid writing this + # out, but it doesn't. If we end up using this in more + # places we can add a `as_dict` and a `from_dict` + # method to these classes + if field_name == CONF_ENTITY_CATEGORY: + value = try_parse_enum(EntityCategory, data.get(field_name)) + else: + value = data.get(field_name) + result[field_name] = value + return descriptions_class(**result) + + +def serialize_entity_description(description: EntityDescription) -> dict[str, Any]: + """Serialize an entity description.""" + as_dict = dataclasses.asdict(description) + return { + field.name: as_dict[field.name] + for field in cached_fields(type(description)) # type: ignore[arg-type] + if field.default != as_dict.get(field.name) + } @dataclasses.dataclass(slots=True, frozen=True) @@ -62,6 +146,114 @@ class PassiveBluetoothDataUpdate(Generic[_T]): self.entity_data.update(new_data.entity_data) self.entity_names.update(new_data.entity_names) + def async_get_restore_data(self) -> RestoredPassiveBluetoothDataUpdate: + """Serialize restore data to storage.""" + return { + "devices": { + key or "": device_info for key, device_info in self.devices.items() + }, + "entity_descriptions": { + key.to_string(): serialize_entity_description(description) + for key, description in self.entity_descriptions.items() + }, + "entity_names": { + key.to_string(): name for key, name in self.entity_names.items() + }, + "entity_data": { + key.to_string(): data for key, data in self.entity_data.items() + }, + } + + @callback + def async_set_restore_data( + self, + restore_data: RestoredPassiveBluetoothDataUpdate, + entity_description_class: type[EntityDescription], + ) -> None: + """Set the restored data from storage.""" + self.devices.update( + { + key or None: device_info + for key, device_info in restore_data["devices"].items() + } + ) + self.entity_descriptions.update( + { + PassiveBluetoothEntityKey.from_string( + key + ): deserialize_entity_description(entity_description_class, description) + for key, description in restore_data["entity_descriptions"].items() + if description + } + ) + self.entity_names.update( + { + PassiveBluetoothEntityKey.from_string(key): name + for key, name in restore_data["entity_names"].items() + } + ) + self.entity_data.update( + { + PassiveBluetoothEntityKey.from_string(key): cast(_T, data) + for key, data in restore_data["entity_data"].items() + } + ) + + +def async_register_coordinator_for_restore( + hass: HomeAssistant, coordinator: PassiveBluetoothProcessorCoordinator +) -> CALLBACK_TYPE: + """Register a coordinator to have its processors data restored.""" + data: PassiveBluetoothProcessorData = hass.data[PASSIVE_UPDATE_PROCESSOR] + coordinators = data.coordinators + coordinators.add(coordinator) + if restore_key := coordinator.restore_key: + coordinator.restore_data = data.all_restore_data.setdefault(restore_key, {}) + + @callback + def _unregister_coordinator_for_restore() -> None: + """Unregister a coordinator.""" + coordinators.remove(coordinator) + + return _unregister_coordinator_for_restore + + +async def async_setup(hass: HomeAssistant) -> None: + """Set up the passive update processor coordinators.""" + storage: Store[dict[str, dict[str, RestoredPassiveBluetoothDataUpdate]]] = Store( + hass, STORAGE_VERSION, STORAGE_KEY + ) + coordinators: set[PassiveBluetoothProcessorCoordinator] = set() + all_restore_data: dict[str, dict[str, RestoredPassiveBluetoothDataUpdate]] = ( + await storage.async_load() or {} + ) + hass.data[PASSIVE_UPDATE_PROCESSOR] = PassiveBluetoothProcessorData( + coordinators, all_restore_data + ) + + async def _async_save_processor_data(_: Any) -> None: + """Save the processor data.""" + await storage.async_save( + { + coordinator.restore_key: coordinator.async_get_restore_data() + for coordinator in coordinators + if coordinator.restore_key + } + ) + + cancel_interval = async_track_time_interval( + hass, _async_save_processor_data, STORAGE_SAVE_INTERVAL + ) + + async def _async_save_processor_data_at_stop(_event: Event) -> None: + """Save the processor data at shutdown.""" + cancel_interval() + await _async_save_processor_data(None) + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_save_processor_data_at_stop + ) + class PassiveBluetoothProcessorCoordinator( Generic[_T], BasePassiveBluetoothCoordinator @@ -90,23 +282,49 @@ class PassiveBluetoothProcessorCoordinator( self._processors: list[PassiveBluetoothDataProcessor] = [] self._update_method = update_method self.last_update_success = True + self.restore_data: dict[str, RestoredPassiveBluetoothDataUpdate] = {} + self.restore_key = None + if config_entry := config_entries.current_entry.get(): + self.restore_key = config_entry.entry_id + self._on_stop.append(async_register_coordinator_for_restore(self.hass, self)) @property def available(self) -> bool: """Return if the device is available.""" return super().available and self.last_update_success + @callback + def async_get_restore_data( + self, + ) -> dict[str, RestoredPassiveBluetoothDataUpdate]: + """Generate the restore data.""" + return { + processor.restore_key: processor.data.async_get_restore_data() + for processor in self._processors + if processor.restore_key + } + @callback def async_register_processor( self, processor: PassiveBluetoothDataProcessor, + entity_description_class: type[EntityDescription] | None = None, ) -> Callable[[], None]: """Register a processor that subscribes to updates.""" - processor.async_register_coordinator(self) + + # entity_description_class will become mandatory + # in the future, but is optional for now to allow + # for a transition period. + processor.async_register_coordinator(self, entity_description_class) @callback def remove_processor() -> None: """Remove a processor.""" + # Save the data before removing the processor + # so if they reload its still there + if restore_key := processor.restore_key: + self.restore_data[restore_key] = processor.data.async_get_restore_data() + self._processors.remove(processor) self._processors.append(processor) @@ -182,12 +400,18 @@ class PassiveBluetoothDataProcessor(Generic[_T]): entity_data: dict[PassiveBluetoothEntityKey, _T] entity_descriptions: dict[PassiveBluetoothEntityKey, EntityDescription] devices: dict[str | None, DeviceInfo] + restore_key: str | None def __init__( self, update_method: Callable[[_T], PassiveBluetoothDataUpdate[_T]], + restore_key: str | None = None, ) -> None: """Initialize the coordinator.""" + try: + self.restore_key = restore_key or async_get_current_platform().domain + except RuntimeError: + self.restore_key = None self._listeners: list[ Callable[[PassiveBluetoothDataUpdate[_T] | None], None] ] = [] @@ -202,15 +426,29 @@ class PassiveBluetoothDataProcessor(Generic[_T]): def async_register_coordinator( self, coordinator: PassiveBluetoothProcessorCoordinator, + entity_description_class: type[EntityDescription] | None, ) -> None: """Register a coordinator.""" self.coordinator = coordinator self.data = PassiveBluetoothDataUpdate() data = self.data + # These attributes to access the data in + # self.data are for backwards compatibility. self.entity_names = data.entity_names self.entity_data = data.entity_data self.entity_descriptions = data.entity_descriptions self.devices = data.devices + if ( + entity_description_class + and (restore_key := self.restore_key) + and (restore_data := coordinator.restore_data) + and (restored_processor_data := restore_data.get(restore_key)) + ): + data.async_set_restore_data( + restored_processor_data, + entity_description_class, + ) + self.async_update_listeners(data) @property def available(self) -> bool: diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index a270b20855a..5906ab0bf25 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -1,15 +1,18 @@ """Tests for the Bluetooth integration.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging import time +from typing import Any from unittest.mock import MagicMock, patch from home_assistant_bluetooth import BluetoothServiceInfo import pytest from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass, BinarySensorEntityDescription, ) @@ -22,13 +25,19 @@ from homeassistant.components.bluetooth import ( ) from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS from homeassistant.components.bluetooth.passive_update_processor import ( + STORAGE_KEY, PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, PassiveBluetoothEntityKey, PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorEntityDescription, +) +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 @@ -41,7 +50,12 @@ from . import ( patch_all_discovered_devices, ) -from tests.common import MockEntityPlatform, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + MockEntityPlatform, + async_fire_time_changed, + async_test_home_assistant, +) _LOGGER = logging.getLogger(__name__) @@ -1092,6 +1106,18 @@ BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( ) +DEVICE_ONLY_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={}, + entity_names={}, + entity_descriptions={}, +) + + async def test_integration_multiple_entity_platforms( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, @@ -1118,21 +1144,21 @@ async def test_integration_multiple_entity_platforms( binary_sensor_processor = PassiveBluetoothDataProcessor( lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE ) - sesnor_processor = PassiveBluetoothDataProcessor( + sensor_processor = PassiveBluetoothDataProcessor( lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE ) coordinator.async_register_processor(binary_sensor_processor) - coordinator.async_register_processor(sesnor_processor) + coordinator.async_register_processor(sensor_processor) cancel_coordinator = coordinator.async_start() binary_sensor_processor.async_add_listener(MagicMock()) - sesnor_processor.async_add_listener(MagicMock()) + sensor_processor.async_add_listener(MagicMock()) mock_add_sensor_entities = MagicMock() mock_add_binary_sensor_entities = MagicMock() - sesnor_processor.async_add_entities_listener( + sensor_processor.async_add_entities_listener( PassiveBluetoothProcessorEntity, mock_add_sensor_entities, ) @@ -1146,14 +1172,14 @@ async def test_integration_multiple_entity_platforms( assert len(mock_add_binary_sensor_entities.mock_calls) == 1 assert len(mock_add_sensor_entities.mock_calls) == 1 - binary_sesnor_entities = [ + binary_sensor_entities = [ *mock_add_binary_sensor_entities.mock_calls[0][1][0], ] - sesnor_entities = [ + sensor_entities = [ *mock_add_sensor_entities.mock_calls[0][1][0], ] - sensor_entity_one: PassiveBluetoothProcessorEntity = sesnor_entities[0] + sensor_entity_one: PassiveBluetoothProcessorEntity = sensor_entities[0] sensor_entity_one.hass = hass assert sensor_entity_one.available is True assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" @@ -1167,7 +1193,7 @@ async def test_integration_multiple_entity_platforms( key="pressure", device_id=None ) - binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sesnor_entities[ + binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sensor_entities[ 0 ] binary_sensor_entity_one.hass = hass @@ -1242,3 +1268,281 @@ async def test_exception_from_coordinator_update_method( assert processor.available is True unregister_processor() cancel_coordinator() + + +async def test_integration_multiple_entity_platforms_with_reload_and_restart( + hass: HomeAssistant, + mock_bleak_scanner_start: MagicMock, + mock_bluetooth_adapters: None, + hass_storage: dict[str, Any], +) -> None: + """Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms with reload.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + entry = MockConfigEntry(domain=DOMAIN, data={}) + + @callback + def _mock_update_method( + service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + current_entry.set(entry) + coordinator = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, + ) + assert coordinator.available is False # no data yet + + binary_sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE, + BINARY_SENSOR_DOMAIN, + ) + sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE, SENSOR_DOMAIN + ) + + unregister_binary_sensor_processor = coordinator.async_register_processor( + binary_sensor_processor, BinarySensorEntityDescription + ) + unregister_sensor_processor = coordinator.async_register_processor( + sensor_processor, SensorEntityDescription + ) + cancel_coordinator = coordinator.async_start() + + binary_sensor_processor.async_add_listener(MagicMock()) + sensor_processor.async_add_listener(MagicMock()) + + mock_add_sensor_entities = MagicMock() + mock_add_binary_sensor_entities = MagicMock() + + sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_sensor_entities, + ) + binary_sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_binary_sensor_entities, + ) + + inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_binary_sensor_entities.mock_calls) == 1 + assert len(mock_add_sensor_entities.mock_calls) == 1 + + binary_sensor_entities = [ + *mock_add_binary_sensor_entities.mock_calls[0][1][0], + ] + sensor_entities = [ + *mock_add_sensor_entities.mock_calls[0][1][0], + ] + + sensor_entity_one: PassiveBluetoothProcessorEntity = sensor_entities[0] + sensor_entity_one.hass = hass + assert sensor_entity_one.available is True + assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" + assert sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="pressure", device_id=None + ) + + binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sensor_entities[ + 0 + ] + binary_sensor_entity_one.hass = hass + assert binary_sensor_entity_one.available is True + assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" + assert binary_sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="motion", device_id=None + ) + cancel_coordinator() + unregister_binary_sensor_processor() + unregister_sensor_processor() + + mock_add_sensor_entities = MagicMock() + mock_add_binary_sensor_entities = MagicMock() + + current_entry.set(entry) + coordinator = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, + ) + binary_sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: DEVICE_ONLY_PASSIVE_BLUETOOTH_DATA_UPDATE, + BINARY_SENSOR_DOMAIN, + ) + sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: DEVICE_ONLY_PASSIVE_BLUETOOTH_DATA_UPDATE, + SENSOR_DOMAIN, + ) + + sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_sensor_entities, + ) + binary_sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_binary_sensor_entities, + ) + + unregister_binary_sensor_processor = coordinator.async_register_processor( + binary_sensor_processor, BinarySensorEntityDescription + ) + unregister_sensor_processor = coordinator.async_register_processor( + sensor_processor, SensorEntityDescription + ) + cancel_coordinator = coordinator.async_start() + + assert len(mock_add_binary_sensor_entities.mock_calls) == 1 + assert len(mock_add_sensor_entities.mock_calls) == 1 + + binary_sensor_entities = [ + *mock_add_binary_sensor_entities.mock_calls[0][1][0], + ] + sensor_entities = [ + *mock_add_sensor_entities.mock_calls[0][1][0], + ] + + sensor_entity_one: PassiveBluetoothProcessorEntity = sensor_entities[0] + sensor_entity_one.hass = hass + assert sensor_entity_one.available is True + assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" + assert sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="pressure", device_id=None + ) + + binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sensor_entities[ + 0 + ] + binary_sensor_entity_one.hass = hass + assert binary_sensor_entity_one.available is True + assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" + assert binary_sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="motion", device_id=None + ) + + await hass.async_stop() + await hass.async_block_till_done() + + assert SENSOR_DOMAIN in hass_storage[STORAGE_KEY]["data"][entry.entry_id] + assert BINARY_SENSOR_DOMAIN in hass_storage[STORAGE_KEY]["data"][entry.entry_id] + + # We don't normally cancel or unregister these at stop, + # but since we are mocking a restart we need to cleanup + cancel_coordinator() + unregister_binary_sensor_processor() + unregister_sensor_processor() + + hass = await async_test_home_assistant(asyncio.get_running_loop()) + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + current_entry.set(entry) + coordinator = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, + ) + assert coordinator.available is False # no data yet + + mock_add_sensor_entities = MagicMock() + mock_add_binary_sensor_entities = MagicMock() + + binary_sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: DEVICE_ONLY_PASSIVE_BLUETOOTH_DATA_UPDATE, + BINARY_SENSOR_DOMAIN, + ) + sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: DEVICE_ONLY_PASSIVE_BLUETOOTH_DATA_UPDATE, + SENSOR_DOMAIN, + ) + + sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_sensor_entities, + ) + binary_sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_binary_sensor_entities, + ) + + unregister_binary_sensor_processor = coordinator.async_register_processor( + binary_sensor_processor, BinarySensorEntityDescription + ) + unregister_sensor_processor = coordinator.async_register_processor( + sensor_processor, SensorEntityDescription + ) + cancel_coordinator = coordinator.async_start() + + assert len(mock_add_binary_sensor_entities.mock_calls) == 1 + assert len(mock_add_sensor_entities.mock_calls) == 1 + + binary_sensor_entities = [ + *mock_add_binary_sensor_entities.mock_calls[0][1][0], + ] + sensor_entities = [ + *mock_add_sensor_entities.mock_calls[0][1][0], + ] + + sensor_entity_one: PassiveBluetoothProcessorEntity = sensor_entities[0] + sensor_entity_one.hass = hass + assert sensor_entity_one.available is False # service data not injected + assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" + assert sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="pressure", device_id=None + ) + + binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sensor_entities[ + 0 + ] + binary_sensor_entity_one.hass = hass + assert binary_sensor_entity_one.available is False # service data not injected + assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" + assert binary_sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="motion", device_id=None + ) + cancel_coordinator() + unregister_binary_sensor_processor() + unregister_sensor_processor() + await hass.async_stop() From 001dda63450cd971902cb226b9176481d149ad6a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 7 Aug 2023 09:42:20 +0200 Subject: [PATCH 0264/1151] Fix weather entities with update_before_add (#97950) Fix weather entities update_before_add --- homeassistant/components/weather/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index f0c32f2d8cc..bfad18cb84a 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1048,6 +1048,12 @@ 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 d72057f41b4306b1e65efa567d04a7e3e482991d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 7 Aug 2023 10:52:14 +0100 Subject: [PATCH 0265/1151] 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 0266/1151] 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 0267/1151] 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 0268/1151] 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 0269/1151] 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 0270/1151] 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 0271/1151] 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 0272/1151] 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 0273/1151] 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 0274/1151] 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 0275/1151] 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 0276/1151] 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 0277/1151] 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 0278/1151] 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 0279/1151] 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 0280/1151] 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 0281/1151] 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 0282/1151] 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 0283/1151] 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 0284/1151] 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 0285/1151] 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 0286/1151] 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 0287/1151] 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 0288/1151] 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 0289/1151] 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 0290/1151] 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 0291/1151] 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 0292/1151] 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 0293/1151] 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 0294/1151] 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 0295/1151] 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 0296/1151] 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 0297/1151] 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 0298/1151] 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 0299/1151] 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 0300/1151] 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 0301/1151] 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 0302/1151] 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 0303/1151] 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 0304/1151] 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 0305/1151] 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 0306/1151] 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 0307/1151] 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 0308/1151] 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 0309/1151] 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 0310/1151] 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 0311/1151] 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 0312/1151] 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 0313/1151] 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 0314/1151] 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 0315/1151] 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 0316/1151] 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 0317/1151] 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 0318/1151] 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 0319/1151] 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 0320/1151] 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 0321/1151] 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 0322/1151] 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 0323/1151] 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 0324/1151] 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 0325/1151] 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 0326/1151] 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 0327/1151] 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 0328/1151] 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 0329/1151] 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 0330/1151] 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 0331/1151] 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 0332/1151] 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 0333/1151] 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 0334/1151] 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 0335/1151] 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 0336/1151] 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 0337/1151] 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 0338/1151] 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 0339/1151] 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 0340/1151] 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 0341/1151] 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 0342/1151] 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 0343/1151] 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 0344/1151] 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 0345/1151] 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 0346/1151] 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 0347/1151] 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 0348/1151] 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 0349/1151] 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 0350/1151] 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 0351/1151] 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 0352/1151] 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 0353/1151] 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 0354/1151] 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 0355/1151] 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 0356/1151] 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 0357/1151] 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 0358/1151] 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 0359/1151] 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 0360/1151] 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 0361/1151] 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 0362/1151] 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 0363/1151] 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 0364/1151] 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 0365/1151] 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 0366/1151] 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 0367/1151] 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 0368/1151] 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 0369/1151] 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 0370/1151] 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 0371/1151] 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 0372/1151] 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 0373/1151] 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 0374/1151] 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 0375/1151] 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 0376/1151] 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 0377/1151] 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 0378/1151] 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 0379/1151] 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 0380/1151] 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 0381/1151] 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 0382/1151] 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 0383/1151] 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 0384/1151] 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 0385/1151] 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 0386/1151] 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 0387/1151] 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 0388/1151] 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 0389/1151] 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 0390/1151] 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 0391/1151] 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 0392/1151] 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 0393/1151] 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 0394/1151] 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 0395/1151] 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 0396/1151] 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 0397/1151] 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 0398/1151] 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 0399/1151] 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 0400/1151] 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 0401/1151] 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 0402/1151] 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 0403/1151] 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 0404/1151] 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 0405/1151] 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 0406/1151] 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 0407/1151] 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 0408/1151] 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 0409/1151] 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 0410/1151] 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 0411/1151] 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 0412/1151] 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 0413/1151] 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 0414/1151] 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 0415/1151] 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 0416/1151] 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 0417/1151] 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 0418/1151] 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 0419/1151] 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 0420/1151] 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 0421/1151] 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 0422/1151] 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 0423/1151] 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 0424/1151] 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 0425/1151] 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 0426/1151] 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 0427/1151] 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 0428/1151] 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 0429/1151] 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 0430/1151] 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 0431/1151] 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 0432/1151] 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 0433/1151] 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 0434/1151] 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 0435/1151] 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 0436/1151] 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 0437/1151] 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 0438/1151] 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 0439/1151] 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 0440/1151] 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 0441/1151] 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 0442/1151] 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 0443/1151] 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) From 8912b19cf4c36278419e63b7263c858b640bbb0c Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Fri, 11 Aug 2023 20:15:42 -0400 Subject: [PATCH 0444/1151] Add Enphase Encharge aggregate sensors (#98276) * Add Encharge aggregate sensors * Update dependency --- .../components/enphase_envoy/manifest.json | 2 +- .../components/enphase_envoy/sensor.py | 71 +++++++++++++++++++ .../components/enphase_envoy/strings.json | 15 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 89 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index f500ac538e7..6969dc3d6ab 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.4.0"], + "requirements": ["pyenphase==1.5.2"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 9ecc205522a..0e4a9b71232 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -8,6 +8,7 @@ import logging from pyenphase import ( EnvoyEncharge, + EnvoyEnchargeAggregate, EnvoyEnchargePower, EnvoyEnpower, EnvoyInverter, @@ -288,6 +289,58 @@ ENPOWER_SENSORS = ( ) +@dataclass +class EnvoyEnchargeAggregateRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyEnchargeAggregate], int] + + +@dataclass +class EnvoyEnchargeAggregateSensorEntityDescription( + SensorEntityDescription, EnvoyEnchargeAggregateRequiredKeysMixin +): + """Describes an Envoy Encharge sensor entity.""" + + +ENCHARGE_AGGREGATE_SENSORS = ( + EnvoyEnchargeAggregateSensorEntityDescription( + key="battery_level", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + value_fn=lambda encharge: encharge.state_of_charge, + ), + EnvoyEnchargeAggregateSensorEntityDescription( + key="reserve_soc", + translation_key="reserve_soc", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + value_fn=lambda encharge: encharge.reserve_state_of_charge, + ), + EnvoyEnchargeAggregateSensorEntityDescription( + key="available_energy", + translation_key="available_energy", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value_fn=lambda encharge: encharge.available_energy, + ), + EnvoyEnchargeAggregateSensorEntityDescription( + key="reserve_energy", + translation_key="reserve_energy", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value_fn=lambda encharge: encharge.backup_reserve, + ), + EnvoyEnchargeAggregateSensorEntityDescription( + key="max_capacity", + translation_key="max_capacity", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value_fn=lambda encharge: encharge.max_available_capacity, + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -329,6 +382,11 @@ async def async_setup_entry( for description in ENCHARGE_POWER_SENSORS for encharge in envoy_data.encharge_power ) + if envoy_data.encharge_aggregate: + entities.extend( + EnvoyEnchargeAggregateEntity(coordinator, description) + for description in ENCHARGE_AGGREGATE_SENSORS + ) if envoy_data.enpower: entities.extend( EnvoyEnpowerEntity(coordinator, description) @@ -482,6 +540,19 @@ class EnvoyEnchargePowerEntity(EnvoyEnchargeEntity): return self.entity_description.value_fn(encharge_power[self._serial_number]) +class EnvoyEnchargeAggregateEntity(EnvoySystemSensorEntity): + """Envoy Encharge Aggregate sensor entity.""" + + entity_description: EnvoyEnchargeAggregateSensorEntityDescription + + @property + def native_value(self) -> int: + """Return the state of the aggregate sensors.""" + encharge_aggregate = self.data.encharge_aggregate + assert encharge_aggregate is not None + return self.entity_description.value_fn(encharge_aggregate) + + class EnvoyEnpowerEntity(EnvoySensorBaseEntity): """Envoy Enpower sensor entity.""" diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index f42e44d7afa..2afd19d87d1 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -63,6 +63,21 @@ }, "lifetime_consumption": { "name": "Lifetime energy consumption" + }, + "reserve_soc": { + "name": "Reserve battery level" + }, + "available_energy": { + "name": "Available battery energy" + }, + "reserve_energy": { + "name": "Reserve battery energy" + }, + "max_capacity": { + "name": "Battery capacity" + }, + "configured_reserve_soc": { + "name": "Configured reserve battery level" } }, "switch": { diff --git a/requirements_all.txt b/requirements_all.txt index 4f99f44c9f1..8eae7bc175f 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.4.0 +pyenphase==1.5.2 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc1bc38b233..7048126d3a8 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.4.0 +pyenphase==1.5.2 # homeassistant.components.everlights pyeverlights==0.1.0 From 8b99d4678f54c439435137c19fec6e7ea354f8de Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Aug 2023 09:10:25 +0200 Subject: [PATCH 0445/1151] Correct checks for non-finite numbers in ESPHome (#98102) --- homeassistant/components/esphome/entity.py | 4 ++-- homeassistant/components/esphome/number.py | 2 +- homeassistant/components/esphome/sensor.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 57ae33beb15..8b69d011804 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -105,8 +105,8 @@ def esphome_state_property( if not self._has_state: return None val = func(self) - if isinstance(val, float) and math.isnan(val): - # Home Assistant doesn't use NAN values in state machine + if isinstance(val, float) and not math.isfinite(val): + # Home Assistant doesn't use NaN or inf values in state machine # (not JSON serializable) return None return val diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 4f3109f5a83..bc694ec39cf 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -73,7 +73,7 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): def native_value(self) -> float | None: """Return the state of the entity.""" state = self._state - if state.missing_state or math.isnan(state.state): + if state.missing_state or not math.isfinite(state.state): return None return state.state diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index af873565fc3..efc77ff53b8 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -96,7 +96,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): def native_value(self) -> datetime | str | None: """Return the state of the entity.""" state = self._state - if math.isnan(state.state) or state.missing_state: + if state.missing_state or not math.isfinite(state.state): return None if self._attr_device_class == SensorDeviceClass.TIMESTAMP: return dt_util.utc_from_timestamp(state.state) From 5042c25bbc84dbc8910c7f171ec4793c749cdf71 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sat, 12 Aug 2023 09:56:23 +0200 Subject: [PATCH 0446/1151] Plugwise climate: remove extra_state_attributes property (#98153) * Remove extra_state_attributes property, replaced by a number * Support HVAC_Mode in set_temperature() * Remove set_temperature() update, as requested --- homeassistant/components/plugwise/climate.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 5be09a062e2..e83b76a76da 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -1,7 +1,6 @@ """Plugwise Climate component for Home Assistant.""" from __future__ import annotations -from collections.abc import Mapping from typing import Any from homeassistant.components.climate import ( @@ -145,14 +144,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): """Return the current preset mode.""" return self.device.get("active_preset") - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return entity specific state attributes.""" - return { - "available_schemas": self.device["available_schedules"], - "selected_schema": self.device["select_schedule"], - } - @plugwise_command async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" From d6498aa39e40f5aa34707533d440736f7b19b963 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Sat, 12 Aug 2023 10:27:45 +0200 Subject: [PATCH 0447/1151] Fix fanSpeed issue (#98293) --- homeassistant/components/tado/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index b57d384124c..0ef6dc17934 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -327,7 +327,7 @@ class TadoConnector: device_type, "ON", mode, - fanSpeed=fan_speed, + fan_speed=fan_speed, swing=swing, ) From be9afd7eae85f567e3bee6e99ff4f8ca625a4261 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 12 Aug 2023 11:03:37 +0200 Subject: [PATCH 0448/1151] Add entity translations to DWD (#98254) * Add device to DWD * Add entity translations to DWD --- .../components/dwd_weather_warnings/sensor.py | 6 +++--- .../components/dwd_weather_warnings/strings.json | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 7bc683d245d..78154e9e4f4 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -56,12 +56,12 @@ from .coordinator import DwdWeatherWarningsCoordinator SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=CURRENT_WARNING_SENSOR, - name="Current Warning Level", + translation_key=CURRENT_WARNING_SENSOR, icon="mdi:close-octagon-outline", ), SensorEntityDescription( key=ADVANCE_WARNING_SENSOR, - name="Advance Warning Level", + translation_key=ADVANCE_WARNING_SENSOR, icon="mdi:close-octagon-outline", ), ) @@ -131,6 +131,7 @@ class DwdWeatherWarningsSensor( """Representation of a DWD-Weather-Warnings sensor.""" _attr_attribution = "Data provided by DWD" + _attr_has_entity_name = True def __init__( self, @@ -142,7 +143,6 @@ class DwdWeatherWarningsSensor( super().__init__(coordinator) self.entity_description = description - 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( diff --git a/homeassistant/components/dwd_weather_warnings/strings.json b/homeassistant/components/dwd_weather_warnings/strings.json index 60e53f90dbd..dc73055174b 100644 --- a/homeassistant/components/dwd_weather_warnings/strings.json +++ b/homeassistant/components/dwd_weather_warnings/strings.json @@ -15,5 +15,15 @@ "already_configured": "Warncell ID / name is already configured.", "invalid_identifier": "[%key:component::dwd_weather_warnings::config::error::invalid_identifier%]" } + }, + "entity": { + "sensor": { + "current_warning_level": { + "name": "Current warning level" + }, + "advance_warning_level": { + "name": "Advance warning level" + } + } } } From ae8f9dcb7749dcd8646f4904fb026a1dbfa8539f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Aug 2023 15:15:09 +0200 Subject: [PATCH 0449/1151] Modernize ipma weather (#98062) * Modernize ipma weather * Add test snapshots * Don't include forecast mode in weather entity unique_id for new config entries * Remove old migration code * Remove outdated test --- homeassistant/components/ipma/config_flow.py | 5 +- homeassistant/components/ipma/const.py | 2 - homeassistant/components/ipma/weather.py | 115 ++++++++++++------ .../ipma/snapshots/test_weather.ambr | 104 ++++++++++++++++ tests/components/ipma/test_config_flow.py | 60 +-------- tests/components/ipma/test_weather.py | 100 ++++++++++++++- 6 files changed, 282 insertions(+), 104 deletions(-) create mode 100644 tests/components/ipma/snapshots/test_weather.ambr diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index eb361d3f9d5..9434aed3097 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -2,10 +2,10 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, FORECAST_MODE, HOME_LOCATION_NAME +from .const import DOMAIN, HOME_LOCATION_NAME class IpmaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -47,7 +47,6 @@ class IpmaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_NAME, default=name): str, vol.Required(CONF_LATITUDE, default=latitude): cv.latitude, vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude, - vol.Required(CONF_MODE, default="daily"): vol.In(FORECAST_MODE), } ), errors=self._errors, diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py index 2d715011e43..c7482770f48 100644 --- a/homeassistant/components/ipma/const.py +++ b/homeassistant/components/ipma/const.py @@ -49,6 +49,4 @@ CONDITION_CLASSES = { ATTR_CONDITION_CLEAR_NIGHT: [-1], } -FORECAST_MODE = ["hourly", "daily"] - ATTRIBUTION = "Instituto Português do Mar e Atmosfera" diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 811eddf91bf..b8e994a7500 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -1,11 +1,14 @@ """Support for IPMA weather service.""" from __future__ import annotations +import asyncio +import contextlib import logging +from typing import Literal import async_timeout from pyipma.api import IPMA_API -from pyipma.forecast import Forecast +from pyipma.forecast import Forecast as IPMAForecast from pyipma.location import Location from homeassistant.components.weather import ( @@ -16,7 +19,9 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -26,8 +31,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import is_up from homeassistant.util import Throttle @@ -53,39 +57,19 @@ async def async_setup_entry( """Add a weather entity from a config_entry.""" api = hass.data[DOMAIN][config_entry.entry_id][DATA_API] location = hass.data[DOMAIN][config_entry.entry_id][DATA_LOCATION] - mode = config_entry.data[CONF_MODE] - - # Migrate old unique_id - @callback - def _async_migrator(entity_entry: er.RegistryEntry): - # Reject if new unique_id - if entity_entry.unique_id.count(",") == 2: - return None - - new_unique_id = ( - f"{location.station_latitude}, {location.station_longitude}, {mode}" - ) - - _LOGGER.info( - "Migrating unique_id from [%s] to [%s]", - entity_entry.unique_id, - new_unique_id, - ) - return {"new_unique_id": new_unique_id} - - await er.async_migrate_entries(hass, config_entry.entry_id, _async_migrator) - async_add_entities([IPMAWeather(location, api, config_entry.data)], True) class IPMAWeather(WeatherEntity, IPMADevice): """Representation of a weather condition.""" + _attr_attribution = ATTRIBUTION _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR - - _attr_attribution = ATTRIBUTION + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) def __init__(self, location: Location, api: IPMA_API, config) -> None: """Initialise the platform with a data instance and station name.""" @@ -95,25 +79,35 @@ class IPMAWeather(WeatherEntity, IPMADevice): self._mode = config.get(CONF_MODE) self._period = 1 if config.get(CONF_MODE) == "hourly" else 24 self._observation = None - self._forecast: list[Forecast] = [] - self._attr_unique_id = f"{self._location.station_latitude}, {self._location.station_longitude}, {self._mode}" + self._daily_forecast: list[IPMAForecast] | None = None + self._hourly_forecast: list[IPMAForecast] | None = None + if self._mode is not None: + self._attr_unique_id = f"{self._location.station_latitude}, {self._location.station_longitude}, {self._mode}" + else: + self._attr_unique_id = ( + f"{self._location.station_latitude}, {self._location.station_longitude}" + ) @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Update Condition and Forecast.""" async with async_timeout.timeout(10): new_observation = await self._location.observation(self._api) - new_forecast = await self._location.forecast(self._api, self._period) if new_observation: self._observation = new_observation else: _LOGGER.warning("Could not update weather observation") - if new_forecast: - self._forecast = new_forecast + if self._period == 24 or self._forecast_listeners["daily"]: + await self._update_forecast("daily", 24, True) else: - _LOGGER.warning("Could not update weather forecast") + self._daily_forecast = None + + if self._period == 1 or self._forecast_listeners["hourly"]: + await self._update_forecast("hourly", 1, True) + else: + self._hourly_forecast = None _LOGGER.debug( "Updated location %s based on %s, current observation %s", @@ -122,6 +116,21 @@ class IPMAWeather(WeatherEntity, IPMADevice): self._observation, ) + async def _update_forecast( + self, + forecast_type: Literal["daily", "hourly"], + period: int, + update_listeners: bool, + ) -> None: + """Update weather forecast.""" + new_forecast = await self._location.forecast(self._api, period) + if new_forecast: + setattr(self, f"_{forecast_type}_forecast", new_forecast) + if update_listeners: + await self.async_update_listeners((forecast_type,)) + else: + _LOGGER.warning("Could not update %s weather forecast", forecast_type) + def _condition_conversion(self, identifier, forecast_dt): """Convert from IPMA weather_type id to HA.""" if identifier == 1 and not is_up(self.hass, forecast_dt): @@ -135,10 +144,12 @@ class IPMAWeather(WeatherEntity, IPMADevice): @property def condition(self): """Return the current condition.""" - if not self._forecast: + forecast = self._hourly_forecast or self._daily_forecast + + if not forecast: return - return self._condition_conversion(self._forecast[0].weather_type.id, None) + return self._condition_conversion(forecast[0].weather_type.id, None) @property def native_temperature(self): @@ -180,10 +191,9 @@ class IPMAWeather(WeatherEntity, IPMADevice): return self._observation.wind_direction - @property - def forecast(self): + def _forecast(self, forecast: list[IPMAForecast] | None) -> list[Forecast]: """Return the forecast array.""" - if not self._forecast: + if not forecast: return [] return [ @@ -198,5 +208,32 @@ class IPMAWeather(WeatherEntity, IPMADevice): ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } - for data_in in self._forecast + for data_in in forecast ] + + @property + def forecast(self) -> list[Forecast]: + """Return the forecast array.""" + return self._forecast( + self._hourly_forecast if self._period == 1 else self._daily_forecast + ) + + async def _try_update_forecast( + self, + forecast_type: Literal["daily", "hourly"], + period: int, + ) -> None: + """Try to update weather forecast.""" + with contextlib.suppress(asyncio.TimeoutError): + async with async_timeout.timeout(10): + await self._update_forecast(forecast_type, period, False) + + async def async_forecast_daily(self) -> list[Forecast]: + """Return the daily forecast in native units.""" + await self._try_update_forecast("daily", 24) + return self._forecast(self._daily_forecast) + + async def async_forecast_hourly(self) -> list[Forecast]: + """Return the hourly forecast in native units.""" + await self._try_update_forecast("hourly", 1) + return self._forecast(self._hourly_forecast) diff --git a/tests/components/ipma/snapshots/test_weather.ambr b/tests/components/ipma/snapshots/test_weather.ambr new file mode 100644 index 00000000000..92e1d1a91b5 --- /dev/null +++ b/tests/components/ipma/snapshots/test_weather.ambr @@ -0,0 +1,104 @@ +# serializer version: 1 +# name: test_forecast_service + dict({ + 'forecast': list([ + dict({ + 'condition': 'rainy', + 'datetime': datetime.datetime(2020, 1, 16, 0, 0), + 'precipitation_probability': '100.0', + 'temperature': 16.2, + 'templow': 10.6, + 'wind_bearing': 'S', + 'wind_speed': 10.0, + }), + ]), + }) +# --- +# name: test_forecast_service.1 + dict({ + 'forecast': list([ + dict({ + 'condition': 'rainy', + 'datetime': datetime.datetime(2020, 1, 15, 1, 0, tzinfo=datetime.timezone.utc), + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + dict({ + 'condition': 'clear-night', + 'datetime': datetime.datetime(2020, 1, 15, 2, 0, tzinfo=datetime.timezone.utc), + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + ]), + }) +# --- +# name: test_forecast_subscription[daily] + list([ + dict({ + 'condition': 'rainy', + 'datetime': '2020-01-16T00:00:00', + 'precipitation_probability': '100.0', + 'temperature': 16.2, + 'templow': 10.6, + 'wind_bearing': 'S', + 'wind_speed': 10.0, + }), + ]) +# --- +# name: test_forecast_subscription[daily].1 + list([ + dict({ + 'condition': 'rainy', + 'datetime': '2020-01-16T00:00:00', + 'precipitation_probability': '100.0', + 'temperature': 16.2, + 'templow': 10.6, + 'wind_bearing': 'S', + 'wind_speed': 10.0, + }), + ]) +# --- +# name: test_forecast_subscription[hourly] + list([ + dict({ + 'condition': 'rainy', + 'datetime': '2020-01-15T01:00:00+00:00', + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2020-01-15T02:00:00+00:00', + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + ]) +# --- +# name: test_forecast_subscription[hourly].1 + list([ + dict({ + 'condition': 'rainy', + 'datetime': '2020-01-15T01:00:00+00:00', + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2020-01-15T02:00:00+00:00', + 'precipitation_probability': 80.0, + 'temperature': 12.0, + 'wind_bearing': 'S', + 'wind_speed': 32.7, + }), + ]) +# --- diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index f9d69ec41ae..5bb1d8b8364 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,15 +1,9 @@ """Tests for IPMA config flow.""" from unittest.mock import Mock, patch -from homeassistant.components.ipma import DOMAIN, config_flow -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE +from homeassistant.components.ipma import config_flow +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.setup import async_setup_component - -from . import MockLocation - -from tests.common import MockConfigEntry, mock_registry async def test_show_config_form() -> None: @@ -120,53 +114,3 @@ async def test_flow_entry_config_entry_already_exists() -> None: assert len(config_form.mock_calls) == 1 assert len(config_entries.mock_calls) == 1 assert len(flow._errors) == 1 - - -async def test_config_entry_migration(hass: HomeAssistant) -> None: - """Tests config entry without mode in unique_id can be migrated.""" - ipma_entry = MockConfigEntry( - domain=DOMAIN, - title="Home", - data={CONF_LATITUDE: 0, CONF_LONGITUDE: 0, CONF_MODE: "daily"}, - ) - ipma_entry.add_to_hass(hass) - - ipma_entry2 = MockConfigEntry( - domain=DOMAIN, - title="Home", - data={CONF_LATITUDE: 0, CONF_LONGITUDE: 0, CONF_MODE: "hourly"}, - ) - ipma_entry2.add_to_hass(hass) - - mock_registry( - hass, - { - "weather.hometown": er.RegistryEntry( - entity_id="weather.hometown", - unique_id="0, 0", - platform="ipma", - config_entry_id=ipma_entry.entry_id, - ), - "weather.hometown_2": er.RegistryEntry( - entity_id="weather.hometown_2", - unique_id="0, 0, hourly", - platform="ipma", - config_entry_id=ipma_entry.entry_id, - ), - }, - ) - - with patch( - "pyipma.location.Location.get", - return_value=MockLocation(), - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - ent_reg = er.async_get(hass) - - weather_home = ent_reg.async_get("weather.hometown") - assert weather_home.unique_id == "0, 0, daily" - - weather_home2 = ent_reg.async_get("weather.hometown_2") - assert weather_home2.unique_id == "0, 0, hourly" diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 285f7ceacb7..71884e0c82e 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -1,9 +1,12 @@ """The tests for the IPMA weather component.""" -from datetime import datetime +import datetime from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest +from syrupy.assertion import SnapshotAssertion +from homeassistant.components.ipma.const import MIN_TIME_BETWEEN_UPDATES from homeassistant.components.weather import ( ATTR_FORECAST, ATTR_FORECAST_CONDITION, @@ -18,6 +21,8 @@ from homeassistant.components.weather import ( ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, + SERVICE_GET_FORECAST, ) from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -25,6 +30,7 @@ from homeassistant.core import HomeAssistant from . import MockLocation from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator TEST_CONFIG = { "name": "HomeTown", @@ -91,7 +97,7 @@ async def test_daily_forecast(hass: HomeAssistant) -> None: assert state.state == "rainy" forecast = state.attributes.get(ATTR_FORECAST)[0] - assert forecast.get(ATTR_FORECAST_TIME) == datetime(2020, 1, 16, 0, 0, 0) + assert forecast.get(ATTR_FORECAST_TIME) == datetime.datetime(2020, 1, 16, 0, 0, 0) assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" assert forecast.get(ATTR_FORECAST_TEMP) == 16.2 assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6 @@ -144,3 +150,93 @@ async def test_failed_get_observation_forecast(hass: HomeAssistant) -> None: assert data.get(ATTR_WEATHER_WIND_SPEED) is None assert data.get(ATTR_WEATHER_WIND_BEARING) is None assert state.attributes.get("friendly_name") == "HomeTown" + + +async def test_forecast_service( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test multiple forecast.""" + + with patch( + "pyipma.location.Location.get", + return_value=MockLocation(), + ): + entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) + 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": "weather.hometown", + "type": "daily", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": "weather.hometown", + "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, + snapshot: SnapshotAssertion, + forecast_type: str, +) -> None: + """Test multiple forecast.""" + client = await hass_ws_client(hass) + + with patch( + "pyipma.location.Location.get", + return_value=MockLocation(), + ): + entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": forecast_type, + "entity_id": "weather.hometown", + } + ) + 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 + + freezer.tick(MIN_TIME_BETWEEN_UPDATES + 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 87753bdb825d3da601736fe46e4ad3d16c4b972a Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 12 Aug 2023 09:12:59 -0700 Subject: [PATCH 0450/1151] Add UniFi power stats for PDU overall AC outlet metrics (#98217) --- homeassistant/components/unifi/manifest.json | 2 +- homeassistant/components/unifi/sensor.py | 48 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_sensor.py | 91 ++++++++++++++++---- 5 files changed, 126 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 3b1fa68638b..8f27263b288 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==52"], + "requirements": ["aiounifi==53"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 367ff1332f4..142bd587853 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -12,11 +12,13 @@ from typing import Generic from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients +from aiounifi.interfaces.devices import Devices 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.device import Device from aiounifi.models.outlet import Outlet from aiounifi.models.port import Port from aiounifi.models.wlan import Wlan @@ -96,6 +98,12 @@ def async_device_outlet_power_supported_fn( return controller.api.outlets[obj_id].caps == 3 +@callback +def async_device_outlet_supported_fn(controller: UniFiController, obj_id: str) -> bool: + """Determine if a device supports reading overall power metrics.""" + return controller.api.devices[obj_id].outlet_ac_power_budget is not None + + @dataclass class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): """Validate and load entities from different UniFi handlers.""" @@ -224,6 +232,46 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( unique_id_fn=lambda controller, obj_id: f"outlet_power-{obj_id}", value_fn=lambda _, obj: obj.power if obj.relay_state else "0", ), + UnifiSensorEntityDescription[Devices, Device]( + key="SmartPower AC power budget", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=1, + has_entity_name=True, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.devices, + 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 device: "AC Power Budget", + object_fn=lambda api, obj_id: api.devices[obj_id], + should_poll=False, + supported_fn=async_device_outlet_supported_fn, + unique_id_fn=lambda controller, obj_id: f"ac_power_budget-{obj_id}", + value_fn=lambda controller, device: device.outlet_ac_power_budget, + ), + UnifiSensorEntityDescription[Devices, Device]( + key="SmartPower AC power consumption", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_display_precision=1, + has_entity_name=True, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.devices, + 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 device: "AC Power Consumption", + object_fn=lambda api, obj_id: api.devices[obj_id], + should_poll=False, + supported_fn=async_device_outlet_supported_fn, + unique_id_fn=lambda controller, obj_id: f"ac_power_conumption-{obj_id}", + value_fn=lambda controller, device: device.outlet_ac_power_consumption, + ), ) diff --git a/requirements_all.txt b/requirements_all.txt index 8eae7bc175f..aa512d9ffd5 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==52 +aiounifi==53 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7048126d3a8..3818300ea82 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==52 +aiounifi==53 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 98a4941caaa..359825514d7 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -278,6 +278,27 @@ PDU_DEVICE_1 = { "x_has_ssh_hostkey": True, } +PDU_OUTLETS_UPDATE_DATA = [ + { + "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": "123.45", + "outlet_power_factor": "0.659", + }, +] + async def test_no_clients( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker @@ -719,31 +740,69 @@ async def test_wlan_client_sensors( assert hass.states.get("sensor.ssid_1").state == "0" +@pytest.mark.parametrize( + ( + "entity_id", + "expected_unique_id", + "expected_value", + "changed_data", + "expected_update_value", + ), + [ + ( + "dummy_usp_pdu_pro_outlet_2_outlet_power", + "outlet_power-01:02:03:04:05:ff_2", + "73.827", + {"outlet_table": PDU_OUTLETS_UPDATE_DATA}, + "123.45", + ), + ( + "dummy_usp_pdu_pro_ac_power_budget", + "ac_power_budget-01:02:03:04:05:ff", + "1875.000", + None, + None, + ), + ( + "dummy_usp_pdu_pro_ac_power_consumption", + "ac_power_conumption-01:02:03:04:05:ff", + "201.683", + {"outlet_ac_power_consumption": "456.78"}, + "456.78", + ), + ], +) async def test_outlet_power_readings( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_unifi_websocket, + entity_id: str, + expected_unique_id: str, + expected_value: any, + changed_data: dict | None, + expected_update_value: any, ) -> 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 + assert len(hass.states.async_all()) == 7 + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 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" + ent_reg_entry = ent_reg.async_get(f"sensor.{entity_id}") + assert ent_reg_entry.unique_id == expected_unique_id 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" + sensor_data = hass.states.get(f"sensor.{entity_id}") + assert sensor_data.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + assert sensor_data.state == expected_value - # Verify state update - pdu_device_state_update = deepcopy(PDU_DEVICE_1) + if changed_data is not None: + updated_device_data = deepcopy(PDU_DEVICE_1) + updated_device_data.update(changed_data) - pdu_device_state_update["outlet_table"][1]["outlet_power"] = "123.45" + mock_unifi_websocket(message=MessageKey.DEVICE, data=updated_device_data) + await hass.async_block_till_done() - 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" + sensor_data = hass.states.get(f"sensor.{entity_id}") + assert sensor_data.state == expected_update_value From 4780ea6a5b8feb38bd43d6e3dd6c4030d3768484 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 12 Aug 2023 12:31:14 -0400 Subject: [PATCH 0451/1151] Bump Python-Roborock to 0.32.3 (#98303) bump to 0.32.3 --- homeassistant/components/roborock/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 05fff332c67..01548a6334c 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/roborock", "iot_class": "local_polling", "loggers": ["roborock"], - "requirements": ["python-roborock==0.32.2"] + "requirements": ["python-roborock==0.32.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index aa512d9ffd5..c03678db911 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2156,7 +2156,7 @@ python-qbittorrent==0.4.3 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==0.32.2 +python-roborock==0.32.3 # homeassistant.components.smarttub python-smarttub==0.0.33 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3818300ea82..37ddf1c45ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1582,7 +1582,7 @@ python-picnic-api==1.1.0 python-qbittorrent==0.4.3 # homeassistant.components.roborock -python-roborock==0.32.2 +python-roborock==0.32.3 # homeassistant.components.smarttub python-smarttub==0.0.33 From 836b2de86f8e358dcfadc1c688631df36dbbc885 Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Sat, 12 Aug 2023 18:36:03 +0200 Subject: [PATCH 0452/1151] Add dataclass for Minecraft Server data (#98297) * Add dataclass for Minecraft server data * Sort dataclass variables --- .../components/minecraft_server/__init__.py | 54 +++++++++++-------- .../components/minecraft_server/entity.py | 4 +- .../components/minecraft_server/sensor.py | 16 +++--- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 6457f19a335..cf0d96af8d2 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from dataclasses import dataclass from datetime import datetime, timedelta import logging from typing import Any @@ -62,6 +63,19 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +@dataclass +class MinecraftServerData: + """Representation of Minecraft server data.""" + + latency: float | None = None + motd: str | None = None + players_max: int | None = None + players_online: int | None = None + players_list: list[str] | None = None + protocol_version: int | None = None + version: str | None = None + + class MinecraftServer: """Representation of a Minecraft server.""" @@ -84,13 +98,7 @@ class MinecraftServer: self._server = JavaServer(self.host, self.port) # Data provided by 3rd party library - self.version: str | None = None - self.protocol_version: int | None = None - self.latency: float | None = None - self.players_online: int | None = None - self.players_max: int | None = None - self.players_list: list[str] | None = None - self.motd: str | None = None + self.data: MinecraftServerData = MinecraftServerData() # Dispatcher signal name self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}" @@ -170,18 +178,18 @@ class MinecraftServer: status_response = await self._server.async_status() # Got answer to request, update properties. - self.version = status_response.version.name - self.protocol_version = status_response.version.protocol - self.players_online = status_response.players.online - self.players_max = status_response.players.max - self.latency = status_response.latency - self.motd = status_response.motd.to_plain() + self.data.version = status_response.version.name + self.data.protocol_version = status_response.version.protocol + self.data.players_online = status_response.players.online + self.data.players_max = status_response.players.max + self.data.latency = status_response.latency + self.data.motd = status_response.motd.to_plain() - self.players_list = [] + self.data.players_list = [] if status_response.players.sample is not None: for player in status_response.players.sample: - self.players_list.append(player.name) - self.players_list.sort() + self.data.players_list.append(player.name) + self.data.players_list.sort() # Inform user once about successful update if necessary. if self._last_status_request_failed: @@ -193,13 +201,13 @@ class MinecraftServer: self._last_status_request_failed = False except OSError as error: # No answer to request, set all properties to unknown. - self.version = None - self.protocol_version = None - self.players_online = None - self.players_max = None - self.latency = None - self.players_list = None - self.motd = None + self.data.version = None + self.data.protocol_version = None + self.data.players_online = None + self.data.players_max = None + self.data.latency = None + self.data.players_list = None + self.data.motd = None # Inform user once about failed update if necessary. if not self._last_status_request_failed: diff --git a/homeassistant/components/minecraft_server/entity.py b/homeassistant/components/minecraft_server/entity.py index 9458a3ef397..63d68d0aa77 100644 --- a/homeassistant/components/minecraft_server/entity.py +++ b/homeassistant/components/minecraft_server/entity.py @@ -29,9 +29,9 @@ class MinecraftServerEntity(Entity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._server.unique_id)}, manufacturer=MANUFACTURER, - model=f"Minecraft Server ({self._server.version})", + model=f"Minecraft Server ({self._server.data.version})", name=self._server.name, - sw_version=str(self._server.protocol_version), + sw_version=f"{self._server.data.protocol_version}", ) self._attr_device_class = device_class self._extra_state_attributes = None diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 045aa3cec4e..74422675718 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -89,7 +89,7 @@ class MinecraftServerVersionSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update version.""" - self._attr_native_value = self._server.version + self._attr_native_value = self._server.data.version class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): @@ -107,7 +107,7 @@ class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update protocol version.""" - self._attr_native_value = self._server.protocol_version + self._attr_native_value = self._server.data.protocol_version class MinecraftServerLatencySensor(MinecraftServerSensorEntity): @@ -126,7 +126,7 @@ class MinecraftServerLatencySensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update latency.""" - self._attr_native_value = self._server.latency + self._attr_native_value = self._server.data.latency class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): @@ -145,13 +145,13 @@ class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update online players state and device state attributes.""" - self._attr_native_value = self._server.players_online + self._attr_native_value = self._server.data.players_online extra_state_attributes = {} - players_list = self._server.players_list + players_list = self._server.data.players_list if players_list is not None and len(players_list) != 0: - extra_state_attributes[ATTR_PLAYERS_LIST] = self._server.players_list + extra_state_attributes[ATTR_PLAYERS_LIST] = players_list self._attr_extra_state_attributes = extra_state_attributes @@ -172,7 +172,7 @@ class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update maximum number of players.""" - self._attr_native_value = self._server.players_max + self._attr_native_value = self._server.data.players_max class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): @@ -190,4 +190,4 @@ class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update MOTD.""" - self._attr_native_value = self._server.motd + self._attr_native_value = self._server.data.motd From 85b097af5076194a802f1e34dff199f2dc5f6114 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 12 Aug 2023 18:39:59 +0200 Subject: [PATCH 0453/1151] Update todoist-api-python to 2.1.1 (#98263) --- homeassistant/components/todoist/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index ac7e899d8a1..22d3b19b6c9 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/todoist", "iot_class": "cloud_polling", "loggers": ["todoist"], - "requirements": ["todoist-api-python==2.0.2"] + "requirements": ["todoist-api-python==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index c03678db911..4061b7bd08e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2554,7 +2554,7 @@ tilt-ble==0.2.3 tmb==0.0.4 # homeassistant.components.todoist -todoist-api-python==2.0.2 +todoist-api-python==2.1.1 # homeassistant.components.tolo tololib==0.1.0b4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37ddf1c45ea..da5ca884f24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1860,7 +1860,7 @@ thermopro-ble==0.4.5 tilt-ble==0.2.3 # homeassistant.components.todoist -todoist-api-python==2.0.2 +todoist-api-python==2.1.1 # homeassistant.components.tolo tololib==0.1.0b4 From 79991c32dcf06aafc13d25789b3a9349c68a5eed Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 12 Aug 2023 11:37:43 -0700 Subject: [PATCH 0454/1151] Bump pyrainbird to 4.0.0 (#98271) --- homeassistant/components/rainbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 986e89783d7..07a0bc0a5f6 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rainbird", "iot_class": "local_polling", "loggers": ["pyrainbird"], - "requirements": ["pyrainbird==3.0.0"] + "requirements": ["pyrainbird==4.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4061b7bd08e..4c7bccd4cdc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1955,7 +1955,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==3.0.0 +pyrainbird==4.0.0 # homeassistant.components.recswitch pyrecswitch==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da5ca884f24..2d944127237 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1453,7 +1453,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.rainbird -pyrainbird==3.0.0 +pyrainbird==4.0.0 # homeassistant.components.risco pyrisco==0.5.7 From bdaa2285fcf96735f6247cdd3321d6a83db8e188 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 12 Aug 2023 13:20:01 -0700 Subject: [PATCH 0455/1151] Google Assistant SDK: Allow responses for send_text_command (#95966) google_assistant_sdk.send_text_command response --- .../google_assistant_sdk/__init__.py | 24 ++++++++++++++++--- .../google_assistant_sdk/helpers.py | 13 +++++++++- .../google_assistant_sdk/test_init.py | 22 ++++++++++------- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index 4a294489c97..24b71dd0180 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -1,6 +1,8 @@ """Support for Google Assistant SDK.""" from __future__ import annotations +import dataclasses + import aiohttp from gassist_text import TextAssistant from google.oauth2.credentials import Credentials @@ -9,7 +11,12 @@ import voluptuous as vol from homeassistant.components import conversation from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery, intent from homeassistant.helpers.config_entry_oauth2_flow import ( @@ -101,19 +108,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_service(hass: HomeAssistant) -> None: """Add the services for Google Assistant SDK.""" - async def send_text_command(call: ServiceCall) -> None: + async def send_text_command(call: ServiceCall) -> ServiceResponse: """Send a text command to Google Assistant SDK.""" commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND] media_players: list[str] | None = call.data.get( SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER ) - await async_send_text_commands(hass, commands, media_players) + command_response_list = await async_send_text_commands( + hass, commands, media_players + ) + if call.return_response: + return { + "responses": [ + dataclasses.asdict(command_response) + for command_response in command_response_list + ] + } + return None hass.services.async_register( DOMAIN, SERVICE_SEND_TEXT_COMMAND, send_text_command, schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA, + supports_response=SupportsResponse.OPTIONAL, ) diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index 1d89e208ced..5ae39c98f3c 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -1,6 +1,7 @@ """Helper classes for Google Assistant SDK integration.""" from __future__ import annotations +from dataclasses import dataclass from http import HTTPStatus import logging from typing import Any @@ -48,9 +49,16 @@ DEFAULT_LANGUAGE_CODES = { } +@dataclass +class CommandResponse: + """Response from a single command to Google Assistant Service.""" + + text: str + + async def async_send_text_commands( hass: HomeAssistant, commands: list[str], media_players: list[str] | None = None -) -> None: +) -> list[CommandResponse]: """Send text commands to Google Assistant Service.""" # There can only be 1 entry (config_flow has single_instance_allowed) entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] @@ -68,6 +76,7 @@ async def async_send_text_commands( with TextAssistant( credentials, language_code, audio_out=bool(media_players) ) as assistant: + command_response_list = [] for command in commands: resp = assistant.assist(command) text_response = resp[0] @@ -91,6 +100,8 @@ async def async_send_text_commands( }, blocking=True, ) + command_response_list.append(CommandResponse(text_response)) + return command_response_list def default_language_code(hass: HomeAssistant): diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index 3cb64a9a441..de89d562d46 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -162,20 +162,26 @@ async def test_send_text_commands( command1 = "open the garage door" command2 = "1234" + command1_response = "what's the PIN?" + command2_response = "opened the garage door" with patch( - "homeassistant.components.google_assistant_sdk.helpers.TextAssistant" - ) as mock_text_assistant: - await hass.services.async_call( + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + side_effect=[ + (command1_response, None, None), + (command2_response, None, None), + ], + ) as mock_assist_call: + response = await hass.services.async_call( DOMAIN, "send_text_command", {"command": [command1, command2]}, blocking=True, + return_response=True, ) - mock_text_assistant.assert_called_once_with( - ExpectedCredentials(), "en-US", audio_out=False - ) - mock_text_assistant.assert_has_calls([call().__enter__().assist(command1)]) - mock_text_assistant.assert_has_calls([call().__enter__().assist(command2)]) + assert response == { + "responses": [{"text": command1_response}, {"text": command2_response}] + } + mock_assist_call.assert_has_calls([call(command1), call(command2)]) @pytest.mark.parametrize( From 483567529f9370dc76a5d58ac7dbde6bacd36672 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 12 Aug 2023 20:10:36 -0500 Subject: [PATCH 0456/1151] Bump flux-led to 1.0.2 (#98312) changelog: https://github.com/Danielhiversen/flux_led/compare/1.0.1...1.0.2 fixes #98310 --- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 689f984722d..d3274738f75 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -54,5 +54,5 @@ "iot_class": "local_push", "loggers": ["flux_led"], "quality_scale": "platinum", - "requirements": ["flux-led==1.0.1"] + "requirements": ["flux-led==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4c7bccd4cdc..15c40edc32d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -800,7 +800,7 @@ fjaraskupan==2.2.0 flipr-api==1.5.0 # homeassistant.components.flux_led -flux-led==1.0.1 +flux-led==1.0.2 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d944127237..2e5e84347c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ fjaraskupan==2.2.0 flipr-api==1.5.0 # homeassistant.components.flux_led -flux-led==1.0.1 +flux-led==1.0.2 # homeassistant.components.homekit # homeassistant.components.recorder From fea4af69d7a034afb8696b682e3288b647864777 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 12 Aug 2023 23:08:33 -0700 Subject: [PATCH 0457/1151] Add missing logging for opower library (#98325) --- homeassistant/components/opower/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 73942231b40..97a605676e1 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["recorder"], "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", + "loggers": ["opower"], "requirements": ["opower==0.0.26"] } From 38cea8f31c81c4a9f5d065567290dc7abe615062 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 13 Aug 2023 12:39:46 +0200 Subject: [PATCH 0458/1151] Plugwise climate: add HVAC_Mode handling to set_temperature() (#98273) * Add HVAC_Mode handling to set_temperature() * Move added code down, as suggested * Implement walrus as suggested Co-authored-by: G Johansson --------- Co-authored-by: G Johansson --- homeassistant/components/plugwise/climate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index e83b76a76da..610ffa34d7c 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any from homeassistant.components.climate import ( + ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ClimateEntity, @@ -161,6 +162,9 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): ): raise ValueError("Invalid temperature change requested") + if mode := kwargs.get(ATTR_HVAC_MODE): + await self.async_set_hvac_mode(mode) + await self.coordinator.api.set_temperature(self.device["location"], data) @plugwise_command From b41d3b465c9bc8563acd69fb149637f144e84739 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 13 Aug 2023 12:57:34 +0200 Subject: [PATCH 0459/1151] Add domestic_hot_water_setpoint number to Plugwise (#98092) * Add max_dhw_temperature number * Update strings.json * Add related tests * Correct test * Black-fix --- homeassistant/components/plugwise/number.py | 9 +++++- .../components/plugwise/strings.json | 3 ++ tests/components/plugwise/test_number.py | 29 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 102d94f91b7..5979480d90f 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -48,6 +48,14 @@ NUMBER_TYPES = ( entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), + PlugwiseNumberEntityDescription( + key="max_dhw_temperature", + translation_key="max_dhw_temperature", + command=lambda api, number, value: api.set_number_setpoint(number, value), + device_class=NumberDeviceClass.TEMPERATURE, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), ) @@ -89,7 +97,6 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" self._attr_mode = NumberMode.BOX - 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) diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index e1b5b5c4053..5210f8a6dc0 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -76,6 +76,9 @@ "number": { "maximum_boiler_temperature": { "name": "Maximum boiler temperature setpoint" + }, + "max_dhw_temperature": { + "name": "Domestic hot water setpoint" } }, "select": { diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py index da31b8038c8..9ca64e104d3 100644 --- a/tests/components/plugwise/test_number.py +++ b/tests/components/plugwise/test_number.py @@ -40,3 +40,32 @@ async def test_anna_max_boiler_temp_change( mock_smile_anna.set_number_setpoint.assert_called_with( "maximum_boiler_temperature", 65.0 ) + + +async def test_adam_number_entities( + hass: HomeAssistant, mock_smile_adam_2: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of a number.""" + state = hass.states.get("number.opentherm_domestic_hot_water_setpoint") + assert state + assert float(state.state) == 60.0 + + +async def test_adam_dhw_setpoint_change( + hass: HomeAssistant, mock_smile_adam_2: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test changing of number entities.""" + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.opentherm_domestic_hot_water_setpoint", + ATTR_VALUE: 55, + }, + blocking=True, + ) + + assert mock_smile_adam_2.set_number_setpoint.call_count == 1 + mock_smile_adam_2.set_number_setpoint.assert_called_with( + "max_dhw_temperature", 55.0 + ) From 00c60151d420d52042b2531c1482aae623a849e5 Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Sun, 13 Aug 2023 13:10:53 +0200 Subject: [PATCH 0460/1151] Add Ezviz siren entity (#93612) * Initial commit * Add siren entity * Update coveragerc * Cleanup unused entity description. * Add restore and fix entity property to standards. * Schedule turn off to match camera firmware * Only add siren for devices that support capability * Removed unused attribute and import. * Add translation * Update camera.py * Update strings.json * Update camera.py * Cleanup * Update homeassistant/components/ezviz/siren.py Co-authored-by: G Johansson * use description * Apply suggestions from code review Co-authored-by: G Johansson * Update strings.json * Dont inherit coordinator class. * Add assumed state * Update homeassistant/components/ezviz/siren.py Co-authored-by: G Johansson * Reset delay listener if trigered --------- Co-authored-by: G Johansson --- .coveragerc | 1 + homeassistant/components/ezviz/__init__.py | 1 + homeassistant/components/ezviz/camera.py | 11 ++ homeassistant/components/ezviz/entity.py | 2 + homeassistant/components/ezviz/siren.py | 133 ++++++++++++++++++++ homeassistant/components/ezviz/strings.json | 16 +++ 6 files changed, 164 insertions(+) create mode 100644 homeassistant/components/ezviz/siren.py diff --git a/.coveragerc b/.coveragerc index d36153ccca7..e64058d93d0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -341,6 +341,7 @@ omit = homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/select.py homeassistant/components/ezviz/sensor.py + homeassistant/components/ezviz/siren.py homeassistant/components/ezviz/switch.py homeassistant/components/ezviz/update.py homeassistant/components/faa_delays/__init__.py diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index c007de78130..12754af25e8 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -42,6 +42,7 @@ PLATFORMS_BY_TYPE: dict[str, list] = { Platform.NUMBER, Platform.SELECT, Platform.SENSOR, + Platform.SIREN, Platform.SWITCH, Platform.UPDATE, ], diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 083e433952f..85b1f316a7b 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -288,6 +288,17 @@ class EzvizCamera(EzvizEntity, Camera): def perform_sound_alarm(self, enable: int) -> None: """Sound the alarm on a camera.""" + ir.async_create_issue( + self.hass, + DOMAIN, + "service_depreciation_sound_alarm", + breaks_in_ha_version="2024.3.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_depreciation_sound_alarm", + ) + try: self.coordinator.ezviz_client.sound_alarm(self._serial, enable) except HTTPError as err: diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index 6fad2b57142..c8ce3daf074 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -45,6 +45,8 @@ class EzvizEntity(CoordinatorEntity[EzvizDataUpdateCoordinator], Entity): class EzvizBaseEntity(Entity): """Generic entity for EZVIZ individual poll entities.""" + _attr_has_entity_name = True + def __init__( self, coordinator: EzvizDataUpdateCoordinator, diff --git a/homeassistant/components/ezviz/siren.py b/homeassistant/components/ezviz/siren.py new file mode 100644 index 00000000000..1f08b389236 --- /dev/null +++ b/homeassistant/components/ezviz/siren.py @@ -0,0 +1,133 @@ +"""Support for EZVIZ sirens.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +from typing import Any + +from pyezviz import HTTPError, PyEzvizError, SupportExt + +from homeassistant.components.siren import ( + SirenEntity, + SirenEntityDescription, + SirenEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.helpers.event as evt +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import DATA_COORDINATOR, DOMAIN +from .coordinator import EzvizDataUpdateCoordinator +from .entity import EzvizBaseEntity + +PARALLEL_UPDATES = 1 +OFF_DELAY = timedelta(seconds=60) # Camera firmware has hard coded turn off. + +SIREN_ENTITY_TYPE = SirenEntityDescription( + key="siren", + translation_key="siren", +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up EZVIZ sensors based on a config entry.""" + coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] + + async_add_entities( + EzvizSirenEntity(coordinator, camera, SIREN_ENTITY_TYPE) + for camera in coordinator.data + for capability, value in coordinator.data[camera]["supportExt"].items() + if capability == str(SupportExt.SupportActiveDefense.value) + if value != "0" + ) + + +class EzvizSirenEntity(EzvizBaseEntity, SirenEntity, RestoreEntity): + """Representation of a EZVIZ Siren entity.""" + + _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF + _attr_should_poll = False + _attr_assumed_state = True + + def __init__( + self, + coordinator: EzvizDataUpdateCoordinator, + serial: str, + description: SirenEntityDescription, + ) -> None: + """Initialize the Siren.""" + super().__init__(coordinator, serial) + self._attr_unique_id = f"{serial}_{description.key}" + self.entity_description = description + self._attr_is_on = False + self._delay_listener: Callable | None = None + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + if not (last_state := await self.async_get_last_state()): + return + self._attr_is_on = last_state.state == STATE_ON + + if self._attr_is_on: + evt.async_call_later(self.hass, OFF_DELAY, self.off_delay_listener) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off camera siren.""" + try: + result = await self.hass.async_add_executor_job( + self.coordinator.ezviz_client.sound_alarm, self._serial, 1 + ) + + except (HTTPError, PyEzvizError) as err: + raise HomeAssistantError( + f"Failed to turn siren off for {self.name}" + ) from err + + if result: + if self._delay_listener is not None: + self._delay_listener() + self._delay_listener = None + + self._attr_is_on = False + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on camera siren.""" + try: + result = self.hass.async_add_executor_job( + self.coordinator.ezviz_client.sound_alarm, self._serial, 2 + ) + + except (HTTPError, PyEzvizError) as err: + raise HomeAssistantError( + f"Failed to turn siren on for {self.name}" + ) from err + + if result: + if self._delay_listener is not None: + self._delay_listener() + self._delay_listener = None + + self._attr_is_on = True + self._delay_listener = evt.async_call_later( + self.hass, OFF_DELAY, self.off_delay_listener + ) + self.async_write_ha_state() + + @callback + def off_delay_listener(self, now: datetime) -> None: + """Switch device off after a delay. + + Camera firmware has hard coded turn off after 60 seconds. + """ + self._attr_is_on = False + self._delay_listener = None + self.async_write_ha_state() diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 3e8797e7c02..373f9af22fc 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -92,6 +92,17 @@ } } } + }, + "service_depreciation_sound_alarm": { + "title": "Ezviz Sound alarm service is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::ezviz::issues::service_depreciation_sound_alarm::title%]", + "description": "Ezviz Sound alarm service is deprecated and will be removed.\nTo sound the alarm, you can instead use the `siren.toggle` service targeting the Siren entity.\n\nPlease remove the use of this service from your automations and scripts and select **submit** to fix this issue." + } + } + } } }, "entity": { @@ -216,6 +227,11 @@ "firmware": { "name": "[%key:component::update::entity_component::firmware::name%]" } + }, + "siren": { + "siren": { + "name": "[%key:component::siren::title%]" + } } }, "services": { From a74d83de6601694786229572abbbecbcdc278aaf Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Sun, 13 Aug 2023 13:41:37 +0200 Subject: [PATCH 0461/1151] Cleanup EZVIZ number entity (#98333) * EZVIZ - Cleanup number entity * NL * Fix naming --- homeassistant/components/ezviz/number.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ezviz/number.py b/homeassistant/components/ezviz/number.py index e4d39894d85..ea7a4812b32 100644 --- a/homeassistant/components/ezviz/number.py +++ b/homeassistant/components/ezviz/number.py @@ -66,7 +66,7 @@ async def async_setup_entry( ] async_add_entities( - EzvizSensor(coordinator, camera, value, entry.entry_id) + EzvizNumber(coordinator, camera, value, entry.entry_id) for camera in coordinator.data for capibility, value in coordinator.data[camera]["supportExt"].items() if capibility == NUMBER_TYPE.supported_ext @@ -74,11 +74,9 @@ async def async_setup_entry( ) -class EzvizSensor(EzvizBaseEntity, NumberEntity): +class EzvizNumber(EzvizBaseEntity, NumberEntity): """Representation of a EZVIZ number entity.""" - _attr_has_entity_name = True - def __init__( self, coordinator: EzvizDataUpdateCoordinator, @@ -86,7 +84,7 @@ class EzvizSensor(EzvizBaseEntity, NumberEntity): value: str, config_entry_id: str, ) -> None: - """Initialize the sensor.""" + """Initialize the entity.""" super().__init__(coordinator, serial) self.sensitivity_type = 3 if value == "3" else 0 self._attr_native_max_value = 100 if value == "3" else 6 From b36681b318f869c3bc961e2b7593337a0f7d8c7d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 13 Aug 2023 15:33:36 +0200 Subject: [PATCH 0462/1151] Update homekit entity feature constants (#98337) --- .../homekit/test_get_accessories.py | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 08a7f8a2206..b57dd2da10f 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -3,8 +3,8 @@ from unittest.mock import Mock, patch import pytest -import homeassistant.components.climate as climate -import homeassistant.components.cover as cover +from homeassistant.components.climate import ClimateEntityFeature +from homeassistant.components.cover import CoverEntityFeature from homeassistant.components.homekit.accessories import TYPES, get_accessory from homeassistant.components.homekit.const import ( ATTR_INTEGRATION, @@ -17,9 +17,9 @@ from homeassistant.components.homekit.const import ( TYPE_SWITCH, TYPE_VALVE, ) -import homeassistant.components.media_player.const as media_player_c +from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.sensor import SensorDeviceClass -import homeassistant.components.vacuum as vacuum +from homeassistant.components.vacuum import VacuumEntityFeature from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, @@ -90,7 +90,7 @@ def test_customize_options(config, name) -> None: "Thermostat", "climate.test", "auto", - {ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_RANGE}, + {ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE}, {}, ), ("HumidifierDehumidifier", "humidifier.test", "auto", {}, {}), @@ -118,7 +118,8 @@ def test_types(type_name, entity_id, state, attrs, config) -> None: "open", { ATTR_DEVICE_CLASS: "garage", - ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE, + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE, }, ), ( @@ -127,26 +128,20 @@ def test_types(type_name, entity_id, state, attrs, config) -> None: "open", { ATTR_DEVICE_CLASS: "window", - ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION, + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, }, ), ( "WindowCovering", "cover.set_position", "open", - {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION}, + {ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION}, ), ( "WindowCovering", "cover.tilt", "open", - {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_TILT_POSITION}, - ), - ( - "WindowCoveringBasic", - "cover.open_window", - "open", - {ATTR_SUPPORTED_FEATURES: (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE)}, + {ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_TILT_POSITION}, ), ( "WindowCoveringBasic", @@ -154,9 +149,19 @@ def test_types(type_name, entity_id, state, attrs, config) -> None: "open", { ATTR_SUPPORTED_FEATURES: ( - cover.SUPPORT_OPEN - | cover.SUPPORT_CLOSE - | cover.SUPPORT_SET_TILT_POSITION + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + ) + }, + ), + ( + "WindowCoveringBasic", + "cover.open_window", + "open", + { + ATTR_SUPPORTED_FEATURES: ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.SET_TILT_POSITION ) }, ), @@ -166,7 +171,7 @@ def test_types(type_name, entity_id, state, attrs, config) -> None: "open", { ATTR_DEVICE_CLASS: "door", - ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION, + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, }, ), ], @@ -188,8 +193,8 @@ def test_type_covers(type_name, entity_id, state, attrs) -> None: "media_player.test", "on", { - ATTR_SUPPORTED_FEATURES: media_player_c.MediaPlayerEntityFeature.TURN_ON - | media_player_c.MediaPlayerEntityFeature.TURN_OFF + ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.TURN_OFF }, {CONF_FEATURE_LIST: {FEATURE_ON_OFF: None}}, ), @@ -334,8 +339,8 @@ def test_type_switches(type_name, entity_id, state, attrs, config) -> None: "vacuum.dock_vacuum", "docked", { - ATTR_SUPPORTED_FEATURES: vacuum.SUPPORT_START - | vacuum.SUPPORT_RETURN_HOME + ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.START + | VacuumEntityFeature.RETURN_HOME }, ), ("Vacuum", "vacuum.basic_vacuum", "off", {}), From fa6ffd994a3f60bb8b7489073c40177ab731ab88 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 13 Aug 2023 15:35:00 +0200 Subject: [PATCH 0463/1151] Update vacuum entity constants for Alexa tests (#98336) * Update vacuum entity constants for Alexa tests * Import VacuumEntityFeature --- tests/components/alexa/test_smart_home.py | 65 ++++++++++++----------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 708b06bab2b..c42ea0a0f6a 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -8,7 +8,7 @@ from homeassistant.components.alexa import smart_home, state_report import homeassistant.components.camera as camera from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.media_player import MediaPlayerEntityFeature -import homeassistant.components.vacuum as vacuum +from homeassistant.components.vacuum import VacuumEntityFeature from homeassistant.config import async_process_ha_core_config from homeassistant.const import STATE_UNKNOWN, UnitOfTemperature from homeassistant.core import Context, Event, HomeAssistant @@ -3872,12 +3872,12 @@ async def test_vacuum_discovery(hass: HomeAssistant) -> None: "docked", { "friendly_name": "Test vacuum 1", - "supported_features": vacuum.SUPPORT_TURN_ON - | vacuum.SUPPORT_TURN_OFF - | vacuum.SUPPORT_START - | vacuum.SUPPORT_STOP - | vacuum.SUPPORT_RETURN_HOME - | vacuum.SUPPORT_PAUSE, + "supported_features": VacuumEntityFeature.TURN_ON + | VacuumEntityFeature.TURN_OFF + | VacuumEntityFeature.START + | VacuumEntityFeature.STOP + | VacuumEntityFeature.RETURN_HOME + | VacuumEntityFeature.PAUSE, }, ) appliance = await discovery_test(device, hass) @@ -3913,12 +3913,12 @@ async def test_vacuum_fan_speed(hass: HomeAssistant) -> None: "cleaning", { "friendly_name": "Test vacuum 2", - "supported_features": vacuum.SUPPORT_TURN_ON - | vacuum.SUPPORT_TURN_OFF - | vacuum.SUPPORT_START - | vacuum.SUPPORT_STOP - | vacuum.SUPPORT_PAUSE - | vacuum.SUPPORT_FAN_SPEED, + "supported_features": VacuumEntityFeature.TURN_ON + | VacuumEntityFeature.TURN_OFF + | VacuumEntityFeature.START + | VacuumEntityFeature.STOP + | VacuumEntityFeature.PAUSE + | VacuumEntityFeature.FAN_SPEED, "fan_speed_list": ["off", "low", "medium", "high", "turbo", "super_sucker"], "fan_speed": "medium", }, @@ -4042,12 +4042,12 @@ async def test_vacuum_pause(hass: HomeAssistant) -> None: "cleaning", { "friendly_name": "Test vacuum 3", - "supported_features": vacuum.SUPPORT_TURN_ON - | vacuum.SUPPORT_TURN_OFF - | vacuum.SUPPORT_START - | vacuum.SUPPORT_STOP - | vacuum.SUPPORT_PAUSE - | vacuum.SUPPORT_FAN_SPEED, + "supported_features": VacuumEntityFeature.TURN_ON + | VacuumEntityFeature.TURN_OFF + | VacuumEntityFeature.START + | VacuumEntityFeature.STOP + | VacuumEntityFeature.PAUSE + | VacuumEntityFeature.FAN_SPEED, "fan_speed_list": ["off", "low", "medium", "high", "turbo", "super_sucker"], "fan_speed": "medium", }, @@ -4080,12 +4080,12 @@ async def test_vacuum_resume(hass: HomeAssistant) -> None: "docked", { "friendly_name": "Test vacuum 4", - "supported_features": vacuum.SUPPORT_TURN_ON - | vacuum.SUPPORT_TURN_OFF - | vacuum.SUPPORT_START - | vacuum.SUPPORT_STOP - | vacuum.SUPPORT_PAUSE - | vacuum.SUPPORT_FAN_SPEED, + "supported_features": VacuumEntityFeature.TURN_ON + | VacuumEntityFeature.TURN_OFF + | VacuumEntityFeature.START + | VacuumEntityFeature.STOP + | VacuumEntityFeature.PAUSE + | VacuumEntityFeature.FAN_SPEED, "fan_speed_list": ["off", "low", "medium", "high", "turbo", "super_sucker"], "fan_speed": "medium", }, @@ -4108,9 +4108,9 @@ async def test_vacuum_discovery_no_turn_on(hass: HomeAssistant) -> None: "cleaning", { "friendly_name": "Test vacuum 5", - "supported_features": vacuum.SUPPORT_TURN_OFF - | vacuum.SUPPORT_START - | vacuum.SUPPORT_RETURN_HOME, + "supported_features": VacuumEntityFeature.TURN_OFF + | VacuumEntityFeature.START + | VacuumEntityFeature.RETURN_HOME, }, ) appliance = await discovery_test(device, hass) @@ -4138,9 +4138,9 @@ async def test_vacuum_discovery_no_turn_off(hass: HomeAssistant) -> None: "cleaning", { "friendly_name": "Test vacuum 6", - "supported_features": vacuum.SUPPORT_TURN_ON - | vacuum.SUPPORT_START - | vacuum.SUPPORT_RETURN_HOME, + "supported_features": VacuumEntityFeature.TURN_ON + | VacuumEntityFeature.START + | VacuumEntityFeature.RETURN_HOME, }, ) appliance = await discovery_test(device, hass) @@ -4169,7 +4169,8 @@ async def test_vacuum_discovery_no_turn_on_or_off(hass: HomeAssistant) -> None: "cleaning", { "friendly_name": "Test vacuum 7", - "supported_features": vacuum.SUPPORT_START | vacuum.SUPPORT_RETURN_HOME, + "supported_features": VacuumEntityFeature.START + | VacuumEntityFeature.RETURN_HOME, }, ) appliance = await discovery_test(device, hass) From e5f7d83912f6a7c0e1ff632bac27a04adee279d5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 13 Aug 2023 17:17:47 +0200 Subject: [PATCH 0464/1151] Update entity feature constants google_assistant (#98335) * Update entity feature constants google_assistant * Update tests * Direct import * Some missed constants * Add fan and cover feature imports --- .../components/google_assistant/trait.py | 91 +++++----- .../google_assistant/test_smart_home.py | 6 +- .../components/google_assistant/test_trait.py | 166 +++++++++++------- 3 files changed, 158 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 36660820efb..425a394b522 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -28,8 +28,16 @@ from homeassistant.components import ( switch, vacuum, ) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature +from homeassistant.components.camera import CameraEntityFeature +from homeassistant.components.climate import ClimateEntityFeature +from homeassistant.components.cover import CoverEntityFeature +from homeassistant.components.fan import FanEntityFeature +from homeassistant.components.humidifier import HumidifierEntityFeature +from homeassistant.components.light import LightEntityFeature from homeassistant.components.lock import STATE_JAMMED, STATE_UNLOCKING -from homeassistant.components.media_player import MediaType +from homeassistant.components.media_player import MediaPlayerEntityFeature, MediaType +from homeassistant.components.vacuum import VacuumEntityFeature from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_BATTERY_LEVEL, @@ -302,7 +310,7 @@ class CameraStreamTrait(_Trait): def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == camera.DOMAIN: - return features & camera.SUPPORT_STREAM + return features & CameraEntityFeature.STREAM return False @@ -612,7 +620,7 @@ class LocatorTrait(_Trait): @staticmethod def supported(domain, features, device_class, _): """Test if state is supported.""" - return domain == vacuum.DOMAIN and features & vacuum.SUPPORT_LOCATE + return domain == vacuum.DOMAIN and features & VacuumEntityFeature.LOCATE def sync_attributes(self): """Return locator attributes for a sync request.""" @@ -652,7 +660,7 @@ class EnergyStorageTrait(_Trait): @staticmethod def supported(domain, features, device_class, _): """Test if state is supported.""" - return domain == vacuum.DOMAIN and features & vacuum.SUPPORT_BATTERY + return domain == vacuum.DOMAIN and features & VacuumEntityFeature.BATTERY def sync_attributes(self): """Return EnergyStorage attributes for a sync request.""" @@ -710,7 +718,7 @@ class StartStopTrait(_Trait): if domain == vacuum.DOMAIN: return True - if domain == cover.DOMAIN and features & cover.SUPPORT_STOP: + if domain == cover.DOMAIN and features & CoverEntityFeature.STOP: return True return False @@ -721,7 +729,7 @@ class StartStopTrait(_Trait): if domain == vacuum.DOMAIN: return { "pausable": self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - & vacuum.SUPPORT_PAUSE + & VacuumEntityFeature.PAUSE != 0 } if domain == cover.DOMAIN: @@ -991,7 +999,7 @@ class TemperatureSettingTrait(_Trait): response["thermostatHumidityAmbient"] = current_humidity if operation in (climate.HVACMode.AUTO, climate.HVACMode.HEAT_COOL): - if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: + if supported & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: response["thermostatTemperatureSetpointHigh"] = round( TemperatureConverter.convert( attrs[climate.ATTR_TARGET_TEMP_HIGH], @@ -1093,7 +1101,7 @@ class TemperatureSettingTrait(_Trait): supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES) svc_data = {ATTR_ENTITY_ID: self.state.entity_id} - if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: + if supported & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE: svc_data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high svc_data[climate.ATTR_TARGET_TEMP_LOW] = temp_low else: @@ -1311,11 +1319,11 @@ class ArmDisArmTrait(_Trait): } state_to_support = { - STATE_ALARM_ARMED_HOME: alarm_control_panel.const.SUPPORT_ALARM_ARM_HOME, - STATE_ALARM_ARMED_AWAY: alarm_control_panel.const.SUPPORT_ALARM_ARM_AWAY, - STATE_ALARM_ARMED_NIGHT: alarm_control_panel.const.SUPPORT_ALARM_ARM_NIGHT, - STATE_ALARM_ARMED_CUSTOM_BYPASS: alarm_control_panel.const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS, - STATE_ALARM_TRIGGERED: alarm_control_panel.const.SUPPORT_ALARM_TRIGGER, + STATE_ALARM_ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME, + STATE_ALARM_ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY, + STATE_ALARM_ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, + STATE_ALARM_TRIGGERED: AlarmControlPanelEntityFeature.TRIGGER, } @staticmethod @@ -1454,9 +1462,9 @@ class FanSpeedTrait(_Trait): def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == fan.DOMAIN: - return features & fan.SUPPORT_SET_SPEED + return features & FanEntityFeature.SET_SPEED if domain == climate.DOMAIN: - return features & climate.SUPPORT_FAN_MODE + return features & ClimateEntityFeature.FAN_MODE return False def sync_attributes(self): @@ -1468,7 +1476,7 @@ class FanSpeedTrait(_Trait): if domain == fan.DOMAIN: reversible = bool( self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - & fan.SUPPORT_DIRECTION + & FanEntityFeature.DIRECTION ) result.update( @@ -1604,7 +1612,7 @@ class ModesTrait(_Trait): @staticmethod def supported(domain, features, device_class, _): """Test if state is supported.""" - if domain == fan.DOMAIN and features & fan.SUPPORT_PRESET_MODE: + if domain == fan.DOMAIN and features & FanEntityFeature.PRESET_MODE: return True if domain == input_select.DOMAIN: @@ -1613,16 +1621,16 @@ class ModesTrait(_Trait): if domain == select.DOMAIN: return True - if domain == humidifier.DOMAIN and features & humidifier.SUPPORT_MODES: + if domain == humidifier.DOMAIN and features & HumidifierEntityFeature.MODES: return True - if domain == light.DOMAIN and features & light.SUPPORT_EFFECT: + if domain == light.DOMAIN and features & LightEntityFeature.EFFECT: return True if domain != media_player.DOMAIN: return False - return features & media_player.SUPPORT_SELECT_SOUND_MODE + return features & MediaPlayerEntityFeature.SELECT_SOUND_MODE def _generate(self, name, settings): """Generate a list of modes.""" @@ -1812,7 +1820,7 @@ class InputSelectorTrait(_Trait): def supported(domain, features, device_class, _): """Test if state is supported.""" if domain == media_player.DOMAIN and ( - features & media_player.SUPPORT_SELECT_SOURCE + features & MediaPlayerEntityFeature.SELECT_SOURCE ): return True @@ -1910,13 +1918,13 @@ class OpenCloseTrait(_Trait): response["discreteOnlyOpenClose"] = True elif ( self.state.domain == cover.DOMAIN - and features & cover.SUPPORT_SET_POSITION == 0 + and features & CoverEntityFeature.SET_POSITION == 0 ): response["discreteOnlyOpenClose"] = True if ( - features & cover.SUPPORT_OPEN == 0 - and features & cover.SUPPORT_CLOSE == 0 + features & CoverEntityFeature.OPEN == 0 + and features & CoverEntityFeature.CLOSE == 0 ): response["queryOnlyOpenClose"] = True @@ -1985,7 +1993,7 @@ class OpenCloseTrait(_Trait): elif position == 100: service = cover.SERVICE_OPEN_COVER should_verify = True - elif features & cover.SUPPORT_SET_POSITION: + elif features & CoverEntityFeature.SET_POSITION: service = cover.SERVICE_SET_COVER_POSITION if position > 0: should_verify = True @@ -2026,7 +2034,8 @@ class VolumeTrait(_Trait): """Test if trait is supported.""" if domain == media_player.DOMAIN: return features & ( - media_player.SUPPORT_VOLUME_SET | media_player.SUPPORT_VOLUME_STEP + MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP ) return False @@ -2035,7 +2044,9 @@ class VolumeTrait(_Trait): """Return volume attributes for a sync request.""" features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) return { - "volumeCanMuteAndUnmute": bool(features & media_player.SUPPORT_VOLUME_MUTE), + "volumeCanMuteAndUnmute": bool( + features & MediaPlayerEntityFeature.VOLUME_MUTE + ), "commandOnlyVolume": self.state.attributes.get(ATTR_ASSUMED_STATE, False), # Volume amounts in SET_VOLUME and VOLUME_RELATIVE are on a scale # from 0 to this value. @@ -2078,7 +2089,7 @@ class VolumeTrait(_Trait): if not ( self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - & media_player.SUPPORT_VOLUME_SET + & MediaPlayerEntityFeature.VOLUME_SET ): raise SmartHomeError(ERR_NOT_SUPPORTED, "Command not supported") @@ -2088,13 +2099,13 @@ class VolumeTrait(_Trait): relative = params["relativeSteps"] features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if features & media_player.SUPPORT_VOLUME_SET: + if features & MediaPlayerEntityFeature.VOLUME_SET: current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) target = max(0.0, min(1.0, current + relative / 100)) await self._set_volume_absolute(data, target) - elif features & media_player.SUPPORT_VOLUME_STEP: + elif features & MediaPlayerEntityFeature.VOLUME_STEP: svc = media_player.SERVICE_VOLUME_UP if relative < 0: svc = media_player.SERVICE_VOLUME_DOWN @@ -2116,7 +2127,7 @@ class VolumeTrait(_Trait): if not ( self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - & media_player.SUPPORT_VOLUME_MUTE + & MediaPlayerEntityFeature.VOLUME_MUTE ): raise SmartHomeError(ERR_NOT_SUPPORTED, "Command not supported") @@ -2158,14 +2169,14 @@ def _verify_pin_challenge(data, state, challenge): MEDIA_COMMAND_SUPPORT_MAPPING = { - COMMAND_MEDIA_NEXT: media_player.SUPPORT_NEXT_TRACK, - COMMAND_MEDIA_PAUSE: media_player.SUPPORT_PAUSE, - COMMAND_MEDIA_PREVIOUS: media_player.SUPPORT_PREVIOUS_TRACK, - COMMAND_MEDIA_RESUME: media_player.SUPPORT_PLAY, - COMMAND_MEDIA_SEEK_RELATIVE: media_player.SUPPORT_SEEK, - COMMAND_MEDIA_SEEK_TO_POSITION: media_player.SUPPORT_SEEK, - COMMAND_MEDIA_SHUFFLE: media_player.SUPPORT_SHUFFLE_SET, - COMMAND_MEDIA_STOP: media_player.SUPPORT_STOP, + COMMAND_MEDIA_NEXT: MediaPlayerEntityFeature.NEXT_TRACK, + COMMAND_MEDIA_PAUSE: MediaPlayerEntityFeature.PAUSE, + COMMAND_MEDIA_PREVIOUS: MediaPlayerEntityFeature.PREVIOUS_TRACK, + COMMAND_MEDIA_RESUME: MediaPlayerEntityFeature.PLAY, + COMMAND_MEDIA_SEEK_RELATIVE: MediaPlayerEntityFeature.SEEK, + COMMAND_MEDIA_SEEK_TO_POSITION: MediaPlayerEntityFeature.SEEK, + COMMAND_MEDIA_SHUFFLE: MediaPlayerEntityFeature.SHUFFLE_SET, + COMMAND_MEDIA_STOP: MediaPlayerEntityFeature.STOP, } MEDIA_COMMAND_ATTRIBUTES = { @@ -2350,7 +2361,7 @@ class ChannelTrait(_Trait): """Test if state is supported.""" if ( domain == media_player.DOMAIN - and (features & media_player.SUPPORT_PLAY_MEDIA) + and (features & MediaPlayerEntityFeature.PLAY_MEDIA) and device_class == media_player.MediaPlayerDeviceClass.TV ): return True diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 6cfa7965074..bf48564c251 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -6,7 +6,7 @@ from unittest.mock import ANY, patch import pytest from pytest_unordered import unordered -from homeassistant.components import camera +from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.climate import ATTR_MAX_TEMP, ATTR_MIN_TEMP, HVACMode from homeassistant.components.demo.binary_sensor import DemoBinarySensor from homeassistant.components.demo.cover import DemoCover @@ -1186,7 +1186,9 @@ async def test_trait_execute_adding_query_data(hass: HomeAssistant) -> None: {"external_url": "https://example.com"}, ) hass.states.async_set( - "camera.office", "idle", {"supported_features": camera.SUPPORT_STREAM} + "camera.office", + "idle", + {"supported_features": CameraEntityFeature.STREAM}, ) with patch( diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index fd6b3a6790b..fcbf16c21c7 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -27,9 +27,22 @@ from homeassistant.components import ( switch, vacuum, ) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature +from homeassistant.components.camera import CameraEntityFeature +from homeassistant.components.climate import ClimateEntityFeature +from homeassistant.components.cover import CoverEntityFeature +from homeassistant.components.fan import FanEntityFeature from homeassistant.components.google_assistant import const, error, helpers, trait from homeassistant.components.google_assistant.error import SmartHomeError -from homeassistant.components.media_player import SERVICE_PLAY_MEDIA, MediaType +from homeassistant.components.humidifier import HumidifierEntityFeature +from homeassistant.components.light import LightEntityFeature +from homeassistant.components.lock import LockEntityFeature +from homeassistant.components.media_player import ( + SERVICE_PLAY_MEDIA, + MediaPlayerEntityFeature, + MediaType, +) +from homeassistant.components.vacuum import VacuumEntityFeature from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -126,7 +139,7 @@ async def test_camera_stream(hass: HomeAssistant) -> None: ) assert helpers.get_google_type(camera.DOMAIN, None) is not None assert trait.CameraStreamTrait.supported( - camera.DOMAIN, camera.SUPPORT_STREAM, None, None + camera.DOMAIN, CameraEntityFeature.STREAM, None, None ) trt = trait.CameraStreamTrait( @@ -364,7 +377,7 @@ async def test_locate_vacuum(hass: HomeAssistant) -> None: """Test locate trait support for vacuum domain.""" assert helpers.get_google_type(vacuum.DOMAIN, None) is not None assert trait.LocatorTrait.supported( - vacuum.DOMAIN, vacuum.SUPPORT_LOCATE, None, None + vacuum.DOMAIN, VacuumEntityFeature.LOCATE, None, None ) trt = trait.LocatorTrait( @@ -372,7 +385,7 @@ async def test_locate_vacuum(hass: HomeAssistant) -> None: State( "vacuum.bla", vacuum.STATE_IDLE, - {ATTR_SUPPORTED_FEATURES: vacuum.SUPPORT_LOCATE}, + {ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.LOCATE}, ), BASIC_CONFIG, ) @@ -395,7 +408,7 @@ async def test_energystorage_vacuum(hass: HomeAssistant) -> None: """Test EnergyStorage trait support for vacuum domain.""" assert helpers.get_google_type(vacuum.DOMAIN, None) is not None assert trait.EnergyStorageTrait.supported( - vacuum.DOMAIN, vacuum.SUPPORT_BATTERY, None, None + vacuum.DOMAIN, VacuumEntityFeature.BATTERY, None, None ) trt = trait.EnergyStorageTrait( @@ -404,7 +417,7 @@ async def test_energystorage_vacuum(hass: HomeAssistant) -> None: "vacuum.bla", vacuum.STATE_DOCKED, { - ATTR_SUPPORTED_FEATURES: vacuum.SUPPORT_BATTERY, + ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.BATTERY, ATTR_BATTERY_LEVEL: 100, }, ), @@ -430,7 +443,7 @@ async def test_energystorage_vacuum(hass: HomeAssistant) -> None: "vacuum.bla", vacuum.STATE_CLEANING, { - ATTR_SUPPORTED_FEATURES: vacuum.SUPPORT_BATTERY, + ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.BATTERY, ATTR_BATTERY_LEVEL: 20, }, ), @@ -469,7 +482,7 @@ async def test_startstop_vacuum(hass: HomeAssistant) -> None: State( "vacuum.bla", vacuum.STATE_PAUSED, - {ATTR_SUPPORTED_FEATURES: vacuum.SUPPORT_PAUSE}, + {ATTR_SUPPORTED_FEATURES: VacuumEntityFeature.PAUSE}, ), BASIC_CONFIG, ) @@ -502,12 +515,14 @@ async def test_startstop_vacuum(hass: HomeAssistant) -> None: async def test_startstop_cover(hass: HomeAssistant) -> None: """Test startStop trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None - assert trait.StartStopTrait.supported(cover.DOMAIN, cover.SUPPORT_STOP, None, None) + assert trait.StartStopTrait.supported( + cover.DOMAIN, CoverEntityFeature.STOP, None, None + ) state = State( "cover.bla", cover.STATE_CLOSED, - {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_STOP}, + {ATTR_SUPPORTED_FEATURES: CoverEntityFeature.STOP}, ) trt = trait.StartStopTrait( @@ -551,7 +566,10 @@ async def test_startstop_cover_assumed(hass: HomeAssistant) -> None: State( "cover.bla", cover.STATE_CLOSED, - {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_STOP, ATTR_ASSUMED_STATE: True}, + { + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.STOP, + ATTR_ASSUMED_STATE: True, + }, ), BASIC_CONFIG, ) @@ -707,7 +725,9 @@ async def test_color_light_temperature_light_bad_temp(hass: HomeAssistant) -> No async def test_light_modes(hass: HomeAssistant) -> None: """Test Light Mode trait.""" assert helpers.get_google_type(light.DOMAIN, None) is not None - assert trait.ModesTrait.supported(light.DOMAIN, light.SUPPORT_EFFECT, None, None) + assert trait.ModesTrait.supported( + light.DOMAIN, LightEntityFeature.EFFECT, None, None + ) trt = trait.ModesTrait( hass, @@ -847,7 +867,7 @@ async def test_temperature_setting_climate_onoff(hass: HomeAssistant) -> None: "climate.bla", climate.HVACMode.AUTO, { - ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, climate.ATTR_HVAC_MODES: [ climate.HVACMode.OFF, climate.HVACMode.COOL, @@ -928,7 +948,7 @@ async def test_temperature_setting_climate_range(hass: HomeAssistant) -> None: { climate.ATTR_CURRENT_TEMPERATURE: 70, climate.ATTR_CURRENT_HUMIDITY: 25, - ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, climate.ATTR_HVAC_MODES: [ STATE_OFF, climate.HVACMode.COOL, @@ -1040,7 +1060,7 @@ async def test_temperature_setting_climate_setpoint(hass: HomeAssistant) -> None "climate.bla", climate.HVACMode.COOL, { - ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE, + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE, climate.ATTR_HVAC_MODES: [STATE_OFF, climate.HVACMode.COOL], climate.ATTR_MIN_TEMP: 10, climate.ATTR_MAX_TEMP: 30, @@ -1230,8 +1250,10 @@ async def test_humidity_setting_humidifier_setpoint(hass: HomeAssistant) -> None async def test_lock_unlock_lock(hass: HomeAssistant) -> None: """Test LockUnlock trait locking support for lock domain.""" assert helpers.get_google_type(lock.DOMAIN, None) is not None - assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None, None) - assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, lock.SUPPORT_OPEN, None) + assert trait.LockUnlockTrait.supported( + lock.DOMAIN, LockEntityFeature.OPEN, None, None + ) + assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, LockEntityFeature.OPEN, None) trt = trait.LockUnlockTrait( hass, State("lock.front_door", lock.STATE_LOCKED), PIN_CONFIG @@ -1254,8 +1276,10 @@ async def test_lock_unlock_lock(hass: HomeAssistant) -> None: async def test_lock_unlock_unlocking(hass: HomeAssistant) -> None: """Test LockUnlock trait locking support for lock domain.""" assert helpers.get_google_type(lock.DOMAIN, None) is not None - assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None, None) - assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, lock.SUPPORT_OPEN, None) + assert trait.LockUnlockTrait.supported( + lock.DOMAIN, LockEntityFeature.OPEN, None, None + ) + assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, LockEntityFeature.OPEN, None) trt = trait.LockUnlockTrait( hass, State("lock.front_door", lock.STATE_UNLOCKING), PIN_CONFIG @@ -1269,8 +1293,10 @@ async def test_lock_unlock_unlocking(hass: HomeAssistant) -> None: async def test_lock_unlock_lock_jammed(hass: HomeAssistant) -> None: """Test LockUnlock trait locking support for lock domain that jams.""" assert helpers.get_google_type(lock.DOMAIN, None) is not None - assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None, None) - assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, lock.SUPPORT_OPEN, None) + assert trait.LockUnlockTrait.supported( + lock.DOMAIN, LockEntityFeature.OPEN, None, None + ) + assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, LockEntityFeature.OPEN, None) trt = trait.LockUnlockTrait( hass, State("lock.front_door", lock.STATE_JAMMED), PIN_CONFIG @@ -1293,7 +1319,9 @@ async def test_lock_unlock_lock_jammed(hass: HomeAssistant) -> None: async def test_lock_unlock_unlock(hass: HomeAssistant) -> None: """Test LockUnlock trait unlocking support for lock domain.""" assert helpers.get_google_type(lock.DOMAIN, None) is not None - assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None, None) + assert trait.LockUnlockTrait.supported( + lock.DOMAIN, LockEntityFeature.OPEN, None, None + ) trt = trait.LockUnlockTrait( hass, State("lock.front_door", lock.STATE_LOCKED), PIN_CONFIG @@ -1363,8 +1391,8 @@ async def test_arm_disarm_arm_away(hass: HomeAssistant) -> None: STATE_ALARM_ARMED_AWAY, { alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True, - ATTR_SUPPORTED_FEATURES: alarm_control_panel.const.SUPPORT_ALARM_ARM_HOME - | alarm_control_panel.const.SUPPORT_ALARM_ARM_AWAY, + ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY, }, ), PIN_CONFIG, @@ -1526,8 +1554,8 @@ async def test_arm_disarm_disarm(hass: HomeAssistant) -> None: STATE_ALARM_DISARMED, { alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True, - ATTR_SUPPORTED_FEATURES: alarm_control_panel.const.SUPPORT_ALARM_TRIGGER - | alarm_control_panel.const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.TRIGGER + | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, }, ), PIN_CONFIG, @@ -1662,7 +1690,9 @@ async def test_arm_disarm_disarm(hass: HomeAssistant) -> None: async def test_fan_speed(hass: HomeAssistant) -> None: """Test FanSpeed trait speed control support for fan domain.""" assert helpers.get_google_type(fan.DOMAIN, None) is not None - assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, None, None) + assert trait.FanSpeedTrait.supported( + fan.DOMAIN, FanEntityFeature.SET_SPEED, None, None + ) trt = trait.FanSpeedTrait( hass, @@ -1700,7 +1730,9 @@ async def test_fan_speed(hass: HomeAssistant) -> None: async def test_fan_speed_without_percentage_step(hass: HomeAssistant) -> None: """Test FanSpeed trait speed control percentage step for fan domain.""" assert helpers.get_google_type(fan.DOMAIN, None) is not None - assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, None, None) + assert trait.FanSpeedTrait.supported( + fan.DOMAIN, FanEntityFeature.SET_SPEED, None, None + ) trt = trait.FanSpeedTrait( hass, @@ -1787,7 +1819,9 @@ async def test_fan_speed_ordered( ): """Test FanSpeed trait speed control support for fan domain.""" assert helpers.get_google_type(fan.DOMAIN, None) is not None - assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, None, None) + assert trait.FanSpeedTrait.supported( + fan.DOMAIN, FanEntityFeature.SET_SPEED, None, None + ) trt = trait.FanSpeedTrait( hass, @@ -1858,7 +1892,7 @@ async def test_fan_reverse( "percentage": 33, "percentage_step": 1.0, "direction": direction_state, - "supported_features": fan.SUPPORT_DIRECTION, + "supported_features": FanEntityFeature.DIRECTION, }, ), BASIC_CONFIG, @@ -1889,7 +1923,7 @@ async def test_climate_fan_speed(hass: HomeAssistant) -> None: """Test FanSpeed trait speed control support for climate domain.""" assert helpers.get_google_type(climate.DOMAIN, None) is not None assert trait.FanSpeedTrait.supported( - climate.DOMAIN, climate.SUPPORT_FAN_MODE, None, None + climate.DOMAIN, ClimateEntityFeature.FAN_MODE, None, None ) trt = trait.FanSpeedTrait( @@ -1951,7 +1985,7 @@ async def test_inputselector(hass: HomeAssistant) -> None: assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.InputSelectorTrait.supported( media_player.DOMAIN, - media_player.MediaPlayerEntityFeature.SELECT_SOURCE, + MediaPlayerEntityFeature.SELECT_SOURCE, None, None, ) @@ -2265,7 +2299,7 @@ async def test_modes_humidifier(hass: HomeAssistant) -> None: """Test Humidifier Mode trait.""" assert helpers.get_google_type(humidifier.DOMAIN, None) is not None assert trait.ModesTrait.supported( - humidifier.DOMAIN, humidifier.SUPPORT_MODES, None, None + humidifier.DOMAIN, HumidifierEntityFeature.MODES, None, None ) trt = trait.ModesTrait( @@ -2279,7 +2313,7 @@ async def test_modes_humidifier(hass: HomeAssistant) -> None: humidifier.MODE_AUTO, humidifier.MODE_AWAY, ], - ATTR_SUPPORTED_FEATURES: humidifier.SUPPORT_MODES, + ATTR_SUPPORTED_FEATURES: HumidifierEntityFeature.MODES, humidifier.ATTR_MIN_HUMIDITY: 30, humidifier.ATTR_MAX_HUMIDITY: 99, humidifier.ATTR_HUMIDITY: 50, @@ -2345,7 +2379,7 @@ async def test_sound_modes(hass: HomeAssistant) -> None: assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.ModesTrait.supported( media_player.DOMAIN, - media_player.MediaPlayerEntityFeature.SELECT_SOUND_MODE, + MediaPlayerEntityFeature.SELECT_SOUND_MODE, None, None, ) @@ -2420,7 +2454,9 @@ async def test_sound_modes(hass: HomeAssistant) -> None: async def test_preset_modes(hass: HomeAssistant) -> None: """Test Mode trait for fan preset modes.""" assert helpers.get_google_type(fan.DOMAIN, None) is not None - assert trait.ModesTrait.supported(fan.DOMAIN, fan.SUPPORT_PRESET_MODE, None, None) + assert trait.ModesTrait.supported( + fan.DOMAIN, FanEntityFeature.PRESET_MODE, None, None + ) trt = trait.ModesTrait( hass, @@ -2430,7 +2466,7 @@ async def test_preset_modes(hass: HomeAssistant) -> None: attributes={ fan.ATTR_PRESET_MODES: ["auto", "whoosh"], fan.ATTR_PRESET_MODE: "auto", - ATTR_SUPPORTED_FEATURES: fan.SUPPORT_PRESET_MODE, + ATTR_SUPPORTED_FEATURES: FanEntityFeature.PRESET_MODE, }, ), BASIC_CONFIG, @@ -2514,7 +2550,7 @@ async def test_openclose_cover(hass: HomeAssistant) -> None: """Test OpenClose trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.OpenCloseTrait.supported( - cover.DOMAIN, cover.SUPPORT_SET_POSITION, None, None + cover.DOMAIN, CoverEntityFeature.SET_POSITION, None, None ) trt = trait.OpenCloseTrait( @@ -2524,7 +2560,7 @@ async def test_openclose_cover(hass: HomeAssistant) -> None: cover.STATE_OPEN, { cover.ATTR_CURRENT_POSITION: 75, - ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION, + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, }, ), BASIC_CONFIG, @@ -2551,14 +2587,16 @@ async def test_openclose_cover_unknown_state(hass: HomeAssistant) -> None: """Test OpenClose trait support for cover domain with unknown state.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.OpenCloseTrait.supported( - cover.DOMAIN, cover.SUPPORT_SET_POSITION, None, None + cover.DOMAIN, CoverEntityFeature.SET_POSITION, None, None ) # No state trt = trait.OpenCloseTrait( hass, State( - "cover.bla", STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN} + "cover.bla", + STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: CoverEntityFeature.OPEN}, ), BASIC_CONFIG, ) @@ -2581,7 +2619,7 @@ async def test_openclose_cover_assumed_state(hass: HomeAssistant) -> None: """Test OpenClose trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.OpenCloseTrait.supported( - cover.DOMAIN, cover.SUPPORT_SET_POSITION, None, None + cover.DOMAIN, CoverEntityFeature.SET_POSITION, None, None ) trt = trait.OpenCloseTrait( @@ -2591,7 +2629,7 @@ async def test_openclose_cover_assumed_state(hass: HomeAssistant) -> None: cover.STATE_OPEN, { ATTR_ASSUMED_STATE: True, - ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION, + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, }, ), BASIC_CONFIG, @@ -2634,14 +2672,17 @@ async def test_openclose_cover_no_position(hass: HomeAssistant) -> None: """Test OpenClose trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.OpenCloseTrait.supported( - cover.DOMAIN, cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE, None, None + cover.DOMAIN, + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE, + None, + None, ) state = State( "cover.bla", cover.STATE_OPEN, { - ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE, + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE, }, ) @@ -2695,10 +2736,10 @@ async def test_openclose_cover_secure(hass: HomeAssistant, device_class) -> None """Test OpenClose trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, device_class) is not None assert trait.OpenCloseTrait.supported( - cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class, None + cover.DOMAIN, CoverEntityFeature.SET_POSITION, device_class, None ) assert trait.OpenCloseTrait.might_2fa( - cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class + cover.DOMAIN, CoverEntityFeature.SET_POSITION, device_class ) trt = trait.OpenCloseTrait( @@ -2708,7 +2749,7 @@ async def test_openclose_cover_secure(hass: HomeAssistant, device_class) -> None cover.STATE_OPEN, { ATTR_DEVICE_CLASS: device_class, - ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION, + ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_POSITION, cover.ATTR_CURRENT_POSITION: 75, }, ), @@ -2796,7 +2837,7 @@ async def test_volume_media_player(hass: HomeAssistant) -> None: assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.VolumeTrait.supported( media_player.DOMAIN, - media_player.MediaPlayerEntityFeature.VOLUME_SET, + MediaPlayerEntityFeature.VOLUME_SET, None, None, ) @@ -2807,7 +2848,7 @@ async def test_volume_media_player(hass: HomeAssistant) -> None: "media_player.bla", media_player.STATE_PLAYING, { - ATTR_SUPPORTED_FEATURES: media_player.MediaPlayerEntityFeature.VOLUME_SET, + ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.VOLUME_SET, media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.3, }, ), @@ -2850,7 +2891,7 @@ async def test_volume_media_player_relative(hass: HomeAssistant) -> None: """Test volume trait support for relative-volume-only media players.""" assert trait.VolumeTrait.supported( media_player.DOMAIN, - media_player.MediaPlayerEntityFeature.VOLUME_STEP, + MediaPlayerEntityFeature.VOLUME_STEP, None, None, ) @@ -2861,7 +2902,7 @@ async def test_volume_media_player_relative(hass: HomeAssistant) -> None: media_player.STATE_PLAYING, { ATTR_ASSUMED_STATE: True, - ATTR_SUPPORTED_FEATURES: media_player.MediaPlayerEntityFeature.VOLUME_STEP, + ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.VOLUME_STEP, }, ), BASIC_CONFIG, @@ -2918,8 +2959,7 @@ async def test_media_player_mute(hass: HomeAssistant) -> None: """Test volume trait support for muting.""" assert trait.VolumeTrait.supported( media_player.DOMAIN, - media_player.MediaPlayerEntityFeature.VOLUME_STEP - | media_player.MediaPlayerEntityFeature.VOLUME_MUTE, + MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE, None, None, ) @@ -2930,8 +2970,8 @@ async def test_media_player_mute(hass: HomeAssistant) -> None: media_player.STATE_PLAYING, { ATTR_SUPPORTED_FEATURES: ( - media_player.MediaPlayerEntityFeature.VOLUME_STEP - | media_player.MediaPlayerEntityFeature.VOLUME_MUTE + MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.VOLUME_MUTE ), media_player.ATTR_MEDIA_VOLUME_MUTED: False, }, @@ -3095,8 +3135,8 @@ async def test_transport_control(hass: HomeAssistant) -> None: media_player.ATTR_MEDIA_POSITION_UPDATED_AT: now - timedelta(seconds=10), media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.5, - ATTR_SUPPORTED_FEATURES: media_player.MediaPlayerEntityFeature.PLAY - | media_player.MediaPlayerEntityFeature.STOP, + ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.STOP, }, ), BASIC_CONFIG, @@ -3210,7 +3250,7 @@ async def test_media_state(hass: HomeAssistant, state) -> None: assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.TransportControlTrait.supported( - media_player.DOMAIN, media_player.MediaPlayerEntityFeature.PLAY, None, None + media_player.DOMAIN, MediaPlayerEntityFeature.PLAY, None, None ) trt = trait.MediaStateTrait( @@ -3222,8 +3262,8 @@ async def test_media_state(hass: HomeAssistant, state) -> None: media_player.ATTR_MEDIA_POSITION: 100, media_player.ATTR_MEDIA_DURATION: 200, media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.5, - ATTR_SUPPORTED_FEATURES: media_player.MediaPlayerEntityFeature.PLAY - | media_player.MediaPlayerEntityFeature.STOP, + ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.STOP, }, ), BASIC_CONFIG, @@ -3244,14 +3284,14 @@ async def test_channel(hass: HomeAssistant) -> None: assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.ChannelTrait.supported( media_player.DOMAIN, - media_player.MediaPlayerEntityFeature.PLAY_MEDIA, + MediaPlayerEntityFeature.PLAY_MEDIA, media_player.MediaPlayerDeviceClass.TV, None, ) assert ( trait.ChannelTrait.supported( media_player.DOMAIN, - media_player.MediaPlayerEntityFeature.PLAY_MEDIA, + MediaPlayerEntityFeature.PLAY_MEDIA, None, None, ) From 54cbc85c13587bedd45c30fd15516b4c355bc47f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 13 Aug 2023 18:57:46 +0200 Subject: [PATCH 0465/1151] Add types-Pillow dependency (#98266) --- homeassistant/components/generic/config_flow.py | 4 ++-- homeassistant/util/pil.py | 2 +- requirements_test.txt | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index ec94d4c227c..eb2d109caeb 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -12,7 +12,7 @@ from typing import Any from aiohttp import web from async_timeout import timeout from httpx import HTTPStatusError, RequestError, TimeoutException -import PIL +import PIL.Image import voluptuous as vol import yarl @@ -137,7 +137,7 @@ def get_image_type(image: bytes) -> str | None: imagefile = io.BytesIO(image) with contextlib.suppress(PIL.UnidentifiedImageError): img = PIL.Image.open(imagefile) - fmt = img.format.lower() + fmt = img.format.lower() if img.format else None if fmt is None: # if PIL can't figure it out, could be svg. diff --git a/homeassistant/util/pil.py b/homeassistant/util/pil.py index 068b807cbe5..58dd48dec5e 100644 --- a/homeassistant/util/pil.py +++ b/homeassistant/util/pil.py @@ -4,7 +4,7 @@ Can only be used by integrations that have pillow in their requirements. """ from __future__ import annotations -from PIL import ImageDraw +from PIL.ImageDraw import ImageDraw def draw_box( diff --git a/requirements_test.txt b/requirements_test.txt index 76a94c758b9..c1c7fdbdcc3 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -41,6 +41,7 @@ types-decorator==5.1.8.3 types-enum34==1.1.8 types-ipaddress==1.0.8 types-paho-mqtt==1.6.0.6 +types-Pillow==10.0.0.2 types-pkg-resources==0.1.3 types-psutil==5.9.5 types-python-dateutil==2.8.19.13 From ee3af297010ff17c937c76f470c0dd3974fba498 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 13 Aug 2023 18:58:34 +0200 Subject: [PATCH 0466/1151] Update coverage to 7.3.0 (#98327) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index c1c7fdbdcc3..e043497c4a8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt astroid==2.15.4 -coverage==7.2.7 +coverage==7.3.0 freezegun==1.2.2 mock-open==1.4.0 mypy==1.5.0 From e25fdebda1b7500a79ad6c51725e4f9e5209719d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 13 Aug 2023 18:58:55 +0200 Subject: [PATCH 0467/1151] Add types-caldav dependency (#98265) --- homeassistant/components/caldav/calendar.py | 1 + requirements_test.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 712873e51ce..57bf8e81e03 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -87,6 +87,7 @@ def setup_platform( calendars = client.principal().calendars() calendar_devices = [] + device_id: str | None for calendar in list(calendars): # If a calendar name was given in the configuration, # ignore all the others diff --git a/requirements_test.txt b/requirements_test.txt index e043497c4a8..9a7dd9b23da 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -36,6 +36,7 @@ tqdm==4.65.0 types-atomicwrites==1.4.5.1 types-croniter==1.0.6 types-backports==0.1.3 +types-caldav==1.2.0.2 types-chardet==0.1.5 types-decorator==5.1.8.3 types-enum34==1.1.8 From ef6e75657af067b7720adfa8127a7ea8075a4a0f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 13 Aug 2023 19:05:15 +0200 Subject: [PATCH 0468/1151] Update attrs to 23.1.0 (#97095) --- 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 282ff1ddb44..938f7a359d4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ astral==2.2 async-timeout==4.0.2 async-upnp-client==0.34.1 atomicwrites-homeassistant==1.4.1 -attrs==22.2.0 +attrs==23.1.0 awesomeversion==22.9.0 bcrypt==4.0.1 bleak-retry-connector==3.1.1 diff --git a/pyproject.toml b/pyproject.toml index 02dbc87fb72..f91776289af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "aiohttp==3.8.5", "astral==2.2", "async-timeout==4.0.2", - "attrs==22.2.0", + "attrs==23.1.0", "atomicwrites-homeassistant==1.4.1", "awesomeversion==22.9.0", "bcrypt==4.0.1", diff --git a/requirements.txt b/requirements.txt index f3cd10a3577..5e20ed3b5b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ aiohttp==3.8.5 astral==2.2 async-timeout==4.0.2 -attrs==22.2.0 +attrs==23.1.0 atomicwrites-homeassistant==1.4.1 awesomeversion==22.9.0 bcrypt==4.0.1 From 5b6a7edd8da94a6bc20399ff428051b2c87c494b Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 13 Aug 2023 11:06:12 -0700 Subject: [PATCH 0469/1151] Add Unifi outlet switches for PDU devices (#98320) Updates the Unifi outlet switching feature to support PDU devices --- homeassistant/components/unifi/switch.py | 11 +- tests/components/unifi/test_sensor.py | 2 +- tests/components/unifi/test_switch.py | 223 +++++++++++++++++++++-- 3 files changed, 214 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index ae339eb8d22..a82b9e35d45 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -106,6 +106,15 @@ async def async_dpi_group_control_fn( ) +@callback +def async_outlet_supports_switching_fn( + controller: UniFiController, obj_id: str +) -> bool: + """Determine if an outlet supports switching.""" + outlet = controller.api.outlets[obj_id] + return outlet.has_relay or outlet.caps in (1, 3) + + async def async_outlet_control_fn( api: aiounifi.Controller, obj_id: str, target: bool ) -> None: @@ -210,7 +219,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = ( name_fn=lambda outlet: outlet.name, object_fn=lambda api, obj_id: api.outlets[obj_id], should_poll=False, - supported_fn=lambda c, obj_id: c.api.outlets[obj_id].has_relay, + supported_fn=async_outlet_supports_switching_fn, unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-outlet-{obj_id.split('_', 1)[1]}", ), UnifiSwitchEntityDescription[Ports, Port]( diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 359825514d7..cf6b74b9765 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -785,7 +785,7 @@ async def test_outlet_power_readings( """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()) == 7 + assert len(hass.states.async_all()) == 9 assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 ent_reg = er.async_get(hass) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index ad5131614af..5344ac901b7 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -4,6 +4,7 @@ from datetime import timedelta from aiounifi.models.message import MessageKey from aiounifi.websocket import WebsocketState +import pytest from homeassistant import config_entries from homeassistant.components.switch import ( @@ -384,7 +385,7 @@ OUTLET_UP1 = { "x_vwirekey": "2dabb7e23b048c88b60123456789", "vwire_table": [], "dot1x_portctrl_enabled": False, - "outlet_overrides": [], + "outlet_overrides": [{"index": 1, "name": "Outlet 1", "relay_state": True}], "outlet_enabled": True, "license_state": "registered", "x_aes_gcm": True, @@ -580,6 +581,152 @@ OUTLET_UP1 = { } +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, +} + WLAN = { "_id": "012345678910111213141516", "bc_filter_enabled": False, @@ -960,56 +1107,92 @@ async def test_dpi_switches_add_second_app( assert hass.states.get("switch.block_media_streaming").state == STATE_ON +@pytest.mark.parametrize( + ("entity_id", "test_data", "outlet_index", "expected_switches"), + [ + ( + "plug_outlet_1", + OUTLET_UP1, + 1, + 1, + ), + ( + "dummy_usp_pdu_pro_usb_outlet_1", + PDU_DEVICE_1, + 1, + 2, + ), + ( + "dummy_usp_pdu_pro_outlet_2", + PDU_DEVICE_1, + 2, + 2, + ), + ], +) async def test_outlet_switches( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + mock_unifi_websocket, + entity_id: str, + test_data: any, + outlet_index: int, + expected_switches: int, ) -> None: """Test the outlet entities.""" config_entry = await setup_unifi_integration( - hass, aioclient_mock, devices_response=[OUTLET_UP1] + hass, aioclient_mock, devices_response=[test_data] ) controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == expected_switches # Validate state object - switch_1 = hass.states.get("switch.plug_outlet_1") + switch_1 = hass.states.get(f"switch.{entity_id}") assert switch_1 is not None assert switch_1.state == STATE_ON assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET # Update state object - device_1 = deepcopy(OUTLET_UP1) - device_1["outlet_table"][0]["relay_state"] = False + device_1 = deepcopy(test_data) + device_1["outlet_table"][outlet_index - 1]["relay_state"] = False mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() - assert hass.states.get("switch.plug_outlet_1").state == STATE_OFF + assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF # Turn off outlet + device_id = test_data["device_id"] aioclient_mock.clear_requests() aioclient_mock.put( - f"https://{controller.host}:1234/api/s/{controller.site}/rest/device/600c8356942a6ade50707b56", + f"https://{controller.host}:1234/api/s/{controller.site}/rest/device/{device_id}", ) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.plug_outlet_1"}, + {ATTR_ENTITY_ID: f"switch.{entity_id}"}, blocking=True, ) + + expected_off_overrides = deepcopy(device_1["outlet_overrides"]) + expected_off_overrides[outlet_index - 1]["relay_state"] = False + assert aioclient_mock.call_count == 1 assert aioclient_mock.mock_calls[0][2] == { - "outlet_overrides": [{"index": 1, "name": "Outlet 1", "relay_state": False}] + "outlet_overrides": expected_off_overrides } # Turn on outlet await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.plug_outlet_1"}, + {ATTR_ENTITY_ID: f"switch.{entity_id}"}, blocking=True, ) + + expected_on_overrides = deepcopy(device_1["outlet_overrides"]) + expected_on_overrides[outlet_index - 1]["relay_state"] = True assert aioclient_mock.call_count == 2 assert aioclient_mock.mock_calls[1][2] == { - "outlet_overrides": [{"index": 1, "name": "Outlet 1", "relay_state": True}] + "outlet_overrides": expected_on_overrides } # Availability signalling @@ -1017,33 +1200,33 @@ async def test_outlet_switches( # Controller disconnects mock_unifi_websocket(state=WebsocketState.DISCONNECTED) await hass.async_block_till_done() - assert hass.states.get("switch.plug_outlet_1").state == STATE_UNAVAILABLE + assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE # Controller reconnects mock_unifi_websocket(state=WebsocketState.RUNNING) await hass.async_block_till_done() - assert hass.states.get("switch.plug_outlet_1").state == STATE_OFF + assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF # Device gets disabled device_1["disabled"] = True mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() - assert hass.states.get("switch.plug_outlet_1").state == STATE_UNAVAILABLE + assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE # Device gets re-enabled device_1["disabled"] = False mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() - assert hass.states.get("switch.plug_outlet_1").state == STATE_OFF + assert hass.states.get(f"switch.{entity_id}").state == STATE_OFF # Unload config entry await hass.config_entries.async_unload(config_entry.entry_id) - assert hass.states.get("switch.plug_outlet_1").state == STATE_UNAVAILABLE + assert hass.states.get(f"switch.{entity_id}").state == STATE_UNAVAILABLE # Remove config entry await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get("switch.plug_outlet_1") is None + assert hass.states.get(f"switch.{entity_id}") is None async def test_new_client_discovered_on_block_control( From 66b01bee490f0f7a31769bb956d01728a2f8fa7b Mon Sep 17 00:00:00 2001 From: Mr-Ker <58399419+Mr-Ker@users.noreply.github.com> Date: Sun, 13 Aug 2023 21:05:57 +0200 Subject: [PATCH 0470/1151] Add support for Bosch 2nd Gen Shutter Contact (#98331) Add support for Bosch 2nd Gen SHCShutterContact2 We only need to check for the shutter contact 2 types as both devices provide the same properties that are used by the bosch_shc component. Resolves: #86295 --- homeassistant/components/bosch_shc/binary_sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bosch_shc/binary_sensor.py b/homeassistant/components/bosch_shc/binary_sensor.py index 25ab320a4c4..348bfe80701 100644 --- a/homeassistant/components/bosch_shc/binary_sensor.py +++ b/homeassistant/components/bosch_shc/binary_sensor.py @@ -25,7 +25,9 @@ async def async_setup_entry( entities: list[BinarySensorEntity] = [] session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] - for binary_sensor in session.device_helper.shutter_contacts: + for binary_sensor in ( + session.device_helper.shutter_contacts + session.device_helper.shutter_contacts2 + ): entities.append( ShutterContactSensor( device=binary_sensor, @@ -37,6 +39,7 @@ async def async_setup_entry( for binary_sensor in ( session.device_helper.motion_detectors + session.device_helper.shutter_contacts + + session.device_helper.shutter_contacts2 + session.device_helper.smoke_detectors + session.device_helper.thermostats + session.device_helper.twinguards From 429f939fee001ff7f5679da61fb838d8a5532be6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Aug 2023 17:23:03 -0500 Subject: [PATCH 0471/1151] Bump zeroconf to 0.75.0 (#98360) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index cd7b9e95e75..a6b840691bb 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.74.0"] + "requirements": ["zeroconf==0.75.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 938f7a359d4..a29fa2af946 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.74.0 +zeroconf==0.75.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 15c40edc32d..f771426929a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2752,7 +2752,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.74.0 +zeroconf==0.75.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e5e84347c2..bd6f9c70f4e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2025,7 +2025,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.74.0 +zeroconf==0.75.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 07e20e1eabf5b591b4f2ca045b0c22b2b499b5b2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 14 Aug 2023 00:23:38 +0200 Subject: [PATCH 0472/1151] Downgrade todoist-api-python to 2.0.2 and attrs to 22.2.0 (#98354) --- homeassistant/components/todoist/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index 22d3b19b6c9..ac7e899d8a1 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/todoist", "iot_class": "cloud_polling", "loggers": ["todoist"], - "requirements": ["todoist-api-python==2.1.1"] + "requirements": ["todoist-api-python==2.0.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a29fa2af946..78d8a7d4ac9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ astral==2.2 async-timeout==4.0.2 async-upnp-client==0.34.1 atomicwrites-homeassistant==1.4.1 -attrs==23.1.0 +attrs==22.2.0 awesomeversion==22.9.0 bcrypt==4.0.1 bleak-retry-connector==3.1.1 diff --git a/pyproject.toml b/pyproject.toml index f91776289af..02dbc87fb72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "aiohttp==3.8.5", "astral==2.2", "async-timeout==4.0.2", - "attrs==23.1.0", + "attrs==22.2.0", "atomicwrites-homeassistant==1.4.1", "awesomeversion==22.9.0", "bcrypt==4.0.1", diff --git a/requirements.txt b/requirements.txt index 5e20ed3b5b2..f3cd10a3577 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ aiohttp==3.8.5 astral==2.2 async-timeout==4.0.2 -attrs==23.1.0 +attrs==22.2.0 atomicwrites-homeassistant==1.4.1 awesomeversion==22.9.0 bcrypt==4.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index f771426929a..2844ad47e98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2554,7 +2554,7 @@ tilt-ble==0.2.3 tmb==0.0.4 # homeassistant.components.todoist -todoist-api-python==2.1.1 +todoist-api-python==2.0.2 # homeassistant.components.tolo tololib==0.1.0b4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd6f9c70f4e..c42ad02da8d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1860,7 +1860,7 @@ thermopro-ble==0.4.5 tilt-ble==0.2.3 # homeassistant.components.todoist -todoist-api-python==2.1.1 +todoist-api-python==2.0.2 # homeassistant.components.tolo tololib==0.1.0b4 From 790c1bc2519703b547ce6f16daa7a9f58857c00a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Aug 2023 19:37:45 -0500 Subject: [PATCH 0473/1151] Decrease event loop latency by binding time.monotonic to loop.time directly (#98288) * Decrease event loop latency by binding time.monotonic to loop.time directly This is a small improvment to decrease event loop latency. While the goal is is to reduce Bluetooth connection time latency, everything using asyncio is a bit more responsive as a result. * relo per comments * fix too fast by adding resolution, ensure monotonic time is patchable by freezegun * fix test that freezes time too late and has a race loop --- homeassistant/runner.py | 5 +++++ tests/common.py | 5 ++++- tests/components/statistics/test_sensor.py | 7 +++++-- tests/patch_time.py | 9 ++++++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 4bbf1a7dada..ed49db37f97 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -8,6 +8,7 @@ import logging import os import subprocess import threading +from time import monotonic import traceback from typing import Any @@ -114,6 +115,10 @@ class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): loop.set_default_executor = warn_use( # type: ignore[method-assign] loop.set_default_executor, "sets default executor on the event loop" ) + # bind the built-in time.monotonic directly as loop.time to avoid the + # overhead of the additional method call since its the most called loop + # method and its roughly 10%+ of all the call time in base_events.py + loop.time = monotonic # type: ignore[method-assign] return loop diff --git a/tests/common.py b/tests/common.py index eb8c8417f16..0431743cccf 100644 --- a/tests/common.py +++ b/tests/common.py @@ -420,6 +420,9 @@ def async_fire_time_changed( _async_fire_time_changed(hass, utc_datetime, fire_all) +_MONOTONIC_RESOLUTION = time.get_clock_info("monotonic").resolution + + @callback def _async_fire_time_changed( hass: HomeAssistant, utc_datetime: datetime | None, fire_all: bool @@ -432,7 +435,7 @@ def _async_fire_time_changed( continue mock_seconds_into_future = timestamp - time.time() - future_seconds = task.when() - hass.loop.time() + future_seconds = task.when() - (hass.loop.time() + _MONOTONIC_RESOLUTION) if fire_all or mock_seconds_into_future >= future_seconds: with patch( diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 4b77e2d0725..780e550f224 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import patch from freezegun import freeze_time +import pytest from homeassistant import config as hass_config from homeassistant.components.recorder import Recorder @@ -1286,12 +1287,14 @@ async def test_initialize_from_database( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS +@pytest.mark.freeze_time( + datetime(dt_util.utcnow().year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC) +) async def test_initialize_from_database_with_maxage( recorder_mock: Recorder, hass: HomeAssistant ) -> None: """Test initializing the statistics from the database.""" - now = dt_util.utcnow() - current_time = datetime(now.year + 1, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC) + current_time = dt_util.utcnow() # Testing correct retrieval from recorder, thus we do not # want purging to occur within the class itself. diff --git a/tests/patch_time.py b/tests/patch_time.py index 2a453053170..5f5dc467c9d 100644 --- a/tests/patch_time.py +++ b/tests/patch_time.py @@ -2,8 +2,9 @@ from __future__ import annotations import datetime +import time -from homeassistant import util +from homeassistant import runner, util from homeassistant.util import dt as dt_util @@ -12,5 +13,11 @@ def _utcnow() -> datetime.datetime: return datetime.datetime.now(datetime.UTC) +def _monotonic() -> float: + """Make monotonic patchable by freezegun.""" + return time.monotonic() + + dt_util.utcnow = _utcnow # type: ignore[assignment] util.utcnow = _utcnow # type: ignore[assignment] +runner.monotonic = _monotonic # type: ignore[assignment] From 96f9b852a210254e8c03245ee2a543d6ad608e1c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Aug 2023 22:47:29 -0500 Subject: [PATCH 0474/1151] Bump zeroconf to 0.76.0 (#98366) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index a6b840691bb..da8cfd26b1f 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.75.0"] + "requirements": ["zeroconf==0.76.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 78d8a7d4ac9..65e5dd33b8d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.75.0 +zeroconf==0.76.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 2844ad47e98..7e21d0778fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2752,7 +2752,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.75.0 +zeroconf==0.76.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c42ad02da8d..99cd624b45b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2025,7 +2025,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.75.0 +zeroconf==0.76.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 066db11620bfae5848d5120a82212a8a1dd7b252 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 14 Aug 2023 10:02:30 +0200 Subject: [PATCH 0475/1151] Exchange WazeRouteCalculator with pywaze in waze_travel_time (#98169) * exchange WazeRouteCalculator with pywaze * directly use async is_valid_config_entry * store pywaze client as property * fix tests * Remove obsolete error logs * Reuse existing httpx client * Remove redundant typing * Do not clcose common httpx client --- .../waze_travel_time/config_flow.py | 3 +- .../components/waze_travel_time/helpers.py | 16 ++++++--- .../components/waze_travel_time/manifest.json | 4 +-- .../components/waze_travel_time/sensor.py | 33 +++++++++-------- requirements_all.txt | 6 ++-- requirements_test_all.txt | 6 ++-- tests/components/waze_travel_time/conftest.py | 35 +++++++------------ .../waze_travel_time/test_sensor.py | 14 ++++---- 8 files changed, 58 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index a743844659c..60134452025 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -129,8 +129,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input: user_input[CONF_REGION] = user_input[CONF_REGION].upper() - if await self.hass.async_add_executor_job( - is_valid_config_entry, + if await is_valid_config_entry( self.hass, user_input[CONF_ORIGIN], user_input[CONF_DESTINATION], diff --git a/homeassistant/components/waze_travel_time/helpers.py b/homeassistant/components/waze_travel_time/helpers.py index 8468bb8ea9a..0659424429f 100644 --- a/homeassistant/components/waze_travel_time/helpers.py +++ b/homeassistant/components/waze_travel_time/helpers.py @@ -1,19 +1,25 @@ """Helpers for Waze Travel Time integration.""" import logging -from WazeRouteCalculator import WazeRouteCalculator, WRCError +from pywaze.route_calculator import WazeRouteCalculator, WRCError +from homeassistant.core import HomeAssistant +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.location import find_coordinates _LOGGER = logging.getLogger(__name__) -def is_valid_config_entry(hass, origin, destination, region): +async def is_valid_config_entry( + hass: HomeAssistant, origin: str, destination: str, region: str +) -> bool: """Return whether the config entry data is valid.""" - origin = find_coordinates(hass, origin) - destination = find_coordinates(hass, destination) + resolved_origin = find_coordinates(hass, origin) + resolved_destination = find_coordinates(hass, destination) + httpx_client = get_async_client(hass) + client = WazeRouteCalculator(region=region, client=httpx_client) try: - WazeRouteCalculator(origin, destination, region).calc_all_routes_info() + await client.calc_all_routes_info(resolved_origin, resolved_destination) except WRCError as error: _LOGGER.error("Error trying to validate entry: %s", error) return False diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 5e19ee6949c..3f1f8c6d67b 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "iot_class": "cloud_polling", - "loggers": ["WazeRouteCalculator", "homeassistant.helpers.location"], - "requirements": ["WazeRouteCalculator==0.14"] + "loggers": ["pywaze", "homeassistant.helpers.location"], + "requirements": ["pywaze==0.3.0"] } diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 2a620e48937..2b3010a39cb 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -5,7 +5,8 @@ from datetime import timedelta import logging from typing import Any -from WazeRouteCalculator import WazeRouteCalculator, WRCError +import httpx +from pywaze.route_calculator import WazeRouteCalculator, WRCError from homeassistant.components.sensor import ( SensorDeviceClass, @@ -23,6 +24,7 @@ from homeassistant.const import ( from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.location import find_coordinates from homeassistant.util.unit_conversion import DistanceConverter @@ -60,6 +62,7 @@ async def async_setup_entry( data = WazeTravelTimeData( region, + get_async_client(hass), config_entry, ) @@ -132,31 +135,33 @@ class WazeTravelTime(SensorEntity): async def first_update(self, _=None) -> None: """Run first update and write state.""" - await self.hass.async_add_executor_job(self.update) + await self.async_update() self.async_write_ha_state() - def update(self) -> None: + async def async_update(self) -> None: """Fetch new state data for the sensor.""" _LOGGER.debug("Fetching Route for %s", self._attr_name) self._waze_data.origin = find_coordinates(self.hass, self._origin) self._waze_data.destination = find_coordinates(self.hass, self._destination) - self._waze_data.update() + await self._waze_data.async_update() class WazeTravelTimeData: """WazeTravelTime Data object.""" - def __init__(self, region: str, config_entry: ConfigEntry) -> None: + def __init__( + self, region: str, client: httpx.AsyncClient, config_entry: ConfigEntry + ) -> None: """Set up WazeRouteCalculator.""" - self.region = region self.config_entry = config_entry + self.client = WazeRouteCalculator(region=region, client=client) self.origin: str | None = None self.destination: str | None = None self.duration = None self.distance = None self.route = None - def update(self): + async def async_update(self): """Update WazeRouteCalculator Sensor.""" _LOGGER.debug( "Getting update for origin: %s destination: %s", @@ -177,17 +182,17 @@ class WazeTravelTimeData: avoid_ferries = self.config_entry.options[CONF_AVOID_FERRIES] units = self.config_entry.options[CONF_UNITS] + routes = {} try: - params = WazeRouteCalculator( + routes = await self.client.calc_all_routes_info( self.origin, self.destination, - self.region, - vehicle_type, - avoid_toll_roads, - avoid_subscription_roads, - avoid_ferries, + vehicle_type=vehicle_type, + avoid_toll_roads=avoid_toll_roads, + avoid_subscription_roads=avoid_subscription_roads, + avoid_ferries=avoid_ferries, + real_time=realtime, ) - routes = params.calc_all_routes_info(real_time=realtime) if incl_filter not in {None, ""}: routes = { diff --git a/requirements_all.txt b/requirements_all.txt index 7e21d0778fa..39ac9a5a19a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,9 +139,6 @@ TwitterAPI==2.7.12 # homeassistant.components.onvif WSDiscovery==2.0.0 -# homeassistant.components.waze_travel_time -WazeRouteCalculator==0.14 - # homeassistant.components.accuweather accuweather==1.0.0 @@ -2226,6 +2223,9 @@ pyvlx==0.2.20 # homeassistant.components.volumio pyvolumio==0.1.5 +# homeassistant.components.waze_travel_time +pywaze==0.3.0 + # homeassistant.components.html5 pywebpush==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99cd624b45b..99970f83219 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -120,9 +120,6 @@ SQLAlchemy==2.0.15 # homeassistant.components.onvif WSDiscovery==2.0.0 -# homeassistant.components.waze_travel_time -WazeRouteCalculator==0.14 - # homeassistant.components.accuweather accuweather==1.0.0 @@ -1634,6 +1631,9 @@ pyvizio==0.1.61 # homeassistant.components.volumio pyvolumio==0.1.5 +# homeassistant.components.waze_travel_time +pywaze==0.3.0 + # homeassistant.components.html5 pywebpush==1.9.2 diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py index 65c2616d1dc..64c05a5dcc1 100644 --- a/tests/components/waze_travel_time/conftest.py +++ b/tests/components/waze_travel_time/conftest.py @@ -2,42 +2,31 @@ from unittest.mock import patch import pytest -from WazeRouteCalculator import WRCError - - -@pytest.fixture(name="mock_wrc", autouse=True) -def mock_wrc_fixture(): - """Mock out WazeRouteCalculator.""" - with patch( - "homeassistant.components.waze_travel_time.sensor.WazeRouteCalculator" - ) as mock_wrc: - yield mock_wrc +from pywaze.route_calculator import WRCError @pytest.fixture(name="mock_update") -def mock_update_fixture(mock_wrc): +def mock_update_fixture(): """Mock an update to the sensor.""" - obj = mock_wrc.return_value - obj.calc_all_routes_info.return_value = {"My route": (150, 300)} + with patch( + "pywaze.route_calculator.WazeRouteCalculator.calc_all_routes_info", + return_value={"My route": (150, 300)}, + ) as mock_wrc: + yield mock_wrc @pytest.fixture(name="validate_config_entry") -def validate_config_entry_fixture(): +def validate_config_entry_fixture(mock_update): """Return valid config entry.""" - with patch( - "homeassistant.components.waze_travel_time.helpers.WazeRouteCalculator" - ) as mock_wrc: - obj = mock_wrc.return_value - obj.calc_all_routes_info.return_value = None - yield mock_wrc + mock_update.return_value = None + return mock_update @pytest.fixture(name="invalidate_config_entry") def invalidate_config_entry_fixture(validate_config_entry): """Return invalid config entry.""" - obj = validate_config_entry.return_value - obj.calc_all_routes_info.return_value = {} - obj.calc_all_routes_info.side_effect = WRCError("test") + validate_config_entry.side_effect = WRCError("test") + return validate_config_entry @pytest.fixture(name="bypass_platform_setup") diff --git a/tests/components/waze_travel_time/test_sensor.py b/tests/components/waze_travel_time/test_sensor.py index a3367a48d2a..adcc334889d 100644 --- a/tests/components/waze_travel_time/test_sensor.py +++ b/tests/components/waze_travel_time/test_sensor.py @@ -1,6 +1,6 @@ """Test Waze Travel Time sensors.""" import pytest -from WazeRouteCalculator import WRCError +from pywaze.route_calculator import WRCError from homeassistant.components.waze_travel_time.const import ( CONF_AVOID_FERRIES, @@ -35,17 +35,17 @@ async def mock_config_fixture(hass, data, options): @pytest.fixture(name="mock_update_wrcerror") -def mock_update_wrcerror_fixture(mock_wrc): +def mock_update_wrcerror_fixture(mock_update): """Mock an update to the sensor failed with WRCError.""" - obj = mock_wrc.return_value - obj.calc_all_routes_info.side_effect = WRCError("test") + mock_update.side_effect = WRCError("test") + return mock_update @pytest.fixture(name="mock_update_keyerror") -def mock_update_keyerror_fixture(mock_wrc): +def mock_update_keyerror_fixture(mock_update): """Mock an update to the sensor failed with KeyError.""" - obj = mock_wrc.return_value - obj.calc_all_routes_info.side_effect = KeyError("test") + mock_update.side_effect = KeyError("test") + return mock_update @pytest.mark.parametrize( From 21acb5527f7907b4f16e5c94813203043d1df502 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:22:53 +0200 Subject: [PATCH 0476/1151] Update beautifulsoup to 4.12.2 (#98372) --- homeassistant/components/scrape/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 23845cc2eac..26603603198 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -6,5 +6,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/scrape", "iot_class": "cloud_polling", - "requirements": ["beautifulsoup4==4.11.1", "lxml==4.9.3"] + "requirements": ["beautifulsoup4==4.12.2", "lxml==4.9.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 39ac9a5a19a..e38135267e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -494,7 +494,7 @@ batinfo==0.4.2 # beacontools[scan]==2.1.0 # homeassistant.components.scrape -beautifulsoup4==4.11.1 +beautifulsoup4==4.12.2 # homeassistant.components.beewi_smartclim # beewi-smartclim==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99970f83219..ec44bde36af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -418,7 +418,7 @@ azure-eventhub==5.11.1 base36==0.1.1 # homeassistant.components.scrape -beautifulsoup4==4.11.1 +beautifulsoup4==4.12.2 # homeassistant.components.zha bellows==0.35.9 From e36a8f6e8bdaaa9c65c827ac3a098b198686ab99 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:23:23 +0200 Subject: [PATCH 0477/1151] Update async-timeout to 4.0.3 (#98370) --- 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 65e5dd33b8d..78973f15520 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,7 +2,7 @@ aiodiscover==1.4.16 aiohttp-cors==0.7.0 aiohttp==3.8.5 astral==2.2 -async-timeout==4.0.2 +async-timeout==4.0.3 async-upnp-client==0.34.1 atomicwrites-homeassistant==1.4.1 attrs==22.2.0 diff --git a/pyproject.toml b/pyproject.toml index 02dbc87fb72..af386239ac5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ requires-python = ">=3.11.0" dependencies = [ "aiohttp==3.8.5", "astral==2.2", - "async-timeout==4.0.2", + "async-timeout==4.0.3", "attrs==22.2.0", "atomicwrites-homeassistant==1.4.1", "awesomeversion==22.9.0", diff --git a/requirements.txt b/requirements.txt index f3cd10a3577..0c55b1f9a9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ # Home Assistant Core aiohttp==3.8.5 astral==2.2 -async-timeout==4.0.2 +async-timeout==4.0.3 attrs==22.2.0 atomicwrites-homeassistant==1.4.1 awesomeversion==22.9.0 From f7d95e00f663270fd0eb99e5be00faf616de28b3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:29:26 +0200 Subject: [PATCH 0478/1151] Update tqdm to 4.66.1 (#98328) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9a7dd9b23da..6df5c7a9b9a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -32,7 +32,7 @@ pytest==7.3.1 requests_mock==1.11.0 respx==0.20.2 syrupy==4.0.8 -tqdm==4.65.0 +tqdm==4.66.1 types-atomicwrites==1.4.5.1 types-croniter==1.0.6 types-backports==0.1.3 From 7cf1ff887d983f012de68a9d94355a98fcc867b9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:31:24 +0200 Subject: [PATCH 0479/1151] Update caldav to 1.3.6 (#98371) --- homeassistant/components/caldav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 16624f2af56..92e2f7e67d8 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/caldav", "iot_class": "cloud_polling", "loggers": ["caldav", "vobject"], - "requirements": ["caldav==1.2.0"] + "requirements": ["caldav==1.3.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index e38135267e9..de2a2302312 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -580,7 +580,7 @@ btsmarthub-devicelist==0.2.3 buienradar==1.0.5 # homeassistant.components.caldav -caldav==1.2.0 +caldav==1.3.6 # homeassistant.components.circuit circuit-webhook==1.0.1 diff --git a/requirements_test.txt b/requirements_test.txt index 6df5c7a9b9a..3ba9ed8abf8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -36,7 +36,7 @@ tqdm==4.66.1 types-atomicwrites==1.4.5.1 types-croniter==1.0.6 types-backports==0.1.3 -types-caldav==1.2.0.2 +types-caldav==1.3.0.0 types-chardet==0.1.5 types-decorator==5.1.8.3 types-enum34==1.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec44bde36af..5618c9bf7ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -478,7 +478,7 @@ bthome-ble==3.0.0 buienradar==1.0.5 # homeassistant.components.caldav -caldav==1.2.0 +caldav==1.3.6 # homeassistant.components.coinbase coinbase==2.1.0 From e0d6210bd0f52a76fb567d095aab1ae61b5e827b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:38:53 +0200 Subject: [PATCH 0480/1151] Create pytest output artifact [ci] (#98106) --- .github/workflows/ci.yaml | 41 +++++++++++++++++++++++++++++++++++---- .gitignore | 1 + 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 31a158c1ffd..6d41c4e1e7f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -734,9 +734,12 @@ jobs: - name: Run pytest (fully) if: needs.info.outputs.test_full_suite == 'true' timeout-minutes: 60 + id: pytest-full run: | . venv/bin/activate python --version + set -o pipefail + python3 -X dev -m pytest \ -qq \ --timeout=9 \ @@ -749,14 +752,17 @@ jobs: --cov-report=xml \ -o console_output_style=count \ -p no:sugar \ - tests + tests \ + 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Run pytest (partially) if: needs.info.outputs.test_full_suite == 'false' timeout-minutes: 10 + id: pytest-partial shell: bash run: | . venv/bin/activate python --version + set -o pipefail if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py" @@ -774,7 +780,14 @@ jobs: --durations=0 \ --durations-min=1 \ -p no:sugar \ - tests/components/${{ matrix.group }} + tests/components/${{ matrix.group }} \ + 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt + - name: Upload pytest output + if: success() || failure() && (steps.pytest-full.conclusion == 'failure' || steps.pytest-partial.conclusion == 'failure') + uses: actions/upload-artifact@v3.1.2 + with: + name: pytest-${{ github.run_number }} + path: pytest-*.txt - name: Upload coverage artifact uses: actions/upload-artifact@v3.1.2 with: @@ -862,10 +875,13 @@ jobs: python3 -m script.translations develop --all - name: Run pytest (partially) timeout-minutes: 20 + id: pytest-partial shell: bash run: | . venv/bin/activate python --version + set -o pipefail + mariadb=$(echo "${{ matrix.mariadb-group }}" | sed "s/:/-/g") python3 -X dev -m pytest \ -qq \ @@ -881,7 +897,14 @@ jobs: tests/components/history \ tests/components/logbook \ tests/components/recorder \ - tests/components/sensor + tests/components/sensor \ + 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt + - name: Upload pytest output + if: success() || failure() && steps.pytest-partial.conclusion == 'failure' + uses: actions/upload-artifact@v3.1.2 + with: + name: pytest-${{ github.run_number }} + path: pytest-*.txt - name: Upload coverage artifact uses: actions/upload-artifact@v3.1.2 with: @@ -969,10 +992,13 @@ jobs: python3 -m script.translations develop --all - name: Run pytest (partially) timeout-minutes: 20 + id: pytest-partial shell: bash run: | . venv/bin/activate python --version + set -o pipefail + postgresql=$(echo "${{ matrix.postgresql-group }}" | sed "s/:/-/g") python3 -X dev -m pytest \ -qq \ @@ -989,7 +1015,14 @@ jobs: tests/components/history \ tests/components/logbook \ tests/components/recorder \ - tests/components/sensor + tests/components/sensor \ + 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt + - name: Upload pytest output + if: success() || failure() && steps.pytest-partial.conclusion == 'failure' + uses: actions/upload-artifact@v3.1.2 + with: + name: pytest-${{ github.run_number }} + path: pytest-*.txt - name: Upload coverage artifact uses: actions/upload-artifact@v3.1.0 with: diff --git a/.gitignore b/.gitignore index 2f3c3e10301..ff20c088eb2 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ htmlcov/ test-reports/ test-results.xml test-output.xml +pytest-*.txt # Translations *.mo From 533a8beac23e529beaed206b47e85c4b8192658a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:42:20 +0200 Subject: [PATCH 0481/1151] Raise ConfigEntryNotReady when unable to connect during setup of AVM Fritz!Smarthome (#97985) --- homeassistant/components/fritzbox/__init__.py | 5 +++- .../components/fritzbox/coordinator.py | 6 ++--- .../components/fritzbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/fritzbox/test_init.py | 23 ++++++++++++++++++- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 54e09f90df7..d199d2c5a2c 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -5,6 +5,7 @@ from abc import ABC, abstractmethod from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase +from requests.exceptions import ConnectionError as RequestConnectionError from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry @@ -16,7 +17,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import Event, HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries @@ -36,6 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await hass.async_add_executor_job(fritz.login) + except RequestConnectionError as err: + raise ConfigEntryNotReady from err except LoginError as err: raise ConfigEntryAuthFailed from err diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index 80087adf9ac..194825e602f 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -6,7 +6,7 @@ from datetime import timedelta from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError from pyfritzhome.devicetypes import FritzhomeTemplate -import requests +from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -51,9 +51,9 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat self.fritz.update_devices() if self.has_templates: self.fritz.update_templates() - except requests.exceptions.ConnectionError as ex: + except RequestConnectionError as ex: raise UpdateFailed from ex - except requests.exceptions.HTTPError: + except HTTPError: # If the device rebooted, login again try: self.fritz.login() diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 29df2f51a34..35b78e91f81 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -7,7 +7,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["pyfritzhome"], - "requirements": ["pyfritzhome==0.6.8"], + "requirements": ["pyfritzhome==0.6.9"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/requirements_all.txt b/requirements_all.txt index de2a2302312..7c1d4ea5bb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1701,7 +1701,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.8 +pyfritzhome==0.6.9 # homeassistant.components.ifttt pyfttt==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5618c9bf7ea..e143e1d9156 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1256,7 +1256,7 @@ pyforked-daapd==0.1.14 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.8 +pyfritzhome==0.6.9 # homeassistant.components.ifttt pyfttt==0.3 diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 28476d88273..dd5a8127185 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -205,7 +205,7 @@ async def test_coordinator_update_when_unreachable( unique_id="any", ) entry.add_to_hass(hass) - fritz().get_devices.side_effect = [ConnectionError(), ""] + fritz().update_devices.side_effect = [ConnectionError(), ""] assert not await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY @@ -258,6 +258,27 @@ async def test_raise_config_entry_not_ready_when_offline(hass: HomeAssistant) -> unique_id="any", ) entry.add_to_hass(hass) + with patch( + "homeassistant.components.fritzbox.Fritzhome.login", + side_effect=ConnectionError(), + ) as mock_login: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + mock_login.assert_called_once() + + entries = hass.config_entries.async_entries() + config_entry = entries[0] + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_raise_config_entry_error_when_login_fail(hass: HomeAssistant) -> None: + """Config entry state is SETUP_ERROR when login to fritzbox fail.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data={CONF_HOST: "any", **MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0]}, + unique_id="any", + ) + entry.add_to_hass(hass) with patch( "homeassistant.components.fritzbox.Fritzhome.login", side_effect=LoginError("user"), From e0ee713bb27b5bec2df68077fe88e35cb46c40ce Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Aug 2023 11:32:55 +0200 Subject: [PATCH 0482/1151] Store preferred border agent ID in thread dataset store (#98375) --- homeassistant/components/thread/__init__.py | 4 ++ .../components/thread/dataset_store.py | 37 ++++++++++++++++++- .../components/thread/websocket_api.py | 34 +++++++++++++++++ tests/components/thread/test_dataset_store.py | 36 ++++++++++++++++++ tests/components/thread/test_websocket_api.py | 29 +++++++++++++++ 5 files changed, 139 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/thread/__init__.py b/homeassistant/components/thread/__init__.py index dd2527763ad..679127e5202 100644 --- a/homeassistant/components/thread/__init__.py +++ b/homeassistant/components/thread/__init__.py @@ -11,7 +11,9 @@ from .dataset_store import ( DatasetEntry, async_add_dataset, async_get_dataset, + async_get_preferred_border_agent_id, async_get_preferred_dataset, + async_set_preferred_border_agent_id, ) from .websocket_api import async_setup as async_setup_ws_api @@ -19,8 +21,10 @@ __all__ = [ "DOMAIN", "DatasetEntry", "async_add_dataset", + "async_get_preferred_border_agent_id", "async_get_dataset", "async_get_preferred_dataset", + "async_set_preferred_border_agent_id", ] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) diff --git a/homeassistant/components/thread/dataset_store.py b/homeassistant/components/thread/dataset_store.py index 55623f7e3a4..96a9cf8e59e 100644 --- a/homeassistant/components/thread/dataset_store.py +++ b/homeassistant/components/thread/dataset_store.py @@ -19,7 +19,7 @@ from homeassistant.util import dt as dt_util, ulid as ulid_util DATA_STORE = "thread.datasets" STORAGE_KEY = "thread.datasets" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 2 +STORAGE_VERSION_MINOR = 3 SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) @@ -86,7 +86,9 @@ class DatasetStoreStore(Store): ) -> dict[str, Any]: """Migrate to the new version.""" if old_major_version == 1: + data = old_data if old_minor_version < 2: + # Deduplicate datasets datasets: dict[str, DatasetEntry] = {} preferred_dataset = old_data["preferred_dataset"] @@ -156,6 +158,9 @@ class DatasetStoreStore(Store): "preferred_dataset": preferred_dataset, "datasets": [dataset.to_json() for dataset in datasets.values()], } + if old_minor_version < 3: + # Add border agent ID + data.setdefault("preferred_border_agent_id", None) return data @@ -167,6 +172,7 @@ class DatasetStore: """Initialize the dataset store.""" self.hass = hass self.datasets: dict[str, DatasetEntry] = {} + self._preferred_border_agent_id: str | None = None self._preferred_dataset: str | None = None self._store: Store[dict[str, Any]] = DatasetStoreStore( hass, @@ -259,6 +265,17 @@ class DatasetStore: """Get dataset by id.""" return self.datasets.get(dataset_id) + @callback + def async_get_preferred_border_agent_id(self) -> str | None: + """Get preferred border agent id.""" + return self._preferred_border_agent_id + + @callback + def async_set_preferred_border_agent_id(self, border_agent_id: str) -> None: + """Set preferred border agent id.""" + self._preferred_border_agent_id = border_agent_id + self.async_schedule_save() + @property @callback def preferred_dataset(self) -> str | None: @@ -279,6 +296,7 @@ class DatasetStore: data = await self._store.async_load() datasets: dict[str, DatasetEntry] = {} + preferred_border_agent_id: str | None = None preferred_dataset: str | None = None if data is not None: @@ -290,9 +308,11 @@ class DatasetStore: source=dataset["source"], tlv=dataset["tlv"], ) + preferred_border_agent_id = data["preferred_border_agent_id"] preferred_dataset = data["preferred_dataset"] self.datasets = datasets + self._preferred_border_agent_id = preferred_border_agent_id self._preferred_dataset = preferred_dataset @callback @@ -305,6 +325,7 @@ class DatasetStore: """Return data of datasets to store in a file.""" data: dict[str, Any] = {} data["datasets"] = [dataset.to_json() for dataset in self.datasets.values()] + data["preferred_border_agent_id"] = self._preferred_border_agent_id data["preferred_dataset"] = self._preferred_dataset return data @@ -331,6 +352,20 @@ async def async_get_dataset(hass: HomeAssistant, dataset_id: str) -> str | None: return entry.tlv +async def async_get_preferred_border_agent_id(hass: HomeAssistant) -> str | None: + """Get the preferred border agent ID.""" + store = await async_get_store(hass) + return store.async_get_preferred_border_agent_id() + + +async def async_set_preferred_border_agent_id( + hass: HomeAssistant, border_agent_id: str +) -> None: + """Get the preferred border agent ID.""" + store = await async_get_store(hass) + store.async_set_preferred_border_agent_id(border_agent_id) + + async def async_get_preferred_dataset(hass: HomeAssistant) -> str | None: """Get the preferred dataset.""" store = await async_get_store(hass) diff --git a/homeassistant/components/thread/websocket_api.py b/homeassistant/components/thread/websocket_api.py index 60941426b7e..853d8c3c893 100644 --- a/homeassistant/components/thread/websocket_api.py +++ b/homeassistant/components/thread/websocket_api.py @@ -20,6 +20,8 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_discover_routers) websocket_api.async_register_command(hass, ws_get_dataset) websocket_api.async_register_command(hass, ws_list_datasets) + websocket_api.async_register_command(hass, ws_get_preferred_border_agent_id) + websocket_api.async_register_command(hass, ws_set_preferred_border_agent_id) websocket_api.async_register_command(hass, ws_set_preferred_dataset) @@ -50,6 +52,38 @@ async def ws_add_dataset( connection.send_result(msg["id"]) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "thread/get_preferred_border_agent_id", + } +) +@websocket_api.async_response +async def ws_get_preferred_border_agent_id( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Get the preferred border agent ID.""" + border_agent_id = await dataset_store.async_get_preferred_border_agent_id(hass) + connection.send_result(msg["id"], {"border_agent_id": border_agent_id}) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "thread/set_preferred_border_agent_id", + vol.Required("border_agent_id"): str, + } +) +@websocket_api.async_response +async def ws_set_preferred_border_agent_id( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Set the preferred border agent ID.""" + border_agent_id = msg["border_agent_id"] + await dataset_store.async_set_preferred_border_agent_id(hass, border_agent_id) + connection.send_result(msg["id"]) + + @websocket_api.require_admin @websocket_api.websocket_command( { diff --git a/tests/components/thread/test_dataset_store.py b/tests/components/thread/test_dataset_store.py index 1ed754dbdcd..1171c597e99 100644 --- a/tests/components/thread/test_dataset_store.py +++ b/tests/components/thread/test_dataset_store.py @@ -319,12 +319,17 @@ async def test_loading_datasets_from_storage( "tlv": DATASET_3, }, ], + "preferred_border_agent_id": "230C6A1AC57F6F4BE262ACF32E5EF52C", "preferred_dataset": "id1", }, } store = await dataset_store.async_get_store(hass) assert len(store.datasets) == 3 + assert ( + store.async_get_preferred_border_agent_id() + == "230C6A1AC57F6F4BE262ACF32E5EF52C" + ) assert store.preferred_dataset == "id1" @@ -512,3 +517,34 @@ async def test_migrate_drop_duplicate_datasets_preferred( f"Dropped duplicated Thread dataset '{DATASET_1_LARGER_TIMESTAMP}' " f"(duplicate of preferred dataset '{DATASET_1}')" ) in caplog.text + + +async def test_migrate_set_default_border_agent_id( + hass: HomeAssistant, hass_storage: dict[str, Any], caplog +) -> None: + """Test migrating the dataset store adds default border agent.""" + hass_storage[dataset_store.STORAGE_KEY] = { + "version": 1, + "minor_version": 2, + "data": { + "datasets": [ + { + "created": "2023-02-02T09:41:13.746514+00:00", + "id": "id1", + "source": "source_1", + "tlv": DATASET_1, + }, + ], + "preferred_dataset": "id1", + }, + } + + store = await dataset_store.async_get_store(hass) + assert store.async_get_preferred_border_agent_id() is None + + +async def test_preferred_border_agent_id(hass: HomeAssistant) -> None: + """Test get and set the preferred border agent ID.""" + assert await dataset_store.async_get_preferred_border_agent_id(hass) is None + await dataset_store.async_set_preferred_border_agent_id(hass, "blah") + assert await dataset_store.async_get_preferred_border_agent_id(hass) == "blah" diff --git a/tests/components/thread/test_websocket_api.py b/tests/components/thread/test_websocket_api.py index 0db16318db1..82450474e92 100644 --- a/tests/components/thread/test_websocket_api.py +++ b/tests/components/thread/test_websocket_api.py @@ -200,6 +200,35 @@ async def test_list_get_dataset( assert msg["error"] == {"code": "not_found", "message": "unknown dataset"} +async def test_preferred_border_agent_id( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test setting and getting the preferred border agent ID.""" + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + await client.send_json_auto_id({"type": "thread/get_preferred_border_agent_id"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"border_agent_id": None} + + await client.send_json_auto_id( + {"type": "thread/set_preferred_border_agent_id", "border_agent_id": "blah"} + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + await client.send_json_auto_id({"type": "thread/get_preferred_border_agent_id"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"border_agent_id": "blah"} + + assert await dataset_store.async_get_preferred_border_agent_id(hass) == "blah" + + async def test_set_preferred_dataset( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: From 4dd102f818ca37c1faafca2022b61f1aa4359ec4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Aug 2023 11:33:07 +0200 Subject: [PATCH 0483/1151] Bump python-otbr-api to 2.4.0 (#98376) --- homeassistant/components/otbr/manifest.json | 2 +- homeassistant/components/thread/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/otbr/manifest.json b/homeassistant/components/otbr/manifest.json index a8a5ae062f7..e62a2d42b1e 100644 --- a/homeassistant/components/otbr/manifest.json +++ b/homeassistant/components/otbr/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/otbr", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["python-otbr-api==2.3.0"] + "requirements": ["python-otbr-api==2.4.0"] } diff --git a/homeassistant/components/thread/manifest.json b/homeassistant/components/thread/manifest.json index 71dbb786eb5..29b7e61d407 100644 --- a/homeassistant/components/thread/manifest.json +++ b/homeassistant/components/thread/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/thread", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["python-otbr-api==2.3.0", "pyroute2==0.7.5"], + "requirements": ["python-otbr-api==2.4.0", "pyroute2==0.7.5"], "zeroconf": ["_meshcop._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 7c1d4ea5bb8..0bd5e55c1a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2141,7 +2141,7 @@ python-opensky==0.2.0 # homeassistant.components.otbr # homeassistant.components.thread -python-otbr-api==2.3.0 +python-otbr-api==2.4.0 # homeassistant.components.picnic python-picnic-api==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e143e1d9156..31e4aa2d284 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1570,7 +1570,7 @@ python-opensky==0.2.0 # homeassistant.components.otbr # homeassistant.components.thread -python-otbr-api==2.3.0 +python-otbr-api==2.4.0 # homeassistant.components.picnic python-picnic-api==1.1.0 From 180ff2449282602f1ef2bf9646460cf0f08a64bf Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 14 Aug 2023 11:50:14 +0200 Subject: [PATCH 0484/1151] Add types-beautifulsoup4 dependency (#98377) --- homeassistant/components/scrape/sensor.py | 1 + requirements_test.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 197f2e003d8..7cd7e2197ab 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -178,6 +178,7 @@ class ScrapeSensor( def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" raw_data = self.coordinator.data + value: str | list[str] | None try: if self._attr is not None: value = raw_data.select(self._select)[self._index][self._attr] diff --git a/requirements_test.txt b/requirements_test.txt index 3ba9ed8abf8..acb70d5fb8c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -36,6 +36,7 @@ tqdm==4.66.1 types-atomicwrites==1.4.5.1 types-croniter==1.0.6 types-backports==0.1.3 +types-beautifulsoup4==4.12.0.6 types-caldav==1.3.0.0 types-chardet==0.1.5 types-decorator==5.1.8.3 From 9ce033daebd7dde8ed71b85ff0d8e27d0e0b9929 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 14 Aug 2023 11:51:08 +0200 Subject: [PATCH 0485/1151] Use default translations by removing names from tplink descriptions (#98338) --- homeassistant/components/tplink/sensor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index ba4949434f7..46909f39dfe 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -50,7 +50,6 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - name="Current Consumption", emeter_attr="power", precision=1, ), @@ -60,7 +59,6 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - name="Total Consumption", emeter_attr="total", precision=3, ), @@ -70,7 +68,6 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - name="Today's Consumption", precision=3, ), TPLinkSensorEntityDescription( From 11b1a42a1c65a689af264471560259767cba2a54 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 14 Aug 2023 12:52:27 +0200 Subject: [PATCH 0486/1151] Add entity translations to Aseko (#98117) --- .../aseko_pool_live/binary_sensor.py | 5 ++--- .../components/aseko_pool_live/entity.py | 2 ++ .../components/aseko_pool_live/sensor.py | 15 +++++++------ .../components/aseko_pool_live/strings.json | 21 +++++++++++++++++++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/aseko_pool_live/binary_sensor.py b/homeassistant/components/aseko_pool_live/binary_sensor.py index 8178e243279..3e0e57fffac 100644 --- a/homeassistant/components/aseko_pool_live/binary_sensor.py +++ b/homeassistant/components/aseko_pool_live/binary_sensor.py @@ -37,19 +37,18 @@ class AsekoBinarySensorEntityDescription( UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = ( AsekoBinarySensorEntityDescription( key="water_flow", - name="Water Flow", + translation_key="water_flow", icon="mdi:waves-arrow-right", value_fn=lambda unit: unit.water_flow, ), AsekoBinarySensorEntityDescription( key="has_alarm", - name="Alarm", + translation_key="alarm", value_fn=lambda unit: unit.has_alarm, device_class=BinarySensorDeviceClass.SAFETY, ), AsekoBinarySensorEntityDescription( key="has_error", - name="Error", value_fn=lambda unit: unit.has_error, device_class=BinarySensorDeviceClass.PROBLEM, ), diff --git a/homeassistant/components/aseko_pool_live/entity.py b/homeassistant/components/aseko_pool_live/entity.py index 54afc80d451..1defbe18345 100644 --- a/homeassistant/components/aseko_pool_live/entity.py +++ b/homeassistant/components/aseko_pool_live/entity.py @@ -11,6 +11,8 @@ from .coordinator import AsekoDataUpdateCoordinator class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]): """Representation of an aseko entity.""" + _attr_has_entity_name = True + def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None: """Initialize the aseko entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/aseko_pool_live/sensor.py b/homeassistant/components/aseko_pool_live/sensor.py index 09c4af31428..d7e5e330705 100644 --- a/homeassistant/components/aseko_pool_live/sensor.py +++ b/homeassistant/components/aseko_pool_live/sensor.py @@ -45,13 +45,16 @@ class VariableSensorEntity(AsekoEntity, SensorEntity): super().__init__(unit, coordinator) self._variable = variable - variable_name = { - "Air temp.": "Air Temperature", - "Cl free": "Free Chlorine", - "Water temp.": "Water Temperature", - }.get(self._variable.name, self._variable.name) + translation_key = { + "Air temp.": "air_temperature", + "Cl free": "free_chlorine", + "Water temp.": "water_temperature", + }.get(self._variable.name) + if translation_key is not None: + self._attr_translation_key = translation_key + else: + self._attr_name = self._variable.name - self._attr_name = f"{self._device_name} {variable_name}" self._attr_unique_id = f"{self._unit.serial_number}{self._variable.type}" self._attr_native_unit_of_measurement = self._variable.unit diff --git a/homeassistant/components/aseko_pool_live/strings.json b/homeassistant/components/aseko_pool_live/strings.json index 7a91b2c9f8b..2a6df30b148 100644 --- a/homeassistant/components/aseko_pool_live/strings.json +++ b/homeassistant/components/aseko_pool_live/strings.json @@ -16,5 +16,26 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" } + }, + "entity": { + "binary_sensor": { + "water_flow": { + "name": "Water flow" + }, + "alarm": { + "name": "Alarm" + } + }, + "sensor": { + "air_temperature": { + "name": "Air temperature" + }, + "free_chlorine": { + "name": "Free chlorine" + }, + "water_temperature": { + "name": "Water temperature" + } + } } } From 398a789ba2cc9267752e14b5201130f0539dec21 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 14 Aug 2023 13:14:49 +0200 Subject: [PATCH 0487/1151] Add entity translations to justnimbus (#98235) --- homeassistant/components/justnimbus/entity.py | 2 + homeassistant/components/justnimbus/sensor.py | 24 +++++------ .../components/justnimbus/strings.json | 40 +++++++++++++++++++ 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/justnimbus/entity.py b/homeassistant/components/justnimbus/entity.py index 968e9581a67..7303d4ec2c7 100644 --- a/homeassistant/components/justnimbus/entity.py +++ b/homeassistant/components/justnimbus/entity.py @@ -13,6 +13,8 @@ class JustNimbusEntity( ): """Defines a base JustNimbus entity.""" + _attr_has_entity_name = True + def __init__( self, *, diff --git a/homeassistant/components/justnimbus/sensor.py b/homeassistant/components/justnimbus/sensor.py index e3d6562c088..156fa37e982 100644 --- a/homeassistant/components/justnimbus/sensor.py +++ b/homeassistant/components/justnimbus/sensor.py @@ -46,7 +46,7 @@ class JustNimbusEntityDescription( SENSOR_TYPES = ( JustNimbusEntityDescription( key="pump_flow", - name="Pump flow", + translation_key="pump_flow", icon="mdi:pump", native_unit_of_measurement=VOLUME_FLOW_RATE_LITERS_PER_MINUTE, state_class=SensorStateClass.MEASUREMENT, @@ -55,7 +55,7 @@ SENSOR_TYPES = ( ), JustNimbusEntityDescription( key="drink_flow", - name="Drink flow", + translation_key="drink_flow", icon="mdi:water-pump", native_unit_of_measurement=VOLUME_FLOW_RATE_LITERS_PER_MINUTE, state_class=SensorStateClass.MEASUREMENT, @@ -64,7 +64,7 @@ SENSOR_TYPES = ( ), JustNimbusEntityDescription( key="pump_pressure", - name="Pump pressure", + translation_key="pump_pressure", native_unit_of_measurement=UnitOfPressure.BAR, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, @@ -73,7 +73,7 @@ SENSOR_TYPES = ( ), JustNimbusEntityDescription( key="pump_starts", - name="Pump starts", + translation_key="pump_starts", icon="mdi:restart", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -81,7 +81,7 @@ SENSOR_TYPES = ( ), JustNimbusEntityDescription( key="pump_hours", - name="Pump hours", + translation_key="pump_hours", icon="mdi:clock", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.HOURS, @@ -91,7 +91,7 @@ SENSOR_TYPES = ( ), JustNimbusEntityDescription( key="reservoir_temp", - name="Reservoir Temperature", + translation_key="reservoir_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -100,7 +100,7 @@ SENSOR_TYPES = ( ), JustNimbusEntityDescription( key="reservoir_content", - name="Reservoir content", + translation_key="reservoir_content", icon="mdi:car-coolant-level", native_unit_of_measurement=UnitOfVolume.LITERS, device_class=SensorDeviceClass.VOLUME, @@ -110,7 +110,7 @@ SENSOR_TYPES = ( ), JustNimbusEntityDescription( key="total_saved", - name="Total saved", + translation_key="total_saved", icon="mdi:water-opacity", native_unit_of_measurement=UnitOfVolume.LITERS, device_class=SensorDeviceClass.VOLUME, @@ -120,7 +120,7 @@ SENSOR_TYPES = ( ), JustNimbusEntityDescription( key="total_replenished", - name="Total replenished", + translation_key="total_replenished", icon="mdi:water", native_unit_of_measurement=UnitOfVolume.LITERS, device_class=SensorDeviceClass.VOLUME, @@ -130,7 +130,7 @@ SENSOR_TYPES = ( ), JustNimbusEntityDescription( key="error_code", - name="Error code", + translation_key="error_code", icon="mdi:bug", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -138,7 +138,7 @@ SENSOR_TYPES = ( ), JustNimbusEntityDescription( key="totver", - name="Total use", + translation_key="total_use", icon="mdi:chart-donut", native_unit_of_measurement=UnitOfVolume.LITERS, device_class=SensorDeviceClass.VOLUME, @@ -148,7 +148,7 @@ SENSOR_TYPES = ( ), JustNimbusEntityDescription( key="reservoir_content_max", - name="Max reservoir content", + translation_key="reservoir_content_max", icon="mdi:waves", native_unit_of_measurement=UnitOfVolume.LITERS, device_class=SensorDeviceClass.VOLUME, diff --git a/homeassistant/components/justnimbus/strings.json b/homeassistant/components/justnimbus/strings.json index 609b1425e93..92ebf19714a 100644 --- a/homeassistant/components/justnimbus/strings.json +++ b/homeassistant/components/justnimbus/strings.json @@ -15,5 +15,45 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "pump_flow": { + "name": "Pump flow" + }, + "drink_flow": { + "name": "Drink flow" + }, + "pump_pressure": { + "name": "Pump pressure" + }, + "pump_starts": { + "name": "Pump starts" + }, + "pump_hours": { + "name": "Pump hours" + }, + "reservoir_temperature": { + "name": "Reservoir temperature" + }, + "reservoir_content": { + "name": "Reservoir content" + }, + "total_saved": { + "name": "Total saved" + }, + "total_replenished": { + "name": "Total replenished" + }, + "error_code": { + "name": "Error code" + }, + "total_use": { + "name": "Total use" + }, + "reservoir_content_max": { + "name": "Maximum reservoir content" + } + } } } From 57cacbc2a7d37a6230b187dfacd95860f7ebb8ac Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 14 Aug 2023 13:16:02 +0200 Subject: [PATCH 0488/1151] Add entity translations to Aurora (#98079) --- homeassistant/components/aurora/binary_sensor.py | 7 +++++-- homeassistant/components/aurora/entity.py | 4 ++-- homeassistant/components/aurora/sensor.py | 2 +- homeassistant/components/aurora/strings.json | 12 ++++++++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index a0e09685a0f..d817ea51988 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -13,9 +13,12 @@ async def async_setup_entry( ) -> None: """Set up the binary_sensor platform.""" coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] - name = f"{coordinator.name} Aurora Visibility Alert" - entity = AuroraSensor(coordinator=coordinator, name=name, icon="mdi:hazard-lights") + entity = AuroraSensor( + coordinator=coordinator, + translation_key="visibility_alert", + icon="mdi:hazard-lights", + ) async_add_entries([entity]) diff --git a/homeassistant/components/aurora/entity.py b/homeassistant/components/aurora/entity.py index 49afe9fb8c8..a52f523f667 100644 --- a/homeassistant/components/aurora/entity.py +++ b/homeassistant/components/aurora/entity.py @@ -19,14 +19,14 @@ class AuroraEntity(CoordinatorEntity[AuroraDataUpdateCoordinator]): def __init__( self, coordinator: AuroraDataUpdateCoordinator, - name: str, + translation_key: str, icon: str, ) -> None: """Initialize the Aurora Entity.""" super().__init__(coordinator=coordinator) - self._attr_name = name + self._attr_translation_key = translation_key self._attr_unique_id = f"{coordinator.latitude}_{coordinator.longitude}" self._attr_icon = icon diff --git a/homeassistant/components/aurora/sensor.py b/homeassistant/components/aurora/sensor.py index a5436e1e219..f44cc23f832 100644 --- a/homeassistant/components/aurora/sensor.py +++ b/homeassistant/components/aurora/sensor.py @@ -17,7 +17,7 @@ async def async_setup_entry( entity = AuroraSensor( coordinator=coordinator, - name=f"{coordinator.name} Aurora Visibility %", + translation_key="visibility", icon="mdi:gauge", ) diff --git a/homeassistant/components/aurora/strings.json b/homeassistant/components/aurora/strings.json index 9beb9c7906d..09ec86bdf4d 100644 --- a/homeassistant/components/aurora/strings.json +++ b/homeassistant/components/aurora/strings.json @@ -25,5 +25,17 @@ } } } + }, + "entity": { + "binary_sensor": { + "visibility_alert": { + "name": "Visibility alert" + } + }, + "sensor": { + "visibility": { + "name": "Visibility" + } + } } } From 6f97270cd2ac61d6f5494aad961dda3fdd608014 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 14 Aug 2023 13:30:25 +0200 Subject: [PATCH 0489/1151] Fix tts notify config validation (#98381) * Add test * Require either entity_id or tts_service --- homeassistant/components/tts/notify.py | 23 +++++++++++++---------- tests/components/tts/test_notify.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tts/notify.py b/homeassistant/components/tts/notify.py index 92244fc41f9..c2576e12bb5 100644 --- a/homeassistant/components/tts/notify.py +++ b/homeassistant/components/tts/notify.py @@ -20,16 +20,19 @@ ENTITY_LEGACY_PROVIDER_GROUP = "entity_or_legacy_provider" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_NAME): cv.string, - vol.Exclusive(CONF_TTS_SERVICE, ENTITY_LEGACY_PROVIDER_GROUP): cv.entity_id, - vol.Exclusive(CONF_ENTITY_ID, ENTITY_LEGACY_PROVIDER_GROUP): cv.entities_domain( - DOMAIN - ), - vol.Required(CONF_MEDIA_PLAYER): cv.entity_id, - vol.Optional(ATTR_LANGUAGE): cv.string, - } +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_TTS_SERVICE, CONF_ENTITY_ID), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_NAME): cv.string, + vol.Exclusive(CONF_TTS_SERVICE, ENTITY_LEGACY_PROVIDER_GROUP): cv.entity_id, + vol.Exclusive( + CONF_ENTITY_ID, ENTITY_LEGACY_PROVIDER_GROUP + ): cv.entities_domain(DOMAIN), + vol.Required(CONF_MEDIA_PLAYER): cv.entity_id, + vol.Optional(ATTR_LANGUAGE): cv.string, + } + ), ) diff --git a/tests/components/tts/test_notify.py b/tests/components/tts/test_notify.py index 22ab151b864..1a776140457 100644 --- a/tests/components/tts/test_notify.py +++ b/tests/components/tts/test_notify.py @@ -68,6 +68,21 @@ async def test_setup_platform(hass: HomeAssistant) -> None: assert hass.services.has_service(notify.DOMAIN, "tts_test") +async def test_setup_platform_missing_key(hass: HomeAssistant) -> None: + """Test platform without required tts_service or entity_id key.""" + config = { + notify.DOMAIN: { + "platform": "tts", + "name": "tts_test", + "media_player": "media_player.demo", + } + } + with assert_setup_component(0, notify.DOMAIN): + assert await async_setup_component(hass, notify.DOMAIN, config) + + assert not hass.services.has_service(notify.DOMAIN, "tts_test") + + async def test_setup_legacy_service(hass: HomeAssistant) -> None: """Set up the demo platform and call service.""" calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) From 9ddf11f6cde0e3f5a13eb0e0f1d74a8997432f33 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 14 Aug 2023 04:32:08 -0700 Subject: [PATCH 0490/1151] Improve rainbird error handling (#98239) --- .../components/rainbird/coordinator.py | 10 +++- homeassistant/components/rainbird/number.py | 12 ++++- homeassistant/components/rainbird/switch.py | 27 ++++++++-- tests/components/rainbird/conftest.py | 12 +++-- tests/components/rainbird/test_init.py | 50 +++++++++++++++++-- tests/components/rainbird/test_number.py | 40 +++++++++++++++ tests/components/rainbird/test_switch.py | 36 +++++++++++++ 7 files changed, 170 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index 6e462603dbb..91319b25e59 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -8,7 +8,11 @@ import logging from typing import TypeVar import async_timeout -from pyrainbird.async_client import AsyncRainbirdController, RainbirdApiException +from pyrainbird.async_client import ( + AsyncRainbirdController, + RainbirdApiException, + RainbirdDeviceBusyException, +) from pyrainbird.data import ModelAndVersion from homeassistant.core import HomeAssistant @@ -84,8 +88,10 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): try: async with async_timeout.timeout(TIMEOUT_SECONDS): return await self._fetch_data() + except RainbirdDeviceBusyException as err: + raise UpdateFailed("Rain Bird device is busy") from err except RainbirdApiException as err: - raise UpdateFailed(f"Error communicating with Device: {err}") from err + raise UpdateFailed("Rain Bird device failure") from err async def _fetch_data(self) -> RainbirdDeviceState: """Fetch data from the Rain Bird device. diff --git a/homeassistant/components/rainbird/number.py b/homeassistant/components/rainbird/number.py index febb960d652..de049f921dd 100644 --- a/homeassistant/components/rainbird/number.py +++ b/homeassistant/components/rainbird/number.py @@ -3,10 +3,13 @@ from __future__ import annotations import logging +from pyrainbird.exceptions import RainbirdApiException, RainbirdDeviceBusyException + from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTime from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -58,4 +61,11 @@ class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity async def async_set_native_value(self, value: float) -> None: """Update the current value.""" - await self.coordinator.controller.set_rain_delay(value) + try: + await self.coordinator.controller.set_rain_delay(value) + except RainbirdDeviceBusyException as err: + raise HomeAssistantError( + "Rain Bird device is busy; Wait and try again" + ) from err + except RainbirdApiException as err: + raise HomeAssistantError("Rain Bird device failure") from err diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 3b945b31db5..ac42e00c676 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -3,11 +3,13 @@ from __future__ import annotations import logging +from pyrainbird.exceptions import RainbirdApiException, RainbirdDeviceBusyException import voluptuous as vol from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -86,15 +88,30 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity) async def async_turn_on(self, **kwargs): """Turn the switch on.""" - await self.coordinator.controller.irrigate_zone( - int(self._zone), - int(kwargs.get(ATTR_DURATION, self._duration_minutes)), - ) + try: + await self.coordinator.controller.irrigate_zone( + int(self._zone), + int(kwargs.get(ATTR_DURATION, self._duration_minutes)), + ) + except RainbirdDeviceBusyException as err: + raise HomeAssistantError( + "Rain Bird device is busy; Wait and try again" + ) from err + except RainbirdApiException as err: + raise HomeAssistantError("Rain Bird device failure") from err + await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs): """Turn the switch off.""" - await self.coordinator.controller.stop_irrigation() + try: + await self.coordinator.controller.stop_irrigation() + except RainbirdDeviceBusyException as err: + raise HomeAssistantError( + "Rain Bird device is busy; Wait and try again" + ) from err + except RainbirdApiException as err: + raise HomeAssistantError("Rain Bird device failure") from err await self.coordinator.async_request_refresh() @property diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index 21ad5230581..9e4e4e546cb 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -72,11 +72,6 @@ CONFIG_ENTRY_DATA = { } -UNAVAILABLE_RESPONSE = AiohttpClientMockResponse( - "POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE -) - - @pytest.fixture def platforms() -> list[Platform]: """Fixture to specify platforms to test.""" @@ -150,6 +145,13 @@ def mock_response(data: str) -> AiohttpClientMockResponse: return AiohttpClientMockResponse("POST", URL, response=rainbird_response(data)) +def mock_response_error( + status: HTTPStatus = HTTPStatus.SERVICE_UNAVAILABLE, +) -> AiohttpClientMockResponse: + """Create a fake AiohttpClientMockResponse.""" + return AiohttpClientMockResponse("POST", URL, status=status) + + @pytest.fixture(name="stations_response") def mock_station_response() -> str: """Mock response to return available stations.""" diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index 1330f1cb4b2..f548d3aacda 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -2,13 +2,21 @@ from __future__ import annotations +from http import HTTPStatus + import pytest from homeassistant.components.rainbird import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from .conftest import CONFIG_ENTRY_DATA, UNAVAILABLE_RESPONSE, ComponentSetup +from .conftest import ( + CONFIG_ENTRY_DATA, + MODEL_AND_VERSION_RESPONSE, + ComponentSetup, + mock_response, + mock_response_error, +) from tests.test_util.aiohttp import AiohttpClientMockResponse @@ -44,16 +52,50 @@ async def test_init_success( @pytest.mark.parametrize( ("yaml_config", "config_entry_data", "responses", "config_entry_states"), [ - ({}, CONFIG_ENTRY_DATA, [UNAVAILABLE_RESPONSE], [ConfigEntryState.SETUP_RETRY]), + ( + {}, + CONFIG_ENTRY_DATA, + [mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)], + [ConfigEntryState.SETUP_RETRY], + ), + ( + {}, + CONFIG_ENTRY_DATA, + [mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR)], + [ConfigEntryState.SETUP_RETRY], + ), + ( + {}, + CONFIG_ENTRY_DATA, + [ + mock_response(MODEL_AND_VERSION_RESPONSE), + mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE), + ], + [ConfigEntryState.SETUP_RETRY], + ), + ( + {}, + CONFIG_ENTRY_DATA, + [ + mock_response(MODEL_AND_VERSION_RESPONSE), + mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR), + ], + [ConfigEntryState.SETUP_RETRY], + ), + ], + ids=[ + "unavailable", + "server-error", + "coordinator-unavailable", + "coordinator-server-error", ], - ids=["config_entry_failure"], ) async def test_communication_failure( hass: HomeAssistant, setup_integration: ComponentSetup, config_entry_states: list[ConfigEntryState], ) -> None: - """Test unable to talk to server on startup, which permanently fails setup.""" + """Test unable to talk to device on startup, which fails setup.""" assert await setup_integration() diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index 1335a1595d3..2c837a75c66 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -1,5 +1,6 @@ """Tests for rainbird number platform.""" +from http import HTTPStatus import pytest @@ -8,6 +9,7 @@ from homeassistant.components.rainbird import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from .conftest import ( @@ -17,6 +19,7 @@ from .conftest import ( SERIAL_NUMBER, ComponentSetup, mock_response, + mock_response_error, ) from tests.test_util.aiohttp import AiohttpClientMocker @@ -87,3 +90,40 @@ async def test_set_value( ) assert len(aioclient_mock.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("status", "expected_msg"), + [ + (HTTPStatus.SERVICE_UNAVAILABLE, "Rain Bird device is busy"), + (HTTPStatus.INTERNAL_SERVER_ERROR, "Rain Bird device failure"), + ], +) +async def test_set_value_error( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + responses: list[str], + config_entry: ConfigEntry, + status: HTTPStatus, + expected_msg: str, +) -> None: + """Test an error while talking to the device.""" + + assert await setup_integration() + + aioclient_mock.mock_calls.clear() + responses.append(mock_response_error(status=status)) + + with pytest.raises(HomeAssistantError, match=expected_msg): + await hass.services.async_call( + number.DOMAIN, + number.SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.rain_bird_controller_rain_delay", + number.ATTR_VALUE: 3, + }, + blocking=True, + ) + + assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index 684287a5d1a..9127a0b0c61 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -1,11 +1,13 @@ """Tests for rainbird sensor platform.""" +from http import HTTPStatus import pytest from homeassistant.components.rainbird import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .conftest import ( ACK_ECHO, @@ -19,6 +21,7 @@ from .conftest import ( ZONE_OFF_RESPONSE, ComponentSetup, mock_response, + mock_response_error, ) from tests.components.switch import common as switch_common @@ -240,3 +243,36 @@ async def test_yaml_imported_config( assert hass.states.get("switch.back_yard") assert not hass.states.get("switch.rain_bird_sprinkler_2") assert hass.states.get("switch.rain_bird_sprinkler_3") + + +@pytest.mark.parametrize( + ("status", "expected_msg"), + [ + (HTTPStatus.SERVICE_UNAVAILABLE, "Rain Bird device is busy"), + (HTTPStatus.INTERNAL_SERVER_ERROR, "Rain Bird device failure"), + ], +) +async def test_switch_error( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + responses: list[AiohttpClientMockResponse], + status: HTTPStatus, + expected_msg: str, +) -> None: + """Test an error talking to the device.""" + + assert await setup_integration() + + aioclient_mock.mock_calls.clear() + responses.append(mock_response_error(status=status)) + + with pytest.raises(HomeAssistantError, match=expected_msg): + await switch_common.async_turn_on(hass, "switch.rain_bird_sprinkler_3") + await hass.async_block_till_done() + + responses.append(mock_response_error(status=status)) + + with pytest.raises(HomeAssistantError, match=expected_msg): + await switch_common.async_turn_off(hass, "switch.rain_bird_sprinkler_3") + await hass.async_block_till_done() From 318b8adbed54bfa591337af80042c4cb3f3feb2f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Aug 2023 13:40:32 +0200 Subject: [PATCH 0491/1151] Set preferred router when importing OTBR dataset (#98379) --- homeassistant/components/otbr/__init__.py | 21 ++++++++++++++++++++- homeassistant/components/otbr/util.py | 5 +++++ tests/components/otbr/__init__.py | 2 ++ tests/components/otbr/conftest.py | 11 ++++++++++- tests/components/otbr/test_init.py | 15 +++++++++++---- 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/otbr/__init__.py b/homeassistant/components/otbr/__init__.py index 8685282acec..09a4499b60f 100644 --- a/homeassistant/components/otbr/__init__.py +++ b/homeassistant/components/otbr/__init__.py @@ -2,11 +2,17 @@ from __future__ import annotations import asyncio +import contextlib import aiohttp import python_otbr_api -from homeassistant.components.thread import async_add_dataset +from homeassistant.components.thread import ( + async_add_dataset, + async_get_preferred_border_agent_id, + async_get_preferred_dataset, + async_set_preferred_border_agent_id, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError @@ -46,6 +52,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if dataset_tlvs: await update_issues(hass, otbrdata, dataset_tlvs) await async_add_dataset(hass, DOMAIN, dataset_tlvs.hex()) + # If this OTBR's dataset is the preferred one, and there is no preferred router, + # make this the preferred router + border_agent_id: bytes | None = None + with contextlib.suppress( + HomeAssistantError, aiohttp.ClientError, asyncio.TimeoutError + ): + border_agent_id = await otbrdata.get_border_agent_id() + if ( + await async_get_preferred_dataset(hass) == dataset_tlvs.hex() + and await async_get_preferred_border_agent_id(hass) is None + and border_agent_id + ): + await async_set_preferred_border_agent_id(hass, border_agent_id.hex()) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/otbr/util.py b/homeassistant/components/otbr/util.py index 4d6efb9a9f0..67f36c09246 100644 --- a/homeassistant/components/otbr/util.py +++ b/homeassistant/components/otbr/util.py @@ -82,6 +82,11 @@ class OTBRData: ) await self.delete_active_dataset() + @_handle_otbr_error + async def get_border_agent_id(self) -> bytes: + """Get the border agent ID.""" + return await self.api.get_border_agent_id() + @_handle_otbr_error async def set_enabled(self, enabled: bool) -> None: """Enable or disable the router.""" diff --git a/tests/components/otbr/__init__.py b/tests/components/otbr/__init__.py index 9f2fd4a4355..a30275d3569 100644 --- a/tests/components/otbr/__init__.py +++ b/tests/components/otbr/__init__.py @@ -26,3 +26,5 @@ DATASET_INSECURE_PASSPHRASE = bytes.fromhex( "0A336069051000112233445566778899AABBCCDDEEFA030E4F70656E54687265616444656D6F01" "0212340410445F2B5CA6F2A93A55CE570A70EFEECB0C0402A0F7F8" ) + +TEST_BORDER_AGENT_ID = bytes.fromhex("230C6A1AC57F6F4BE262ACF32E5EF52C") diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py index e7d5ac8980e..75922e99aa0 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -6,7 +6,12 @@ import pytest from homeassistant.components import otbr from homeassistant.core import HomeAssistant -from . import CONFIG_ENTRY_DATA_MULTIPAN, CONFIG_ENTRY_DATA_THREAD, DATASET_CH16 +from . import ( + CONFIG_ENTRY_DATA_MULTIPAN, + CONFIG_ENTRY_DATA_THREAD, + DATASET_CH16, + TEST_BORDER_AGENT_ID, +) from tests.common import MockConfigEntry @@ -23,6 +28,8 @@ async def otbr_config_entry_multipan_fixture(hass): config_entry.add_to_hass(hass) with patch( "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), patch( + "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID ), patch( "homeassistant.components.otbr.util.compute_pskc" ): # Patch to speed up tests @@ -41,6 +48,8 @@ async def otbr_config_entry_thread_fixture(hass): config_entry.add_to_hass(hass) with patch( "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), patch( + "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID ), patch( "homeassistant.components.otbr.util.compute_pskc" ): # Patch to speed up tests diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index 49694cf5585..63229f4b2e7 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -7,7 +7,7 @@ import aiohttp import pytest import python_otbr_api -from homeassistant.components import otbr +from homeassistant.components import otbr, thread from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import issue_registry as ir @@ -21,6 +21,7 @@ from . import ( DATASET_CH16, DATASET_INSECURE_NW_KEY, DATASET_INSECURE_PASSPHRASE, + TEST_BORDER_AGENT_ID, ) from tests.common import MockConfigEntry @@ -36,6 +37,8 @@ DATASET_NO_CHANNEL = bytes.fromhex( async def test_import_dataset(hass: HomeAssistant) -> None: """Test the active dataset is imported at setup.""" issue_registry = ir.async_get(hass) + assert await thread.async_get_preferred_border_agent_id(hass) is None + assert await thread.async_get_preferred_dataset(hass) is None config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA_MULTIPAN, @@ -47,11 +50,15 @@ async def test_import_dataset(hass: HomeAssistant) -> None: with patch( "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 ), patch( - "homeassistant.components.thread.dataset_store.DatasetStore.async_add" - ) as mock_add: + "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID + ): assert await hass.config_entries.async_setup(config_entry.entry_id) - mock_add.assert_called_once_with(otbr.DOMAIN, DATASET_CH16.hex()) + assert ( + await thread.async_get_preferred_border_agent_id(hass) + == TEST_BORDER_AGENT_ID.hex() + ) + assert await thread.async_get_preferred_dataset(hass) == DATASET_CH16.hex() assert not issue_registry.async_get_issue( domain=otbr.DOMAIN, issue_id=f"insecure_thread_network_{config_entry.entry_id}" ) From a093c383c3cb92154dabb9c0fcd6c03da96ee397 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 14 Aug 2023 13:43:08 +0200 Subject: [PATCH 0492/1151] Remove Verisure default lock code (#94676) --- homeassistant/components/verisure/__init__.py | 32 ++++++- .../components/verisure/config_flow.py | 25 ++---- homeassistant/components/verisure/lock.py | 23 ++--- .../components/verisure/strings.json | 6 +- tests/components/verisure/conftest.py | 1 + tests/components/verisure/test_config_flow.py | 83 +------------------ 6 files changed, 51 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 94e8d667d75..302bd23b66f 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -5,14 +5,16 @@ from contextlib import suppress import os from pathlib import Path +from homeassistant.components.lock import CONF_DEFAULT_CODE, DOMAIN as LOCK_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR -from .const import DOMAIN +from .const import CONF_LOCK_DEFAULT_CODE, DOMAIN, LOGGER from .coordinator import VerisureDataUpdateCoordinator PLATFORMS = [ @@ -41,6 +43,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator + # Migrate lock default code from config entry to lock entity + # Set up all platforms for this device/entry. await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -72,3 +76,29 @@ def migrate_cookie_files(hass: HomeAssistant, entry: ConfigEntry) -> None: cookie_file.rename( hass.config.path(STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}") ) + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + LOGGER.debug("Migrating from version %s", entry.version) + + if entry.version == 1: + config_entry_default_code = entry.options.get(CONF_LOCK_DEFAULT_CODE) + entity_reg = er.async_get(hass) + entries = er.async_entries_for_config_entry(entity_reg, entry.entry_id) + for entity in entries: + if entity.entity_id.startswith("lock"): + entity_reg.async_update_entity_options( + entity.entity_id, + LOCK_DOMAIN, + {CONF_DEFAULT_CODE: config_entry_default_code}, + ) + new_options = entry.options.copy() + del new_options[CONF_LOCK_DEFAULT_CODE] + + entry.version = 2 + hass.config_entries.async_update_entry(entry, options=new_options) + + LOGGER.info("Migration to version %s successful", entry.version) + + return True diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 1fcf0eb9de2..d945463fa5e 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -21,7 +21,6 @@ from homeassistant.helpers.storage import STORAGE_DIR from .const import ( CONF_GIID, CONF_LOCK_CODE_DIGITS, - CONF_LOCK_DEFAULT_CODE, DEFAULT_LOCK_CODE_DIGITS, DOMAIN, LOGGER, @@ -31,7 +30,7 @@ from .const import ( class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Verisure.""" - VERSION = 1 + VERSION = 2 email: str entry: ConfigEntry @@ -306,16 +305,10 @@ class VerisureOptionsFlowHandler(OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage Verisure options.""" - errors = {} + errors: dict[str, Any] = {} if user_input is not None: - if len(user_input[CONF_LOCK_DEFAULT_CODE]) not in [ - 0, - user_input[CONF_LOCK_CODE_DIGITS], - ]: - errors["base"] = "code_format_mismatch" - else: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) return self.async_show_form( step_id="init", @@ -323,14 +316,12 @@ class VerisureOptionsFlowHandler(OptionsFlow): { vol.Optional( CONF_LOCK_CODE_DIGITS, - default=self.entry.options.get( - CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS - ), + description={ + "suggested_value": self.entry.options.get( + CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS + ) + }, ): int, - vol.Optional( - CONF_LOCK_DEFAULT_CODE, - default=self.entry.options.get(CONF_LOCK_DEFAULT_CODE, ""), - ): str, } ), errors=errors, diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 94a27784e78..ad9590d2524 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -20,7 +20,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( CONF_GIID, CONF_LOCK_CODE_DIGITS, - CONF_LOCK_DEFAULT_CODE, DEFAULT_LOCK_CODE_DIGITS, DOMAIN, LOGGER, @@ -129,25 +128,15 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" - code = kwargs.get( - ATTR_CODE, self.coordinator.entry.options.get(CONF_LOCK_DEFAULT_CODE) - ) - if code is None: - LOGGER.error("Code required but none provided") - return - - await self.async_set_lock_state(code, STATE_UNLOCKED) + code = kwargs.get(ATTR_CODE) + if code: + await self.async_set_lock_state(code, STATE_UNLOCKED) async def async_lock(self, **kwargs: Any) -> None: """Send lock command.""" - code = kwargs.get( - ATTR_CODE, self.coordinator.entry.options.get(CONF_LOCK_DEFAULT_CODE) - ) - if code is None: - LOGGER.error("Code required but none provided") - return - - await self.async_set_lock_state(code, STATE_LOCKED) + code = kwargs.get(ATTR_CODE) + if code: + await self.async_set_lock_state(code, STATE_LOCKED) async def async_set_lock_state(self, code: str, state: str) -> None: """Send set lock state command.""" diff --git a/homeassistant/components/verisure/strings.json b/homeassistant/components/verisure/strings.json index f715529b36b..051f17262a0 100644 --- a/homeassistant/components/verisure/strings.json +++ b/homeassistant/components/verisure/strings.json @@ -48,13 +48,9 @@ "step": { "init": { "data": { - "lock_code_digits": "Number of digits in PIN code for locks", - "lock_default_code": "Default PIN code for locks, used if none is given" + "lock_code_digits": "Number of digits in PIN code for locks" } } - }, - "error": { - "code_format_mismatch": "The default PIN code does not match the required number of digits" } }, "entity": { diff --git a/tests/components/verisure/conftest.py b/tests/components/verisure/conftest.py index 8ddc3a99815..8e1da712a5c 100644 --- a/tests/components/verisure/conftest.py +++ b/tests/components/verisure/conftest.py @@ -23,6 +23,7 @@ def mock_config_entry() -> MockConfigEntry: CONF_GIID: "12345", CONF_PASSWORD: "SuperS3cr3t!", }, + version=2, ) diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index af102cced98..94a0963fdf6 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -11,7 +11,6 @@ from homeassistant.components import dhcp from homeassistant.components.verisure.const import ( CONF_GIID, CONF_LOCK_CODE_DIGITS, - CONF_LOCK_DEFAULT_CODE, DEFAULT_LOCK_CODE_DIGITS, DOMAIN, ) @@ -561,48 +560,9 @@ async def test_reauth_flow_errors( assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.parametrize( - ("input", "output"), - [ - ( - { - CONF_LOCK_CODE_DIGITS: 5, - CONF_LOCK_DEFAULT_CODE: "12345", - }, - { - CONF_LOCK_CODE_DIGITS: 5, - CONF_LOCK_DEFAULT_CODE: "12345", - }, - ), - ( - { - CONF_LOCK_DEFAULT_CODE: "", - }, - { - CONF_LOCK_DEFAULT_CODE: "", - CONF_LOCK_CODE_DIGITS: DEFAULT_LOCK_CODE_DIGITS, - }, - ), - ( - { - CONF_LOCK_CODE_DIGITS: 5, - }, - { - CONF_LOCK_CODE_DIGITS: 5, - CONF_LOCK_DEFAULT_CODE: "", - }, - ), - ], -) -async def test_options_flow( - hass: HomeAssistant, input: dict[str, int | str], output: dict[str, int | str] -) -> None: +async def test_options_flow(hass: HomeAssistant) -> None: """Test options config flow.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={}, - ) + entry = MockConfigEntry(domain=DOMAIN, unique_id="12345", data={}, version=2) entry.add_to_hass(hass) with patch( @@ -619,43 +579,8 @@ async def test_options_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input=input, + user_input={CONF_LOCK_CODE_DIGITS: 4}, ) assert result.get("type") == FlowResultType.CREATE_ENTRY - assert result.get("data") == output - - -async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: - """Test options config flow with a code format mismatch.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={}, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.verisure.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.get("type") == FlowResultType.FORM - assert result.get("step_id") == "init" - assert result.get("errors") == {} - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_LOCK_CODE_DIGITS: 5, - CONF_LOCK_DEFAULT_CODE: "123", - }, - ) - - assert result.get("type") == FlowResultType.FORM - assert result.get("step_id") == "init" - assert result.get("errors") == {"base": "code_format_mismatch"} + assert result.get("data") == {CONF_LOCK_CODE_DIGITS: DEFAULT_LOCK_CODE_DIGITS} From 525f39fe28611fc084e9001a2b809a5bc4951c28 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:10:45 +0200 Subject: [PATCH 0493/1151] Update todoist-api-python to 2.1.2 (#98383) --- homeassistant/components/todoist/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index ac7e899d8a1..a83cdbe1b09 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/todoist", "iot_class": "cloud_polling", "loggers": ["todoist"], - "requirements": ["todoist-api-python==2.0.2"] + "requirements": ["todoist-api-python==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0bd5e55c1a4..114a1e6b5e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2554,7 +2554,7 @@ tilt-ble==0.2.3 tmb==0.0.4 # homeassistant.components.todoist -todoist-api-python==2.0.2 +todoist-api-python==2.1.2 # homeassistant.components.tolo tololib==0.1.0b4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31e4aa2d284..ec9f4da9090 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1860,7 +1860,7 @@ thermopro-ble==0.4.5 tilt-ble==0.2.3 # homeassistant.components.todoist -todoist-api-python==2.0.2 +todoist-api-python==2.1.2 # homeassistant.components.tolo tololib==0.1.0b4 From b0f68f1ef30cb350b6011460ff955c3032599db7 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 14 Aug 2023 15:07:20 +0200 Subject: [PATCH 0494/1151] Use @require_admin decorator (#98061) Co-authored-by: Robert Resch Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/api/__init__.py | 14 ++-- .../components/config/config_entries.py | 46 ++++++------- homeassistant/components/http/decorators.py | 66 +++++++++++++++---- .../components/media_source/local_source.py | 6 +- .../components/repairs/websocket_api.py | 17 ++--- homeassistant/components/zwave_js/api.py | 5 +- homeassistant/helpers/data_entry_flow.py | 2 +- .../components/config/test_config_entries.py | 46 +++++++++++++ 8 files changed, 136 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index b465a6b7037..f264806ad47 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_READ from homeassistant.bootstrap import DATA_LOGGING -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import HomeAssistantView, require_admin from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, MATCH_ALL, @@ -110,10 +110,9 @@ class APIEventStream(HomeAssistantView): url = URL_API_STREAM name = "api:stream" + @require_admin async def get(self, request): """Provide a streaming interface for the event bus.""" - if not request["hass_user"].is_admin: - raise Unauthorized() hass = request.app["hass"] stop_obj = object() to_write = asyncio.Queue() @@ -278,10 +277,9 @@ class APIEventView(HomeAssistantView): url = "/api/events/{event_type}" name = "api:event" + @require_admin async def post(self, request, event_type): """Fire events.""" - if not request["hass_user"].is_admin: - raise Unauthorized() body = await request.text() try: event_data = json_loads(body) if body else None @@ -385,10 +383,9 @@ class APITemplateView(HomeAssistantView): url = URL_API_TEMPLATE name = "api:template" + @require_admin async def post(self, request): """Render a template.""" - if not request["hass_user"].is_admin: - raise Unauthorized() try: data = await request.json() tpl = _cached_template(data["template"], request.app["hass"]) @@ -405,10 +402,9 @@ class APIErrorLog(HomeAssistantView): url = URL_API_ERROR_LOG name = "api:error_log" + @require_admin async def get(self, request): """Retrieve API error log.""" - if not request["hass_user"].is_admin: - raise Unauthorized() return web.FileResponse(request.app["hass"].data[DATA_LOGGING]) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index d58616ff38f..9691994512c 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.components import websocket_api -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import HomeAssistantView, require_admin from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import DependencyError, Unauthorized import homeassistant.helpers.config_validation as cv @@ -138,12 +138,11 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): """Not implemented.""" raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"]) - # pylint: disable=arguments-differ + @require_admin( + error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") + ) async def post(self, request): """Handle a POST request.""" - if not request["hass_user"].is_admin: - raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") - # pylint: disable=no-value-for-parameter try: return await super().post(request) @@ -164,19 +163,18 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView): url = "/api/config/config_entries/flow/{flow_id}" name = "api:config:config_entries:flow:resource" - async def get(self, request, flow_id): + @require_admin( + error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") + ) + async def get(self, request, /, flow_id): """Get the current state of a data_entry_flow.""" - if not request["hass_user"].is_admin: - raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") - return await super().get(request, flow_id) - # pylint: disable=arguments-differ + @require_admin( + error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") + ) async def post(self, request, flow_id): """Handle a POST request.""" - if not request["hass_user"].is_admin: - raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") - # pylint: disable=no-value-for-parameter return await super().post(request, flow_id) @@ -206,15 +204,14 @@ class OptionManagerFlowIndexView(FlowManagerIndexView): url = "/api/config/config_entries/options/flow" name = "api:config:config_entries:option:flow" - # pylint: disable=arguments-differ + @require_admin( + error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) + ) async def post(self, request): """Handle a POST request. handler in request is entry_id. """ - if not request["hass_user"].is_admin: - raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) - # pylint: disable=no-value-for-parameter return await super().post(request) @@ -225,19 +222,18 @@ class OptionManagerFlowResourceView(FlowManagerResourceView): url = "/api/config/config_entries/options/flow/{flow_id}" name = "api:config:config_entries:options:flow:resource" - async def get(self, request, flow_id): + @require_admin( + error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) + ) + async def get(self, request, /, flow_id): """Get the current state of a data_entry_flow.""" - if not request["hass_user"].is_admin: - raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) - return await super().get(request, flow_id) - # pylint: disable=arguments-differ + @require_admin( + error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) + ) async def post(self, request, flow_id): """Handle a POST request.""" - if not request["hass_user"].is_admin: - raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) - # pylint: disable=no-value-for-parameter return await super().post(request, flow_id) diff --git a/homeassistant/components/http/decorators.py b/homeassistant/components/http/decorators.py index 45bd34fa49f..ce5b1b18c06 100644 --- a/homeassistant/components/http/decorators.py +++ b/homeassistant/components/http/decorators.py @@ -1,8 +1,9 @@ """Decorators for the Home Assistant API.""" from __future__ import annotations -from collections.abc import Awaitable, Callable -from typing import Concatenate, ParamSpec, TypeVar +from collections.abc import Callable, Coroutine +from functools import wraps +from typing import Any, Concatenate, ParamSpec, TypeVar, overload from aiohttp.web import Request, Response @@ -12,20 +13,61 @@ from .view import HomeAssistantView _HomeAssistantViewT = TypeVar("_HomeAssistantViewT", bound=HomeAssistantView) _P = ParamSpec("_P") +_FuncType = Callable[ + Concatenate[_HomeAssistantViewT, Request, _P], Coroutine[Any, Any, Response] +] + + +@overload +def require_admin( + _func: None = None, + *, + error: Unauthorized | None = None, +) -> Callable[[_FuncType[_HomeAssistantViewT, _P]], _FuncType[_HomeAssistantViewT, _P]]: + ... + + +@overload +def require_admin( + _func: _FuncType[_HomeAssistantViewT, _P], +) -> _FuncType[_HomeAssistantViewT, _P]: + ... def require_admin( - func: Callable[Concatenate[_HomeAssistantViewT, Request, _P], Awaitable[Response]] -) -> Callable[Concatenate[_HomeAssistantViewT, Request, _P], Awaitable[Response]]: + _func: _FuncType[_HomeAssistantViewT, _P] | None = None, + *, + error: Unauthorized | None = None, +) -> ( + Callable[[_FuncType[_HomeAssistantViewT, _P]], _FuncType[_HomeAssistantViewT, _P]] + | _FuncType[_HomeAssistantViewT, _P] +): """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() + def decorator_require_admin( + func: _FuncType[_HomeAssistantViewT, _P] + ) -> _FuncType[_HomeAssistantViewT, _P]: + """Wrap the provided with_admin function.""" - return await func(self, request, *args, **kwargs) + @wraps(func) + 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 error or Unauthorized() - return with_admin + return await func(self, request, *args, **kwargs) + + return with_admin + + # See if we're being called as @require_admin or @require_admin(). + if _func is None: + # We're called with brackets. + return decorator_require_admin + + # We're called as @require_admin without brackets. + return decorator_require_admin(_func) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 89437a6b2e0..ac6623a3af8 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -12,9 +12,9 @@ from aiohttp.web_request import FileField import voluptuous as vol from homeassistant.components import http, websocket_api +from homeassistant.components.http import require_admin from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import Unauthorized from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES @@ -254,11 +254,9 @@ class UploadMediaView(http.HomeAssistantView): } ) + @require_admin async def post(self, request: web.Request) -> web.Response: """Handle upload.""" - if not request["hass_user"].is_admin: - raise Unauthorized() - # Increase max payload request._client_max_size = MAX_UPLOAD_SIZE # pylint: disable=protected-access diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index c5408054318..0c6230e4c35 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -12,6 +12,7 @@ from homeassistant import data_entry_flow from homeassistant.auth.permissions.const import POLICY_EDIT from homeassistant.components import websocket_api from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.http.decorators import require_admin from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized from homeassistant.helpers.data_entry_flow import ( @@ -88,6 +89,7 @@ class RepairsFlowIndexView(FlowManagerIndexView): url = "/api/repairs/issues/fix" name = "api:repairs:issues:fix" + @require_admin(error=Unauthorized(permission=POLICY_EDIT)) @RequestDataValidator( vol.Schema( { @@ -99,9 +101,6 @@ class RepairsFlowIndexView(FlowManagerIndexView): ) async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle a POST request.""" - if not request["hass_user"].is_admin: - raise Unauthorized(permission=POLICY_EDIT) - try: result = await self._flow_mgr.async_init( data["handler"], @@ -125,18 +124,12 @@ class RepairsFlowResourceView(FlowManagerResourceView): url = "/api/repairs/issues/fix/{flow_id}" name = "api:repairs:issues:fix:resource" - async def get(self, request: web.Request, flow_id: str) -> web.Response: + @require_admin(error=Unauthorized(permission=POLICY_EDIT)) + async def get(self, request: web.Request, /, flow_id: str) -> web.Response: """Get the current state of a data_entry_flow.""" - if not request["hass_user"].is_admin: - raise Unauthorized(permission=POLICY_EDIT) - return await super().get(request, flow_id) - # pylint: disable=arguments-differ + @require_admin(error=Unauthorized(permission=POLICY_EDIT)) async def post(self, request: web.Request, flow_id: str) -> web.Response: """Handle a POST request.""" - if not request["hass_user"].is_admin: - raise Unauthorized(permission=POLICY_EDIT) - - # pylint: disable=no-value-for-parameter return await super().post(request, flow_id) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 6d2461df3e4..6781ccacdc7 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -55,6 +55,7 @@ from zwave_js_server.model.utils import ( from zwave_js_server.util.node import async_set_config_parameter from homeassistant.components import websocket_api +from homeassistant.components.http import require_admin from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.websocket_api import ( ERR_INVALID_FORMAT, @@ -65,7 +66,6 @@ from homeassistant.components.websocket_api import ( ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import Unauthorized from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.device_registry as dr @@ -2149,10 +2149,9 @@ class FirmwareUploadView(HomeAssistantView): super().__init__() self._dev_reg = dev_reg + @require_admin async def post(self, request: web.Request, device_id: str) -> web.Response: """Handle upload.""" - if not request["hass_user"].is_admin: - raise Unauthorized() hass = request.app["hass"] try: diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index e3e4b4f0de8..aa4ef36b251 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -90,7 +90,7 @@ class FlowManagerIndexView(_BaseFlowManagerView): class FlowManagerResourceView(_BaseFlowManagerView): """View to interact with the flow manager.""" - async def get(self, request: web.Request, flow_id: str) -> web.Response: + async def get(self, request: web.Request, /, flow_id: str) -> web.Response: """Get the current state of a data_entry_flow.""" try: result = await self._flow_mgr.async_configure(flow_id) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index bf94e36a9b4..4684b4148b1 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -825,6 +825,52 @@ async def test_options_flow(hass: HomeAssistant, client) -> None: } +@pytest.mark.parametrize( + ("endpoint", "method"), + [ + ("/api/config/config_entries/options/flow", "post"), + ("/api/config/config_entries/options/flow/1", "get"), + ("/api/config/config_entries/options/flow/1", "post"), + ], +) +async def test_options_flow_unauth( + hass: HomeAssistant, client, hass_admin_user: MockUser, endpoint: str, method: str +) -> None: + """Test unauthorized on options flow.""" + + class TestFlow(core_ce.ConfigFlow): + @staticmethod + @callback + def async_get_options_flow(config_entry): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + async def async_step_init(self, user_input=None): + schema = OrderedDict() + schema[vol.Required("enabled")] = bool + return self.async_show_form( + step_id="user", + data_schema=schema, + description_placeholders={"enabled": "Set to true to be true"}, + ) + + return OptionsFlowHandler() + + mock_integration(hass, MockModule("test")) + mock_entity_platform(hass, "config_flow.test", None) + MockConfigEntry( + domain="test", + entry_id="test1", + source="bla", + ).add_to_hass(hass) + entry = hass.config_entries.async_entries()[0] + + hass_admin_user.groups = [] + + with patch.dict(HANDLERS, {"test": TestFlow}): + resp = await getattr(client, method)(endpoint, json={"handler": entry.entry_id}) + + assert resp.status == HTTPStatus.UNAUTHORIZED + + async def test_two_step_options_flow(hass: HomeAssistant, client) -> None: """Test we can finish a two step options flow.""" mock_integration( From e0fd83daab61ce90e40d3460ee495bf3a2c88438 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Aug 2023 15:47:18 +0200 Subject: [PATCH 0495/1151] Store preferred border agent ID for each thread dataset (#98384) --- homeassistant/components/otbr/__init__.py | 30 ++++------ homeassistant/components/thread/__init__.py | 4 -- .../components/thread/dataset_store.py | 59 +++++++++---------- .../components/thread/websocket_api.py | 22 ++----- tests/components/otbr/test_init.py | 10 ++-- tests/components/otbr/test_websocket_api.py | 2 +- tests/components/thread/test_dataset_store.py | 28 +++++---- tests/components/thread/test_websocket_api.py | 37 ++++++++---- 8 files changed, 92 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/otbr/__init__.py b/homeassistant/components/otbr/__init__.py index 09a4499b60f..ac59bacbd97 100644 --- a/homeassistant/components/otbr/__init__.py +++ b/homeassistant/components/otbr/__init__.py @@ -7,12 +7,7 @@ import contextlib import aiohttp import python_otbr_api -from homeassistant.components.thread import ( - async_add_dataset, - async_get_preferred_border_agent_id, - async_get_preferred_dataset, - async_set_preferred_border_agent_id, -) +from homeassistant.components.thread import async_add_dataset from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError @@ -50,21 +45,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) as err: raise ConfigEntryNotReady("Unable to connect") from err if dataset_tlvs: - await update_issues(hass, otbrdata, dataset_tlvs) - await async_add_dataset(hass, DOMAIN, dataset_tlvs.hex()) - # If this OTBR's dataset is the preferred one, and there is no preferred router, - # make this the preferred router - border_agent_id: bytes | None = None + border_agent_id: str | None = None with contextlib.suppress( HomeAssistantError, aiohttp.ClientError, asyncio.TimeoutError ): - border_agent_id = await otbrdata.get_border_agent_id() - if ( - await async_get_preferred_dataset(hass) == dataset_tlvs.hex() - and await async_get_preferred_border_agent_id(hass) is None - and border_agent_id - ): - await async_set_preferred_border_agent_id(hass, border_agent_id.hex()) + border_agent_bytes = await otbrdata.get_border_agent_id() + if border_agent_bytes: + border_agent_id = border_agent_bytes.hex() + await update_issues(hass, otbrdata, dataset_tlvs) + await async_add_dataset( + hass, + DOMAIN, + dataset_tlvs.hex(), + preferred_border_agent_id=border_agent_id, + ) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/thread/__init__.py b/homeassistant/components/thread/__init__.py index 679127e5202..dd2527763ad 100644 --- a/homeassistant/components/thread/__init__.py +++ b/homeassistant/components/thread/__init__.py @@ -11,9 +11,7 @@ from .dataset_store import ( DatasetEntry, async_add_dataset, async_get_dataset, - async_get_preferred_border_agent_id, async_get_preferred_dataset, - async_set_preferred_border_agent_id, ) from .websocket_api import async_setup as async_setup_ws_api @@ -21,10 +19,8 @@ __all__ = [ "DOMAIN", "DatasetEntry", "async_add_dataset", - "async_get_preferred_border_agent_id", "async_get_dataset", "async_get_preferred_dataset", - "async_set_preferred_border_agent_id", ] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) diff --git a/homeassistant/components/thread/dataset_store.py b/homeassistant/components/thread/dataset_store.py index 96a9cf8e59e..22e2c1822c1 100644 --- a/homeassistant/components/thread/dataset_store.py +++ b/homeassistant/components/thread/dataset_store.py @@ -33,6 +33,7 @@ class DatasetPreferredError(HomeAssistantError): class DatasetEntry: """Dataset store entry.""" + preferred_border_agent_id: str | None source: str tlv: str @@ -73,6 +74,7 @@ class DatasetEntry: return { "created": self.created.isoformat(), "id": self.id, + "preferred_border_agent_id": self.preferred_border_agent_id, "source": self.source, "tlv": self.tlv, } @@ -97,6 +99,7 @@ class DatasetStoreStore(Store): entry = DatasetEntry( created=created, id=dataset["id"], + preferred_border_agent_id=None, source=dataset["source"], tlv=dataset["tlv"], ) @@ -160,7 +163,8 @@ class DatasetStoreStore(Store): } if old_minor_version < 3: # Add border agent ID - data.setdefault("preferred_border_agent_id", None) + for dataset in data["datasets"]: + dataset.setdefault("preferred_border_agent_id", None) return data @@ -172,7 +176,6 @@ class DatasetStore: """Initialize the dataset store.""" self.hass = hass self.datasets: dict[str, DatasetEntry] = {} - self._preferred_border_agent_id: str | None = None self._preferred_dataset: str | None = None self._store: Store[dict[str, Any]] = DatasetStoreStore( hass, @@ -183,7 +186,9 @@ class DatasetStore: ) @callback - def async_add(self, source: str, tlv: str) -> None: + def async_add( + self, source: str, tlv: str, preferred_border_agent_id: str | None + ) -> None: """Add dataset, does nothing if it already exists.""" # Make sure the tlv is valid dataset = tlv_parser.parse_tlv(tlv) @@ -245,7 +250,9 @@ class DatasetStore: self.async_schedule_save() return - entry = DatasetEntry(source=source, tlv=tlv) + entry = DatasetEntry( + preferred_border_agent_id=preferred_border_agent_id, source=source, tlv=tlv + ) self.datasets[entry.id] = entry # Set to preferred if there is no preferred dataset if self._preferred_dataset is None: @@ -266,14 +273,13 @@ class DatasetStore: return self.datasets.get(dataset_id) @callback - def async_get_preferred_border_agent_id(self) -> str | None: - """Get preferred border agent id.""" - return self._preferred_border_agent_id - - @callback - def async_set_preferred_border_agent_id(self, border_agent_id: str) -> None: - """Set preferred border agent id.""" - self._preferred_border_agent_id = border_agent_id + def async_set_preferred_border_agent_id( + self, dataset_id: str, border_agent_id: str + ) -> None: + """Set preferred border agent id of a dataset.""" + self.datasets[dataset_id] = dataclasses.replace( + self.datasets[dataset_id], preferred_border_agent_id=border_agent_id + ) self.async_schedule_save() @property @@ -296,7 +302,6 @@ class DatasetStore: data = await self._store.async_load() datasets: dict[str, DatasetEntry] = {} - preferred_border_agent_id: str | None = None preferred_dataset: str | None = None if data is not None: @@ -305,14 +310,13 @@ class DatasetStore: datasets[dataset["id"]] = DatasetEntry( created=created, id=dataset["id"], + preferred_border_agent_id=dataset["preferred_border_agent_id"], source=dataset["source"], tlv=dataset["tlv"], ) - preferred_border_agent_id = data["preferred_border_agent_id"] preferred_dataset = data["preferred_dataset"] self.datasets = datasets - self._preferred_border_agent_id = preferred_border_agent_id self._preferred_dataset = preferred_dataset @callback @@ -325,7 +329,6 @@ class DatasetStore: """Return data of datasets to store in a file.""" data: dict[str, Any] = {} data["datasets"] = [dataset.to_json() for dataset in self.datasets.values()] - data["preferred_border_agent_id"] = self._preferred_border_agent_id data["preferred_dataset"] = self._preferred_dataset return data @@ -338,10 +341,16 @@ async def async_get_store(hass: HomeAssistant) -> DatasetStore: return store -async def async_add_dataset(hass: HomeAssistant, source: str, tlv: str) -> None: +async def async_add_dataset( + hass: HomeAssistant, + source: str, + tlv: str, + *, + preferred_border_agent_id: str | None = None, +) -> None: """Add a dataset.""" store = await async_get_store(hass) - store.async_add(source, tlv) + store.async_add(source, tlv, preferred_border_agent_id) async def async_get_dataset(hass: HomeAssistant, dataset_id: str) -> str | None: @@ -352,20 +361,6 @@ async def async_get_dataset(hass: HomeAssistant, dataset_id: str) -> str | None: return entry.tlv -async def async_get_preferred_border_agent_id(hass: HomeAssistant) -> str | None: - """Get the preferred border agent ID.""" - store = await async_get_store(hass) - return store.async_get_preferred_border_agent_id() - - -async def async_set_preferred_border_agent_id( - hass: HomeAssistant, border_agent_id: str -) -> None: - """Get the preferred border agent ID.""" - store = await async_get_store(hass) - store.async_set_preferred_border_agent_id(border_agent_id) - - async def async_get_preferred_dataset(hass: HomeAssistant) -> str | None: """Get the preferred dataset.""" store = await async_get_store(hass) diff --git a/homeassistant/components/thread/websocket_api.py b/homeassistant/components/thread/websocket_api.py index 853d8c3c893..5b289cf1694 100644 --- a/homeassistant/components/thread/websocket_api.py +++ b/homeassistant/components/thread/websocket_api.py @@ -20,7 +20,6 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_discover_routers) websocket_api.async_register_command(hass, ws_get_dataset) websocket_api.async_register_command(hass, ws_list_datasets) - websocket_api.async_register_command(hass, ws_get_preferred_border_agent_id) websocket_api.async_register_command(hass, ws_set_preferred_border_agent_id) websocket_api.async_register_command(hass, ws_set_preferred_dataset) @@ -52,25 +51,11 @@ async def ws_add_dataset( connection.send_result(msg["id"]) -@websocket_api.require_admin -@websocket_api.websocket_command( - { - vol.Required("type"): "thread/get_preferred_border_agent_id", - } -) -@websocket_api.async_response -async def ws_get_preferred_border_agent_id( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] -) -> None: - """Get the preferred border agent ID.""" - border_agent_id = await dataset_store.async_get_preferred_border_agent_id(hass) - connection.send_result(msg["id"], {"border_agent_id": border_agent_id}) - - @websocket_api.require_admin @websocket_api.websocket_command( { vol.Required("type"): "thread/set_preferred_border_agent_id", + vol.Required("dataset_id"): str, vol.Required("border_agent_id"): str, } ) @@ -79,8 +64,10 @@ async def ws_set_preferred_border_agent_id( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Set the preferred border agent ID.""" + dataset_id = msg["dataset_id"] border_agent_id = msg["border_agent_id"] - await dataset_store.async_set_preferred_border_agent_id(hass, border_agent_id) + store = await dataset_store.async_get_store(hass) + store.async_set_preferred_border_agent_id(dataset_id, border_agent_id) connection.send_result(msg["id"]) @@ -186,6 +173,7 @@ async def ws_list_datasets( "network_name": dataset.network_name, "pan_id": dataset.pan_id, "preferred": dataset.id == preferred_dataset, + "preferred_border_agent_id": dataset.preferred_border_agent_id, "source": dataset.source, } ) diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index 63229f4b2e7..18a60cfa196 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -37,7 +37,6 @@ DATASET_NO_CHANNEL = bytes.fromhex( async def test_import_dataset(hass: HomeAssistant) -> None: """Test the active dataset is imported at setup.""" issue_registry = ir.async_get(hass) - assert await thread.async_get_preferred_border_agent_id(hass) is None assert await thread.async_get_preferred_dataset(hass) is None config_entry = MockConfigEntry( @@ -54,8 +53,9 @@ async def test_import_dataset(hass: HomeAssistant) -> None: ): assert await hass.config_entries.async_setup(config_entry.entry_id) + dataset_store = await thread.dataset_store.async_get_store(hass) assert ( - await thread.async_get_preferred_border_agent_id(hass) + list(dataset_store.datasets.values())[0].preferred_border_agent_id == TEST_BORDER_AGENT_ID.hex() ) assert await thread.async_get_preferred_dataset(hass) == DATASET_CH16.hex() @@ -94,7 +94,7 @@ async def test_import_share_radio_channel_collision( ) as mock_add: assert await hass.config_entries.async_setup(config_entry.entry_id) - mock_add.assert_called_once_with(otbr.DOMAIN, DATASET_CH16.hex()) + mock_add.assert_called_once_with(otbr.DOMAIN, DATASET_CH16.hex(), None) assert issue_registry.async_get_issue( domain=otbr.DOMAIN, issue_id=f"otbr_zha_channel_collision_{config_entry.entry_id}", @@ -127,7 +127,7 @@ async def test_import_share_radio_no_channel_collision( ) as mock_add: assert await hass.config_entries.async_setup(config_entry.entry_id) - mock_add.assert_called_once_with(otbr.DOMAIN, dataset.hex()) + mock_add.assert_called_once_with(otbr.DOMAIN, dataset.hex(), None) assert not issue_registry.async_get_issue( domain=otbr.DOMAIN, issue_id=f"otbr_zha_channel_collision_{config_entry.entry_id}", @@ -158,7 +158,7 @@ async def test_import_insecure_dataset(hass: HomeAssistant, dataset: bytes) -> N ) as mock_add: assert await hass.config_entries.async_setup(config_entry.entry_id) - mock_add.assert_called_once_with(otbr.DOMAIN, dataset.hex()) + mock_add.assert_called_once_with(otbr.DOMAIN, dataset.hex(), None) assert issue_registry.async_get_issue( domain=otbr.DOMAIN, issue_id=f"insecure_thread_network_{config_entry.entry_id}" ) diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index d62213ce78b..f149e89cc45 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -109,7 +109,7 @@ async def test_create_network( assert set_enabled_mock.mock_calls[0][1][0] is False assert set_enabled_mock.mock_calls[1][1][0] is True get_active_dataset_tlvs_mock.assert_called_once() - mock_add.assert_called_once_with(otbr.DOMAIN, DATASET_CH16.hex()) + mock_add.assert_called_once_with(otbr.DOMAIN, DATASET_CH16.hex(), None) async def test_create_network_no_entry( diff --git a/tests/components/thread/test_dataset_store.py b/tests/components/thread/test_dataset_store.py index 1171c597e99..77102f92019 100644 --- a/tests/components/thread/test_dataset_store.py +++ b/tests/components/thread/test_dataset_store.py @@ -254,7 +254,7 @@ async def test_load_datasets(hass: HomeAssistant) -> None: store1 = await dataset_store.async_get_store(hass) for dataset in datasets: - store1.async_add(dataset["source"], dataset["tlv"]) + store1.async_add(dataset["source"], dataset["tlv"], None) assert len(store1.datasets) == 3 for dataset in store1.datasets.values(): @@ -303,33 +303,31 @@ async def test_loading_datasets_from_storage( { "created": "2023-02-02T09:41:13.746514+00:00", "id": "id1", + "preferred_border_agent_id": "230C6A1AC57F6F4BE262ACF32E5EF52C", "source": "source_1", "tlv": DATASET_1, }, { "created": "2023-02-02T09:41:13.746514+00:00", "id": "id2", + "preferred_border_agent_id": None, "source": "source_2", "tlv": DATASET_2, }, { "created": "2023-02-02T09:41:13.746514+00:00", "id": "id3", + "preferred_border_agent_id": None, "source": "source_3", "tlv": DATASET_3, }, ], - "preferred_border_agent_id": "230C6A1AC57F6F4BE262ACF32E5EF52C", "preferred_dataset": "id1", }, } store = await dataset_store.async_get_store(hass) assert len(store.datasets) == 3 - assert ( - store.async_get_preferred_border_agent_id() - == "230C6A1AC57F6F4BE262ACF32E5EF52C" - ) assert store.preferred_dataset == "id1" @@ -540,11 +538,17 @@ async def test_migrate_set_default_border_agent_id( } store = await dataset_store.async_get_store(hass) - assert store.async_get_preferred_border_agent_id() is None + assert store.datasets[store._preferred_dataset].preferred_border_agent_id is None -async def test_preferred_border_agent_id(hass: HomeAssistant) -> None: - """Test get and set the preferred border agent ID.""" - assert await dataset_store.async_get_preferred_border_agent_id(hass) is None - await dataset_store.async_set_preferred_border_agent_id(hass, "blah") - assert await dataset_store.async_get_preferred_border_agent_id(hass) == "blah" +async def test_set_preferred_border_agent_id(hass: HomeAssistant) -> None: + """Test set the preferred border agent ID of a dataset.""" + assert await dataset_store.async_get_preferred_dataset(hass) is None + + await dataset_store.async_add_dataset( + hass, "source", DATASET_1, preferred_border_agent_id="blah" + ) + + store = await dataset_store.async_get_store(hass) + assert len(store.datasets) == 1 + assert list(store.datasets.values())[0].preferred_border_agent_id == "blah" diff --git a/tests/components/thread/test_websocket_api.py b/tests/components/thread/test_websocket_api.py index 82450474e92..bfe71b8b21c 100644 --- a/tests/components/thread/test_websocket_api.py +++ b/tests/components/thread/test_websocket_api.py @@ -160,6 +160,7 @@ async def test_list_get_dataset( "network_name": "OpenThreadDemo", "pan_id": "1234", "preferred": True, + "preferred_border_agent_id": None, "source": "Google", }, { @@ -170,6 +171,7 @@ async def test_list_get_dataset( "network_name": "HomeAssistant!", "pan_id": "1234", "preferred": False, + "preferred_border_agent_id": None, "source": "Multipan", }, { @@ -180,6 +182,7 @@ async def test_list_get_dataset( "network_name": "~🐣🐥🐤~", "pan_id": "1234", "preferred": False, + "preferred_border_agent_id": None, "source": "🎅", }, ] @@ -200,33 +203,45 @@ async def test_list_get_dataset( assert msg["error"] == {"code": "not_found", "message": "unknown dataset"} -async def test_preferred_border_agent_id( +async def test_set_preferred_border_agent_id( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: - """Test setting and getting the preferred border agent ID.""" + """Test setting the preferred border agent ID.""" assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() client = await hass_ws_client(hass) - await client.send_json_auto_id({"type": "thread/get_preferred_border_agent_id"}) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] == {"border_agent_id": None} - await client.send_json_auto_id( - {"type": "thread/set_preferred_border_agent_id", "border_agent_id": "blah"} + {"type": "thread/add_dataset_tlv", "source": "test", "tlv": DATASET_1} ) msg = await client.receive_json() assert msg["success"] assert msg["result"] is None - await client.send_json_auto_id({"type": "thread/get_preferred_border_agent_id"}) + await client.send_json_auto_id({"type": "thread/list_datasets"}) msg = await client.receive_json() assert msg["success"] - assert msg["result"] == {"border_agent_id": "blah"} + datasets = msg["result"]["datasets"] + dataset_id = datasets[0]["dataset_id"] + assert datasets[0]["preferred_border_agent_id"] is None - assert await dataset_store.async_get_preferred_border_agent_id(hass) == "blah" + await client.send_json_auto_id( + { + "type": "thread/set_preferred_border_agent_id", + "dataset_id": dataset_id, + "border_agent_id": "blah", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + await client.send_json_auto_id({"type": "thread/list_datasets"}) + msg = await client.receive_json() + assert msg["success"] + datasets = msg["result"]["datasets"] + assert datasets[0]["preferred_border_agent_id"] == "blah" async def test_set_preferred_dataset( From 1869177f0836899664ac402737149a04c9fcc2df Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Aug 2023 15:47:55 +0200 Subject: [PATCH 0496/1151] Rename some incorrectly named assist_pipeline tests (#98389) --- tests/components/assist_pipeline/test_pipeline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index f6a62a630d2..32468e3af91 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -31,7 +31,7 @@ async def load_homeassistant(hass) -> None: assert await async_setup_component(hass, "homeassistant", {}) -async def test_load_datasets(hass: HomeAssistant, init_components) -> None: +async def test_load_pipelines(hass: HomeAssistant, init_components) -> None: """Make sure that we can load/save data correctly.""" pipelines = [ @@ -92,10 +92,10 @@ async def test_load_datasets(hass: HomeAssistant, init_components) -> None: assert store1.async_get_preferred_item() == store2.async_get_preferred_item() -async def test_loading_datasets_from_storage( +async def test_loading_pipelines_from_storage( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: - """Test loading stored datasets on start.""" + """Test loading stored pipelines on start.""" hass_storage[STORAGE_KEY] = { "version": 1, "minor_version": 1, From d059c9924a5dcce58f2fb3c470f8ab63637ccd2a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:50:43 +0200 Subject: [PATCH 0497/1151] Update attrs to 23.1.0 (#98385) --- 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 78973f15520..29c654cd05c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ astral==2.2 async-timeout==4.0.3 async-upnp-client==0.34.1 atomicwrites-homeassistant==1.4.1 -attrs==22.2.0 +attrs==23.1.0 awesomeversion==22.9.0 bcrypt==4.0.1 bleak-retry-connector==3.1.1 diff --git a/pyproject.toml b/pyproject.toml index af386239ac5..3003e3a29ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "aiohttp==3.8.5", "astral==2.2", "async-timeout==4.0.3", - "attrs==22.2.0", + "attrs==23.1.0", "atomicwrites-homeassistant==1.4.1", "awesomeversion==22.9.0", "bcrypt==4.0.1", diff --git a/requirements.txt b/requirements.txt index 0c55b1f9a9e..72fa57f6b1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ aiohttp==3.8.5 astral==2.2 async-timeout==4.0.3 -attrs==22.2.0 +attrs==23.1.0 atomicwrites-homeassistant==1.4.1 awesomeversion==22.9.0 bcrypt==4.0.1 From 2272a9db0082db7398df023e60e9965e249eed90 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 14 Aug 2023 15:54:43 +0200 Subject: [PATCH 0498/1151] Improve picotts (#98391) --- homeassistant/components/picotts/tts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/picotts/tts.py b/homeassistant/components/picotts/tts.py index 23e94b5206d..4d9f1755145 100644 --- a/homeassistant/components/picotts/tts.py +++ b/homeassistant/components/picotts/tts.py @@ -51,7 +51,7 @@ class PicoProvider(Provider): with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpf: fname = tmpf.name - cmd = ["pico2wave", "--wave", fname, "-l", language, message] + cmd = ["pico2wave", "--wave", fname, "-l", language, "--", message] subprocess.call(cmd) data = None try: From d4753ebd3b310a60c633aeedfa50336b415750b2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Aug 2023 16:46:55 +0200 Subject: [PATCH 0499/1151] Include border agent ID in thread router discovery (#98378) --- homeassistant/components/thread/discovery.py | 9 +++++++-- tests/components/thread/__init__.py | 16 ++++++++++++++++ tests/components/thread/test_discovery.py | 4 ++++ tests/components/thread/test_websocket_api.py | 2 ++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/thread/discovery.py b/homeassistant/components/thread/discovery.py index d07469f36fb..ce721a20e28 100644 --- a/homeassistant/components/thread/discovery.py +++ b/homeassistant/components/thread/discovery.py @@ -32,6 +32,7 @@ class ThreadRouterDiscoveryData: """Thread router discovery data.""" addresses: list[str] | None + border_agent_id: str | None brand: str | None extended_address: str | None extended_pan_id: str | None @@ -61,13 +62,16 @@ def async_discovery_data_from_service( # For legacy backwards compatibility zeroconf allows properties to be set # as strings but we never do that so we can safely cast here. service_properties = cast(dict[bytes, bytes | None], service.properties) + + border_agent_id = service_properties.get(b"id") ext_addr = service_properties.get(b"xa") ext_pan_id = service_properties.get(b"xp") - network_name = try_decode(service_properties.get(b"nn")) model_name = try_decode(service_properties.get(b"mn")) + network_name = try_decode(service_properties.get(b"nn")) server = service.server - vendor_name = try_decode(service_properties.get(b"vn")) thread_version = try_decode(service_properties.get(b"tv")) + vendor_name = try_decode(service_properties.get(b"vn")) + unconfigured = None brand = KNOWN_BRANDS.get(vendor_name) if brand == "homeassistant": @@ -84,6 +88,7 @@ def async_discovery_data_from_service( return ThreadRouterDiscoveryData( addresses=service.parsed_addresses(), + border_agent_id=border_agent_id.hex() if border_agent_id is not None else None, brand=brand, extended_address=ext_addr.hex() if ext_addr is not None else None, extended_pan_id=ext_pan_id.hex() if ext_pan_id is not None else None, diff --git a/tests/components/thread/__init__.py b/tests/components/thread/__init__.py index e7435b8e94a..f9d527919b4 100644 --- a/tests/components/thread/__init__.py +++ b/tests/components/thread/__init__.py @@ -93,6 +93,7 @@ ROUTER_DISCOVERY_HASS = { "server": "core-silabs-multiprotocol.local.", "properties": { b"rv": b"1", + b"id": b"#\x0cj\x1a\xc5\x7foK\xe2b\xac\xf3.^\xf5,", b"vn": b"HomeAssistant", b"mn": b"OpenThreadBorderRouter", b"nn": b"OpenThread HC", @@ -105,6 +106,7 @@ ROUTER_DISCOVERY_HASS = { b"sq": b"3", b"bb": b"\xf0\xbf", b"dn": b"DefaultDomain", + b"omr": b"@\xfd \xbe\x89IZ\x00\x01", }, "interface_index": None, } @@ -119,6 +121,7 @@ ROUTER_DISCOVERY_HASS_BAD_DATA = { "server": "core-silabs-multiprotocol.local.", "properties": { b"rv": b"1", + b"id": b"#\x0cj\x1a\xc5\x7foK\xe2b\xac\xf3.^\xf5,", b"vn": b"HomeAssistant\xff", # Invalid UTF-8 b"mn": b"OpenThreadBorderRouter", b"nn": b"OpenThread HC", @@ -131,6 +134,7 @@ ROUTER_DISCOVERY_HASS_BAD_DATA = { b"sq": b"3", b"bb": b"\xf0\xbf", b"dn": b"DefaultDomain", + b"omr": b"@\xfd \xbe\x89IZ\x00\x01", }, "interface_index": None, } @@ -145,6 +149,7 @@ ROUTER_DISCOVERY_HASS_MISSING_DATA = { "server": "core-silabs-multiprotocol.local.", "properties": { b"rv": b"1", + b"id": b"#\x0cj\x1a\xc5\x7foK\xe2b\xac\xf3.^\xf5,", b"mn": b"OpenThreadBorderRouter", b"nn": b"OpenThread HC", b"xp": b"\xe6\x0f\xc7\xc1\x86!,\xe5", @@ -156,6 +161,7 @@ ROUTER_DISCOVERY_HASS_MISSING_DATA = { b"sq": b"3", b"bb": b"\xf0\xbf", b"dn": b"DefaultDomain", + b"omr": b"@\xfd \xbe\x89IZ\x00\x01", }, "interface_index": None, } @@ -171,6 +177,7 @@ ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA = { "server": "core-silabs-multiprotocol.local.", "properties": { b"rv": b"1", + b"id": b"#\x0cj\x1a\xc5\x7foK\xe2b\xac\xf3.^\xf5,", b"vn": b"HomeAssistant", b"mn": b"OpenThreadBorderRouter", b"nn": b"OpenThread HC", @@ -182,6 +189,7 @@ ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA = { b"sq": b"3", b"bb": b"\xf0\xbf", b"dn": b"DefaultDomain", + b"omr": b"@\xfd \xbe\x89IZ\x00\x01", }, "interface_index": None, } @@ -197,6 +205,7 @@ ROUTER_DISCOVERY_HASS_NO_ACTIVE_TIMESTAMP = { "server": "core-silabs-multiprotocol.local.", "properties": { b"rv": b"1", + b"id": b"#\x0cj\x1a\xc5\x7foK\xe2b\xac\xf3.^\xf5,", b"vn": b"HomeAssistant", b"mn": b"OpenThreadBorderRouter", b"nn": b"OpenThread HC", @@ -208,6 +217,7 @@ ROUTER_DISCOVERY_HASS_NO_ACTIVE_TIMESTAMP = { b"sq": b"3", b"bb": b"\xf0\xbf", b"dn": b"DefaultDomain", + b"omr": b"@\xfd \xbe\x89IZ\x00\x01", }, "interface_index": None, } @@ -223,6 +233,7 @@ ROUTER_DISCOVERY_HASS_NO_STATE_BITMAP = { "server": "core-silabs-multiprotocol.local.", "properties": { b"rv": b"1", + b"id": b"#\x0cj\x1a\xc5\x7foK\xe2b\xac\xf3.^\xf5,", b"vn": b"HomeAssistant", b"mn": b"OpenThreadBorderRouter", b"nn": b"OpenThread HC", @@ -234,6 +245,7 @@ ROUTER_DISCOVERY_HASS_NO_STATE_BITMAP = { b"sq": b"3", b"bb": b"\xf0\xbf", b"dn": b"DefaultDomain", + b"omr": b"@\xfd \xbe\x89IZ\x00\x01", }, "interface_index": None, } @@ -249,6 +261,7 @@ ROUTER_DISCOVERY_HASS_BAD_STATE_BITMAP = { "server": "core-silabs-multiprotocol.local.", "properties": { b"rv": b"1", + b"id": b"#\x0cj\x1a\xc5\x7foK\xe2b\xac\xf3.^\xf5,", b"vn": b"HomeAssistant", b"mn": b"OpenThreadBorderRouter", b"nn": b"OpenThread HC", @@ -261,6 +274,7 @@ ROUTER_DISCOVERY_HASS_BAD_STATE_BITMAP = { b"sq": b"3", b"bb": b"\xf0\xbf", b"dn": b"DefaultDomain", + b"omr": b"@\xfd \xbe\x89IZ\x00\x01", }, "interface_index": None, } @@ -276,6 +290,7 @@ ROUTER_DISCOVERY_HASS_STATE_BITMAP_NOT_ACTIVE = { "server": "core-silabs-multiprotocol.local.", "properties": { b"rv": b"1", + b"id": b"#\x0cj\x1a\xc5\x7foK\xe2b\xac\xf3.^\xf5,", b"vn": b"HomeAssistant", b"mn": b"OpenThreadBorderRouter", b"nn": b"OpenThread HC", @@ -288,6 +303,7 @@ ROUTER_DISCOVERY_HASS_STATE_BITMAP_NOT_ACTIVE = { b"sq": b"3", b"bb": b"\xf0\xbf", b"dn": b"DefaultDomain", + b"omr": b"@\xfd \xbe\x89IZ\x00\x01", }, "interface_index": None, } diff --git a/tests/components/thread/test_discovery.py b/tests/components/thread/test_discovery.py index 84fe4c30974..4d43142b7b7 100644 --- a/tests/components/thread/test_discovery.py +++ b/tests/components/thread/test_discovery.py @@ -72,6 +72,7 @@ async def test_discover_routers(hass: HomeAssistant, mock_async_zeroconf: None) "aeeb2f594b570bbf", discovery.ThreadRouterDiscoveryData( addresses=["192.168.0.115"], + border_agent_id="230c6a1ac57f6f4be262acf32e5ef52c", brand="homeassistant", extended_address="aeeb2f594b570bbf", extended_pan_id="e60fc7c186212ce5", @@ -98,6 +99,7 @@ async def test_discover_routers(hass: HomeAssistant, mock_async_zeroconf: None) "f6a99b425a67abed", discovery.ThreadRouterDiscoveryData( addresses=["192.168.0.124"], + border_agent_id="bc3740c3e963aa8735bebecd7cc503c7", brand="google", extended_address="f6a99b425a67abed", extended_pan_id="9e75e256f61409a3", @@ -176,6 +178,7 @@ async def test_discover_routers_unconfigured( "aeeb2f594b570bbf", discovery.ThreadRouterDiscoveryData( addresses=["192.168.0.115"], + border_agent_id="230c6a1ac57f6f4be262acf32e5ef52c", brand="homeassistant", extended_address="aeeb2f594b570bbf", extended_pan_id="e60fc7c186212ce5", @@ -221,6 +224,7 @@ async def test_discover_routers_bad_data( "aeeb2f594b570bbf", discovery.ThreadRouterDiscoveryData( addresses=["192.168.0.115"], + border_agent_id="230c6a1ac57f6f4be262acf32e5ef52c", brand=None, extended_address="aeeb2f594b570bbf", extended_pan_id="e60fc7c186212ce5", diff --git a/tests/components/thread/test_websocket_api.py b/tests/components/thread/test_websocket_api.py index bfe71b8b21c..75e1b313132 100644 --- a/tests/components/thread/test_websocket_api.py +++ b/tests/components/thread/test_websocket_api.py @@ -332,6 +332,7 @@ async def test_discover_routers( "event": { "data": { "addresses": ["192.168.0.115"], + "border_agent_id": "230c6a1ac57f6f4be262acf32e5ef52c", "brand": "homeassistant", "extended_address": "aeeb2f594b570bbf", "extended_pan_id": "e60fc7c186212ce5", @@ -361,6 +362,7 @@ async def test_discover_routers( "event": { "data": { "addresses": ["192.168.0.124"], + "border_agent_id": "bc3740c3e963aa8735bebecd7cc503c7", "brand": "google", "extended_address": "f6a99b425a67abed", "extended_pan_id": "9e75e256f61409a3", From 77b421887befe1478fe40798e301d25721cd30c4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 14 Aug 2023 16:58:57 +0200 Subject: [PATCH 0500/1151] Add entity translations for August (#98077) --- .../components/august/binary_sensor.py | 31 ++++-------- homeassistant/components/august/button.py | 3 +- homeassistant/components/august/camera.py | 7 +-- homeassistant/components/august/entity.py | 1 + homeassistant/components/august/lock.py | 3 +- homeassistant/components/august/sensor.py | 10 +--- homeassistant/components/august/strings.json | 22 +++++++++ tests/components/august/test_binary_sensor.py | 48 ++++++++++--------- tests/components/august/test_init.py | 4 +- 9 files changed, 69 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 2cbeeeee5aa..b19a9833a47 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -109,10 +109,6 @@ def _native_datetime() -> datetime: class AugustBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes August binary_sensor entity.""" - # AugustBinarySensor does not support UNDEFINED or None, - # restrict the type to str. - name: str = "" - @dataclass class AugustDoorbellRequiredKeysMixin: @@ -128,34 +124,28 @@ class AugustDoorbellBinarySensorEntityDescription( ): """Describes August binary_sensor entity.""" - # AugustDoorbellBinarySensor does not support UNDEFINED or None, - # restrict the type to str. - name: str = "" - SENSOR_TYPE_DOOR = AugustBinarySensorEntityDescription( - key="door_open", - name="Open", + key="open", + device_class=BinarySensorDeviceClass.DOOR, ) SENSOR_TYPES_VIDEO_DOORBELL = ( AugustDoorbellBinarySensorEntityDescription( - key="doorbell_motion", - name="Motion", + key="motion", device_class=BinarySensorDeviceClass.MOTION, value_fn=_retrieve_motion_state, is_time_based=True, ), AugustDoorbellBinarySensorEntityDescription( - key="doorbell_image_capture", - name="Image Capture", + key="image capture", + translation_key="image_capture", icon="mdi:file-image", value_fn=_retrieve_image_capture_state, is_time_based=True, ), AugustDoorbellBinarySensorEntityDescription( - key="doorbell_online", - name="Online", + key="online", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, value_fn=_retrieve_online_state, @@ -166,8 +156,7 @@ SENSOR_TYPES_VIDEO_DOORBELL = ( SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = ( AugustDoorbellBinarySensorEntityDescription( - key="doorbell_ding", - name="Ding", + key="ding", device_class=BinarySensorDeviceClass.OCCUPANCY, value_fn=_retrieve_ding_state, is_time_based=True, @@ -236,8 +225,7 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): self.entity_description = description self._data = data self._device = device - self._attr_name = f"{device.device_name} {description.name}" - self._attr_unique_id = f"{self._device_id}_{description.name.lower()}" + self._attr_unique_id = f"{self._device_id}_{description.key}" @callback def _update_from_data(self): @@ -284,8 +272,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): self.entity_description = description self._check_for_off_update_listener = None self._data = data - self._attr_name = f"{device.device_name} {description.name}" - self._attr_unique_id = f"{self._device_id}_{description.name.lower()}" + self._attr_unique_id = f"{self._device_id}_{description.key}" @callback def _update_from_data(self): diff --git a/homeassistant/components/august/button.py b/homeassistant/components/august/button.py index c96db61ca1a..b8f66aea02b 100644 --- a/homeassistant/components/august/button.py +++ b/homeassistant/components/august/button.py @@ -24,10 +24,11 @@ async def async_setup_entry( class AugustWakeLockButton(AugustEntityMixin, ButtonEntity): """Representation of an August lock wake button.""" + _attr_translation_key = "wake" + def __init__(self, data: AugustData, device: Lock) -> None: """Initialize the lock wake button.""" super().__init__(data, device) - self._attr_name = f"{device.device_name} Wake" self._attr_unique_id = f"{self._device_id}_wake" async def async_press(self) -> None: diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index a3cc18ab9c0..4c3c124953a 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -33,16 +33,17 @@ async def async_setup_entry( class AugustCamera(AugustEntityMixin, Camera): - """An implementation of a August security camera.""" + """An implementation of an August security camera.""" + + _attr_translation_key = "camera" def __init__(self, data, device, session, timeout): - """Initialize a August security camera.""" + """Initialize an August security camera.""" super().__init__(data, device) self._timeout = timeout self._session = session self._image_url = None self._image_content = None - self._attr_name = f"{device.device_name} Camera" self._attr_unique_id = f"{self._device_id:s}_camera" @property diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index bd81dc0c96f..47f3b8be74f 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -19,6 +19,7 @@ class AugustEntityMixin(Entity): """Base implementation for August device.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, data: AugustData, device: Doorbell | Lock) -> None: """Initialize an August device.""" diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 9e8b2470b4e..e082cd1cfab 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -37,11 +37,12 @@ async def async_setup_entry( class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): """Representation of an August lock.""" + _attr_name = None + def __init__(self, data, device): """Initialize the lock.""" super().__init__(data, device) self._lock_status = None - self._attr_name = device.device_name self._attr_unique_id = f"{self._device_id:s}_lock" self._update_from_data() diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 169a344e2bd..2c688ae7615 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -75,7 +75,6 @@ class AugustSensorEntityDescription( SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail]( key="device_battery", - name="Battery", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, value_fn=_retrieve_device_battery_state, @@ -83,7 +82,6 @@ SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail]( SENSOR_TYPE_KEYPAD_BATTERY = AugustSensorEntityDescription[KeypadDetail]( key="linked_keypad_battery", - name="Battery", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, value_fn=_retrieve_linked_keypad_battery_state, @@ -176,6 +174,8 @@ async def _async_migrate_old_unique_ids(hass, devices): class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): """Representation of an August lock operation sensor.""" + _attr_translation_key = "operator" + def __init__(self, data, device): """Initialize the sensor.""" super().__init__(data, device) @@ -188,11 +188,6 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): self._entity_picture = None self._update_from_data() - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._device.device_name} Operator" - @callback def _update_from_data(self): """Get the latest state of the sensor and update activity.""" @@ -278,7 +273,6 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[_T]): super().__init__(data, device) self.entity_description = description self._old_device = old_device - self._attr_name = f"{device.device_name} {description.name}" self._attr_unique_id = f"{self._device_id}_{description.key}" self._update_from_data() diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json index 88362c9fd66..7e33ec30881 100644 --- a/homeassistant/components/august/strings.json +++ b/homeassistant/components/august/strings.json @@ -37,5 +37,27 @@ "title": "Reauthenticate an August account" } } + }, + "entity": { + "binary_sensor": { + "image_capture": { + "name": "Image capture" + } + }, + "button": { + "wake": { + "name": "Wake" + } + }, + "camera": { + "camera": { + "name": "[%key:component::camera::title%]" + } + }, + "sensor": { + "operator": { + "name": "Operator" + } + } } } diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 2787cdbe23d..50cac4445ab 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -41,7 +41,7 @@ async def test_doorsense(hass: HomeAssistant) -> None: await _create_august_with_devices(hass, [lock_one]) binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_ON @@ -50,7 +50,7 @@ async def test_doorsense(hass: HomeAssistant) -> None: await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_ON @@ -58,7 +58,7 @@ async def test_doorsense(hass: HomeAssistant) -> None: await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_OFF @@ -74,7 +74,7 @@ async def test_lock_bridge_offline(hass: HomeAssistant) -> None: await _create_august_with_devices(hass, [lock_one], activities=activities) binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_UNAVAILABLE @@ -93,11 +93,11 @@ async def test_create_doorbell(hass: HomeAssistant) -> None: ) assert binary_sensor_k98gidt45gul_name_image_capture.state == STATE_OFF binary_sensor_k98gidt45gul_name_online = hass.states.get( - "binary_sensor.k98gidt45gul_name_online" + "binary_sensor.k98gidt45gul_name_connectivity" ) assert binary_sensor_k98gidt45gul_name_online.state == STATE_ON binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_ding" + "binary_sensor.k98gidt45gul_name_occupancy" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF binary_sensor_k98gidt45gul_name_motion = hass.states.get( @@ -120,10 +120,12 @@ async def test_create_doorbell_offline(hass: HomeAssistant) -> None: ) assert binary_sensor_tmt100_name_motion.state == STATE_UNAVAILABLE binary_sensor_tmt100_name_online = hass.states.get( - "binary_sensor.tmt100_name_online" + "binary_sensor.tmt100_name_connectivity" ) assert binary_sensor_tmt100_name_online.state == STATE_OFF - binary_sensor_tmt100_name_ding = hass.states.get("binary_sensor.tmt100_name_ding") + binary_sensor_tmt100_name_ding = hass.states.get( + "binary_sensor.tmt100_name_occupancy" + ) assert binary_sensor_tmt100_name_ding.state == STATE_UNAVAILABLE @@ -140,11 +142,11 @@ async def test_create_doorbell_with_motion(hass: HomeAssistant) -> None: ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON binary_sensor_k98gidt45gul_name_online = hass.states.get( - "binary_sensor.k98gidt45gul_name_online" + "binary_sensor.k98gidt45gul_name_connectivity" ) assert binary_sensor_k98gidt45gul_name_online.state == STATE_ON binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_ding" + "binary_sensor.k98gidt45gul_name_occupancy" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) @@ -174,7 +176,7 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: ) assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_ding" + "binary_sensor.k98gidt45gul_name_occupancy" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF @@ -242,7 +244,7 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_ding" + "binary_sensor.k98gidt45gul_name_occupancy" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF @@ -273,7 +275,7 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: await hass.async_block_till_done() binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_ding" + "binary_sensor.k98gidt45gul_name_occupancy" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_ON new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) @@ -286,7 +288,7 @@ async def test_doorbell_update_via_pubnub(hass: HomeAssistant) -> None: await hass.async_block_till_done() binary_sensor_k98gidt45gul_name_ding = hass.states.get( - "binary_sensor.k98gidt45gul_name_ding" + "binary_sensor.k98gidt45gul_name_occupancy" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF @@ -317,7 +319,7 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: ) binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_ON @@ -332,7 +334,7 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_OFF @@ -346,14 +348,14 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_ON async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_ON @@ -361,7 +363,7 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_ON @@ -369,7 +371,7 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_ON @@ -383,14 +385,14 @@ async def test_door_sense_update_via_pubnub(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_ON async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) await hass.async_block_till_done() binary_sensor_online_with_doorsense_name = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name.state == STATE_ON @@ -404,6 +406,6 @@ async def test_create_lock_with_doorbell(hass: HomeAssistant) -> None: await _create_august_with_devices(hass, [lock_one]) ding_sensor = hass.states.get( - "binary_sensor.a6697750d607098bae8d6baa11ef8063_name_ding" + "binary_sensor.a6697750d607098bae8d6baa11ef8063_name_occupancy" ) assert ding_sensor.state == STATE_OFF diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index fe297c97a57..36a7f73f8a8 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -186,11 +186,11 @@ async def test_lock_has_doorsense(hass: HomeAssistant) -> None: await _create_august_with_devices(hass, [doorsenselock, nodoorsenselock]) binary_sensor_online_with_doorsense_name_open = hass.states.get( - "binary_sensor.online_with_doorsense_name_open" + "binary_sensor.online_with_doorsense_name_door" ) assert binary_sensor_online_with_doorsense_name_open.state == STATE_ON binary_sensor_missing_doorsense_id_name_open = hass.states.get( - "binary_sensor.missing_doorsense_id_name_open" + "binary_sensor.missing_with_doorsense_name_door" ) assert binary_sensor_missing_doorsense_id_name_open is None From 54223fe06c5afa3d0c18007c1be41fec24f350ca Mon Sep 17 00:00:00 2001 From: Marco Ranieri Date: Mon, 14 Aug 2023 17:47:50 +0200 Subject: [PATCH 0501/1151] Enable Alexa Unlock directive (#97127) Co-authored-by: Jan Bouwhuis --- homeassistant/components/alexa/handlers.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 06ce4f88b56..3e995e9ffe2 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -474,7 +474,24 @@ async def async_api_unlock( context: ha.Context, ) -> AlexaResponse: """Process an unlock request.""" - if config.locale not in {"de-DE", "en-US", "ja-JP"}: + if config.locale not in { + "ar-SA", + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "hi-IN", + "it-IT", + "ja-JP", + "pt-BR", + }: msg = ( "The unlock directive is not supported for the following locales:" f" {config.locale}" From 85c2216cd7d2b7371df811a15e046cd6e8b1dd56 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Aug 2023 17:48:11 +0200 Subject: [PATCH 0502/1151] Ensure headers middleware handles errors too (#98397) --- homeassistant/components/http/headers.py | 26 ++++++++++++++++-------- tests/components/http/test_headers.py | 24 +++++++++++++++++++++- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/http/headers.py b/homeassistant/components/http/headers.py index b53f354b144..20c0a58967b 100644 --- a/homeassistant/components/http/headers.py +++ b/homeassistant/components/http/headers.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from aiohttp.web import Application, Request, StreamResponse, middleware +from aiohttp.web_exceptions import HTTPException from homeassistant.core import callback @@ -12,20 +13,29 @@ from homeassistant.core import callback def setup_headers(app: Application, use_x_frame_options: bool) -> None: """Create headers middleware for the app.""" + added_headers = { + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + "Server": "", # Empty server header, to prevent aiohttp of setting one. + } + + if use_x_frame_options: + added_headers["X-Frame-Options"] = "SAMEORIGIN" + @middleware async def headers_middleware( request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] ) -> StreamResponse: """Process request and add headers to the responses.""" - response = await handler(request) - response.headers["Referrer-Policy"] = "no-referrer" - response.headers["X-Content-Type-Options"] = "nosniff" + try: + response = await handler(request) + except HTTPException as err: + for key, value in added_headers.items(): + err.headers[key] = value + raise err - # Set an empty server header, to prevent aiohttp of setting one. - response.headers["Server"] = "" - - if use_x_frame_options: - response.headers["X-Frame-Options"] = "SAMEORIGIN" + for key, value in added_headers.items(): + response.headers[key] = value return response diff --git a/tests/components/http/test_headers.py b/tests/components/http/test_headers.py index 6d7dbad68f6..16b897b9f99 100644 --- a/tests/components/http/test_headers.py +++ b/tests/components/http/test_headers.py @@ -2,21 +2,28 @@ from http import HTTPStatus from aiohttp import web +from aiohttp.web_exceptions import HTTPUnauthorized from homeassistant.components.http.headers import setup_headers from tests.typing import ClientSessionGenerator -async def mock_handler(request): +async def mock_handler(_: web.Request) -> web.Response: """Return OK.""" return web.Response(text="OK") +async def mock_handler_error(_: web.Request) -> web.Response: + """Return Unauthorized.""" + raise HTTPUnauthorized(text="Ah ah ah, you didn't say the magic word") + + async def test_headers_added(aiohttp_client: ClientSessionGenerator) -> None: """Test that headers are being added on each request.""" app = web.Application() app.router.add_get("/", mock_handler) + app.router.add_get("/error", mock_handler_error) setup_headers(app, use_x_frame_options=True) @@ -29,11 +36,20 @@ async def test_headers_added(aiohttp_client: ClientSessionGenerator) -> None: assert resp.headers["X-Content-Type-Options"] == "nosniff" assert resp.headers["X-Frame-Options"] == "SAMEORIGIN" + resp = await mock_api_client.get("/error") + + assert resp.status == HTTPStatus.UNAUTHORIZED + assert resp.headers["Referrer-Policy"] == "no-referrer" + assert resp.headers["Server"] == "" + assert resp.headers["X-Content-Type-Options"] == "nosniff" + assert resp.headers["X-Frame-Options"] == "SAMEORIGIN" + async def test_allow_framing(aiohttp_client: ClientSessionGenerator) -> None: """Test that we allow framing when disabled.""" app = web.Application() app.router.add_get("/", mock_handler) + app.router.add_get("/error", mock_handler_error) setup_headers(app, use_x_frame_options=False) @@ -42,3 +58,9 @@ async def test_allow_framing(aiohttp_client: ClientSessionGenerator) -> None: assert resp.status == HTTPStatus.OK assert "X-Frame-Options" not in resp.headers + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/error") + + assert resp.status == HTTPStatus.UNAUTHORIZED + assert "X-Frame-Options" not in resp.headers From d6fcdeac06dcec668c6ae521bd7676c6d76c6bf3 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 14 Aug 2023 18:03:17 +0200 Subject: [PATCH 0503/1151] Avoid leaking backtrace on connection lost in arcam (#98277) * Avoid leaking backtrace on connection lost * Correct ruff error after rebase --- .../components/arcam_fmj/media_player.py | 45 +++++++++++-- tests/components/arcam_fmj/conftest.py | 3 + .../components/arcam_fmj/test_media_player.py | 63 +++++++++++++++++-- 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 0173005eb2f..12114ec04b8 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -1,10 +1,11 @@ """Arcam media player.""" from __future__ import annotations +import functools import logging from typing import Any -from arcam.fmj import SourceCodes +from arcam.fmj import ConnectionFailed, SourceCodes from arcam.fmj.state import State from homeassistant.components.media_player import ( @@ -19,6 +20,7 @@ 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.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -57,6 +59,21 @@ async def async_setup_entry( ) +def convert_exception(func): + """Return decorator to convert a connection error into a home assistant error.""" + + @functools.wraps(func) + async def _convert_exception(*args, **kwargs): + try: + return await func(*args, **kwargs) + except ConnectionFailed as exception: + raise HomeAssistantError( + f"Connection failed to device during {func}" + ) from exception + + return _convert_exception + + class ArcamFmj(MediaPlayerEntity): """Representation of a media device.""" @@ -105,7 +122,10 @@ class ArcamFmj(MediaPlayerEntity): async def async_added_to_hass(self) -> None: """Once registered, add listener for events.""" await self._state.start() - await self._state.update() + try: + await self._state.update() + except ConnectionFailed as connection: + _LOGGER.debug("Connection lost during addition: %s", connection) @callback def _data(host: str) -> None: @@ -137,13 +157,18 @@ class ArcamFmj(MediaPlayerEntity): async def async_update(self) -> None: """Force update of state.""" _LOGGER.debug("Update state %s", self.name) - await self._state.update() + try: + await self._state.update() + except ConnectionFailed as connection: + _LOGGER.debug("Connection lost during update: %s", connection) + @convert_exception async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" await self._state.set_mute(mute) self.async_write_ha_state() + @convert_exception async def async_select_source(self, source: str) -> None: """Select a specific source.""" try: @@ -155,31 +180,37 @@ class ArcamFmj(MediaPlayerEntity): await self._state.set_source(value) self.async_write_ha_state() + @convert_exception async def async_select_sound_mode(self, sound_mode: str) -> None: """Select a specific source.""" try: await self._state.set_decode_mode(sound_mode) - except (KeyError, ValueError): - _LOGGER.error("Unsupported sound_mode %s", sound_mode) - return + except (KeyError, ValueError) as exception: + raise HomeAssistantError( + f"Unsupported sound_mode {sound_mode}" + ) from exception self.async_write_ha_state() + @convert_exception async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._state.set_volume(round(volume * 99.0)) self.async_write_ha_state() + @convert_exception async def async_volume_up(self) -> None: """Turn volume up for media player.""" await self._state.inc_volume() self.async_write_ha_state() + @convert_exception async def async_volume_down(self) -> None: """Turn volume up for media player.""" await self._state.dec_volume() self.async_write_ha_state() + @convert_exception async def async_turn_on(self) -> None: """Turn the media player on.""" if self._state.get_power() is not None: @@ -189,6 +220,7 @@ class ArcamFmj(MediaPlayerEntity): _LOGGER.debug("Firing event to turn on device") self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id}) + @convert_exception async def async_turn_off(self) -> None: """Turn the media player off.""" await self._state.set_power(False) @@ -230,6 +262,7 @@ class ArcamFmj(MediaPlayerEntity): return root + @convert_exception async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index 693cdc685c9..ba32951efe4 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.arcam_fmj.const import DEFAULT_NAME from homeassistant.components.arcam_fmj.media_player import ArcamFmj from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockEntityPlatform @@ -94,6 +95,8 @@ async def player_setup_fixture(hass, state_1, state_2, client): if zone == 2: return state_2 + await async_setup_component(hass, "homeassistant", {}) + with patch("homeassistant.components.arcam_fmj.Client", return_value=client), patch( "homeassistant.components.arcam_fmj.media_player.State", side_effect=state_mock ), patch("homeassistant.components.arcam_fmj._run_client", return_value=None): diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 2607ab817df..b9c86140cb9 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -2,14 +2,20 @@ from math import isclose from unittest.mock import ANY, PropertyMock, patch -from arcam.fmj import DecodeMode2CH, DecodeModeMCH, SourceCodes +from arcam.fmj import ConnectionFailed, DecodeMode2CH, DecodeModeMCH, SourceCodes import pytest +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, + ATTR_MEDIA_VOLUME_LEVEL, ATTR_SOUND_MODE, ATTR_SOUND_MODE_LIST, SERVICE_SELECT_SOURCE, + SERVICE_VOLUME_SET, MediaType, ) from homeassistant.const import ( @@ -20,6 +26,7 @@ from homeassistant.const import ( ATTR_NAME, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .conftest import MOCK_HOST, MOCK_UUID @@ -106,12 +113,33 @@ async def test_name(player) -> None: assert data.attributes["friendly_name"] == "Zone 1" -async def test_update(player, state) -> None: +async def test_update(hass: HomeAssistant, player_setup: str, state) -> None: """Test update.""" - await update(player, force_refresh=True) + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + service_data={ATTR_ENTITY_ID: player_setup}, + blocking=True, + ) state.update.assert_called_with() +async def test_update_lost( + hass: HomeAssistant, player_setup: str, state, caplog: pytest.LogCaptureFixture +) -> None: + """Test update, with connection loss is ignored.""" + state.update.side_effect = ConnectionFailed() + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + service_data={ATTR_ENTITY_ID: player_setup}, + blocking=True, + ) + state.update.assert_called_with() + assert "Connection lost during update" in caplog.text + + @pytest.mark.parametrize( ("source", "value"), [("PVR", SourceCodes.PVR), ("BD", SourceCodes.BD), ("INVALID", None)], @@ -220,12 +248,37 @@ async def test_volume_level(player, state) -> None: @pytest.mark.parametrize(("volume", "call"), [(0.0, 0), (0.5, 50), (1.0, 99)]) -async def test_set_volume_level(player, state, volume, call) -> None: +async def test_set_volume_level( + hass: HomeAssistant, player_setup: str, state, volume, call +) -> None: """Test setting volume.""" - await player.async_set_volume_level(volume) + + await hass.services.async_call( + "media_player", + SERVICE_VOLUME_SET, + service_data={ATTR_ENTITY_ID: player_setup, ATTR_MEDIA_VOLUME_LEVEL: volume}, + blocking=True, + ) + state.set_volume.assert_called_with(call) +async def test_set_volume_level_lost( + hass: HomeAssistant, player_setup: str, state +) -> None: + """Test setting volume, with a lost connection.""" + + state.set_volume.side_effect = ConnectionFailed() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "media_player", + SERVICE_VOLUME_SET, + service_data={ATTR_ENTITY_ID: player_setup, ATTR_MEDIA_VOLUME_LEVEL: 0.0}, + blocking=True, + ) + + @pytest.mark.parametrize( ("source", "media_content_type"), [ From c3c00e69849429ae9be24e2cc40432405a4e79c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 14 Aug 2023 18:21:12 +0200 Subject: [PATCH 0504/1151] Update aioairzone to v0.6.6 (#98399) --- .../components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airzone/test_climate.py | 7 ++++++ tests/components/airzone/test_config_flow.py | 25 ++++++++++++++++++- tests/components/airzone/test_coordinator.py | 10 +++++++- tests/components/airzone/test_init.py | 8 +++++- tests/components/airzone/test_sensor.py | 4 +++ tests/components/airzone/util.py | 21 ++++++++++++++++ 9 files changed, 75 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 39adf08236e..711da2ec993 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.5"] + "requirements": ["aioairzone==0.6.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 114a1e6b5e8..6f7cbba02ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -188,7 +188,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.1 # homeassistant.components.airzone -aioairzone==0.6.5 +aioairzone==0.6.6 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec9f4da9090..de2b973d57c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.1 # homeassistant.components.airzone -aioairzone==0.6.5 +aioairzone==0.6.6 # 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 3e68c056566..1f8667d0344 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -54,6 +54,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.util.dt import utcnow from .util import ( + HVAC_DHW_MOCK, HVAC_MOCK, HVAC_SYSTEMS_MOCK, HVAC_WEBSERVER_MOCK, @@ -226,6 +227,9 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MIN_TEMP] = 10 with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK_CHANGED, ), patch( @@ -437,6 +441,9 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: del HVAC_MOCK_NO_SET_POINT[API_SYSTEMS][0][API_DATA][0][API_SET_POINT] with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK_NO_SET_POINT, ), patch( diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index d703a232c7b..10aaf07885b 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import patch from aioairzone.const import API_MAC, API_SYSTEMS from aioairzone.exceptions import ( AirzoneError, + HotWaterNotAvailable, InvalidMethod, InvalidSystem, SystemOutOfRange, @@ -19,7 +20,14 @@ from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .util import CONFIG, CONFIG_ID1, HVAC_MOCK, HVAC_VERSION_MOCK, HVAC_WEBSERVER_MOCK +from .util import ( + CONFIG, + CONFIG_ID1, + HVAC_DHW_MOCK, + HVAC_MOCK, + HVAC_VERSION_MOCK, + HVAC_WEBSERVER_MOCK, +) from tests.common import MockConfigEntry @@ -41,6 +49,9 @@ async def test_form(hass: HomeAssistant) -> None: "homeassistant.components.airzone.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ), patch( @@ -87,6 +98,9 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: "homeassistant.components.airzone.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + side_effect=HotWaterNotAvailable, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", side_effect=InvalidSystem, ) as mock_hvac, patch( @@ -186,6 +200,9 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: "homeassistant.components.airzone.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ), patch( @@ -264,6 +281,9 @@ async def test_dhcp_connection_error(hass: HomeAssistant) -> None: "homeassistant.components.airzone.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ), patch( @@ -317,6 +337,9 @@ async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: "homeassistant.components.airzone.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + side_effect=HotWaterNotAvailable, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", side_effect=InvalidSystem, ) as mock_hvac, patch( diff --git a/tests/components/airzone/test_coordinator.py b/tests/components/airzone/test_coordinator.py index bcfdad8ead8..62f6a15fe35 100644 --- a/tests/components/airzone/test_coordinator.py +++ b/tests/components/airzone/test_coordinator.py @@ -2,7 +2,12 @@ from unittest.mock import patch -from aioairzone.exceptions import AirzoneError, InvalidMethod, SystemOutOfRange +from aioairzone.exceptions import ( + AirzoneError, + HotWaterNotAvailable, + InvalidMethod, + SystemOutOfRange, +) from homeassistant.components.airzone.const import DOMAIN from homeassistant.components.airzone.coordinator import SCAN_INTERVAL @@ -26,6 +31,9 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + side_effect=HotWaterNotAvailable, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ) as mock_hvac, patch( diff --git a/tests/components/airzone/test_init.py b/tests/components/airzone/test_init.py index bb7cb06d1c2..2214e5d07ab 100644 --- a/tests/components/airzone/test_init.py +++ b/tests/components/airzone/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from aioairzone.exceptions import InvalidMethod, SystemOutOfRange +from aioairzone.exceptions import HotWaterNotAvailable, InvalidMethod, SystemOutOfRange from homeassistant.components.airzone.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -23,6 +23,9 @@ async def test_unique_id_migrate(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + side_effect=HotWaterNotAvailable, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ), patch( @@ -45,6 +48,9 @@ async def test_unique_id_migrate(hass: HomeAssistant) -> None: ) with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + side_effect=HotWaterNotAvailable, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ), patch( diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py index 1d778761ee1..cce8a452a15 100644 --- a/tests/components/airzone/test_sensor.py +++ b/tests/components/airzone/test_sensor.py @@ -10,6 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow from .util import ( + HVAC_DHW_MOCK, HVAC_MOCK, HVAC_SYSTEMS_MOCK, HVAC_VERSION_MOCK, @@ -86,6 +87,9 @@ async def test_airzone_sensors_availability( del HVAC_MOCK_UNAVAILABLE_ZONE[API_SYSTEMS][0][API_DATA][1] with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK_UNAVAILABLE_ZONE, ), patch( diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index 4afcaeac232..74cda7c8017 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -3,6 +3,12 @@ from unittest.mock import patch from aioairzone.const import ( + API_ACS_MAX_TEMP, + API_ACS_MIN_TEMP, + API_ACS_ON, + API_ACS_POWER_MODE, + API_ACS_SET_POINT, + API_ACS_TEMP, API_AIR_DEMAND, API_COLD_ANGLE, API_COLD_STAGE, @@ -266,6 +272,18 @@ HVAC_MOCK = { ] } +HVAC_DHW_MOCK = { + API_DATA: { + API_SYSTEM_ID: 0, + API_ACS_TEMP: 43, + API_ACS_SET_POINT: 45, + API_ACS_MAX_TEMP: 75, + API_ACS_MIN_TEMP: 30, + API_ACS_ON: 1, + API_ACS_POWER_MODE: 0, + } +} + HVAC_SYSTEMS_MOCK = { API_SYSTEMS: [ { @@ -301,6 +319,9 @@ async def async_init_integration( config_entry.add_to_hass(hass) with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ), patch( From 318aa9b95ad064b15da6dfa2ed41c084535bfd08 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 14 Aug 2023 19:35:31 +0200 Subject: [PATCH 0505/1151] Add entity translations to Goodwe (#98224) * Add entity translations to Goodwe * Add entity translations to Goodwe --- homeassistant/components/goodwe/button.py | 3 ++- homeassistant/components/goodwe/number.py | 9 ++++++--- homeassistant/components/goodwe/select.py | 2 +- homeassistant/components/goodwe/strings.json | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/goodwe/button.py b/homeassistant/components/goodwe/button.py index 55ba33b63f6..12cad42547d 100644 --- a/homeassistant/components/goodwe/button.py +++ b/homeassistant/components/goodwe/button.py @@ -34,7 +34,7 @@ class GoodweButtonEntityDescription( SYNCHRONIZE_CLOCK = GoodweButtonEntityDescription( key="synchronize_clock", - name="Synchronize inverter clock", + translation_key="synchronize_clock", icon="mdi:clock-check-outline", entity_category=EntityCategory.CONFIG, action=lambda inv: inv.write_setting("time", datetime.now()), @@ -66,6 +66,7 @@ class GoodweButtonEntity(ButtonEntity): """Entity representing the inverter clock synchronization button.""" _attr_should_poll = False + _attr_has_entity_name = True entity_description: GoodweButtonEntityDescription def __init__( diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py index 7e31dd14037..a3e4190f309 100644 --- a/homeassistant/components/goodwe/number.py +++ b/homeassistant/components/goodwe/number.py @@ -45,10 +45,12 @@ def _get_setting_unit(inverter: Inverter, setting: str) -> str: NUMBERS = ( + # Only one of the export limits are added. + # Availability is checked in the filter method. # Export limit in W GoodweNumberEntityDescription( key="grid_export_limit", - name="Grid export limit", + translation_key="grid_export_limit", icon="mdi:transmission-tower", entity_category=EntityCategory.CONFIG, device_class=NumberDeviceClass.POWER, @@ -63,7 +65,7 @@ NUMBERS = ( # Export limit in % GoodweNumberEntityDescription( key="grid_export_limit", - name="Grid export limit", + translation_key="grid_export_limit", icon="mdi:transmission-tower", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, @@ -76,7 +78,7 @@ NUMBERS = ( ), GoodweNumberEntityDescription( key="battery_discharge_depth", - name="Depth of discharge (on-grid)", + translation_key="battery_discharge_depth", icon="mdi:battery-arrow-down", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, @@ -120,6 +122,7 @@ class InverterNumberEntity(NumberEntity): """Inverter numeric setting entity.""" _attr_should_poll = False + _attr_has_entity_name = True entity_description: GoodweNumberEntityDescription def __init__( diff --git a/homeassistant/components/goodwe/select.py b/homeassistant/components/goodwe/select.py index 012d73f792c..bc22376e4d9 100644 --- a/homeassistant/components/goodwe/select.py +++ b/homeassistant/components/goodwe/select.py @@ -31,7 +31,6 @@ _OPTION_TO_MODE: dict[str, OperationMode] = { OPERATION_MODE = SelectEntityDescription( key="operation_mode", - name="Inverter operation mode", icon="mdi:solar-power", entity_category=EntityCategory.CONFIG, translation_key="operation_mode", @@ -72,6 +71,7 @@ class InverterOperationModeEntity(SelectEntity): """Entity representing the inverter operation mode.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__( self, diff --git a/homeassistant/components/goodwe/strings.json b/homeassistant/components/goodwe/strings.json index 28765c005af..ec4ea80e22a 100644 --- a/homeassistant/components/goodwe/strings.json +++ b/homeassistant/components/goodwe/strings.json @@ -18,8 +18,22 @@ } }, "entity": { + "button": { + "synchronize_clock": { + "name": "Synchronize inverter clock" + } + }, + "number": { + "grid_export_limit": { + "name": "Grid export limit" + }, + "battery_discharge_depth": { + "name": "Depth of discharge (on-grid)" + } + }, "select": { "operation_mode": { + "name": "Inverter operation mode", "state": { "general": "General mode", "off_grid": "Off grid mode", From 80fa0340483a90d46b2389414ea027e96d050d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 14 Aug 2023 18:36:58 +0100 Subject: [PATCH 0506/1151] ipma: remove abmantis from codeowners (#98304) * ipma: remove abmantis from codeowners I am not currently maintaining this integration. * Run hassfest * Try again --------- Co-authored-by: Franck Nijhof --- CODEOWNERS | 4 ++-- homeassistant/components/ipma/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 084d83b0da1..bd1b8ed49f0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -606,8 +606,8 @@ build.json @home-assistant/supervisor /homeassistant/components/iotawatt/ @gtdiehl @jyavenard /tests/components/iotawatt/ @gtdiehl @jyavenard /homeassistant/components/iperf3/ @rohankapoorcom -/homeassistant/components/ipma/ @dgomes @abmantis -/tests/components/ipma/ @dgomes @abmantis +/homeassistant/components/ipma/ @dgomes +/tests/components/ipma/ @dgomes /homeassistant/components/ipp/ @ctalkington /tests/components/ipp/ @ctalkington /homeassistant/components/iqvia/ @bachya diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 4f86295db08..4fea047e834 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -1,7 +1,7 @@ { "domain": "ipma", "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)", - "codeowners": ["@dgomes", "@abmantis"], + "codeowners": ["@dgomes"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", "iot_class": "cloud_polling", From 6294014fcd3d89656a1aeca808f79e0a11634aa4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Aug 2023 20:09:50 +0200 Subject: [PATCH 0507/1151] Bump python-otbr-api to 2.5.0 (#98403) --- homeassistant/components/otbr/manifest.json | 2 +- homeassistant/components/thread/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/otbr/manifest.json b/homeassistant/components/otbr/manifest.json index e62a2d42b1e..cf6aba33e80 100644 --- a/homeassistant/components/otbr/manifest.json +++ b/homeassistant/components/otbr/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/otbr", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["python-otbr-api==2.4.0"] + "requirements": ["python-otbr-api==2.5.0"] } diff --git a/homeassistant/components/thread/manifest.json b/homeassistant/components/thread/manifest.json index 29b7e61d407..eeac24a626f 100644 --- a/homeassistant/components/thread/manifest.json +++ b/homeassistant/components/thread/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/thread", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["python-otbr-api==2.4.0", "pyroute2==0.7.5"], + "requirements": ["python-otbr-api==2.5.0", "pyroute2==0.7.5"], "zeroconf": ["_meshcop._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 6f7cbba02ca..4133e60181c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2141,7 +2141,7 @@ python-opensky==0.2.0 # homeassistant.components.otbr # homeassistant.components.thread -python-otbr-api==2.4.0 +python-otbr-api==2.5.0 # homeassistant.components.picnic python-picnic-api==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de2b973d57c..621ca8ba98b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1570,7 +1570,7 @@ python-opensky==0.2.0 # homeassistant.components.otbr # homeassistant.components.thread -python-otbr-api==2.4.0 +python-otbr-api==2.5.0 # homeassistant.components.picnic python-picnic-api==1.1.0 From 69b3ae4588778cc2b10b5352440c7891fd139964 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Aug 2023 14:07:17 -0500 Subject: [PATCH 0508/1151] Bump zeroconf to 0.78.0 (#98405) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index da8cfd26b1f..6f3020244fa 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.76.0"] + "requirements": ["zeroconf==0.78.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 29c654cd05c..37ec683aff0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.76.0 +zeroconf==0.78.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 4133e60181c..f4d8381f0c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2752,7 +2752,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.76.0 +zeroconf==0.78.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 621ca8ba98b..05f81b16e78 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2025,7 +2025,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.76.0 +zeroconf==0.78.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 97134668174cd13711dc652a641902a104e5093b Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 14 Aug 2023 21:42:47 +0200 Subject: [PATCH 0509/1151] Add sensor when meter last sent its data to Discovergy (#97223) * Add sensor for last reading by Discovergy * Rename sensor to make it clear what it actually is * Revert back to single sensor classe and extend entity_description with a value function --- homeassistant/components/discovergy/sensor.py | 36 ++++++++++++++----- .../components/discovergy/strings.json | 3 ++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/discovergy/sensor.py b/homeassistant/components/discovergy/sensor.py index b243f9adc54..5b8fb864987 100644 --- a/homeassistant/components/discovergy/sensor.py +++ b/homeassistant/components/discovergy/sensor.py @@ -1,7 +1,9 @@ """Discovergy sensor entity.""" +from collections.abc import Callable from dataclasses import dataclass, field +from datetime import datetime -from pydiscovergy.models import Meter +from pydiscovergy.models import Meter, Reading from homeassistant.components.sensor import ( SensorDeviceClass, @@ -11,6 +13,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + EntityCategory, UnitOfElectricPotential, UnitOfEnergy, UnitOfPower, @@ -19,7 +22,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant 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 from . import DiscovergyData, DiscovergyUpdateCoordinator @@ -32,6 +34,9 @@ PARALLEL_UPDATES = 1 class DiscovergyMixin: """Mixin for alternative keys.""" + value_fn: Callable[[Reading, str, int], datetime | float | None] = field( + default=lambda reading, key, scale: float(reading.values[key] / scale) + ) alternative_keys: list[str] = field(default_factory=lambda: []) scale: int = field(default_factory=lambda: 1000) @@ -144,6 +149,17 @@ ELECTRICITY_SENSORS: tuple[DiscovergySensorEntityDescription, ...] = ( ), ) +ADDITIONAL_SENSORS: tuple[DiscovergySensorEntityDescription, ...] = ( + DiscovergySensorEntityDescription( + key="last_transmitted", + translation_key="last_transmitted", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda reading, key, scale: reading.time_with_timezone, + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -160,18 +176,22 @@ async def async_setup_entry( elif meter.measurement_type == "GAS": sensors = GAS_SENSORS + coordinator: DiscovergyUpdateCoordinator = data.coordinators[meter.meter_id] + if sensors is not None: for description in sensors: # check if this meter has this data, then add this sensor for key in {description.key, *description.alternative_keys}: - coordinator: DiscovergyUpdateCoordinator = data.coordinators[ - meter.meter_id - ] if key in coordinator.data.values: entities.append( DiscovergySensor(key, description, meter, coordinator) ) + for description in ADDITIONAL_SENSORS: + entities.append( + DiscovergySensor(description.key, description, meter, coordinator) + ) + async_add_entities(entities, False) @@ -204,8 +224,8 @@ class DiscovergySensor(CoordinatorEntity[DiscovergyUpdateCoordinator], SensorEnt ) @property - def native_value(self) -> StateType: + def native_value(self) -> datetime | float | None: """Return the sensor state.""" - return float( - self.coordinator.data.values[self.data_key] / self.entity_description.scale + return self.entity_description.value_fn( + self.coordinator.data, self.data_key, self.entity_description.scale ) diff --git a/homeassistant/components/discovergy/strings.json b/homeassistant/components/discovergy/strings.json index e8dbbab2021..5147440e1b7 100644 --- a/homeassistant/components/discovergy/strings.json +++ b/homeassistant/components/discovergy/strings.json @@ -60,6 +60,9 @@ }, "phase_3_power": { "name": "Phase 3 power" + }, + "last_transmitted": { + "name": "Last transmitted" } } } From 49a9d0e43996b4ab72d482805e1286cca003eba3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 14 Aug 2023 22:26:20 +0200 Subject: [PATCH 0510/1151] Add entity translations to hunterdouglas powerview (#98232) --- .../hunterdouglas_powerview/button.py | 14 ++-- .../hunterdouglas_powerview/cover.py | 64 +++++++++---------- .../hunterdouglas_powerview/entity.py | 2 + .../hunterdouglas_powerview/select.py | 3 +- .../hunterdouglas_powerview/sensor.py | 4 +- .../hunterdouglas_powerview/strings.json | 37 +++++++++++ 6 files changed, 81 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/button.py b/homeassistant/components/hunterdouglas_powerview/button.py index cb2da3ba8fa..2e0bc1c413a 100644 --- a/homeassistant/components/hunterdouglas_powerview/button.py +++ b/homeassistant/components/hunterdouglas_powerview/button.py @@ -7,7 +7,11 @@ from typing import Any, Final from aiopvapi.resources.shade import BaseShade, factory as PvShade -from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant @@ -36,21 +40,20 @@ class PowerviewButtonDescription( BUTTONS: Final = [ PowerviewButtonDescription( key="calibrate", - name="Calibrate", + translation_key="calibrate", icon="mdi:swap-vertical-circle-outline", entity_category=EntityCategory.DIAGNOSTIC, press_action=lambda shade: shade.calibrate(), ), PowerviewButtonDescription( key="identify", - name="Identify", - icon="mdi:crosshairs-question", + device_class=ButtonDeviceClass.IDENTIFY, entity_category=EntityCategory.DIAGNOSTIC, press_action=lambda shade: shade.jog(), ), PowerviewButtonDescription( key="favorite", - name="Favorite", + translation_key="favorite", icon="mdi:heart", entity_category=EntityCategory.DIAGNOSTIC, press_action=lambda shade: shade.favorite(), @@ -104,7 +107,6 @@ class PowerviewButton(ShadeEntity, ButtonEntity): """Initialize the button entity.""" super().__init__(coordinator, device_info, room_name, shade, name) self.entity_description: PowerviewButtonDescription = description - self._attr_name = f"{self._shade_name} {description.name}" self._attr_unique_id = f"{self._attr_unique_id}_{description.key}" async def async_press(self) -> None: diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index dfb1a7ad967..5cb84658c50 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -118,7 +118,11 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): """Representation of a powerview shade.""" _attr_device_class = CoverDeviceClass.SHADE - _attr_supported_features = CoverEntityFeature(0) + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.SET_POSITION + ) def __init__( self, @@ -131,7 +135,6 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): """Initialize the shade.""" super().__init__(coordinator, device_info, room_name, shade, name) self._shade: BaseShade = shade - self._attr_name = self._shade_name self._scheduled_transition_update: CALLBACK_TYPE | None = None if self._device_info.model != LEGACY_DEVICE_MODEL: self._attr_supported_features |= CoverEntityFeature.STOP @@ -346,26 +349,14 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): class PowerViewShade(PowerViewShadeBase): """Represent a standard shade.""" - def __init__( - self, - coordinator: PowerviewShadeUpdateCoordinator, - device_info: PowerviewDeviceInfo, - room_name: str, - shade: BaseShade, - name: str, - ) -> None: - """Initialize the shade.""" - super().__init__(coordinator, device_info, room_name, shade, name) - self._attr_supported_features |= ( - CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.SET_POSITION - ) + _attr_name = None -class PowerViewShadeWithTiltBase(PowerViewShade): +class PowerViewShadeWithTiltBase(PowerViewShadeBase): """Representation for PowerView shades with tilt capabilities.""" + _attr_name = None + def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, @@ -453,9 +444,11 @@ class PowerViewShadeWithTiltOnClosed(PowerViewShadeWithTiltBase): API Class: ShadeBottomUpTiltOnClosed + ShadeBottomUpTiltOnClosed90 Type 1 - Bottom Up w/ 90° Tilt - Shade 44 - a shade thought to have been a firmware issue (type 0 usually dont tilt) + Shade 44 - a shade thought to have been a firmware issue (type 0 usually don't tilt) """ + _attr_name = None + @property def open_position(self) -> PowerviewShadeMove: """Return the open position and required additional positions.""" @@ -570,7 +563,7 @@ class PowerViewShadeTiltOnly(PowerViewShadeWithTiltBase): self._max_tilt = self._shade.shade_limits.tilt_max -class PowerViewShadeTopDown(PowerViewShade): +class PowerViewShadeTopDown(PowerViewShadeBase): """Representation of a shade that lowers from the roof to the floor. These shades are inverted where MAX_POSITION equates to closed and MIN_POSITION is open @@ -579,6 +572,8 @@ class PowerViewShadeTopDown(PowerViewShade): Type 6 - Top Down """ + _attr_name = None + @property def current_cover_position(self) -> int: """Return the current position of cover.""" @@ -594,7 +589,7 @@ class PowerViewShadeTopDown(PowerViewShade): await self._async_set_cover_position(100 - kwargs[ATTR_POSITION]) -class PowerViewShadeDualRailBase(PowerViewShade): +class PowerViewShadeDualRailBase(PowerViewShadeBase): """Representation of a shade with top/down bottom/up capabilities. Base methods shared between the two shades created @@ -613,11 +608,13 @@ class PowerViewShadeDualRailBase(PowerViewShade): class PowerViewShadeTDBUBottom(PowerViewShadeDualRailBase): """Representation of the bottom PowerViewShadeDualRailBase shade. - These shades have top/down bottom up functionality and two entiites. + These shades have top/down bottom up functionality and two entities. Sibling Class: PowerViewShadeTDBUTop API Class: ShadeTopDownBottomUp """ + _attr_translation_key = "bottom" + def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, @@ -629,7 +626,6 @@ class PowerViewShadeTDBUBottom(PowerViewShadeDualRailBase): """Initialize the shade.""" super().__init__(coordinator, device_info, room_name, shade, name) self._attr_unique_id = f"{self._shade.id}_bottom" - self._attr_name = f"{self._shade_name} Bottom" @callback def _clamp_cover_limit(self, target_hass_position: int) -> int: @@ -655,11 +651,13 @@ class PowerViewShadeTDBUBottom(PowerViewShadeDualRailBase): class PowerViewShadeTDBUTop(PowerViewShadeDualRailBase): """Representation of the top PowerViewShadeDualRailBase shade. - These shades have top/down bottom up functionality and two entiites. + These shades have top/down bottom up functionality and two entities. Sibling Class: PowerViewShadeTDBUBottom API Class: ShadeTopDownBottomUp """ + _attr_translation_key = "top" + def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, @@ -671,7 +669,6 @@ class PowerViewShadeTDBUTop(PowerViewShadeDualRailBase): """Initialize the shade.""" super().__init__(coordinator, device_info, room_name, shade, name) self._attr_unique_id = f"{self._shade.id}_top" - self._attr_name = f"{self._shade_name} Top" @property def should_poll(self) -> bool: @@ -711,7 +708,7 @@ class PowerViewShadeTDBUTop(PowerViewShadeDualRailBase): @callback def _clamp_cover_limit(self, target_hass_position: int) -> int: - """Dont allow a cover to go into an impossbile position.""" + """Don't allow a cover to go into an impossbile position.""" cover_bottom = hd_position_to_hass(self.positions.primary, MAX_POSITION) return min(target_hass_position, (100 - cover_bottom)) @@ -730,7 +727,7 @@ class PowerViewShadeTDBUTop(PowerViewShadeDualRailBase): ) -class PowerViewShadeDualOverlappedBase(PowerViewShade): +class PowerViewShadeDualOverlappedBase(PowerViewShadeBase): """Represent a shade that has a front sheer and rear opaque panel. This equates to two shades being controlled by one motor @@ -783,6 +780,8 @@ class PowerViewShadeDualOverlappedCombined(PowerViewShadeDualOverlappedBase): Type 8 - Duolite (front and rear shades) """ + _attr_translation_key = "combined" + # type def __init__( self, @@ -795,7 +794,6 @@ class PowerViewShadeDualOverlappedCombined(PowerViewShadeDualOverlappedBase): """Initialize the shade.""" super().__init__(coordinator, device_info, room_name, shade, name) self._attr_unique_id = f"{self._shade.id}_combined" - self._attr_name = f"{self._shade_name} Combined" @property def is_closed(self) -> bool: @@ -842,7 +840,7 @@ class PowerViewShadeDualOverlappedCombined(PowerViewShadeDualOverlappedBase): class PowerViewShadeDualOverlappedFront(PowerViewShadeDualOverlappedBase): - """Represent the shade front panel - These have a opaque panel too. + """Represent the shade front panel - These have an opaque panel too. This equates to two shades being controlled by one motor. The front shade must be completely down before the rear shade will move. @@ -857,6 +855,8 @@ class PowerViewShadeDualOverlappedFront(PowerViewShadeDualOverlappedBase): Type 10 - Duolite with 180° Tilt """ + _attr_translation_key = "front" + def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, @@ -868,7 +868,6 @@ class PowerViewShadeDualOverlappedFront(PowerViewShadeDualOverlappedBase): """Initialize the shade.""" super().__init__(coordinator, device_info, room_name, shade, name) self._attr_unique_id = f"{self._shade.id}_front" - self._attr_name = f"{self._shade_name} Front" @property def should_poll(self) -> bool: @@ -906,7 +905,7 @@ class PowerViewShadeDualOverlappedFront(PowerViewShadeDualOverlappedBase): class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase): - """Represent the shade front panel - These have a opaque panel too. + """Represent the shade front panel - These have an opaque panel too. This equates to two shades being controlled by one motor. The front shade must be completely down before the rear shade will move. @@ -921,6 +920,8 @@ class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase): Type 10 - Duolite with 180° Tilt """ + _attr_translation_key = "rear" + def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, @@ -932,7 +933,6 @@ class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase): """Initialize the shade.""" super().__init__(coordinator, device_info, room_name, shade, name) self._attr_unique_id = f"{self._shade.id}_rear" - self._attr_name = f"{self._shade_name} Rear" @property def should_poll(self) -> bool: diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 08f3c749fc5..78f63e16879 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -25,6 +25,8 @@ from .shade_data import PowerviewShadeData, PowerviewShadePositions class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]): """Base class for hunter douglas entities.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, diff --git a/homeassistant/components/hunterdouglas_powerview/select.py b/homeassistant/components/hunterdouglas_powerview/select.py index 7de7d3e8735..37d1193e0e5 100644 --- a/homeassistant/components/hunterdouglas_powerview/select.py +++ b/homeassistant/components/hunterdouglas_powerview/select.py @@ -47,7 +47,7 @@ class PowerviewSelectDescription( DROPDOWNS: Final = [ PowerviewSelectDescription( key="powersource", - name="Power Source", + translation_key="power_source", icon="mdi:power-plug-outline", current_fn=lambda shade: POWER_SUPPLY_TYPE_MAP.get( shade.raw_data.get(ATTR_BATTERY_KIND), None @@ -106,7 +106,6 @@ class PowerViewSelect(ShadeEntity, SelectEntity): """Initialize the select entity.""" super().__init__(coordinator, device_info, room_name, shade, name) self.entity_description: PowerviewSelectDescription = description - self._attr_name = f"{self._shade_name} {description.name}" self._attr_unique_id = f"{self._attr_unique_id}_{description.key}" @property diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index b36457324e1..825ca140f14 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -55,7 +55,6 @@ class PowerviewSensorDescription( SENSORS: Final = [ PowerviewSensorDescription( key="charge", - name="Battery", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, native_value_fn=lambda shade: round( @@ -69,7 +68,7 @@ SENSORS: Final = [ ), PowerviewSensorDescription( key="signal", - name="Signal", + translation_key="signal_strength", icon="mdi:signal", native_unit_of_measurement=PERCENTAGE, native_value_fn=lambda shade: round( @@ -129,7 +128,6 @@ class PowerViewSensor(ShadeEntity, SensorEntity): """Initialize the select entity.""" super().__init__(coordinator, device_info, room_name, shade, name) self.entity_description = description - self._attr_name = f"{self._shade_name} {description.name}" self._attr_unique_id = f"{self._attr_unique_id}_{description.key}" self._attr_native_unit_of_measurement = description.native_unit_of_measurement diff --git a/homeassistant/components/hunterdouglas_powerview/strings.json b/homeassistant/components/hunterdouglas_powerview/strings.json index ec26e423e06..7c17788be83 100644 --- a/homeassistant/components/hunterdouglas_powerview/strings.json +++ b/homeassistant/components/hunterdouglas_powerview/strings.json @@ -20,5 +20,42 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "button": { + "calibrate": { + "name": "Calibrate" + }, + "favorite": { + "name": "Favorite" + } + }, + "cover": { + "bottom": { + "name": "Bottom" + }, + "top": { + "name": "Top" + }, + "combined": { + "name": "Combined" + }, + "front": { + "name": "Front" + }, + "rear": { + "name": "Rear" + } + }, + "select": { + "power_source": { + "name": "Power source" + } + }, + "sensor": { + "signal_strength": { + "name": "[%key:component::sensor::entity_component::signal_strength::name%]" + } + } } } From e3438baf49b447074193bf33a5505dad209873f9 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 14 Aug 2023 20:23:16 -0400 Subject: [PATCH 0511/1151] Add select platform to Enphase integration (#98368) * Add select platform to Enphase integration * Review comments pt1 * Review comments pt2 * Review comments * Additional tweaks from code review * .coveragerc --------- Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../components/enphase_envoy/const.py | 2 +- .../components/enphase_envoy/manifest.json | 2 +- .../components/enphase_envoy/select.py | 171 ++++++++++++++++++ .../components/enphase_envoy/strings.json | 36 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/enphase_envoy/select.py diff --git a/.coveragerc b/.coveragerc index e64058d93d0..014dc2f0f39 100644 --- a/.coveragerc +++ b/.coveragerc @@ -305,6 +305,7 @@ omit = homeassistant/components/enphase_envoy/binary_sensor.py homeassistant/components/enphase_envoy/coordinator.py homeassistant/components/enphase_envoy/entity.py + homeassistant/components/enphase_envoy/select.py homeassistant/components/enphase_envoy/sensor.py homeassistant/components/enphase_envoy/switch.py homeassistant/components/entur_public_transport/* diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 828abe8fe4c..d1c6618502e 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, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SELECT, 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 6969dc3d6ab..62f7c73ef76 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.5.2"], + "requirements": ["pyenphase==1.6.0"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/select.py b/homeassistant/components/enphase_envoy/select.py new file mode 100644 index 00000000000..75c9ce0cf7c --- /dev/null +++ b/homeassistant/components/enphase_envoy/select.py @@ -0,0 +1,171 @@ +"""Select platform for Enphase Envoy solar energy monitor.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging +from typing import Any + +from pyenphase import EnvoyDryContactSettings +from pyenphase.models.dry_contacts import DryContactAction, DryContactMode + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +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 EnvoyRelayRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyDryContactSettings], str] + update_fn: Callable[[Any, Any, Any], Any] + + +@dataclass +class EnvoyRelaySelectEntityDescription( + SelectEntityDescription, EnvoyRelayRequiredKeysMixin +): + """Describes an Envoy Dry Contact Relay select entity.""" + + +RELAY_MODE_MAP = { + DryContactMode.MANUAL: "standard", + DryContactMode.STATE_OF_CHARGE: "battery", +} +REVERSE_RELAY_MODE_MAP = {v: k for k, v in RELAY_MODE_MAP.items()} +RELAY_ACTION_MAP = { + DryContactAction.APPLY: "powered", + DryContactAction.SHED: "not_powered", + DryContactAction.SCHEDULE: "schedule", + DryContactAction.NONE: "none", +} +REVERSE_RELAY_ACTION_MAP = {v: k for k, v in RELAY_ACTION_MAP.items()} +MODE_OPTIONS = list(REVERSE_RELAY_MODE_MAP) +ACTION_OPTIONS = list(REVERSE_RELAY_ACTION_MAP) + +RELAY_ENTITIES = ( + EnvoyRelaySelectEntityDescription( + key="mode", + translation_key="relay_mode", + options=MODE_OPTIONS, + value_fn=lambda relay: RELAY_MODE_MAP[relay.mode], + update_fn=lambda envoy, relay, value: envoy.update_dry_contact( + { + "id": relay.id, + "mode": REVERSE_RELAY_MODE_MAP[value], + } + ), + ), + EnvoyRelaySelectEntityDescription( + key="grid_action", + translation_key="relay_grid_action", + options=ACTION_OPTIONS, + value_fn=lambda relay: RELAY_ACTION_MAP[relay.grid_action], + update_fn=lambda envoy, relay, value: envoy.update_dry_contact( + { + "id": relay.id, + "grid_action": REVERSE_RELAY_ACTION_MAP[value], + } + ), + ), + EnvoyRelaySelectEntityDescription( + key="microgrid_action", + translation_key="relay_microgrid_action", + options=ACTION_OPTIONS, + value_fn=lambda relay: RELAY_ACTION_MAP[relay.micro_grid_action], + update_fn=lambda envoy, relay, value: envoy.update_dry_contact( + { + "id": relay.id, + "micro_grid_action": REVERSE_RELAY_ACTION_MAP[value], + } + ), + ), + EnvoyRelaySelectEntityDescription( + key="generator_action", + translation_key="relay_generator_action", + options=ACTION_OPTIONS, + value_fn=lambda relay: RELAY_ACTION_MAP[relay.generator_action], + update_fn=lambda envoy, relay, value: envoy.update_dry_contact( + { + "id": relay.id, + "generator_action": REVERSE_RELAY_ACTION_MAP[value], + } + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Enphase Envoy select 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[SelectEntity] = [] + if envoy_data.dry_contact_settings: + entities.extend( + EnvoyRelaySelectEntity(coordinator, entity, relay) + for entity in RELAY_ENTITIES + for relay in envoy_data.dry_contact_settings + ) + async_add_entities(entities) + + +class EnvoyRelaySelectEntity(EnvoyBaseEntity, SelectEntity): + """Representation of an Enphase Enpower select entity.""" + + entity_description: EnvoyRelaySelectEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyRelaySelectEntityDescription, + relay: str, + ) -> None: + """Initialize the Enphase relay select entity.""" + super().__init__(coordinator, description) + self.envoy = coordinator.envoy + assert self.envoy is not None + assert self.data is not None + self.enpower = self.data.enpower + assert self.enpower is not None + self._serial_number = self.enpower.serial_number + self.relay = self.data.dry_contact_settings[relay] + self.relay_id = relay + self._attr_unique_id = ( + f"{self._serial_number}_relay_{relay}_{self.entity_description.key}" + ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, relay)}, + manufacturer="Enphase", + model="Dry contact relay", + name=self.relay.load_name, + sw_version=str(self.enpower.firmware_version), + via_device=(DOMAIN, self._serial_number), + ) + + @property + def current_option(self) -> str: + """Return the state of the Enpower switch.""" + return self.entity_description.value_fn( + self.data.dry_contact_settings[self.relay_id] + ) + + async def async_select_option(self, option: str) -> None: + """Update the relay.""" + await self.entity_description.update_fn(self.envoy, self.relay, option) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 2afd19d87d1..bab16bc6c58 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -36,6 +36,42 @@ "name": "Grid status" } }, + "select": { + "relay_mode": { + "name": "Mode", + "state": { + "standard": "Standard", + "battery": "Battery level" + } + }, + "relay_grid_action": { + "name": "Grid action", + "state": { + "powered": "Powered", + "not_powered": "Not powered", + "schedule": "Follow schedule", + "none": "None" + } + }, + "relay_microgrid_action": { + "name": "Microgrid action", + "state": { + "powered": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::powered%]", + "not_powered": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::not_powered%]", + "schedule": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::schedule%]", + "none": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::none%]" + } + }, + "relay_generator_action": { + "name": "Generator action", + "state": { + "powered": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::powered%]", + "not_powered": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::not_powered%]", + "schedule": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::schedule%]", + "none": "[%key:component::enphase_envoy::entity::select::relay_grid_action::state::none%]" + } + } + }, "sensor": { "last_reported": { "name": "Last reported" diff --git a/requirements_all.txt b/requirements_all.txt index f4d8381f0c1..6f755d7c860 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.5.2 +pyenphase==1.6.0 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05f81b16e78..51c87f8176a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1226,7 +1226,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.5.2 +pyenphase==1.6.0 # homeassistant.components.everlights pyeverlights==0.1.0 From ced4af1e22f7cc5eb7b2a8dc385e97b5294a8079 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Aug 2023 22:39:05 -0400 Subject: [PATCH 0512/1151] Ignore smartthings storage on fresh install (#98418) * Ignore smartthings storage on fresh install * Also unload existing things when going for clean install * Rename param * Fix tests --- homeassistant/components/smartthings/__init__.py | 2 +- homeassistant/components/smartthings/config_flow.py | 7 ++++++- homeassistant/components/smartthings/smartapp.py | 11 ++++++++--- tests/components/smartthings/test_config_flow.py | 6 ++++-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 4e694556598..22856bdb05b 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -58,7 +58,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize the SmartThings platform.""" - await setup_smartapp_endpoint(hass) + await setup_smartapp_endpoint(hass, False) return True diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index 0328c3a7f8e..5e3451dfbce 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -50,6 +50,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.installed_app_id = None self.refresh_token = None self.location_id = None + self.endpoints_initialized = False async def async_step_import(self, user_input=None): """Occurs when a previously entry setup fails and is re-initiated.""" @@ -57,7 +58,11 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Validate and confirm webhook setup.""" - await setup_smartapp_endpoint(self.hass) + if not self.endpoints_initialized: + self.endpoints_initialized = True + await setup_smartapp_endpoint( + self.hass, len(self._async_current_entries()) == 0 + ) webhook_url = get_webhook_url(self.hass) # Abort if the webhook is invalid diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 9b17034ab3b..78c0bfa86b1 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -197,7 +197,7 @@ def setup_smartapp(hass, app): return smartapp -async def setup_smartapp_endpoint(hass: HomeAssistant): +async def setup_smartapp_endpoint(hass: HomeAssistant, fresh_install: bool): """Configure the SmartApp webhook in hass. SmartApps are an extension point within the SmartThings ecosystem and @@ -205,11 +205,16 @@ async def setup_smartapp_endpoint(hass: HomeAssistant): """ if hass.data.get(DOMAIN): # already setup - return + if not fresh_install: + return + + # We're doing a fresh install, clean up + await unload_smartapp_endpoint(hass) # Get/create config to store a unique id for this hass instance. store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) - if not (config := await store.async_load()): + + if fresh_install or not (config := await store.async_load()): # Create config config = { CONF_INSTANCE_ID: str(uuid4()), diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 5c44a5af2e9..168756b0dfe 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -187,6 +187,7 @@ async def test_entry_created_existing_app_new_oauth_client( smartthings_mock.apps.return_value = [app] smartthings_mock.generate_app_oauth.return_value = app_oauth_client smartthings_mock.locations.return_value = [location] + smartthings_mock.create_app = AsyncMock(return_value=(app, app_oauth_client)) request = Mock() request.installed_app_id = installed_app_id request.auth_token = token @@ -366,7 +367,7 @@ async def test_entry_created_with_cloudhook( "async_create_cloudhook", AsyncMock(return_value="http://cloud.test"), ) as mock_create_cloudhook: - await smartapp.setup_smartapp_endpoint(hass) + await smartapp.setup_smartapp_endpoint(hass, True) # Webhook confirmation shown result = await hass.config_entries.flow.async_init( @@ -377,7 +378,8 @@ async def test_entry_created_with_cloudhook( assert result["description_placeholders"][ "webhook_url" ] == smartapp.get_webhook_url(hass) - assert mock_create_cloudhook.call_count == 1 + # One is done by app fixture, one done by new config entry + assert mock_create_cloudhook.call_count == 2 # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) From 6c7f50b5b27b4a43fe93cdcc32b7ee0b1cb89e2a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Aug 2023 08:27:50 +0200 Subject: [PATCH 0513/1151] Simplify error handling in otbr async_setup_entry (#98395) * Simplify error handling in otbr async_setup_entry * Create issue if the OTBR does not support border agent ID * Update test * Improve test coverage * Catch the right exception --- homeassistant/components/otbr/__init__.py | 24 ++++++++------ homeassistant/components/otbr/strings.json | 4 +++ homeassistant/components/otbr/util.py | 9 +++-- tests/components/otbr/test_init.py | 38 ++++++++++++++++++++-- 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/otbr/__init__.py b/homeassistant/components/otbr/__init__.py index ac59bacbd97..0f4374d95bd 100644 --- a/homeassistant/components/otbr/__init__.py +++ b/homeassistant/components/otbr/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -import contextlib import aiohttp import python_otbr_api @@ -11,7 +10,7 @@ from homeassistant.components.thread import async_add_dataset from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType @@ -37,6 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: otbrdata = OTBRData(entry.data["url"], api, entry.entry_id) try: + border_agent_id = await otbrdata.get_border_agent_id() dataset_tlvs = await otbrdata.get_active_dataset_tlvs() except ( HomeAssistantError, @@ -44,20 +44,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: asyncio.TimeoutError, ) as err: raise ConfigEntryNotReady("Unable to connect") from err + if border_agent_id is None: + ir.async_create_issue( + hass, + DOMAIN, + f"get_get_border_agent_id_unsupported_{otbrdata.entry_id}", + is_fixable=False, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key="get_get_border_agent_id_unsupported", + ) + return False if dataset_tlvs: - border_agent_id: str | None = None - with contextlib.suppress( - HomeAssistantError, aiohttp.ClientError, asyncio.TimeoutError - ): - border_agent_bytes = await otbrdata.get_border_agent_id() - if border_agent_bytes: - border_agent_id = border_agent_bytes.hex() await update_issues(hass, otbrdata, dataset_tlvs) await async_add_dataset( hass, DOMAIN, dataset_tlvs.hex(), - preferred_border_agent_id=border_agent_id, + preferred_border_agent_id=border_agent_id.hex(), ) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/otbr/strings.json b/homeassistant/components/otbr/strings.json index 129cbec4468..838ebeb5b8c 100644 --- a/homeassistant/components/otbr/strings.json +++ b/homeassistant/components/otbr/strings.json @@ -16,6 +16,10 @@ } }, "issues": { + "get_get_border_agent_id_unsupported": { + "title": "The OTBR does not support border agent ID", + "description": "Your OTBR does not support border agent ID.\n\nTo fix this issue, update the OTBR to the latest version and restart Home Assistant.\nTo update the OTBR, update the Open Thread Border Router or Silicon Labs Multiprotocol add-on if you use the OTBR from the add-on, otherwise update your self managed OTBR." + }, "insecure_thread_network": { "title": "Insecure Thread network settings detected", "description": "Your Thread network is using a default network key or pass phrase.\n\nThis is a security risk, please create a new Thread network." diff --git a/homeassistant/components/otbr/util.py b/homeassistant/components/otbr/util.py index 67f36c09246..4cbf7ce6a08 100644 --- a/homeassistant/components/otbr/util.py +++ b/homeassistant/components/otbr/util.py @@ -83,9 +83,12 @@ class OTBRData: await self.delete_active_dataset() @_handle_otbr_error - async def get_border_agent_id(self) -> bytes: - """Get the border agent ID.""" - return await self.api.get_border_agent_id() + async def get_border_agent_id(self) -> bytes | None: + """Get the border agent ID or None if not supported by the router.""" + try: + return await self.api.get_border_agent_id() + except python_otbr_api.GetBorderAgentIdNotSupportedError: + return None @_handle_otbr_error async def set_enabled(self, enabled: bool) -> None: diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index 18a60cfa196..1b5c1e8b60a 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -89,12 +89,16 @@ async def test_import_share_radio_channel_collision( config_entry.add_to_hass(hass) with patch( "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), patch( + "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID ), patch( "homeassistant.components.thread.dataset_store.DatasetStore.async_add" ) as mock_add: assert await hass.config_entries.async_setup(config_entry.entry_id) - mock_add.assert_called_once_with(otbr.DOMAIN, DATASET_CH16.hex(), None) + mock_add.assert_called_once_with( + otbr.DOMAIN, DATASET_CH16.hex(), TEST_BORDER_AGENT_ID.hex() + ) assert issue_registry.async_get_issue( domain=otbr.DOMAIN, issue_id=f"otbr_zha_channel_collision_{config_entry.entry_id}", @@ -122,12 +126,16 @@ async def test_import_share_radio_no_channel_collision( config_entry.add_to_hass(hass) with patch( "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=dataset + ), patch( + "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID ), patch( "homeassistant.components.thread.dataset_store.DatasetStore.async_add" ) as mock_add: assert await hass.config_entries.async_setup(config_entry.entry_id) - mock_add.assert_called_once_with(otbr.DOMAIN, dataset.hex(), None) + mock_add.assert_called_once_with( + otbr.DOMAIN, dataset.hex(), TEST_BORDER_AGENT_ID.hex() + ) assert not issue_registry.async_get_issue( domain=otbr.DOMAIN, issue_id=f"otbr_zha_channel_collision_{config_entry.entry_id}", @@ -153,12 +161,16 @@ async def test_import_insecure_dataset(hass: HomeAssistant, dataset: bytes) -> N config_entry.add_to_hass(hass) with patch( "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=dataset + ), patch( + "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID ), patch( "homeassistant.components.thread.dataset_store.DatasetStore.async_add" ) as mock_add: assert await hass.config_entries.async_setup(config_entry.entry_id) - mock_add.assert_called_once_with(otbr.DOMAIN, dataset.hex(), None) + mock_add.assert_called_once_with( + otbr.DOMAIN, dataset.hex(), TEST_BORDER_AGENT_ID.hex() + ) assert issue_registry.async_get_issue( domain=otbr.DOMAIN, issue_id=f"insecure_thread_network_{config_entry.entry_id}" ) @@ -186,6 +198,25 @@ async def test_config_entry_not_ready(hass: HomeAssistant, error) -> None: assert not await hass.config_entries.async_setup(config_entry.entry_id) +async def test_border_agent_id_not_supported(hass: HomeAssistant) -> None: + """Test border router does not support border agent ID.""" + + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA_MULTIPAN, + domain=otbr.DOMAIN, + options={}, + title="My OTBR", + ) + config_entry.add_to_hass(hass) + with patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), patch( + "python_otbr_api.OTBR.get_border_agent_id", + side_effect=python_otbr_api.GetBorderAgentIdNotSupportedError, + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + + async def test_config_entry_update(hass: HomeAssistant) -> None: """Test update config entry settings.""" config_entry = MockConfigEntry( @@ -197,6 +228,7 @@ async def test_config_entry_update(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) mock_api = MagicMock() mock_api.get_active_dataset_tlvs = AsyncMock(return_value=None) + mock_api.get_border_agent_id = AsyncMock(return_value=TEST_BORDER_AGENT_ID) with patch("python_otbr_api.OTBR", return_value=mock_api) as mock_otrb_api: assert await hass.config_entries.async_setup(config_entry.entry_id) From 71b92265af68f74f4128013321bd70c80fa8c754 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Aug 2023 09:17:47 +0200 Subject: [PATCH 0514/1151] Include border agent id in response to WS otbr/info (#98394) * Include border agent id in response to WS otbr/info * Assert border agent ID is not None --- homeassistant/components/otbr/websocket_api.py | 5 +++++ tests/components/otbr/test_websocket_api.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/otbr/websocket_api.py b/homeassistant/components/otbr/websocket_api.py index 9b57cd8ebd1..449f0ccb44d 100644 --- a/homeassistant/components/otbr/websocket_api.py +++ b/homeassistant/components/otbr/websocket_api.py @@ -47,17 +47,22 @@ async def websocket_info( data: OTBRData = hass.data[DOMAIN] try: + border_agent_id = await data.get_border_agent_id() dataset = await data.get_active_dataset() dataset_tlvs = await data.get_active_dataset_tlvs() except HomeAssistantError as exc: connection.send_error(msg["id"], "get_dataset_failed", str(exc)) return + # The border agent ID is checked when the OTBR config entry is setup, + # we can assert it's not None + assert border_agent_id is not None connection.send_result( msg["id"], { "url": data.url, "active_dataset_tlvs": dataset_tlvs.hex() if dataset_tlvs else None, + "border_agent_id": border_agent_id.hex(), "channel": dataset.channel if dataset else None, }, ) diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index f149e89cc45..7877045c8a4 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -8,7 +8,7 @@ from homeassistant.components import otbr, thread from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from . import BASE_URL, DATASET_CH15, DATASET_CH16 +from . import BASE_URL, DATASET_CH15, DATASET_CH16, TEST_BORDER_AGENT_ID from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import WebSocketGenerator @@ -31,7 +31,11 @@ async def test_get_info( with patch( "python_otbr_api.OTBR.get_active_dataset", return_value=python_otbr_api.ActiveDataSet(channel=16), - ), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16): + ), patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), patch( + "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID + ): await websocket_client.send_json_auto_id({"type": "otbr/info"}) msg = await websocket_client.receive_json() @@ -40,6 +44,7 @@ async def test_get_info( "url": BASE_URL, "active_dataset_tlvs": DATASET_CH16.hex().lower(), "channel": 16, + "border_agent_id": TEST_BORDER_AGENT_ID.hex(), } @@ -68,6 +73,8 @@ async def test_get_info_fetch_fails( with patch( "python_otbr_api.OTBR.get_active_dataset", side_effect=python_otbr_api.OTBRError, + ), patch( + "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID ): await websocket_client.send_json_auto_id({"type": "otbr/info"}) msg = await websocket_client.receive_json() From e6ea70fd00d1ebddb26b92a6c2d62f55bc94a4f7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Aug 2023 09:40:05 +0200 Subject: [PATCH 0515/1151] Adjust thread router discovery typing (#98439) * Adjust thread router discovery typing * Adjust debug logs --- homeassistant/components/thread/discovery.py | 50 ++++++++++------ tests/components/thread/__init__.py | 31 +++++++++- tests/components/thread/test_diagnostics.py | 60 ++++++++++++++++++++ tests/components/thread/test_discovery.py | 24 +++++--- 4 files changed, 139 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/thread/discovery.py b/homeassistant/components/thread/discovery.py index ce721a20e28..3395353b7bf 100644 --- a/homeassistant/components/thread/discovery.py +++ b/homeassistant/components/thread/discovery.py @@ -31,11 +31,11 @@ TYPE_PTR = 12 class ThreadRouterDiscoveryData: """Thread router discovery data.""" - addresses: list[str] | None + addresses: list[str] border_agent_id: str | None brand: str | None - extended_address: str | None - extended_pan_id: str | None + extended_address: str + extended_pan_id: str model_name: str | None network_name: str | None server: str | None @@ -46,6 +46,8 @@ class ThreadRouterDiscoveryData: def async_discovery_data_from_service( service: AsyncServiceInfo, + ext_addr: bytes, + ext_pan_id: bytes, ) -> ThreadRouterDiscoveryData: """Get a ThreadRouterDiscoveryData from an AsyncServiceInfo.""" @@ -64,8 +66,6 @@ def async_discovery_data_from_service( service_properties = cast(dict[bytes, bytes | None], service.properties) border_agent_id = service_properties.get(b"id") - ext_addr = service_properties.get(b"xa") - ext_pan_id = service_properties.get(b"xp") model_name = try_decode(service_properties.get(b"mn")) network_name = try_decode(service_properties.get(b"nn")) server = service.server @@ -90,8 +90,8 @@ def async_discovery_data_from_service( addresses=service.parsed_addresses(), border_agent_id=border_agent_id.hex() if border_agent_id is not None else None, brand=brand, - extended_address=ext_addr.hex() if ext_addr is not None else None, - extended_pan_id=ext_pan_id.hex() if ext_pan_id is not None else None, + extended_address=ext_addr.hex(), + extended_pan_id=ext_pan_id.hex(), model_name=model_name, network_name=network_name, server=server, @@ -121,7 +121,19 @@ def async_read_zeroconf_cache(aiozc: AsyncZeroconf) -> list[ThreadRouterDiscover # data is not fully in the cache, so ignore for now continue - results.append(async_discovery_data_from_service(info)) + # Service properties are always bytes if they are set from the network. + # For legacy backwards compatibility zeroconf allows properties to be set + # as strings but we never do that so we can safely cast here. + service_properties = cast(dict[bytes, bytes | None], info.properties) + + if not (xa := service_properties.get(b"xa")): + _LOGGER.debug("Ignoring record without xa %s", info) + continue + if not (xp := service_properties.get(b"xp")): + _LOGGER.debug("Ignoring record without xp %s", info) + continue + + results.append(async_discovery_data_from_service(info, xa, xp)) return results @@ -182,18 +194,22 @@ class ThreadRouterDiscovery: # as strings but we never do that so we can safely cast here. service_properties = cast(dict[bytes, bytes | None], service.properties) + # We need xa and xp, bail out if either is missing if not (xa := service_properties.get(b"xa")): - _LOGGER.debug("_add_update_service failed to find xa in %s", service) + _LOGGER.info( + "Discovered unsupported Thread router without extended address: %s", + service, + ) + return + if not (xp := service_properties.get(b"xp")): + _LOGGER.info( + "Discovered unsupported Thread router without extended pan ID: %s", + service, + ) return - # We use the extended mac address as key, bail out if it's missing - try: - extended_mac_address = xa.hex() - except UnicodeDecodeError as err: - _LOGGER.debug("_add_update_service failed to parse service %s", err) - return - - data = async_discovery_data_from_service(service) + data = async_discovery_data_from_service(service, xa, xp) + extended_mac_address = xa.hex() if name in self._known_routers and self._known_routers[name] == ( extended_mac_address, data, diff --git a/tests/components/thread/__init__.py b/tests/components/thread/__init__.py index f9d527919b4..7ca6cbaf2ed 100644 --- a/tests/components/thread/__init__.py +++ b/tests/components/thread/__init__.py @@ -150,6 +150,7 @@ ROUTER_DISCOVERY_HASS_MISSING_DATA = { "properties": { b"rv": b"1", b"id": b"#\x0cj\x1a\xc5\x7foK\xe2b\xac\xf3.^\xf5,", + # vn is missing b"mn": b"OpenThreadBorderRouter", b"nn": b"OpenThread HC", b"xp": b"\xe6\x0f\xc7\xc1\x86!,\xe5", @@ -167,7 +168,7 @@ ROUTER_DISCOVERY_HASS_MISSING_DATA = { } -ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA = { +ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA_XA = { "type_": "_meshcop._udp.local.", "name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.", "addresses": [b"\xc0\xa8\x00s"], @@ -195,6 +196,34 @@ ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA = { } +ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA_XP = { + "type_": "_meshcop._udp.local.", + "name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.", + "addresses": [b"\xc0\xa8\x00s"], + "port": 49153, + "weight": 0, + "priority": 0, + "server": "core-silabs-multiprotocol.local.", + "properties": { + b"rv": b"1", + b"id": b"#\x0cj\x1a\xc5\x7foK\xe2b\xac\xf3.^\xf5,", + b"vn": b"HomeAssistant", + b"mn": b"OpenThreadBorderRouter", + b"nn": b"OpenThread HC", + b"tv": b"1.3.0", + b"xa": b"\xae\xeb/YKW\x0b\xbf", + b"sb": b"\x00\x00\x01\xb1", + b"at": b"\x00\x00\x00\x00\x00\x01\x00\x00", + b"pt": b"\x8f\x06Q~", + b"sq": b"3", + b"bb": b"\xf0\xbf", + b"dn": b"DefaultDomain", + b"omr": b"@\xfd \xbe\x89IZ\x00\x01", + }, + "interface_index": None, +} + + ROUTER_DISCOVERY_HASS_NO_ACTIVE_TIMESTAMP = { "type_": "_meshcop._udp.local.", "name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.", diff --git a/tests/components/thread/test_diagnostics.py b/tests/components/thread/test_diagnostics.py index a551315205b..94ca4373715 100644 --- a/tests/components/thread/test_diagnostics.py +++ b/tests/components/thread/test_diagnostics.py @@ -106,6 +106,48 @@ TEST_ZEROCONF_RECORD_4 = ServiceInfo( # Make sure this generates an invalid DNSPointer TEST_ZEROCONF_RECORD_4.name = "office._meshcop._udp.lo\x00cal." +# This has no XA +TEST_ZEROCONF_RECORD_5 = ServiceInfo( + type_="_meshcop._udp.local.", + name="bad_1._meshcop._udp.local.", + addresses=["127.0.0.1", "fe80::10ed:6406:4ee9:85e0"], + port=8080, + properties={ + "rv": "1", + "vn": "Apple", + "nn": "OpenThread HC", + "xp": "\xe6\x0f\xc7\xc1\x86!,\xe5", + "tv": "1.2.0", + "sb": "\x00\x00\x01\xb1", + "at": "\x00\x00\x00\x00\x00\x01\x00\x00", + "pt": "\x8f\x06Q~", + "sq": "3", + "bb": "\xf0\xbf", + "dn": "DefaultDomain", + }, +) + +# This has no XP +TEST_ZEROCONF_RECORD_6 = ServiceInfo( + type_="_meshcop._udp.local.", + name="bad_2._meshcop._udp.local.", + addresses=["127.0.0.1", "fe80::10ed:6406:4ee9:85e0"], + port=8080, + properties={ + "rv": "1", + "vn": "Apple", + "nn": "OpenThread HC", + "tv": "1.2.0", + "xa": "\xae\xeb/YKW\x0b\xbf", + "sb": "\x00\x00\x01\xb1", + "at": "\x00\x00\x00\x00\x00\x01\x00\x00", + "pt": "\x8f\x06Q~", + "sq": "3", + "bb": "\xf0\xbf", + "dn": "DefaultDomain", + }, +) + @dataclasses.dataclass class MockRoute: @@ -177,6 +219,24 @@ async def test_diagnostics( TEST_ZEROCONF_RECORD_4.dns_pointer(created=now), ] ) + # Test for record without xa + cache.async_add_records( + [ + *TEST_ZEROCONF_RECORD_5.dns_addresses(created=now), + TEST_ZEROCONF_RECORD_5.dns_service(created=now), + TEST_ZEROCONF_RECORD_5.dns_text(created=now), + TEST_ZEROCONF_RECORD_5.dns_pointer(created=now), + ] + ) + # Test for record without xp + cache.async_add_records( + [ + *TEST_ZEROCONF_RECORD_6.dns_addresses(created=now), + TEST_ZEROCONF_RECORD_6.dns_service(created=now), + TEST_ZEROCONF_RECORD_6.dns_text(created=now), + TEST_ZEROCONF_RECORD_6.dns_pointer(created=now), + ] + ) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/thread/test_discovery.py b/tests/components/thread/test_discovery.py index 4d43142b7b7..12eddb0b92a 100644 --- a/tests/components/thread/test_discovery.py +++ b/tests/components/thread/test_discovery.py @@ -16,7 +16,8 @@ from . import ( ROUTER_DISCOVERY_HASS_BAD_DATA, ROUTER_DISCOVERY_HASS_BAD_STATE_BITMAP, ROUTER_DISCOVERY_HASS_MISSING_DATA, - ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA, + ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA_XA, + ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA_XP, ROUTER_DISCOVERY_HASS_NO_ACTIVE_TIMESTAMP, ROUTER_DISCOVERY_HASS_NO_STATE_BITMAP, ROUTER_DISCOVERY_HASS_STATE_BITMAP_NOT_ACTIVE, @@ -152,7 +153,7 @@ async def test_discover_routers(hass: HomeAssistant, mock_async_zeroconf: None) async def test_discover_routers_unconfigured( hass: HomeAssistant, mock_async_zeroconf: None, data, unconfigured ) -> None: - """Test discovering thread routers with bad or missing vendor mDNS data.""" + """Test discovering thread routers and setting the unconfigured flag.""" mock_async_zeroconf.async_add_service_listener = AsyncMock() mock_async_zeroconf.async_remove_service_listener = AsyncMock() mock_async_zeroconf.async_get_service_info = AsyncMock() @@ -195,7 +196,7 @@ async def test_discover_routers_unconfigured( @pytest.mark.parametrize( "data", (ROUTER_DISCOVERY_HASS_BAD_DATA, ROUTER_DISCOVERY_HASS_MISSING_DATA) ) -async def test_discover_routers_bad_data( +async def test_discover_routers_bad_or_missing_optional_data( hass: HomeAssistant, mock_async_zeroconf: None, data ) -> None: """Test discovering thread routers with bad or missing vendor mDNS data.""" @@ -238,8 +239,15 @@ async def test_discover_routers_bad_data( ) -async def test_discover_routers_missing_mandatory_data( - hass: HomeAssistant, mock_async_zeroconf: None +@pytest.mark.parametrize( + "service", + [ + ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA_XA, + ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA_XP, + ], +) +async def test_discover_routers_bad_or_missing_mandatory_data( + hass: HomeAssistant, mock_async_zeroconf: None, service ) -> None: """Test discovering thread routers with missing mandatory mDNS data.""" mock_async_zeroconf.async_add_service_listener = AsyncMock() @@ -261,12 +269,12 @@ async def test_discover_routers_missing_mandatory_data( # Discover a service with missing mandatory data mock_async_zeroconf.async_get_service_info.return_value = AsyncServiceInfo( - **ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA + **service ) listener.add_service( None, - ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA["type_"], - ROUTER_DISCOVERY_HASS_MISSING_MANDATORY_DATA["name"], + service["type_"], + service["name"], ) await hass.async_block_till_done() router_discovered_removed.assert_not_called() From 94ad4786c3bf11ec194c6c43cfd634577bd44d4a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Aug 2023 09:48:29 +0200 Subject: [PATCH 0516/1151] Include extended address in response to WS otbr/info (#98440) --- .../components/otbr/websocket_api.py | 33 ++--------- tests/components/otbr/test_websocket_api.py | 58 ++----------------- 2 files changed, 9 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/otbr/websocket_api.py b/homeassistant/components/otbr/websocket_api.py index 449f0ccb44d..0693bc3a325 100644 --- a/homeassistant/components/otbr/websocket_api.py +++ b/homeassistant/components/otbr/websocket_api.py @@ -24,7 +24,6 @@ def async_setup(hass: HomeAssistant) -> None: """Set up the OTBR Websocket API.""" websocket_api.async_register_command(hass, websocket_info) websocket_api.async_register_command(hass, websocket_create_network) - websocket_api.async_register_command(hass, websocket_get_extended_address) websocket_api.async_register_command(hass, websocket_set_channel) websocket_api.async_register_command(hass, websocket_set_network) @@ -50,8 +49,9 @@ async def websocket_info( border_agent_id = await data.get_border_agent_id() dataset = await data.get_active_dataset() dataset_tlvs = await data.get_active_dataset_tlvs() + extended_address = await data.get_extended_address() except HomeAssistantError as exc: - connection.send_error(msg["id"], "get_dataset_failed", str(exc)) + connection.send_error(msg["id"], "otbr_info_failed", str(exc)) return # The border agent ID is checked when the OTBR config entry is setup, @@ -60,10 +60,11 @@ async def websocket_info( connection.send_result( msg["id"], { - "url": data.url, "active_dataset_tlvs": dataset_tlvs.hex() if dataset_tlvs else None, "border_agent_id": border_agent_id.hex(), "channel": dataset.channel if dataset else None, + "extended_address": extended_address.hex(), + "url": data.url, }, ) @@ -192,32 +193,6 @@ async def websocket_set_network( connection.send_result(msg["id"]) -@websocket_api.websocket_command( - { - "type": "otbr/get_extended_address", - } -) -@websocket_api.require_admin -@websocket_api.async_response -async def websocket_get_extended_address( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict -) -> None: - """Get extended address (EUI-64).""" - if DOMAIN not in hass.data: - connection.send_error(msg["id"], "not_loaded", "No OTBR API loaded") - return - - data: OTBRData = hass.data[DOMAIN] - - try: - extended_address = await data.get_extended_address() - except HomeAssistantError as exc: - connection.send_error(msg["id"], "get_extended_address_failed", str(exc)) - return - - connection.send_result(msg["id"], {"extended_address": extended_address.hex()}) - - @websocket_api.websocket_command( { "type": "otbr/set_channel", diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index 7877045c8a4..cba046a2a9d 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -35,6 +35,9 @@ async def test_get_info( "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 ), patch( "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID + ), patch( + "python_otbr_api.OTBR.get_extended_address", + return_value=bytes.fromhex("4EF6C4F3FF750626"), ): await websocket_client.send_json_auto_id({"type": "otbr/info"}) msg = await websocket_client.receive_json() @@ -45,6 +48,7 @@ async def test_get_info( "active_dataset_tlvs": DATASET_CH16.hex().lower(), "channel": 16, "border_agent_id": TEST_BORDER_AGENT_ID.hex(), + "extended_address": "4EF6C4F3FF750626".lower(), } @@ -80,7 +84,7 @@ async def test_get_info_fetch_fails( msg = await websocket_client.receive_json() assert not msg["success"] - assert msg["error"]["code"] == "get_dataset_failed" + assert msg["error"]["code"] == "otbr_info_failed" async def test_create_network( @@ -442,58 +446,6 @@ async def test_set_network_fails_3( assert msg["error"]["code"] == "set_enabled_failed" -async def test_get_extended_address( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - otbr_config_entry_multipan, - websocket_client, -) -> None: - """Test get extended address.""" - - with patch( - "python_otbr_api.OTBR.get_extended_address", - return_value=bytes.fromhex("4EF6C4F3FF750626"), - ): - await websocket_client.send_json_auto_id({"type": "otbr/get_extended_address"}) - msg = await websocket_client.receive_json() - - assert msg["success"] - assert msg["result"] == {"extended_address": "4EF6C4F3FF750626".lower()} - - -async def test_get_extended_address_no_entry( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - hass_ws_client: WebSocketGenerator, -) -> None: - """Test get extended address.""" - await async_setup_component(hass, "otbr", {}) - websocket_client = await hass_ws_client(hass) - await websocket_client.send_json_auto_id({"type": "otbr/get_extended_address"}) - - msg = await websocket_client.receive_json() - assert not msg["success"] - assert msg["error"]["code"] == "not_loaded" - - -async def test_get_extended_address_fetch_fails( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - otbr_config_entry_multipan, - websocket_client, -) -> None: - """Test get extended address.""" - with patch( - "python_otbr_api.OTBR.get_extended_address", - side_effect=python_otbr_api.OTBRError, - ): - await websocket_client.send_json_auto_id({"type": "otbr/get_extended_address"}) - msg = await websocket_client.receive_json() - - assert not msg["success"] - assert msg["error"]["code"] == "get_extended_address_failed" - - async def test_set_channel( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, From 2da3b7177d57fa5d1c46c3bc2e9edfe14d95ea23 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 15 Aug 2023 02:57:10 -0500 Subject: [PATCH 0517/1151] Update pyipp to 0.14.3 (#98434) --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 7cdf6767362..e8bd4425ef3 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_polling", "loggers": ["deepmerge", "pyipp"], "quality_scale": "platinum", - "requirements": ["pyipp==0.14.2"], + "requirements": ["pyipp==0.14.3"], "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 6f755d7c860..4fd926a4855 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1746,7 +1746,7 @@ pyintesishome==1.8.0 pyipma==3.0.6 # homeassistant.components.ipp -pyipp==0.14.2 +pyipp==0.14.3 # homeassistant.components.iqvia pyiqvia==2022.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51c87f8176a..bc2d9ef5e63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1289,7 +1289,7 @@ pyinsteon==1.4.3 pyipma==3.0.6 # homeassistant.components.ipp -pyipp==0.14.2 +pyipp==0.14.3 # homeassistant.components.iqvia pyiqvia==2022.04.0 From eb4745012a30240e868e35dbff2747bc248953bd Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 15 Aug 2023 03:02:38 -0500 Subject: [PATCH 0518/1151] Update rokuecp to 0.18.1 (#98432) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index f9b81dc8ddd..6fe70a3ab65 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -11,7 +11,7 @@ "iot_class": "local_polling", "loggers": ["rokuecp"], "quality_scale": "silver", - "requirements": ["rokuecp==0.18.0"], + "requirements": ["rokuecp==0.18.1"], "ssdp": [ { "st": "roku:ecp", diff --git a/requirements_all.txt b/requirements_all.txt index 4fd926a4855..da09cbde9be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2305,7 +2305,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.18.0 +rokuecp==0.18.1 # homeassistant.components.roomba roombapy==1.6.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc2d9ef5e63..10fd8af6ce6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1686,7 +1686,7 @@ rflink==0.0.65 ring-doorbell==0.7.2 # homeassistant.components.roku -rokuecp==0.18.0 +rokuecp==0.18.1 # homeassistant.components.roomba roombapy==1.6.8 From 262483f3f6d5fc843af42905ee8176876f658e27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Aug 2023 03:29:28 -0500 Subject: [PATCH 0519/1151] Replace async_timeout with asyncio.timeout A-B (#98415) --- homeassistant/components/accuweather/__init__.py | 2 +- homeassistant/components/accuweather/config_flow.py | 2 +- homeassistant/components/acmeda/config_flow.py | 4 ++-- homeassistant/components/ads/__init__.py | 4 ++-- .../components/aemet/weather_update_coordinator.py | 4 ++-- homeassistant/components/airly/__init__.py | 4 ++-- homeassistant/components/airly/config_flow.py | 4 ++-- homeassistant/components/airzone/coordinator.py | 4 ++-- homeassistant/components/airzone_cloud/coordinator.py | 4 ++-- homeassistant/components/alexa/auth.py | 4 ++-- homeassistant/components/alexa/state_report.py | 6 +++--- homeassistant/components/analytics/analytics.py | 4 ++-- homeassistant/components/androidtv_remote/__init__.py | 4 ++-- homeassistant/components/anova/coordinator.py | 4 ++-- homeassistant/components/api/__init__.py | 4 ++-- homeassistant/components/arcam_fmj/__init__.py | 4 ++-- homeassistant/components/assist_pipeline/websocket_api.py | 3 +-- homeassistant/components/atag/__init__.py | 4 ++-- homeassistant/components/awair/__init__.py | 3 +-- homeassistant/components/axis/device.py | 4 ++-- homeassistant/components/baf/__init__.py | 4 ++-- homeassistant/components/baf/config_flow.py | 4 ++-- homeassistant/components/bluesound/media_player.py | 7 +++---- homeassistant/components/bluetooth/api.py | 4 ++-- homeassistant/components/brother/__init__.py | 4 ++-- homeassistant/components/brunt/__init__.py | 4 ++-- homeassistant/components/buienradar/util.py | 4 ++-- 27 files changed, 52 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index cdc23fe7e47..e98b19e8e82 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -1,6 +1,7 @@ """The AccuWeather component.""" from __future__ import annotations +from asyncio import timeout from datetime import timedelta import logging from typing import Any @@ -8,7 +9,6 @@ from typing import Any from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from aiohttp import ClientSession from aiohttp.client_exceptions import ClientConnectorError -from async_timeout import timeout from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/accuweather/config_flow.py b/homeassistant/components/accuweather/config_flow.py index 1480f6c1352..b1d113dad73 100644 --- a/homeassistant/components/accuweather/config_flow.py +++ b/homeassistant/components/accuweather/config_flow.py @@ -2,12 +2,12 @@ from __future__ import annotations import asyncio +from asyncio import timeout from typing import Any from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectorError -from async_timeout import timeout import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py index f1bd0613f1e..b0dd287f428 100644 --- a/homeassistant/components/acmeda/config_flow.py +++ b/homeassistant/components/acmeda/config_flow.py @@ -2,11 +2,11 @@ from __future__ import annotations import asyncio +from asyncio import timeout from contextlib import suppress from typing import Any import aiopulse -import async_timeout import voluptuous as vol from homeassistant import config_entries @@ -43,7 +43,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): hubs: list[aiopulse.Hub] = [] with suppress(asyncio.TimeoutError): - async with async_timeout.timeout(5): + async with timeout(5): async for hub in aiopulse.Hub.discover(): if hub.id not in already_configured: hubs.append(hub) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 5d1e9f2b656..1f80553031b 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -1,12 +1,12 @@ """Support for Automation Device Specification (ADS).""" import asyncio +from asyncio import timeout from collections import namedtuple import ctypes import logging import struct import threading -import async_timeout import pyads import voluptuous as vol @@ -301,7 +301,7 @@ class AdsEntity(Entity): self._ads_hub.add_device_notification, ads_var, plctype, update ) try: - async with async_timeout.timeout(10): + async with timeout(10): await self._event.wait() except asyncio.TimeoutError: _LOGGER.debug("Variable %s: Timeout during first update", ads_var) diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 5242540748f..5e9ce6af677 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -1,6 +1,7 @@ """Weather data coordinator for the AEMET OpenData service.""" from __future__ import annotations +from asyncio import timeout from dataclasses import dataclass, field from datetime import timedelta import logging @@ -41,7 +42,6 @@ from aemet_opendata.helpers import ( get_forecast_hour_value, get_forecast_interval_value, ) -import async_timeout from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -139,7 +139,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): data = {} - async with async_timeout.timeout(120): + async with timeout(120): weather_response = await self._get_aemet_weather() data = self._convert_weather_response(weather_response) return data diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index f52bdca4b86..982687c7723 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -1,6 +1,7 @@ """The Airly integration.""" from __future__ import annotations +from asyncio import timeout from datetime import timedelta import logging from math import ceil @@ -9,7 +10,6 @@ from aiohttp import ClientSession from aiohttp.client_exceptions import ClientConnectorError from airly import Airly from airly.exceptions import AirlyError -import async_timeout from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.config_entries import ConfigEntry @@ -167,7 +167,7 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator): measurements = self.airly.create_measurements_session_point( self.latitude, self.longitude ) - async with async_timeout.timeout(20): + async with timeout(20): try: await measurements.update() except (AirlyError, ClientConnectorError) as error: diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 5d41116eaa1..27c7b0f91e3 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -1,13 +1,13 @@ """Adds config flow for Airly.""" from __future__ import annotations +from asyncio import timeout from http import HTTPStatus from typing import Any from aiohttp import ClientSession from airly import Airly from airly.exceptions import AirlyError -import async_timeout import voluptuous as vol from homeassistant import config_entries @@ -105,7 +105,7 @@ async def test_location( measurements = airly.create_measurements_session_point( latitude=latitude, longitude=longitude ) - async with async_timeout.timeout(10): + async with timeout(10): await measurements.update() current = measurements.current diff --git a/homeassistant/components/airzone/coordinator.py b/homeassistant/components/airzone/coordinator.py index ba0296557a1..6053c587550 100644 --- a/homeassistant/components/airzone/coordinator.py +++ b/homeassistant/components/airzone/coordinator.py @@ -1,13 +1,13 @@ """The Airzone integration.""" from __future__ import annotations +from asyncio import timeout from datetime import timedelta import logging from typing import Any from aioairzone.exceptions import AirzoneError from aioairzone.localapi import AirzoneLocalApi -import async_timeout from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -35,7 +35,7 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" - async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC): + async with timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC): try: await self.airzone.update() except AirzoneError as error: diff --git a/homeassistant/components/airzone_cloud/coordinator.py b/homeassistant/components/airzone_cloud/coordinator.py index edd99355092..37b31c68ee7 100644 --- a/homeassistant/components/airzone_cloud/coordinator.py +++ b/homeassistant/components/airzone_cloud/coordinator.py @@ -1,13 +1,13 @@ """The Airzone Cloud integration coordinator.""" from __future__ import annotations +from asyncio import timeout from datetime import timedelta import logging from typing import Any from aioairzone_cloud.cloudapi import AirzoneCloudApi from aioairzone_cloud.exceptions import AirzoneCloudError -import async_timeout from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -35,7 +35,7 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" - async with async_timeout.timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC): + async with timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC): try: await self.airzone.update() except AirzoneCloudError as error: diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 61a87d9ebab..58095340146 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -1,5 +1,6 @@ """Support for Alexa skill auth.""" import asyncio +from asyncio import timeout from datetime import datetime, timedelta from http import HTTPStatus import json @@ -7,7 +8,6 @@ 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 HomeAssistant, callback @@ -113,7 +113,7 @@ class Auth: 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): + async with timeout(10): response = await session.post( LWA_TOKEN_URI, headers=LWA_HEADERS, diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index bbaa8a240f7..786b2ee5227 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from asyncio import timeout from http import HTTPStatus import json import logging @@ -10,7 +11,6 @@ from typing import TYPE_CHECKING, Any, cast from uuid import uuid4 import aiohttp -import async_timeout from homeassistant.components import event from homeassistant.const import MATCH_ALL, STATE_ON @@ -364,7 +364,7 @@ async def async_send_changereport_message( assert config.endpoint is not None try: - async with async_timeout.timeout(DEFAULT_TIMEOUT): + async with timeout(DEFAULT_TIMEOUT): response = await session.post( config.endpoint, headers=headers, @@ -517,7 +517,7 @@ async def async_send_doorbell_event_message( assert config.endpoint is not None try: - async with async_timeout.timeout(DEFAULT_TIMEOUT): + async with timeout(DEFAULT_TIMEOUT): response = await session.post( config.endpoint, headers=headers, diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index a106e3f0068..1c81eacd14a 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -2,13 +2,13 @@ from __future__ import annotations import asyncio +from asyncio import timeout from dataclasses import asdict as dataclass_asdict, dataclass from datetime import datetime from typing import Any import uuid import aiohttp -import async_timeout from homeassistant.components import hassio from homeassistant.components.api import ATTR_INSTALLATION_TYPE @@ -313,7 +313,7 @@ class Analytics: ) try: - async with async_timeout.timeout(30): + async with timeout(30): response = await self.session.post(self.endpoint, json=payload) if response.status == 200: LOGGER.info( diff --git a/homeassistant/components/androidtv_remote/__init__.py b/homeassistant/components/androidtv_remote/__init__.py index 4c58f82b8e7..9471504808c 100644 --- a/homeassistant/components/androidtv_remote/__init__.py +++ b/homeassistant/components/androidtv_remote/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from asyncio import timeout import logging from androidtvremote2 import ( @@ -10,7 +11,6 @@ from androidtvremote2 import ( ConnectionClosed, InvalidAuth, ) -import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.add_is_available_updated_callback(is_available_updated) try: - async with async_timeout.timeout(5.0): + async with timeout(5.0): await api.async_connect() except InvalidAuth as exc: # The Android TV is hard reset or the certificate and key files were deleted. diff --git a/homeassistant/components/anova/coordinator.py b/homeassistant/components/anova/coordinator.py index 436a1e469ba..94bd9bec9aa 100644 --- a/homeassistant/components/anova/coordinator.py +++ b/homeassistant/components/anova/coordinator.py @@ -1,9 +1,9 @@ """Support for Anova Coordinators.""" +from asyncio import timeout from datetime import timedelta import logging from anova_wifi import AnovaOffline, AnovaPrecisionCooker, APCUpdate -import async_timeout from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo @@ -47,7 +47,7 @@ class AnovaCoordinator(DataUpdateCoordinator[APCUpdate]): async def _async_update_data(self) -> APCUpdate: try: - async with async_timeout.timeout(5): + async with timeout(5): return await self.anova_device.update() except AnovaOffline as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index f264806ad47..7b13833ccab 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -1,12 +1,12 @@ """Rest API for Home Assistant.""" import asyncio +from asyncio import timeout from functools import lru_cache from http import HTTPStatus import logging from aiohttp import web from aiohttp.web_exceptions import HTTPBadRequest -import async_timeout import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_READ @@ -148,7 +148,7 @@ class APIEventStream(HomeAssistantView): while True: try: - async with async_timeout.timeout(STREAM_PING_INTERVAL): + async with timeout(STREAM_PING_INTERVAL): payload = await to_write.get() if payload is stop_obj: diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 9c77690ac22..d9ab17dba86 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -1,11 +1,11 @@ """Arcam component.""" import asyncio +from asyncio import timeout import logging from typing import Any from arcam.fmj import ConnectionFailed from arcam.fmj.client import Client -import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform @@ -66,7 +66,7 @@ async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> N while True: try: - async with async_timeout.timeout(interval): + async with timeout(interval): await client.start() _LOGGER.debug("Client connected %s", client.host) diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index bf61b9776e9..57e2cc8b398 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -7,7 +7,6 @@ from collections.abc import AsyncGenerator, Callable import logging from typing import Any -import async_timeout import voluptuous as vol from homeassistant.components import conversation, stt, tts, websocket_api @@ -207,7 +206,7 @@ async def websocket_run( try: # Task contains a timeout - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): await run_task except asyncio.TimeoutError: pipeline_input.run.process_event( diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 5f0552e9d77..2d04ca798e0 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -1,8 +1,8 @@ """The ATAG Integration.""" +from asyncio import timeout from datetime import timedelta import logging -import async_timeout from pyatag import AtagException, AtagOne from homeassistant.config_entries import ConfigEntry @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_update_data(): """Update data via library.""" - async with async_timeout.timeout(20): + async with timeout(20): try: await atag.update() except AtagException as err: diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index bfd95fece2a..083c7d48b03 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -1,12 +1,11 @@ """The awair component.""" from __future__ import annotations -from asyncio import gather +from asyncio import gather, timeout from dataclasses import dataclass from datetime import timedelta from aiohttp import ClientSession -from async_timeout import timeout from python_awair import Awair, AwairLocal from python_awair.air_data import AirData from python_awair.devices import AwairBaseDevice, AwairLocalDevice diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 8f3c8b9a8b6..0c132814e39 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -1,10 +1,10 @@ """Axis network device abstraction.""" import asyncio +from asyncio import timeout from types import MappingProxyType from typing import Any -import async_timeout import axis from axis.configuration import Configuration from axis.errors import Unauthorized @@ -253,7 +253,7 @@ async def get_axis_device( ) try: - async with async_timeout.timeout(30): + async with timeout(30): await device.vapix.initialize() return device diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index c9e51c79b82..dd784b214f7 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -2,10 +2,10 @@ from __future__ import annotations import asyncio +from asyncio import timeout from aiobafi6 import Device, Service from aiobafi6.discovery import PORT -import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, Platform @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: run_future = device.async_run() try: - async with async_timeout.timeout(RUN_TIMEOUT): + async with timeout(RUN_TIMEOUT): await device.async_wait_available() except asyncio.TimeoutError as ex: run_future.cancel() diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 3f37df1b70a..bbae3914533 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -2,12 +2,12 @@ from __future__ import annotations import asyncio +from asyncio import timeout import logging from typing import Any from aiobafi6 import Device, Service from aiobafi6.discovery import PORT -import async_timeout import voluptuous as vol from homeassistant import config_entries @@ -27,7 +27,7 @@ async def async_try_connect(ip_address: str) -> Device: device = Device(Service(ip_addresses=[ip_address], port=PORT)) run_future = device.async_run() try: - async with async_timeout.timeout(RUN_TIMEOUT): + async with timeout(RUN_TIMEOUT): await device.async_wait_available() except asyncio.TimeoutError as ex: raise CannotConnect from ex diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 91984cf6247..eba03963ebc 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from asyncio import CancelledError +from asyncio import CancelledError, timeout from datetime import timedelta from http import HTTPStatus import logging @@ -12,7 +12,6 @@ from urllib import parse import aiohttp from aiohttp.client_exceptions import ClientError from aiohttp.hdrs import CONNECTION, KEEP_ALIVE -import async_timeout import voluptuous as vol import xmltodict @@ -355,7 +354,7 @@ class BluesoundPlayer(MediaPlayerEntity): try: websession = async_get_clientsession(self._hass) - async with async_timeout.timeout(10): + async with timeout(10): response = await websession.get(url) if response.status == HTTPStatus.OK: @@ -396,7 +395,7 @@ class BluesoundPlayer(MediaPlayerEntity): _LOGGER.debug("Calling URL: %s", url) try: - async with async_timeout.timeout(125): + async with timeout(125): response = await self._polling_session.get( url, headers={CONNECTION: KEEP_ALIVE} ) diff --git a/homeassistant/components/bluetooth/api.py b/homeassistant/components/bluetooth/api.py index 6c232e2a42c..be35a9d255d 100644 --- a/homeassistant/components/bluetooth/api.py +++ b/homeassistant/components/bluetooth/api.py @@ -4,11 +4,11 @@ These APIs are the only documented way to interact with the bluetooth integratio """ from __future__ import annotations +import asyncio from asyncio import Future from collections.abc import Callable, Iterable from typing import TYPE_CHECKING, cast -import async_timeout from home_assistant_bluetooth import BluetoothServiceInfoBleak from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback @@ -152,7 +152,7 @@ async def async_process_advertisements( ) try: - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): return await done finally: unload() diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 5f05caf0fc1..27ac97a27dc 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -1,10 +1,10 @@ """The Brother component.""" from __future__ import annotations +from asyncio import timeout from datetime import timedelta import logging -import async_timeout from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError from homeassistant.config_entries import ConfigEntry @@ -79,7 +79,7 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]): async def _async_update_data(self) -> BrotherSensors: """Update data via library.""" try: - async with async_timeout.timeout(20): + async with timeout(20): data = await self.brother.async_update() except (ConnectionError, SnmpError, UnsupportedModelError) as error: raise UpdateFailed(error) from error diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index 979b3f5b005..660c43f1004 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -1,10 +1,10 @@ """The brunt component.""" from __future__ import annotations +from asyncio import timeout import logging from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError -import async_timeout from brunt import BruntClientAsync, Thing from homeassistant.config_entries import ConfigEntry @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: Error 401 is the API response for things that are not part of the account, could happen when a device is deleted from the account. """ try: - async with async_timeout.timeout(10): + async with timeout(10): things = await bapi.async_get_things(force=True) return {thing.serial: thing for thing in things} except ServerDisconnectedError as err: diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 54f3732afe4..8fce65c0600 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -1,11 +1,11 @@ """Shared utilities for different supported platforms.""" import asyncio +from asyncio import timeout from datetime import datetime, timedelta from http import HTTPStatus import logging import aiohttp -import async_timeout from buienradar.buienradar import parse_data from buienradar.constants import ( ATTRIBUTION, @@ -92,7 +92,7 @@ class BrData: resp = None try: websession = async_get_clientsession(self.hass) - async with async_timeout.timeout(10): + async with timeout(10): resp = await websession.get(url) result[STATUS_CODE] = resp.status From b1e5b3be34e10de3561ddfa733c95f660bbd96f4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 15 Aug 2023 10:43:19 +0200 Subject: [PATCH 0520/1151] Bump Reolink_aio to 0.7.7 (#98425) --- homeassistant/components/reolink/binary_sensor.py | 4 ++++ homeassistant/components/reolink/host.py | 2 +- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py index 850aa110171..996f2c6b3ab 100644 --- a/homeassistant/components/reolink/binary_sensor.py +++ b/homeassistant/components/reolink/binary_sensor.py @@ -5,6 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from reolink_aio.api import ( + DUAL_LENS_DUAL_MOTION_MODELS, FACE_DETECTION_TYPE, PERSON_DETECTION_TYPE, PET_DETECTION_TYPE, @@ -128,6 +129,9 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt super().__init__(reolink_data, channel) self.entity_description = entity_description + if self._host.api.model in DUAL_LENS_DUAL_MOTION_MODELS: + self._attr_name = f"{entity_description.name} lens {self._channel}" + self._attr_unique_id = ( f"{self._host.unique_id}_{self._channel}_{entity_description.key}" ) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index feeff9312c7..a679cb34f4b 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -163,7 +163,7 @@ class ReolinkHost: else: _LOGGER.debug( "Camera model %s most likely does not push its initial state" - "upon ONVIF subscription, do not check", + " upon ONVIF subscription, do not check", self._api.model, ) self._cancel_onvif_check = async_call_later( diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index fa61f873cca..f350bb4f948 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.6"] + "requirements": ["reolink-aio==0.7.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index da09cbde9be..ef6f4af565c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2284,7 +2284,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.6 +reolink-aio==0.7.7 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10fd8af6ce6..ca1951057fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1677,7 +1677,7 @@ renault-api==0.1.13 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.6 +reolink-aio==0.7.7 # homeassistant.components.rflink rflink==0.0.65 From 3b9d6f2ddebbffb02563a7fbbba67c6e8a126168 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Aug 2023 10:59:42 +0200 Subject: [PATCH 0521/1151] Add setup function to the component loader (#98148) * Add setup function to the component loader * Update test * Setup the loader in safe mode and in check_config script --- homeassistant/bootstrap.py | 3 ++ homeassistant/loader.py | 26 ++++++++-------- homeassistant/scripts/check_config.py | 3 +- tests/common.py | 22 ++++---------- .../components/device_automation/test_init.py | 30 +++++++++---------- .../components/websocket_api/test_commands.py | 2 +- 6 files changed, 38 insertions(+), 48 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 6a667884962..196a00dda7c 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -134,6 +134,7 @@ async def async_setup_hass( _LOGGER.info("Config directory: %s", runtime_config.config_dir) + loader.async_setup(hass) config_dict = None basic_setup_success = False @@ -185,6 +186,8 @@ async def async_setup_hass( hass.config.internal_url = old_config.internal_url hass.config.external_url = old_config.external_url hass.config.config_dir = old_config.config_dir + # Setup loader cache after the config dir has been set + loader.async_setup(hass) if safe_mode: _LOGGER.info("Starting in safe mode") diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 6c083b6a024..340888a2f7a 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -166,6 +166,13 @@ class Manifest(TypedDict, total=False): loggers: list[str] +def async_setup(hass: HomeAssistant) -> None: + """Set up the necessary data structures.""" + _async_mount_config_dir(hass) + hass.data[DATA_COMPONENTS] = {} + hass.data[DATA_INTEGRATIONS] = {} + + def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest: """Generate a manifest from a legacy module.""" return { @@ -802,9 +809,7 @@ class Integration: def get_component(self) -> ComponentProtocol: """Return the component.""" - cache: dict[str, ComponentProtocol] = self.hass.data.setdefault( - DATA_COMPONENTS, {} - ) + cache: dict[str, ComponentProtocol] = self.hass.data[DATA_COMPONENTS] if self.domain in cache: return cache[self.domain] @@ -824,7 +829,7 @@ class Integration: def get_platform(self, platform_name: str) -> ModuleType: """Return a platform for an integration.""" - cache: dict[str, ModuleType] = self.hass.data.setdefault(DATA_COMPONENTS, {}) + cache: dict[str, ModuleType] = self.hass.data[DATA_COMPONENTS] full_name = f"{self.domain}.{platform_name}" if full_name in cache: return cache[full_name] @@ -883,11 +888,7 @@ async def async_get_integrations( hass: HomeAssistant, domains: Iterable[str] ) -> dict[str, Integration | Exception]: """Get integrations.""" - if (cache := hass.data.get(DATA_INTEGRATIONS)) is None: - if not _async_mount_config_dir(hass): - return {domain: IntegrationNotFound(domain) for domain in domains} - cache = hass.data[DATA_INTEGRATIONS] = {} - + cache = hass.data[DATA_INTEGRATIONS] results: dict[str, Integration | Exception] = {} needed: dict[str, asyncio.Future[None]] = {} in_progress: dict[str, asyncio.Future[None]] = {} @@ -993,10 +994,7 @@ def _load_file( comp_or_platform ] - if (cache := hass.data.get(DATA_COMPONENTS)) is None: - if not _async_mount_config_dir(hass): - return None - cache = hass.data[DATA_COMPONENTS] = {} + cache = hass.data[DATA_COMPONENTS] for path in (f"{base}.{comp_or_platform}" for base in base_paths): try: @@ -1066,7 +1064,7 @@ class Components: def __getattr__(self, comp_name: str) -> ModuleWrapper: """Fetch a component.""" # Test integration cache - integration = self._hass.data.get(DATA_INTEGRATIONS, {}).get(comp_name) + integration = self._hass.data[DATA_INTEGRATIONS].get(comp_name) if isinstance(integration, Integration): component: ComponentProtocol | None = integration.get_component() diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 5384b86cb98..7c4a200bbc5 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -11,7 +11,7 @@ import os from typing import Any from unittest.mock import patch -from homeassistant import core +from homeassistant import core, loader from homeassistant.config import get_default_config_dir from homeassistant.config_entries import ConfigEntries from homeassistant.exceptions import HomeAssistantError @@ -232,6 +232,7 @@ def check(config_dir, secrets=False): async def async_check_config(config_dir): """Check the HA config.""" hass = core.HomeAssistant() + loader.async_setup(hass) hass.config.config_dir = config_dir hass.config_entries = ConfigEntries(hass, {}) await ar.async_load(hass) diff --git a/tests/common.py b/tests/common.py index 0431743cccf..95947719ef4 100644 --- a/tests/common.py +++ b/tests/common.py @@ -256,6 +256,7 @@ async def async_test_home_assistant(event_loop, load_registries=True): # Load the registries entity.async_setup(hass) + loader.async_setup(hass) if load_registries: with patch( "homeassistant.helpers.storage.Store.async_load", return_value=None @@ -1339,16 +1340,10 @@ def mock_integration( integration._import_platform = mock_import_platform _LOGGER.info("Adding mock integration: %s", module.DOMAIN) - 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 = hass.data[loader.DATA_INTEGRATIONS] 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 = hass.data[loader.DATA_COMPONENTS] module_cache[module.DOMAIN] = module return integration @@ -1374,15 +1369,8 @@ def mock_platform( platform_path is in form hue.config_flow. """ 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) + integration_cache = hass.data[loader.DATA_INTEGRATIONS] + module_cache = hass.data[loader.DATA_COMPONENTS] if domain not in integration_cache: mock_integration(hass, MockModule(domain)) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 65fee1053ae..74150af67ae 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -304,7 +304,7 @@ async def test_websocket_get_action_capabilities( return {"extra_fields": vol.Schema({vol.Optional("code"): str})} return {} - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_action"] module.async_get_action_capabilities = _async_get_action_capabilities @@ -406,7 +406,7 @@ async def test_websocket_get_action_capabilities_bad_action( await async_setup_component(hass, "device_automation", {}) expected_capabilities = {} - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_action"] module.async_get_action_capabilities = Mock( side_effect=InvalidDeviceAutomationConfig @@ -459,7 +459,7 @@ async def test_websocket_get_condition_capabilities( """List condition capabilities.""" return await toggle_entity.async_get_condition_capabilities(hass, config) - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_condition"] module.async_get_condition_capabilities = _async_get_condition_capabilities @@ -569,7 +569,7 @@ async def test_websocket_get_condition_capabilities_bad_condition( await async_setup_component(hass, "device_automation", {}) expected_capabilities = {} - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_condition"] module.async_get_condition_capabilities = Mock( side_effect=InvalidDeviceAutomationConfig @@ -747,7 +747,7 @@ async def test_websocket_get_trigger_capabilities( """List trigger capabilities.""" return await toggle_entity.async_get_trigger_capabilities(hass, config) - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_trigger"] module.async_get_trigger_capabilities = _async_get_trigger_capabilities @@ -857,7 +857,7 @@ async def test_websocket_get_trigger_capabilities_bad_trigger( await async_setup_component(hass, "device_automation", {}) expected_capabilities = {} - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_trigger"] module.async_get_trigger_capabilities = Mock( side_effect=InvalidDeviceAutomationConfig @@ -912,7 +912,7 @@ async def test_automation_with_device_action( ) -> None: """Test automation with a device action.""" - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_action"] module.async_call_action_from_config = AsyncMock() @@ -949,7 +949,7 @@ async def test_automation_with_dynamically_validated_action( ) -> None: """Test device automation with an action which is dynamically validated.""" - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_action"] module.async_validate_action_config = AsyncMock() @@ -1003,7 +1003,7 @@ async def test_automation_with_device_condition( ) -> None: """Test automation with a device condition.""" - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_condition"] module.async_condition_from_config = Mock() @@ -1037,7 +1037,7 @@ async def test_automation_with_dynamically_validated_condition( ) -> None: """Test device automation with a condition which is dynamically validated.""" - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_condition"] module.async_validate_condition_config = AsyncMock() @@ -1102,7 +1102,7 @@ async def test_automation_with_device_trigger( ) -> None: """Test automation with a device trigger.""" - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_trigger"] module.async_attach_trigger = AsyncMock() @@ -1136,7 +1136,7 @@ async def test_automation_with_dynamically_validated_trigger( ) -> None: """Test device automation with a trigger which is dynamically validated.""" - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_trigger"] module.async_attach_trigger = AsyncMock() module.async_validate_trigger_config = AsyncMock(wraps=lambda hass, config: config) @@ -1457,7 +1457,7 @@ async def test_automation_with_unknown_device( ) -> None: """Test device automation with a trigger with an unknown device.""" - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_trigger"] module.async_validate_trigger_config = AsyncMock() @@ -1492,7 +1492,7 @@ async def test_automation_with_device_wrong_domain( ) -> None: """Test device automation where the device doesn't have the right config entry.""" - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_trigger"] module.async_validate_trigger_config = AsyncMock() @@ -1534,7 +1534,7 @@ async def test_automation_with_device_component_not_loaded( ) -> None: """Test device automation where the device's config entry is not loaded.""" - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_trigger"] module.async_validate_trigger_config = AsyncMock() module.async_attach_trigger = AsyncMock() diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 3a68bbd88d3..85c0ac62b25 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1810,7 +1810,7 @@ async def test_execute_script_with_dynamically_validated_action( ws_client = await hass_ws_client(hass) - module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module_cache = hass.data[loader.DATA_COMPONENTS] module = module_cache["fake_integration.device_action"] module.async_call_action_from_config = AsyncMock() module.async_validate_action_config = AsyncMock( From 87b7fc6c61329d33bdcbcfc9baf590387666c15b Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 15 Aug 2023 03:04:45 -0600 Subject: [PATCH 0522/1151] Bump pylitterbot to 2023.4.4 (#98414) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 2a4a3447eb6..81375dd3a6c 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["pylitterbot"], - "requirements": ["pylitterbot==2023.4.2"] + "requirements": ["pylitterbot==2023.4.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index ef6f4af565c..f86b6afc2e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1809,7 +1809,7 @@ pylibrespot-java==0.1.1 pylitejet==0.5.0 # homeassistant.components.litterrobot -pylitterbot==2023.4.2 +pylitterbot==2023.4.4 # homeassistant.components.lutron_caseta pylutron-caseta==0.18.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca1951057fb..9bc5a8b53ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ pylibrespot-java==0.1.1 pylitejet==0.5.0 # homeassistant.components.litterrobot -pylitterbot==2023.4.2 +pylitterbot==2023.4.4 # homeassistant.components.lutron_caseta pylutron-caseta==0.18.1 From ed18c6a0137ea8ef9315a6fcf8968edbae7590af Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 15 Aug 2023 11:43:47 +0200 Subject: [PATCH 0523/1151] Refactor Rest Switch with ManualTriggerEntity (#97403) * Refactor Rest Switch with ManualTriggerEntity * Fix test * Fix 2 * review comments * remove async_added_to_hass * update on startup --- homeassistant/components/rest/switch.py | 52 ++++++++++++++++++------- tests/components/rest/test_switch.py | 10 ++--- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 827f4bad0b3..0a220204997 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -18,7 +18,9 @@ from homeassistant.components.switch import ( from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_HEADERS, + CONF_ICON, CONF_METHOD, + CONF_NAME, CONF_PARAMS, CONF_PASSWORD, CONF_RESOURCE, @@ -33,8 +35,10 @@ from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, TEMPLATE_ENTITY_BASE_SCHEMA, - TemplateEntity, + ManualTriggerEntity, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -44,6 +48,14 @@ CONF_BODY_ON = "body_on" CONF_IS_ON_TEMPLATE = "is_on_template" CONF_STATE_RESOURCE = "state_resource" +TRIGGER_ENTITY_OPTIONS = ( + CONF_AVAILABILITY, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_PICTURE, + CONF_UNIQUE_ID, +) + DEFAULT_METHOD = "post" DEFAULT_BODY_OFF = "OFF" DEFAULT_BODY_ON = "ON" @@ -71,6 +83,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_AVAILABILITY): cv.template, } ) @@ -83,10 +96,17 @@ async def async_setup_platform( ) -> None: """Set up the RESTful switch.""" resource: str = config[CONF_RESOURCE] - unique_id: str | None = config.get(CONF_UNIQUE_ID) + name = config.get(CONF_NAME) or template.Template(DEFAULT_NAME, hass) + + trigger_entity_config = {CONF_NAME: name} + + for key in TRIGGER_ENTITY_OPTIONS: + if key not in config: + continue + trigger_entity_config[key] = config[key] try: - switch = RestSwitch(hass, config, unique_id) + switch = RestSwitch(hass, config, trigger_entity_config) req = await switch.get_device_state(hass) if req.status_code >= HTTPStatus.BAD_REQUEST: @@ -102,23 +122,17 @@ async def async_setup_platform( raise PlatformNotReady(f"No route to resource/endpoint: {resource}") from exc -class RestSwitch(TemplateEntity, SwitchEntity): +class RestSwitch(ManualTriggerEntity, SwitchEntity): """Representation of a switch that can be toggled using REST.""" def __init__( self, hass: HomeAssistant, config: ConfigType, - unique_id: str | None, + trigger_entity_config: ConfigType, ) -> None: """Initialize the REST switch.""" - TemplateEntity.__init__( - self, - hass, - config=config, - fallback_name=DEFAULT_NAME, - unique_id=unique_id, - ) + ManualTriggerEntity.__init__(self, hass, trigger_entity_config) auth: httpx.BasicAuth | None = None username: str | None = None @@ -138,8 +152,6 @@ class RestSwitch(TemplateEntity, SwitchEntity): self._timeout: int = config[CONF_TIMEOUT] self._verify_ssl: bool = config[CONF_VERIFY_SSL] - self._attr_device_class = config.get(CONF_DEVICE_CLASS) - self._body_on.hass = hass self._body_off.hass = hass if (is_on_template := self._is_on_template) is not None: @@ -148,6 +160,11 @@ class RestSwitch(TemplateEntity, SwitchEntity): template.attach(hass, self._headers) template.attach(hass, self._params) + async def async_added_to_hass(self) -> None: + """Handle adding to Home Assistant.""" + await super().async_added_to_hass() + await self.async_update() + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" body_on_t = self._body_on.async_render(parse_result=False) @@ -198,13 +215,18 @@ class RestSwitch(TemplateEntity, SwitchEntity): async def async_update(self) -> None: """Get the current state, catching errors.""" + req = None try: - await self.get_device_state(self.hass) + req = await self.get_device_state(self.hass) except asyncio.TimeoutError: _LOGGER.exception("Timed out while fetching data") except httpx.RequestError as err: _LOGGER.exception("Error while fetching data: %s", err) + if req: + self._process_manual_data(req.text) + self.async_write_ha_state() + async def get_device_state(self, hass: HomeAssistant) -> httpx.Response: """Get the latest data from REST API and update the state.""" websession = get_async_client(hass, self._verify_ssl) diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index a6895183d4e..8bd13550960 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -111,7 +111,7 @@ async def test_setup_minimum(hass: HomeAssistant) -> None: with assert_setup_component(1, SWITCH_DOMAIN): assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() - assert route.call_count == 1 + assert route.call_count == 2 @respx.mock @@ -129,7 +129,7 @@ async def test_setup_query_params(hass: HomeAssistant) -> None: assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() - assert route.call_count == 1 + assert route.call_count == 2 @respx.mock @@ -148,7 +148,7 @@ async def test_setup(hass: HomeAssistant) -> None: } assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() - assert route.call_count == 1 + assert route.call_count == 2 assert_setup_component(1, SWITCH_DOMAIN) @@ -170,7 +170,7 @@ async def test_setup_with_state_resource(hass: HomeAssistant) -> None: } assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() - assert route.call_count == 1 + assert route.call_count == 2 assert_setup_component(1, SWITCH_DOMAIN) @@ -195,7 +195,7 @@ async def test_setup_with_templated_headers_params(hass: HomeAssistant) -> None: } assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() - assert route.call_count == 1 + assert route.call_count == 2 last_call = route.calls[-1] last_request: httpx.Request = last_call.request assert last_request.headers.get("Accept") == CONTENT_TYPE_JSON From a87878f72362529e8c28aa5eef60ff67fc5ec113 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Aug 2023 12:26:37 +0200 Subject: [PATCH 0524/1151] Make image upload mimetype to match frontend (#98411) --- homeassistant/components/image_upload/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/image_upload/__init__.py b/homeassistant/components/image_upload/__init__.py index 569df9c65e4..6486d584b0e 100644 --- a/homeassistant/components/image_upload/__init__.py +++ b/homeassistant/components/image_upload/__init__.py @@ -78,8 +78,10 @@ class ImageStorageCollection(collection.DictStorageCollection): data = self.CREATE_SCHEMA(dict(data)) uploaded_file: FileField = data["file"] - if not uploaded_file.content_type.startswith("image/"): - raise vol.Invalid("Only images are allowed") + if not uploaded_file.content_type.startswith( + ("image/gif", "image/jpeg", "image/png") + ): + raise vol.Invalid("Only jpeg, png, and gif images are allowed") data[CONF_ID] = secrets.token_hex(16) data["filesize"] = await self.hass.async_add_executor_job(self._move_data, data) From 35b914af975a43bc6eda207f2f3b207aeb226153 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Aug 2023 13:28:43 +0200 Subject: [PATCH 0525/1151] Disable polling in buienradar weather entity (#98443) --- homeassistant/components/buienradar/sensor.py | 4 +- homeassistant/components/buienradar/util.py | 2 +- .../components/buienradar/weather.py | 92 ++++++++----------- 3 files changed, 41 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index e52000edf7f..00740eb4801 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -753,9 +753,9 @@ class BrSensor(SensorEntity): self._timeframe = None @callback - def data_updated(self, data): + def data_updated(self, data: BrData): """Update data.""" - if self.hass and self._load_data(data): + if self.hass and self._load_data(data.data): self.async_write_ha_state() @callback diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 8fce65c0600..9d0c2a575c9 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -75,7 +75,7 @@ class BrData: # Update all devices for dev in self.devices: - dev.data_updated(self.data) + dev.data_updated(self) async def schedule_update(self, minute=1): """Schedule an update after minute minutes.""" diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index c2a276eed1c..aedfcf82aea 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -48,7 +48,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback # Reuse data and API logic from the sensor implementation @@ -99,11 +99,13 @@ async def async_setup_entry( coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)} - # create weather data: - data = BrData(hass, coordinates, DEFAULT_TIMEFRAME, None) - hass.data[DOMAIN][entry.entry_id][Platform.WEATHER] = data - # create weather device: + # create weather entity: _LOGGER.debug("Initializing buienradar weather: coordinates %s", coordinates) + entities = [BrWeather(config, coordinates)] + + # create weather data: + data = BrData(hass, coordinates, DEFAULT_TIMEFRAME, entities) + hass.data[DOMAIN][entry.entry_id][Platform.WEATHER] = data # create condition helper if DATA_CONDITION not in hass.data[DOMAIN]: @@ -113,7 +115,7 @@ async def async_setup_entry( for condi in condlst: hass.data[DOMAIN][DATA_CONDITION][condi] = cond - async_add_entities([BrWeather(data, config, coordinates)]) + async_add_entities(entities) # schedule the first update in 1 minute from now: await data.schedule_update(1) @@ -127,75 +129,57 @@ class BrWeather(WeatherEntity): _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_visibility_unit = UnitOfLength.METERS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND + _attr_should_poll = False - def __init__(self, data, config, coordinates): + def __init__(self, config, coordinates): """Initialize the platform with a data instance and station name.""" self._stationname = config.get(CONF_NAME, "Buienradar") - self._attr_name = ( - self._stationname or f"BR {data.stationname or '(unknown station)'}" - ) - self._data = data + self._attr_name = self._stationname or f"BR {'(unknown station)'}" + self._attr_condition = None self._attr_unique_id = "{:2.6f}{:2.6f}".format( coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE] ) - @property - def attribution(self): - """Return the attribution.""" - return self._data.attribution + @callback + def data_updated(self, data: BrData) -> None: + """Update data.""" + if not self.hass: + return - @property - def condition(self): + self._attr_attribution = data.attribution + self._attr_condition = self._calc_condition(data) + self._attr_forecast = self._calc_forecast(data) + self._attr_humidity = data.humidity + self._attr_name = ( + self._stationname or f"BR {data.stationname or '(unknown station)'}" + ) + self._attr_native_pressure = data.pressure + self._attr_native_temperature = data.temperature + self._attr_native_visibility = data.visibility + self._attr_native_wind_speed = data.wind_speed + self._attr_wind_bearing = data.wind_bearing + self.async_write_ha_state() + + def _calc_condition(self, data: BrData): """Return the current condition.""" if ( - self._data - and self._data.condition - and (ccode := self._data.condition.get(CONDCODE)) + data.condition + and (ccode := data.condition.get(CONDCODE)) and (conditions := self.hass.data[DOMAIN].get(DATA_CONDITION)) ): return conditions.get(ccode) + return None - @property - def native_temperature(self): - """Return the current temperature.""" - return self._data.temperature - - @property - def native_pressure(self): - """Return the current pressure.""" - return self._data.pressure - - @property - def humidity(self): - """Return the name of the sensor.""" - return self._data.humidity - - @property - def native_visibility(self): - """Return the current visibility in m.""" - return self._data.visibility - - @property - def native_wind_speed(self): - """Return the current windspeed in m/s.""" - return self._data.wind_speed - - @property - def wind_bearing(self): - """Return the current wind bearing (degrees).""" - return self._data.wind_bearing - - @property - def forecast(self): + def _calc_forecast(self, data: BrData): """Return the forecast array.""" fcdata_out = [] cond = self.hass.data[DOMAIN][DATA_CONDITION] - if not self._data.forecast: + if not data.forecast: return None - for data_in in self._data.forecast: + for data_in in data.forecast: # remap keys from external library to # keys understood by the weather component: condcode = data_in.get(CONDITION, []).get(CONDCODE) From 71d985e4d664b2e3234344b29b2fb6a3f5f015de Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Aug 2023 14:32:15 +0200 Subject: [PATCH 0526/1151] Use asyncio.timeout [i-n] (#98450) --- homeassistant/components/ialarm/__init__.py | 5 ++--- homeassistant/components/iammeter/sensor.py | 5 ++--- homeassistant/components/image/__init__.py | 3 +-- homeassistant/components/imap/coordinator.py | 3 +-- homeassistant/components/intellifire/coordinator.py | 4 ++-- homeassistant/components/ipma/__init__.py | 3 +-- homeassistant/components/ipma/sensor.py | 4 ++-- homeassistant/components/ipma/weather.py | 5 ++--- homeassistant/components/isy994/__init__.py | 3 +-- homeassistant/components/isy994/config_flow.py | 4 ++-- homeassistant/components/izone/config_flow.py | 4 +--- homeassistant/components/kaiterra/api_data.py | 3 +-- homeassistant/components/kmtronic/__init__.py | 4 ++-- homeassistant/components/kraken/__init__.py | 3 +-- .../components/landisgyr_heat_meter/config_flow.py | 3 +-- .../components/landisgyr_heat_meter/coordinator.py | 4 ++-- homeassistant/components/laundrify/coordinator.py | 4 ++-- homeassistant/components/led_ble/__init__.py | 3 +-- homeassistant/components/lifx_cloud/scene.py | 5 ++--- homeassistant/components/logi_circle/__init__.py | 3 +-- homeassistant/components/logi_circle/config_flow.py | 3 +-- homeassistant/components/london_underground/sensor.py | 4 ++-- homeassistant/components/loqed/coordinator.py | 4 ++-- homeassistant/components/lutron_caseta/__init__.py | 3 +-- homeassistant/components/lutron_caseta/config_flow.py | 3 +-- homeassistant/components/lyric/__init__.py | 4 ++-- homeassistant/components/mailbox/__init__.py | 3 +-- homeassistant/components/matter/__init__.py | 5 ++--- homeassistant/components/mazda/__init__.py | 4 ++-- homeassistant/components/meater/__init__.py | 4 ++-- homeassistant/components/media_player/__init__.py | 3 +-- homeassistant/components/melcloud/__init__.py | 3 +-- homeassistant/components/melcloud/config_flow.py | 3 +-- homeassistant/components/microsoft_face/__init__.py | 3 +-- homeassistant/components/mjpeg/camera.py | 5 ++--- homeassistant/components/mobile_app/notify.py | 3 +-- homeassistant/components/mullvad/__init__.py | 4 ++-- homeassistant/components/mutesync/__init__.py | 4 ++-- homeassistant/components/mutesync/config_flow.py | 3 +-- homeassistant/components/mysensors/gateway.py | 5 ++--- homeassistant/components/nam/__init__.py | 3 +-- homeassistant/components/nam/config_flow.py | 5 ++--- homeassistant/components/nextdns/__init__.py | 5 ++--- homeassistant/components/nextdns/config_flow.py | 3 +-- homeassistant/components/nina/__init__.py | 4 ++-- homeassistant/components/no_ip/__init__.py | 3 +-- homeassistant/components/nuki/__init__.py | 10 +++++----- homeassistant/components/nut/__init__.py | 4 ++-- homeassistant/components/nzbget/coordinator.py | 4 ++-- 49 files changed, 78 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index b258c702725..b2c1800914e 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio import logging -from async_timeout import timeout from pyialarm import IAlarm from homeassistant.components.alarm_control_panel import SCAN_INTERVAL @@ -27,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ialarm = IAlarm(host, port) try: - async with timeout(10): + async with asyncio.timeout(10): mac = await hass.async_add_executor_job(ialarm.get_mac) except (asyncio.TimeoutError, ConnectionError) as ex: raise ConfigEntryNotReady from ex @@ -81,7 +80,7 @@ class IAlarmDataUpdateCoordinator(DataUpdateCoordinator[None]): async def _async_update_data(self) -> None: """Fetch data from iAlarm.""" try: - async with timeout(10): + async with asyncio.timeout(10): await self.hass.async_add_executor_job(self._update_data) except ConnectionError as error: raise UpdateFailed(error) from error diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index 206b5def832..ca468200370 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -5,7 +5,6 @@ import asyncio from datetime import timedelta import logging -import async_timeout from iammeter import real_time_api from iammeter.power_meter import IamMeterError import voluptuous as vol @@ -52,7 +51,7 @@ async def async_setup_platform( config_port = config[CONF_PORT] config_name = config[CONF_NAME] try: - async with async_timeout.timeout(PLATFORM_TIMEOUT): + async with asyncio.timeout(PLATFORM_TIMEOUT): api = await real_time_api(config_host, config_port) except (IamMeterError, asyncio.TimeoutError) as err: _LOGGER.error("Device is not ready") @@ -60,7 +59,7 @@ async def async_setup_platform( async def async_update_data(): try: - async with async_timeout.timeout(PLATFORM_TIMEOUT): + async with asyncio.timeout(PLATFORM_TIMEOUT): return await api.get_data() except (IamMeterError, asyncio.TimeoutError) as err: raise UpdateFailed from err diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index e4bc1664fd9..d1895053f02 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -11,7 +11,6 @@ from random import SystemRandom from typing import Final, final from aiohttp import hdrs, web -import async_timeout import httpx from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView @@ -72,7 +71,7 @@ def valid_image_content_type(content_type: str | None) -> str: async def _async_get_image(image_entity: ImageEntity, timeout: int) -> Image: """Fetch image from an image entity.""" with suppress(asyncio.CancelledError, asyncio.TimeoutError, ImageContentTypeError): - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): if image_bytes := await image_entity.async_image(): content_type = valid_image_content_type(image_entity.content_type) image = Image(content_type, image_bytes) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index b644c300979..b9b541997a3 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -11,7 +11,6 @@ import logging from typing import Any from aioimaplib import AUTH, IMAP4_SSL, NONAUTH, SELECTED, AioImapException -import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -408,7 +407,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): idle: asyncio.Future = await self.imap_client.idle_start() await self.imap_client.wait_server_push() self.imap_client.idle_done() - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await idle # From python 3.11 asyncio.TimeoutError is an alias of TimeoutError diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index f9502f70ee7..4045c19217b 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -1,10 +1,10 @@ """The IntelliFire integration.""" from __future__ import annotations +import asyncio from datetime import timedelta from aiohttp import ClientConnectionError -from async_timeout import timeout from intellifire4py import IntellifirePollData from intellifire4py.intellifire import IntellifireAPILocal @@ -38,7 +38,7 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData await self._api.start_background_polling() # Don't return uninitialized poll data - async with timeout(15): + async with asyncio.timeout(15): try: await self._api.poll() except (ConnectionError, ClientConnectionError) as exception: diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index dd46593998e..5ff89fa8ed5 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -2,7 +2,6 @@ import asyncio import logging -import async_timeout from pyipma import IPMAException from pyipma.api import IPMA_API from pyipma.location import Location @@ -32,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b api = IPMA_API(async_get_clientsession(hass)) try: - async with async_timeout.timeout(30): + async with asyncio.timeout(30): location = await Location.get(api, float(latitude), float(longitude)) except (IPMAException, asyncio.TimeoutError) as err: raise ConfigEntryNotReady( diff --git a/homeassistant/components/ipma/sensor.py b/homeassistant/components/ipma/sensor.py index f02f8b7d9d0..1bd257a3994 100644 --- a/homeassistant/components/ipma/sensor.py +++ b/homeassistant/components/ipma/sensor.py @@ -1,11 +1,11 @@ """Support for IPMA sensors.""" from __future__ import annotations +import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging -import async_timeout from pyipma.api import IPMA_API from pyipma.location import Location @@ -83,7 +83,7 @@ class IPMASensor(SensorEntity, IPMADevice): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Update Fire risk.""" - async with async_timeout.timeout(10): + async with asyncio.timeout(10): self._attr_native_value = await self.entity_description.value_fn( self._location, self._api ) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index b8e994a7500..d4d11aa26e8 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -6,7 +6,6 @@ import contextlib import logging from typing import Literal -import async_timeout from pyipma.api import IPMA_API from pyipma.forecast import Forecast as IPMAForecast from pyipma.location import Location @@ -91,7 +90,7 @@ class IPMAWeather(WeatherEntity, IPMADevice): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Update Condition and Forecast.""" - async with async_timeout.timeout(10): + async with asyncio.timeout(10): new_observation = await self._location.observation(self._api) if new_observation: @@ -225,7 +224,7 @@ class IPMAWeather(WeatherEntity, IPMADevice): ) -> None: """Try to update weather forecast.""" with contextlib.suppress(asyncio.TimeoutError): - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await self._update_forecast(forecast_type, period, False) async def async_forecast_daily(self) -> list[Forecast]: diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index f19e21b4f6d..c611bf83050 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -5,7 +5,6 @@ import asyncio from urllib.parse import urlparse from aiohttp import CookieJar -import async_timeout from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError from pyisy.constants import CONFIG_NETWORKING, CONFIG_PORTAL import voluptuous as vol @@ -101,7 +100,7 @@ async def async_setup_entry( ) try: - async with async_timeout.timeout(60): + async with asyncio.timeout(60): await isy.initialize() except asyncio.TimeoutError as err: raise ConfigEntryNotReady( diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index d6bbf236c13..9f16b4a0d0c 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -1,13 +1,13 @@ """Config flow for Universal Devices ISY/IoX integration.""" from __future__ import annotations +import asyncio from collections.abc import Mapping import logging from typing import Any from urllib.parse import urlparse, urlunparse from aiohttp import CookieJar -import async_timeout from pyisy import ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError from pyisy.configuration import Configuration from pyisy.connection import Connection @@ -97,7 +97,7 @@ async def validate_input( ) try: - async with async_timeout.timeout(30): + async with asyncio.timeout(30): isy_conf_xml = await isy_conn.test_connection() except ISYInvalidAuthError as error: raise InvalidAuth from error diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py index af5205feb07..8e6fe584456 100644 --- a/homeassistant/components/izone/config_flow.py +++ b/homeassistant/components/izone/config_flow.py @@ -4,8 +4,6 @@ import asyncio from contextlib import suppress import logging -from async_timeout import timeout - from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_entry_flow from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -28,7 +26,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: disco = await async_start_discovery_service(hass) with suppress(asyncio.TimeoutError): - async with timeout(TIMEOUT_DISCOVERY): + async with asyncio.timeout(TIMEOUT_DISCOVERY): await controller_ready.wait() if not disco.pi_disco.controllers: diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index 980c01d02a1..09d470af1de 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -3,7 +3,6 @@ import asyncio from logging import getLogger from aiohttp.client_exceptions import ClientConnectorError, ClientResponseError -import async_timeout from kaiterra_async_client import AQIStandard, KaiterraAPIClient, Units from homeassistant.const import CONF_API_KEY, CONF_DEVICE_ID, CONF_DEVICES, CONF_TYPE @@ -53,7 +52,7 @@ class KaiterraApiData: """Get the data from Kaiterra API.""" try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): data = await self._api.get_latest_sensor_readings(self._devices) except (ClientResponseError, ClientConnectorError, asyncio.TimeoutError) as err: _LOGGER.debug("Couldn't fetch data from Kaiterra API: %s", err) diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index ef4e8ebb303..638884dff26 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -1,9 +1,9 @@ """The kmtronic integration.""" +import asyncio from datetime import timedelta import logging import aiohttp -import async_timeout from pykmtronic.auth import Auth from pykmtronic.hub import KMTronicHubAPI @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_data(): try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await hub.async_update_relays() except aiohttp.client_exceptions.ClientResponseError as err: raise UpdateFailed(f"Wrong credentials: {err}") from err diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index 1cfade2a6b7..395de951bbd 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -5,7 +5,6 @@ import asyncio from datetime import timedelta import logging -import async_timeout import krakenex import pykrakenapi @@ -73,7 +72,7 @@ class KrakenData: once. """ try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return await self._hass.async_add_executor_job(self._get_kraken_data) except pykrakenapi.pykrakenapi.KrakenAPIError as error: if "Unknown asset pair" in str(error): diff --git a/homeassistant/components/landisgyr_heat_meter/config_flow.py b/homeassistant/components/landisgyr_heat_meter/config_flow.py index 0353e5e63c7..4f7966ae90f 100644 --- a/homeassistant/components/landisgyr_heat_meter/config_flow.py +++ b/homeassistant/components/landisgyr_heat_meter/config_flow.py @@ -5,7 +5,6 @@ import asyncio import logging from typing import Any -import async_timeout import serial from serial.tools import list_ports import ultraheat_api @@ -105,7 +104,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): reader = ultraheat_api.UltraheatReader(port) heat_meter = ultraheat_api.HeatMeterService(reader) try: - async with async_timeout.timeout(ULTRAHEAT_TIMEOUT): + async with asyncio.timeout(ULTRAHEAT_TIMEOUT): # validate and retrieve the model and device number for a unique id data = await self.hass.async_add_executor_job(heat_meter.read) diff --git a/homeassistant/components/landisgyr_heat_meter/coordinator.py b/homeassistant/components/landisgyr_heat_meter/coordinator.py index c85c661e79c..27231dc7b92 100644 --- a/homeassistant/components/landisgyr_heat_meter/coordinator.py +++ b/homeassistant/components/landisgyr_heat_meter/coordinator.py @@ -1,8 +1,8 @@ """Data update coordinator for the ultraheat api.""" +import asyncio import logging -import async_timeout import serial from ultraheat_api.response import HeatMeterResponse from ultraheat_api.service import HeatMeterService @@ -31,7 +31,7 @@ class UltraheatCoordinator(DataUpdateCoordinator[HeatMeterResponse]): async def _async_update_data(self) -> HeatMeterResponse: """Fetch data from API endpoint.""" try: - async with async_timeout.timeout(ULTRAHEAT_TIMEOUT): + async with asyncio.timeout(ULTRAHEAT_TIMEOUT): return await self.hass.async_add_executor_job(self.api.read) except (FileNotFoundError, serial.serialutil.SerialException) as err: raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/laundrify/coordinator.py b/homeassistant/components/laundrify/coordinator.py index 47728a38983..121d2cd913f 100644 --- a/homeassistant/components/laundrify/coordinator.py +++ b/homeassistant/components/laundrify/coordinator.py @@ -1,8 +1,8 @@ """Custom DataUpdateCoordinator for the laundrify integration.""" +import asyncio from datetime import timedelta import logging -import async_timeout from laundrify_aio import LaundrifyAPI from laundrify_aio.exceptions import ApiConnectionException, UnauthorizedException @@ -36,7 +36,7 @@ class LaundrifyUpdateCoordinator(DataUpdateCoordinator[dict[str, LaundrifyDevice try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. - async with async_timeout.timeout(REQUEST_TIMEOUT): + async with asyncio.timeout(REQUEST_TIMEOUT): return {m["_id"]: m for m in await self.laundrify_api.get_machines()} except UnauthorizedException as err: # Raising ConfigEntryAuthFailed will cancel future updates diff --git a/homeassistant/components/led_ble/__init__.py b/homeassistant/components/led_ble/__init__.py index 768300ff534..1bdb8bf8ec9 100644 --- a/homeassistant/components/led_ble/__init__.py +++ b/homeassistant/components/led_ble/__init__.py @@ -5,7 +5,6 @@ import asyncio from datetime import timedelta import logging -import async_timeout from led_ble import BLEAK_EXCEPTIONS, LEDBLE from homeassistant.components import bluetooth @@ -78,7 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise try: - async with async_timeout.timeout(DEVICE_TIMEOUT): + async with asyncio.timeout(DEVICE_TIMEOUT): await startup_event.wait() except asyncio.TimeoutError as ex: raise ConfigEntryNotReady( diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index ce03a595f64..bcf8ed1dc2c 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -8,7 +8,6 @@ from typing import Any import aiohttp from aiohttp.hdrs import AUTHORIZATION -import async_timeout import voluptuous as vol from homeassistant.components.scene import Scene @@ -48,7 +47,7 @@ async def async_setup_platform( try: httpsession = async_get_clientsession(hass) - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): scenes_resp = await httpsession.get(url, headers=headers) except (asyncio.TimeoutError, aiohttp.ClientError): @@ -90,7 +89,7 @@ class LifxCloudScene(Scene): try: httpsession = async_get_clientsession(self.hass) - async with async_timeout.timeout(self._timeout): + async with asyncio.timeout(self._timeout): await httpsession.put(url, headers=self._headers) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 93e23be5d8d..a14cd60c993 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -2,7 +2,6 @@ import asyncio from aiohttp.client_exceptions import ClientResponseError -import async_timeout from logi_circle import LogiCircle from logi_circle.exception import AuthorizationFailed import voluptuous as vol @@ -154,7 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False try: - async with async_timeout.timeout(_TIMEOUT): + async with asyncio.timeout(_TIMEOUT): # Ensure the cameras property returns the same Camera objects for # all devices. Performs implicit login and session validation. await logi_circle.synchronize_cameras() diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index ff7528ac9f6..9785940aca2 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -3,7 +3,6 @@ import asyncio from collections import OrderedDict from http import HTTPStatus -import async_timeout from logi_circle import LogiCircle from logi_circle.exception import AuthorizationFailed import voluptuous as vol @@ -158,7 +157,7 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) try: - async with async_timeout.timeout(_TIMEOUT): + async with asyncio.timeout(_TIMEOUT): await logi_session.authorize(code) except AuthorizationFailed: (self.hass.data[DATA_FLOW_IMPL][DOMAIN][EXTERNAL_ERRORS]) = "invalid_auth" diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index 8217b3913a8..7e52186fa51 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -1,10 +1,10 @@ """Sensor for checking the status of London Underground tube lines.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging -import async_timeout from london_tube_status import TubeData import voluptuous as vol @@ -90,7 +90,7 @@ class LondonTubeCoordinator(DataUpdateCoordinator): self._data = data async def _async_update_data(self): - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await self._data.update() return self._data.data diff --git a/homeassistant/components/loqed/coordinator.py b/homeassistant/components/loqed/coordinator.py index 42e0d523aba..d33cd8772b2 100644 --- a/homeassistant/components/loqed/coordinator.py +++ b/homeassistant/components/loqed/coordinator.py @@ -1,9 +1,9 @@ """Provides the coordinator for a LOQED lock.""" +import asyncio import logging from typing import TypedDict from aiohttp.web import Request -import async_timeout from loqedAPI import loqed from homeassistant.components import cloud, webhook @@ -86,7 +86,7 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]): async def _async_update_data(self) -> StatusMessage: """Fetch data from API endpoint.""" - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return await self._api.async_get_lock_details() async def _handle_webhook( diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 0a6a2aa8211..41369046d51 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -8,7 +8,6 @@ import logging import ssl from typing import Any, cast -import async_timeout from pylutron_caseta import BUTTON_STATUS_PRESSED from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol @@ -173,7 +172,7 @@ async def async_setup_entry( timed_out = True with contextlib.suppress(asyncio.TimeoutError): - async with async_timeout.timeout(BRIDGE_TIMEOUT): + async with asyncio.timeout(BRIDGE_TIMEOUT): await bridge.connect() timed_out = False diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index 74819e25e8e..9b243a3ec98 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -6,7 +6,6 @@ import logging import os import ssl -import async_timeout from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY, async_pair from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol @@ -226,7 +225,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return None try: - async with async_timeout.timeout(BRIDGE_TIMEOUT): + async with asyncio.timeout(BRIDGE_TIMEOUT): await bridge.connect() except asyncio.TimeoutError: _LOGGER.error( diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index c2c1c9ae77a..a407afaa207 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -1,6 +1,7 @@ """The Honeywell Lyric integration.""" from __future__ import annotations +import asyncio from datetime import timedelta from http import HTTPStatus import logging @@ -10,7 +11,6 @@ from aiolyric import Lyric from aiolyric.exceptions import LyricAuthenticationException, LyricException from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation -import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise UpdateFailed(exception) from exception try: - async with async_timeout.timeout(60): + async with asyncio.timeout(60): await lyric.get_locations() return lyric except LyricAuthenticationException as exception: diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 75cea546b71..679abfd3164 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -10,7 +10,6 @@ from typing import Any, Final from aiohttp import web from aiohttp.web_exceptions import HTTPNotFound -import async_timeout from homeassistant.components import frontend from homeassistant.components.http import HomeAssistantView @@ -267,7 +266,7 @@ class MailboxMediaView(MailboxView): mailbox = self.get_mailbox(platform) with suppress(asyncio.CancelledError, asyncio.TimeoutError): - async with async_timeout.timeout(10): + async with asyncio.timeout(10): try: stream = await mailbox.async_get_media(msgid) except StreamError as err: diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index 59c5ec9efc8..a2aa2c5ceff 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from contextlib import suppress -import async_timeout from matter_server.client import MatterClient from matter_server.client.exceptions import CannotConnect, InvalidServerVersion from matter_server.common.errors import MatterError, NodeCommissionFailed, NodeNotExists @@ -42,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: matter_client = MatterClient(entry.data[CONF_URL], async_get_clientsession(hass)) try: - async with async_timeout.timeout(CONNECT_TIMEOUT): + async with asyncio.timeout(CONNECT_TIMEOUT): await matter_client.connect() except (CannotConnect, asyncio.TimeoutError) as err: raise ConfigEntryNotReady("Failed to connect to matter server") from err @@ -87,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: - async with async_timeout.timeout(LISTEN_READY_TIMEOUT): + async with asyncio.timeout(LISTEN_READY_TIMEOUT): await init_ready.wait() except asyncio.TimeoutError as err: listen_task.cancel() diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 1322a7db300..f375b8a75cd 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -1,11 +1,11 @@ """The Mazda Connected Services integration.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging from typing import TYPE_CHECKING -import async_timeout from pymazda import ( Client as MazdaAPI, MazdaAccountLockedException, @@ -53,7 +53,7 @@ PLATFORMS = [ async def with_timeout(task, timeout_seconds=30): """Run an async task with a timeout.""" - async with async_timeout.timeout(timeout_seconds): + async with asyncio.timeout(timeout_seconds): return await task diff --git a/homeassistant/components/meater/__init__.py b/homeassistant/components/meater/__init__.py index 6db3093567d..12fdb7f3a06 100644 --- a/homeassistant/components/meater/__init__.py +++ b/homeassistant/components/meater/__init__.py @@ -1,8 +1,8 @@ """The Meater Temperature Probe integration.""" +import asyncio from datetime import timedelta import logging -import async_timeout from meater import ( AuthenticationError, MeaterApi, @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. - async with async_timeout.timeout(10): + async with asyncio.timeout(10): devices: list[MeaterProbe] = await meater_api.get_all_devices() except AuthenticationError as err: raise ConfigEntryAuthFailed("The API call wasn't authenticated") from err diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 501fa5c3fb8..2acb516fa95 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -19,7 +19,6 @@ from urllib.parse import quote, urlparse from aiohttp import web from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE from aiohttp.typedefs import LooseHeaders -import async_timeout import voluptuous as vol from yarl import URL @@ -1259,7 +1258,7 @@ async def async_fetch_image( content, content_type = (None, None) websession = async_get_clientsession(hass) with suppress(asyncio.TimeoutError): - async with async_timeout.timeout(10): + async with asyncio.timeout(10): response = await websession.get(url) if response.status == HTTPStatus.OK: content = await response.read() diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 2d7354f250f..68b40d8567f 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -7,7 +7,6 @@ import logging from typing import Any from aiohttp import ClientConnectionError -from async_timeout import timeout from pymelcloud import Device, get_devices import voluptuous as vol @@ -152,7 +151,7 @@ async def mel_devices_setup( """Query connected devices from MELCloud.""" session = async_get_clientsession(hass) try: - async with timeout(10): + async with asyncio.timeout(10): all_devices = await get_devices( token, session, diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index 3d6d42c8b7a..0ff17ea751a 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -5,7 +5,6 @@ import asyncio from http import HTTPStatus from aiohttp import ClientError, ClientResponseError -from async_timeout import timeout import pymelcloud import voluptuous as vol @@ -78,7 +77,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): """Create client.""" try: - async with timeout(10): + async with asyncio.timeout(10): if (acquired_token := token) is None: acquired_token = await pymelcloud.login( username, diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index f57a9146858..6e47ad79f5b 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -7,7 +7,6 @@ import logging import aiohttp from aiohttp.hdrs import CONTENT_TYPE -import async_timeout import voluptuous as vol from homeassistant.components import camera @@ -314,7 +313,7 @@ class MicrosoftFace: payload = None try: - async with async_timeout.timeout(self.timeout): + async with asyncio.timeout(self.timeout): response = await getattr(self.websession, method)( url, data=payload, headers=headers, params=params ) diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index dab5b477ede..a2b2de4eda8 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -7,7 +7,6 @@ from contextlib import suppress import aiohttp from aiohttp import web -import async_timeout import httpx from yarl import URL @@ -144,7 +143,7 @@ class MjpegCamera(Camera): websession = async_get_clientsession(self.hass, verify_ssl=self._verify_ssl) try: - async with async_timeout.timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT): response = await websession.get(self._still_image_url, auth=self._auth) image = await response.read() @@ -206,7 +205,7 @@ class MjpegCamera(Camera): async for chunk in stream.aiter_bytes(BUFFER_SIZE): if not self.hass.is_running: break - async with async_timeout.timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT): await response.write(chunk) return response diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index dc9f8aaedcd..47b997e410c 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -7,7 +7,6 @@ from http import HTTPStatus import logging import aiohttp -import async_timeout from homeassistant.components.notify import ( ATTR_DATA, @@ -166,7 +165,7 @@ class MobileAppNotificationService(BaseNotificationService): target_data["registration_info"] = reg_info try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): response = await async_get_clientsession(self._hass).post( push_url, json=target_data ) diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index b8551682f1f..cd692f00537 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -1,8 +1,8 @@ """The Mullvad VPN integration.""" +import asyncio from datetime import timedelta import logging -import async_timeout from mullvad_api import MullvadAPI from homeassistant.config_entries import ConfigEntry @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Mullvad VPN integration.""" async def async_get_mullvad_api_data(): - async with async_timeout.timeout(10): + async with asyncio.timeout(10): api = await hass.async_add_executor_job(MullvadAPI) return api.data diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py index aa5e0d70fe9..cbbbbaa6a11 100644 --- a/homeassistant/components/mutesync/__init__.py +++ b/homeassistant/components/mutesync/__init__.py @@ -1,9 +1,9 @@ """The mütesync integration.""" from __future__ import annotations +import asyncio import logging -import async_timeout import mutesync from homeassistant.config_entries import ConfigEntry @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_data(): """Update the data.""" - async with async_timeout.timeout(2.5): + async with asyncio.timeout(2.5): state = await client.get_state() if state["muted"] is None or state["in_meeting"] is None: diff --git a/homeassistant/components/mutesync/config_flow.py b/homeassistant/components/mutesync/config_flow.py index 7ebbc718a5b..e06c0b07c87 100644 --- a/homeassistant/components/mutesync/config_flow.py +++ b/homeassistant/components/mutesync/config_flow.py @@ -5,7 +5,6 @@ import asyncio from typing import Any import aiohttp -import async_timeout import mutesync import voluptuous as vol @@ -27,7 +26,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, """ session = async_get_clientsession(hass) try: - async with async_timeout.timeout(5): + async with asyncio.timeout(5): token = await mutesync.authenticate(session, data["host"]) except aiohttp.ClientResponseError as error: if error.status == 403: diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 1d016a791e3..ce602e6266d 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -9,7 +9,6 @@ import socket import sys from typing import Any -import async_timeout from mysensors import BaseAsyncGateway, Message, Sensor, mysensors import voluptuous as vol @@ -107,7 +106,7 @@ async def try_connect( connect_task = None try: connect_task = asyncio.create_task(gateway.start()) - async with async_timeout.timeout(GATEWAY_READY_TIMEOUT): + async with asyncio.timeout(GATEWAY_READY_TIMEOUT): await gateway_ready.wait() return True except asyncio.TimeoutError: @@ -299,7 +298,7 @@ async def _gw_start( # Gatways connected via mqtt doesn't send gateway ready message. return try: - async with async_timeout.timeout(GATEWAY_READY_TIMEOUT): + async with asyncio.timeout(GATEWAY_READY_TIMEOUT): await gateway_ready.wait() except asyncio.TimeoutError: _LOGGER.warning( diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 5004bafeb1b..d5881f52d8d 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -6,7 +6,6 @@ import logging from typing import cast from aiohttp.client_exceptions import ClientConnectorError, ClientError -import async_timeout from nettigo_air_monitor import ( ApiError, AuthFailedError, @@ -111,7 +110,7 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator[NAMSensors]): async def _async_update_data(self) -> NAMSensors: """Update data via library.""" try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): data = await self.nam.async_update() # We do not need to catch AuthFailed exception here because sensor data is # always available without authorization. diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index eef4c33e5f0..7eee84a66a4 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -8,7 +8,6 @@ import logging from typing import Any from aiohttp.client_exceptions import ClientConnectorError -import async_timeout from nettigo_air_monitor import ( ApiError, AuthFailedError, @@ -51,7 +50,7 @@ async def async_get_config(hass: HomeAssistant, host: str) -> NamConfig: options = ConnectionOptions(host) nam = await NettigoAirMonitor.create(websession, options) - async with async_timeout.timeout(10): + async with asyncio.timeout(10): mac = await nam.async_get_mac_address() return NamConfig(mac, nam.auth_enabled) @@ -67,7 +66,7 @@ async def async_check_credentials( nam = await NettigoAirMonitor.create(websession, options) - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await nam.async_check_credentials() diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 3865136b2ac..011b487910f 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -7,7 +7,6 @@ import logging from typing import TypeVar from aiohttp.client_exceptions import ClientConnectorError -from async_timeout import timeout from nextdns import ( AnalyticsDnssec, AnalyticsEncryption, @@ -75,7 +74,7 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]): async def _async_update_data(self) -> CoordinatorDataT: """Update data via internal method.""" try: - async with timeout(10): + async with asyncio.timeout(10): return await self._async_update_data_internal() except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: raise UpdateFailed(err) from err @@ -162,7 +161,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: websession = async_get_clientsession(hass) try: - async with timeout(10): + async with asyncio.timeout(10): nextdns = await NextDns.create(websession, api_key) except (ApiError, ClientConnectorError, asyncio.TimeoutError) as err: raise ConfigEntryNotReady from err diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py index 5c9bf04cfc1..3985644a478 100644 --- a/homeassistant/components/nextdns/config_flow.py +++ b/homeassistant/components/nextdns/config_flow.py @@ -5,7 +5,6 @@ import asyncio from typing import Any from aiohttp.client_exceptions import ClientConnectorError -from async_timeout import timeout from nextdns import ApiError, InvalidApiKeyError, NextDns import voluptuous as vol @@ -38,7 +37,7 @@ class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: self.api_key = user_input[CONF_API_KEY] try: - async with timeout(10): + async with asyncio.timeout(10): self.nextdns = await NextDns.create( websession, user_input[CONF_API_KEY] ) diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index fbb8e32bebe..dfb556deeb5 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -1,11 +1,11 @@ """The Nina integration.""" from __future__ import annotations +import asyncio from dataclasses import dataclass import re from typing import Any -from async_timeout import timeout from pynina import ApiError, Nina from homeassistant.config_entries import ConfigEntry @@ -103,7 +103,7 @@ class NINADataUpdateCoordinator( async def _async_update_data(self) -> dict[str, list[NinaWarningData]]: """Update data.""" - async with timeout(10): + async with asyncio.timeout(10): try: await self._nina.update() except ApiError as err: diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 6688888df01..e91b5cec92d 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -6,7 +6,6 @@ import logging import aiohttp from aiohttp.hdrs import AUTHORIZATION, USER_AGENT -import async_timeout import voluptuous as vol from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME @@ -100,7 +99,7 @@ async def _update_no_ip( } try: - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): resp = await session.get(url, params=params, headers=headers) body = await resp.text() diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index f72abc410ef..3b846d73477 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1,6 +1,7 @@ """The nuki component.""" from __future__ import annotations +import asyncio from collections import defaultdict from datetime import timedelta from http import HTTPStatus @@ -8,7 +9,6 @@ import logging from typing import Generic, TypeVar from aiohttp import web -import async_timeout from pynuki import NukiBridge, NukiLock, NukiOpener from pynuki.bridge import InvalidCredentialsException from pynuki.device import NukiDevice @@ -126,7 +126,7 @@ async def _create_webhook( ir.async_delete_issue(hass, DOMAIN, "https_webhook") try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await hass.async_add_executor_job( _register_webhook, bridge, entry.entry_id, url ) @@ -216,7 +216,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Stop and remove the Nuki webhook.""" webhook.async_unregister(hass, entry.entry_id) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await hass.async_add_executor_job( _remove_webhook, bridge, entry.entry_id ) @@ -252,7 +252,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload the Nuki entry.""" webhook.async_unregister(hass, entry.entry_id) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await hass.async_add_executor_job( _remove_webhook, hass.data[DOMAIN][entry.entry_id][DATA_BRIDGE], @@ -301,7 +301,7 @@ class NukiCoordinator(DataUpdateCoordinator[None]): try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. - async with async_timeout.timeout(10): + async with asyncio.timeout(10): events = await self.hass.async_add_executor_job( self.update_devices, self.locks + self.openers ) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 9ffe1016aec..8b0d8fe4640 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -1,12 +1,12 @@ """The nut component.""" from __future__ import annotations +import asyncio from dataclasses import dataclass from datetime import timedelta import logging from typing import cast -import async_timeout from pynut2.nut2 import PyNUTClient, PyNUTError from homeassistant.config_entries import ConfigEntry @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_data() -> dict[str, str]: """Fetch data from NUT.""" - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await hass.async_add_executor_job(data.update) if not data.status: raise UpdateFailed("Error fetching UPS state") diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py index c037619d31b..7326fa50dd5 100644 --- a/homeassistant/components/nzbget/coordinator.py +++ b/homeassistant/components/nzbget/coordinator.py @@ -1,10 +1,10 @@ """Provides the NZBGet DataUpdateCoordinator.""" +import asyncio from collections.abc import Mapping from datetime import timedelta import logging from typing import Any -from async_timeout import timeout from pynzbgetapi import NZBGetAPI, NZBGetAPIException from homeassistant.const import ( @@ -96,7 +96,7 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): } try: - async with timeout(4): + async with asyncio.timeout(4): return await self.hass.async_add_executor_job(_update_data) except NZBGetAPIException as error: raise UpdateFailed(f"Invalid response from API: {error}") from error From 8b0fdd6fd21832ec4a2ef4a66f38676681c61f88 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Aug 2023 14:34:18 +0200 Subject: [PATCH 0527/1151] Use asyncio.timeout [s-z] (#98452) --- homeassistant/components/songpal/media_player.py | 3 +-- homeassistant/components/sonos/speaker.py | 3 +-- homeassistant/components/squeezebox/config_flow.py | 3 +-- homeassistant/components/srp_energy/sensor.py | 4 ++-- homeassistant/components/starlink/coordinator.py | 8 ++++---- homeassistant/components/startca/sensor.py | 4 ++-- homeassistant/components/supla/__init__.py | 4 ++-- homeassistant/components/switchbot/coordinator.py | 3 +-- homeassistant/components/syncthru/__init__.py | 4 ++-- homeassistant/components/system_bridge/__init__.py | 7 +++---- .../components/system_bridge/config_flow.py | 3 +-- .../components/system_bridge/coordinator.py | 3 +-- homeassistant/components/tado/device_tracker.py | 3 +-- homeassistant/components/tellduslive/config_flow.py | 3 +-- homeassistant/components/thethingsnetwork/sensor.py | 3 +-- .../components/tplink_omada/coordinator.py | 4 ++-- homeassistant/components/tradfri/config_flow.py | 3 +-- homeassistant/components/unifi/controller.py | 5 ++--- homeassistant/components/upb/config_flow.py | 3 +-- homeassistant/components/upnp/__init__.py | 3 +-- homeassistant/components/viaggiatreno/sensor.py | 3 +-- homeassistant/components/voicerss/tts.py | 3 +-- homeassistant/components/voip/voip.py | 13 ++++++------- homeassistant/components/volvooncall/__init__.py | 4 ++-- homeassistant/components/webostv/media_player.py | 3 +-- homeassistant/components/worxlandroid/sensor.py | 3 +-- homeassistant/components/wyoming/data.py | 5 +---- homeassistant/components/xiaomi_miio/__init__.py | 6 +++--- homeassistant/components/yandextts/tts.py | 3 +-- homeassistant/components/yeelight/scanner.py | 3 +-- homeassistant/components/yolink/__init__.py | 3 +-- homeassistant/components/yolink/coordinator.py | 4 ++-- homeassistant/components/zwave_js/__init__.py | 3 +-- homeassistant/components/zwave_js/config_flow.py | 3 +-- tests/components/sonos/test_init.py | 8 +------- tests/components/upb/test_config_flow.py | 2 +- tests/components/voip/test_voip.py | 9 ++++----- tests/components/wemo/test_wemo_device.py | 5 ++--- 38 files changed, 62 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index bc096d23437..2d2c5892636 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -5,7 +5,6 @@ import asyncio from collections import OrderedDict import logging -import async_timeout from songpal import ( ConnectChange, ContentChange, @@ -68,7 +67,7 @@ async def async_setup_entry( device = Device(endpoint) try: - async with async_timeout.timeout( + async with asyncio.timeout( 10 ): # set timeout to avoid blocking the setup process await device.get_supported_methods() diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index e576d3f7908..b73ca6a77e4 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -10,7 +10,6 @@ import logging import time from typing import Any, cast -import async_timeout import defusedxml.ElementTree as ET from soco.core import SoCo from soco.events_base import Event as SonosEvent, SubscriptionBase @@ -1122,7 +1121,7 @@ class SonosSpeaker: return True try: - async with async_timeout.timeout(5): + async with asyncio.timeout(5): while not _test_groups(groups): await hass.data[DATA_SONOS].topology_condition.wait() except asyncio.TimeoutError: diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index bb175ee00be..2c96046b97c 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -4,7 +4,6 @@ from http import HTTPStatus import logging from typing import TYPE_CHECKING -import async_timeout from pysqueezebox import Server, async_discover import voluptuous as vol @@ -131,7 +130,7 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # no host specified, see if we can discover an unconfigured LMS server try: - async with async_timeout.timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT): await self._discover() return await self.async_step_edit() except asyncio.TimeoutError: diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index cdfd53d40a0..946b2aedb13 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -1,9 +1,9 @@ """Support for SRP Energy Sensor.""" from __future__ import annotations +import asyncio from datetime import timedelta -import async_timeout from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout from homeassistant.components.sensor import ( @@ -52,7 +52,7 @@ async def async_setup_entry( end_date = dt_util.now(phx_time_zone) start_date = end_date - timedelta(days=1) - async with async_timeout.timeout(10): + async with asyncio.timeout(10): hourly_usage = await hass.async_add_executor_job( api.usage, start_date, diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index f6f3623f8d4..3359706372e 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -1,11 +1,11 @@ """Contains the shared Coordinator for Starlink systems.""" from __future__ import annotations +import asyncio from dataclasses import dataclass from datetime import timedelta import logging -import async_timeout from starlink_grpc import ( AlertDict, ChannelContext, @@ -48,7 +48,7 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): ) async def _async_update_data(self) -> StarlinkData: - async with async_timeout.timeout(4): + async with asyncio.timeout(4): try: status = await self.hass.async_add_executor_job( status_data, self.channel_context @@ -59,7 +59,7 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): async def async_stow_starlink(self, stow: bool) -> None: """Set whether Starlink system tied to this coordinator should be stowed.""" - async with async_timeout.timeout(4): + async with asyncio.timeout(4): try: await self.hass.async_add_executor_job( set_stow_state, not stow, self.channel_context @@ -69,7 +69,7 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): async def async_reboot_starlink(self) -> None: """Reboot the Starlink system tied to this coordinator.""" - async with async_timeout.timeout(4): + async with asyncio.timeout(4): try: await self.hass.async_add_executor_job(reboot, self.channel_context) except GrpcError as exc: diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 3334afded00..50224944849 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -1,12 +1,12 @@ """Support for Start.ca Bandwidth Monitor.""" from __future__ import annotations +import asyncio from datetime import timedelta from http import HTTPStatus import logging from xml.parsers.expat import ExpatError -import async_timeout import voluptuous as vol import xmltodict @@ -213,7 +213,7 @@ class StartcaData: """Get the Start.ca bandwidth data from the web service.""" _LOGGER.debug("Updating Start.ca usage data") url = f"https://www.start.ca/support/usage/api?key={self.api_key}" - async with async_timeout.timeout(REQUEST_TIMEOUT): + async with asyncio.timeout(REQUEST_TIMEOUT): req = await self.websession.get(url) if req.status != HTTPStatus.OK: _LOGGER.error("Request failed with status: %u", req.status) diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 0d1308ca5a6..14d617ba88e 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -1,10 +1,10 @@ """Support for Supla devices.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging -import async_timeout from asyncpysupla import SuplaAPI import voluptuous as vol @@ -99,7 +99,7 @@ async def discover_devices(hass, hass_config): for server_name, server in hass.data[DOMAIN][SUPLA_SERVERS].items(): async def _fetch_channels(): - async with async_timeout.timeout(SCAN_INTERVAL.total_seconds()): + async with asyncio.timeout(SCAN_INTERVAL.total_seconds()): channels = { channel["id"]: channel # pylint: disable-next=cell-var-from-loop diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index c12e8122e52..39f2a4aa6da 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -6,7 +6,6 @@ import contextlib import logging from typing import TYPE_CHECKING -import async_timeout import switchbot from switchbot import SwitchbotModel @@ -117,7 +116,7 @@ class SwitchbotDataUpdateCoordinator(ActiveBluetoothDataUpdateCoordinator[None]) async def async_wait_ready(self) -> bool: """Wait for the device to be ready.""" with contextlib.suppress(asyncio.TimeoutError): - async with async_timeout.timeout(DEVICE_STARTUP_TIMEOUT): + async with asyncio.timeout(DEVICE_STARTUP_TIMEOUT): await self._ready_event.wait() return True return False diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index db546266328..8d17f038819 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -1,10 +1,10 @@ """The syncthru component.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging -import async_timeout from pysyncthru import ConnectionMode, SyncThru, SyncThruAPINotSupported from homeassistant.config_entries import ConfigEntry @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_data() -> SyncThru: """Fetch data from the printer.""" try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await printer.update() except SyncThruAPINotSupported as api_error: # if an exception is thrown, printer does not support syncthru diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 29b127bf8db..058d03163ef 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio import logging -import async_timeout from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, @@ -67,7 +66,7 @@ async def async_setup_entry( session=async_get_clientsession(hass), ) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): if not await version.check_supported(): raise ConfigEntryNotReady( "You are not running a supported version of System Bridge. Please" @@ -91,7 +90,7 @@ async def async_setup_entry( entry=entry, ) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await coordinator.async_get_data(MODULES) except AuthenticationException as exception: _LOGGER.error("Authentication failed for %s: %s", entry.title, exception) @@ -109,7 +108,7 @@ async def async_setup_entry( try: # Wait for initial data - async with async_timeout.timeout(10): + async with asyncio.timeout(10): while not coordinator.is_ready: _LOGGER.debug( "Waiting for initial data from %s (%s)", diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index a73740e5dbd..a7dea5d6ab2 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -6,7 +6,6 @@ from collections.abc import Mapping import logging from typing import Any -import async_timeout from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, @@ -55,7 +54,7 @@ async def _validate_input( data[CONF_API_KEY], ) try: - async with async_timeout.timeout(15): + async with asyncio.timeout(15): await websocket_client.connect(session=async_get_clientsession(hass)) hass.async_create_task(websocket_client.listen()) response = await websocket_client.get_data(GetData(modules=["system"])) diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index adb88efd5ec..145e01ed29a 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -7,7 +7,6 @@ from datetime import timedelta import logging from typing import Any -import async_timeout from pydantic import BaseModel # pylint: disable=no-name-in-module from systembridgeconnector.exceptions import ( AuthenticationException, @@ -183,7 +182,7 @@ class SystemBridgeDataUpdateCoordinator( async def _setup_websocket(self) -> None: """Use WebSocket for updates.""" try: - async with async_timeout.timeout(20): + async with asyncio.timeout(20): await self.websocket_client.connect( session=async_get_clientsession(self.hass), ) diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 4d50bc35c3b..1365c9f23a3 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -8,7 +8,6 @@ from http import HTTPStatus import logging import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -109,7 +108,7 @@ class TadoDeviceScanner(DeviceScanner): last_results = [] try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): # Format the URL here, so we can log the template URL if # anything goes wrong without exposing username and password. url = self.tadoapiurl.format( diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index c87b3998a27..060b90a7d70 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -3,7 +3,6 @@ import asyncio import logging import os -import async_timeout from tellduslive import Session, supports_local_api import voluptuous as vol @@ -91,7 +90,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): auth_url = await self.hass.async_add_executor_job(self._get_auth_url) if not auth_url: return self.async_abort(reason="unknown_authorize_url_generation") diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index e14bd944d36..06005d7e4ed 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -7,7 +7,6 @@ import logging import aiohttp from aiohttp.hdrs import ACCEPT, AUTHORIZATION -import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -134,7 +133,7 @@ class TtnDataStorage: """Get the current state from The Things Network Data Storage.""" try: session = async_get_clientsession(self._hass) - async with async_timeout.timeout(DEFAULT_TIMEOUT): + async with asyncio.timeout(DEFAULT_TIMEOUT): response = await session.get(self._url, headers=self._headers) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/tplink_omada/coordinator.py b/homeassistant/components/tplink_omada/coordinator.py index 3ff73501bdc..e9048a678ca 100644 --- a/homeassistant/components/tplink_omada/coordinator.py +++ b/homeassistant/components/tplink_omada/coordinator.py @@ -1,9 +1,9 @@ """Generic Omada API coordinator.""" +import asyncio from datetime import timedelta import logging from typing import Generic, TypeVar -import async_timeout from tplink_omada_client.exceptions import OmadaClientException from tplink_omada_client.omadaclient import OmadaSiteClient @@ -37,7 +37,7 @@ class OmadaCoordinator(DataUpdateCoordinator[dict[str, T]], Generic[T]): async def _async_update_data(self) -> dict[str, T]: """Fetch data from API endpoint.""" try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return await self.poll_update() except OmadaClientException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 1e9b63bb325..2a3052c1f7b 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -5,7 +5,6 @@ import asyncio from typing import Any from uuid import uuid4 -import async_timeout from pytradfri import Gateway, RequestError from pytradfri.api.aiocoap_api import APIFactory import voluptuous as vol @@ -141,7 +140,7 @@ async def authenticate( api_factory = await APIFactory.init(host, psk_id=identity) try: - async with async_timeout.timeout(5): + async with asyncio.timeout(5): key = await api_factory.generate_psk(security_code) except RequestError as err: raise AuthError("invalid_security_code") from err diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 6ac4e622736..649d7c30fdb 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -11,7 +11,6 @@ from aiohttp import CookieJar import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.websocket import WebsocketState -import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -375,7 +374,7 @@ class UniFiController: async def async_reconnect(self) -> None: """Try to reconnect UniFi Network session.""" try: - async with async_timeout.timeout(5): + async with asyncio.timeout(5): await self.api.login() self.api.start_websocket() @@ -444,7 +443,7 @@ async def get_unifi_controller( ) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await controller.check_unifi_os() await controller.login() return controller diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py index 728d46acd76..318ba44f557 100644 --- a/homeassistant/components/upb/config_flow.py +++ b/homeassistant/components/upb/config_flow.py @@ -4,7 +4,6 @@ from contextlib import suppress import logging from urllib.parse import urlparse -import async_timeout import upb_lib import voluptuous as vol @@ -45,7 +44,7 @@ async def _validate_input(data): upb.connect(_connected_callback) with suppress(asyncio.TimeoutError): - async with async_timeout.timeout(VALIDATE_TIMEOUT): + async with asyncio.timeout(VALIDATE_TIMEOUT): await connected_event.wait() upb.disconnect() diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 5f77d58c5ea..bb505c08ad0 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from datetime import timedelta -import async_timeout from async_upnp_client.exceptions import UpnpConnectionError from homeassistant.components import ssdp @@ -71,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await device_discovered_event.wait() except asyncio.TimeoutError as err: raise ConfigEntryNotReady(f"Device not discovered: {usn}") from err diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 9326db64d0a..4043cc865c7 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -7,7 +7,6 @@ import logging import time import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -79,7 +78,7 @@ async def async_http_request(hass, uri): """Perform actual request.""" try: session = async_get_clientsession(hass) - async with async_timeout.timeout(REQUEST_TIMEOUT): + async with asyncio.timeout(REQUEST_TIMEOUT): req = await session.get(uri) if req.status != HTTPStatus.OK: return {"error": req.status} diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index 072e0ee431d..5bdc8bee3ac 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -4,7 +4,6 @@ from http import HTTPStatus import logging import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider @@ -196,7 +195,7 @@ class VoiceRSSProvider(Provider): form_data["hl"] = language try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): request = await websession.post(VOICERSS_API_URL, data=form_data) if request.status != HTTPStatus.OK: diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index ca78b604169..efa62e0e8f4 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -10,7 +10,6 @@ from pathlib import Path import time from typing import TYPE_CHECKING -import async_timeout from voip_utils import ( CallInfo, RtcpState, @@ -259,7 +258,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): self._clear_audio_queue() # Run pipeline with a timeout - async with async_timeout.timeout(self.pipeline_timeout): + async with asyncio.timeout(self.pipeline_timeout): await async_pipeline_from_audio_stream( self.hass, context=self._context, @@ -315,7 +314,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): """ # Timeout if no audio comes in for a while. # This means the caller hung up. - async with async_timeout.timeout(self.audio_timeout): + async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() while chunk: @@ -326,7 +325,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): # Buffer until command starts return True - async with async_timeout.timeout(self.audio_timeout): + async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() return False @@ -343,7 +342,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): # Timeout if no audio comes in for a while. # This means the caller hung up. - async with async_timeout.timeout(self.audio_timeout): + async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() while chunk: @@ -353,7 +352,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): yield chunk - async with async_timeout.timeout(self.audio_timeout): + async with asyncio.timeout(self.audio_timeout): chunk = await self._audio_queue.get() def _clear_audio_queue(self) -> None: @@ -395,7 +394,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): tts_samples = len(audio_bytes) / (WIDTH * CHANNELS) tts_seconds = tts_samples / RATE - async with async_timeout.timeout(tts_seconds + self.tts_extra_timeout): + async with asyncio.timeout(tts_seconds + self.tts_extra_timeout): # Assume TTS audio is 16Khz 16-bit mono await self._async_send_audio(audio_bytes) except asyncio.TimeoutError as err: diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 06f8d0ad5a2..4ec1bf4a4ba 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -1,9 +1,9 @@ """Support for Volvo On Call.""" +import asyncio import logging from aiohttp.client_exceptions import ClientResponseError -import async_timeout from volvooncall import Connection from volvooncall.dashboard import Instrument @@ -186,7 +186,7 @@ class VolvoUpdateCoordinator(DataUpdateCoordinator[None]): async def _async_update_data(self) -> None: """Fetch data from API endpoint.""" - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await self.volvo_data.update() diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 11903ebdd68..61bef8c693c 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -12,7 +12,6 @@ import ssl from typing import Any, Concatenate, ParamSpec, TypeVar, cast from aiowebostv import WebOsClient, WebOsTvPairError -import async_timeout from homeassistant import util from homeassistant.components.media_player import ( @@ -480,7 +479,7 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): websession = async_get_clientsession(self.hass) with suppress(asyncio.TimeoutError): - async with async_timeout.timeout(10): + async with asyncio.timeout(10): response = await websession.get(url, ssl=ssl_context) if response.status == HTTPStatus.OK: content = await response.read() diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 834a0b95f42..111acc5fff6 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -5,7 +5,6 @@ import asyncio import logging import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -95,7 +94,7 @@ class WorxLandroidSensor(SensorEntity): try: session = async_get_clientsession(self.hass) - async with async_timeout.timeout(self.timeout): + async with asyncio.timeout(self.timeout): auth = aiohttp.helpers.BasicAuth("admin", self.pin) mower_response = await session.get(self.url, auth=auth) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/wyoming/data.py b/homeassistant/components/wyoming/data.py index 1fe4d60b974..64b92eb8471 100644 --- a/homeassistant/components/wyoming/data.py +++ b/homeassistant/components/wyoming/data.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -import async_timeout from wyoming.client import AsyncTcpClient from wyoming.info import Describe, Info @@ -55,9 +54,7 @@ async def load_wyoming_info( for _ in range(retries + 1): try: - async with AsyncTcpClient(host, port) as client, async_timeout.timeout( - timeout - ): + async with AsyncTcpClient(host, port) as client, asyncio.timeout(timeout): # Describe -> Info await client.write_event(Describe().event()) while True: diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 541b077f6f0..0291ca2c8bd 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -1,13 +1,13 @@ """Support for Xiaomi Miio.""" from __future__ import annotations +import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass from datetime import timedelta import logging from typing import Any -import async_timeout from miio import ( AirFresh, AirFreshA1, @@ -176,7 +176,7 @@ def _async_update_data_default(hass, device): async def _async_fetch_data(): """Fetch data from the device.""" - async with async_timeout.timeout(POLLING_TIMEOUT_SEC): + async with asyncio.timeout(POLLING_TIMEOUT_SEC): state = await hass.async_add_executor_job(device.status) _LOGGER.debug("Got new state: %s", state) return state @@ -265,7 +265,7 @@ def _async_update_data_vacuum( """Fetch data from the device using async_add_executor_job.""" async def execute_update() -> VacuumCoordinatorData: - async with async_timeout.timeout(POLLING_TIMEOUT_SEC): + async with asyncio.timeout(POLLING_TIMEOUT_SEC): state = await hass.async_add_executor_job(update) _LOGGER.debug("Got new vacuum state: %s", state) return state diff --git a/homeassistant/components/yandextts/tts.py b/homeassistant/components/yandextts/tts.py index 755207c272d..481678100de 100644 --- a/homeassistant/components/yandextts/tts.py +++ b/homeassistant/components/yandextts/tts.py @@ -4,7 +4,6 @@ from http import HTTPStatus import logging import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider @@ -120,7 +119,7 @@ class YandexSpeechKitProvider(Provider): actual_language = language try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): url_param = { "text": message, "lang": actual_language, diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 7c6bbd2d2ee..43e976eeeac 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -10,7 +10,6 @@ import logging from typing import Self from urllib.parse import urlparse -import async_timeout from async_upnp_client.search import SsdpSearchListener from async_upnp_client.utils import CaseInsensitiveDict @@ -157,7 +156,7 @@ class YeelightScanner: listener.async_search((host, SSDP_TARGET[1])) with contextlib.suppress(asyncio.TimeoutError): - async with async_timeout.timeout(DISCOVERY_TIMEOUT): + async with asyncio.timeout(DISCOVERY_TIMEOUT): await host_event.wait() self._host_discovered_events[host].remove(host_event) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index c3633800685..20129b819ce 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from datetime import timedelta from typing import Any -import async_timeout from yolink.const import ATTR_DEVICE_SMART_REMOTER from yolink.device import YoLinkDevice from yolink.exception import YoLinkAuthFailError, YoLinkClientError @@ -111,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) yolink_home = YoLinkHome() try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await yolink_home.async_setup( auth_mgr, YoLinkHomeMessageListener(hass, entry) ) diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py index e322961d179..9055b2d044e 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -1,10 +1,10 @@ """YoLink DataUpdateCoordinator.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging -import async_timeout from yolink.device import YoLinkDevice from yolink.exception import YoLinkAuthFailError, YoLinkClientError @@ -41,7 +41,7 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): async def _async_update_data(self) -> dict: """Fetch device state.""" try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): device_state_resp = await self.device.fetch_state() device_state = device_state_resp.data.get(ATTR_DEVICE_STATE) if self.paired_device is not None and device_state is not None: diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index d477964d229..2e6ff4f0b34 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -7,7 +7,6 @@ from collections.abc import Coroutine from contextlib import suppress 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, RemoveNodeReason from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion @@ -146,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # connect and throw error if connection failed try: - async with timeout(CONNECT_TIMEOUT): + async with asyncio.timeout(CONNECT_TIMEOUT): await client.connect() except InvalidServerVersion as err: if use_addon: diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 071b562ceea..752e3545114 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -7,7 +7,6 @@ import logging from typing import Any import aiohttp -from async_timeout import timeout from serial.tools import list_ports import voluptuous as vol from zwave_js_server.version import VersionInfo, get_server_version @@ -115,7 +114,7 @@ async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo: async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo: """Return Z-Wave JS version info.""" try: - async with timeout(SERVER_VERSION_TIMEOUT): + async with asyncio.timeout(SERVER_VERSION_TIMEOUT): version_info: VersionInfo = await get_server_version( ws_address, async_get_clientsession(hass) ) diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index d4072055407..a3f74127283 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -1,14 +1,8 @@ """Tests for the Sonos config flow.""" import asyncio import logging -import sys from unittest.mock import Mock, patch -if sys.version_info[:2] < (3, 11): - from async_timeout import timeout as asyncio_timeout -else: - from asyncio import timeout as asyncio_timeout - import pytest from homeassistant import config_entries, data_entry_flow @@ -377,7 +371,7 @@ async def test_async_poll_manual_hosts_6( caplog.clear() # The discovery events should not fire, wait with a timeout. with pytest.raises(asyncio.TimeoutError): - async with asyncio_timeout(1.0): + async with asyncio.timeout(1.0): await speaker_1_activity.event.wait() await hass.async_block_till_done() assert "Activity on Living Room" not in caplog.text diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index 40f2b5591f1..d2fbe27248d 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -82,7 +82,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: from asyncio import TimeoutError with patch( - "homeassistant.components.upb.config_flow.async_timeout.timeout", + "homeassistant.components.upb.config_flow.asyncio.timeout", side_effect=TimeoutError, ): result = await valid_tcp_flow(hass, sync_complete=False) diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index 9b3f5d963dc..361e4e7f0e2 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -3,7 +3,6 @@ import asyncio import time from unittest.mock import AsyncMock, Mock, patch -import async_timeout import pytest from homeassistant.components import assist_pipeline, voip @@ -118,7 +117,7 @@ async def test_pipeline( rtp_protocol.on_chunk(bytes(_ONE_SECOND)) # Wait for mock pipeline to exhaust the audio stream - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await done.wait() @@ -159,7 +158,7 @@ async def test_pipeline_timeout(hass: HomeAssistant, voip_device: VoIPDevice) -> rtp_protocol.on_chunk(bytes(_ONE_SECOND)) # Wait for mock pipeline to time out - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await done.wait() @@ -200,7 +199,7 @@ async def test_stt_stream_timeout(hass: HomeAssistant, voip_device: VoIPDevice) rtp_protocol.on_chunk(bytes(_ONE_SECOND)) # Wait for mock pipeline to time out - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await done.wait() @@ -319,5 +318,5 @@ async def test_tts_timeout( rtp_protocol.on_chunk(bytes(_ONE_SECOND * 4)) # Wait for mock pipeline to exhaust the audio stream - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await done.wait() diff --git a/tests/components/wemo/test_wemo_device.py b/tests/components/wemo/test_wemo_device.py index b715dd4ba72..5c8353fc8bc 100644 --- a/tests/components/wemo/test_wemo_device.py +++ b/tests/components/wemo/test_wemo_device.py @@ -4,7 +4,6 @@ from dataclasses import asdict from datetime import timedelta from unittest.mock import call, patch -import async_timeout import pytest from pywemo.exceptions import ActionException, PyWeMoException from pywemo.subscribe import EVENT_TYPE_LONG_PRESS @@ -77,7 +76,7 @@ async def test_long_press_event( "testing_params", ) - async with async_timeout.timeout(8): + async with asyncio.timeout(8): await got_event.wait() assert event_data == { @@ -108,7 +107,7 @@ async def test_subscription_callback( pywemo_registry.callbacks[device.wemo.name], device.wemo, "", "" ) - async with async_timeout.timeout(8): + async with asyncio.timeout(8): await got_callback.wait() assert device.last_update_success From 346a7292d73d27f43355b09a035ba84b8ebdaef3 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 15 Aug 2023 08:49:19 -0400 Subject: [PATCH 0528/1151] Update Enphase dry contact relay DeviceInfo and name (#98429) Switch relay binary_sensor to relay device --- .../components/enphase_envoy/binary_sensor.py | 14 ++++++++------ .../components/enphase_envoy/strings.json | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index 68368719fc4..0e70a9fe98b 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -60,7 +60,9 @@ ENCHARGE_SENSORS = ( ) RELAY_STATUS_SENSOR = BinarySensorEntityDescription( - key="relay_status", icon="mdi:power-plug", has_entity_name=True + key="relay_status", + translation_key="relay", + icon="mdi:power-plug", ) @@ -219,17 +221,17 @@ class EnvoyRelayBinarySensorEntity(EnvoyBaseBinarySensorEntity): enpower = self.data.enpower assert enpower is not None self.relay_id = relay_id + self.relay = self.data.dry_contact_settings[self.relay_id] self._serial_number = enpower.serial_number self._attr_unique_id = f"{self._serial_number}_relay_{relay_id}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._serial_number)}, + identifiers={(DOMAIN, relay_id)}, manufacturer="Enphase", - model="Enpower", - name=f"Enpower {self._serial_number}", + model="Dry contact relay", + name=self.relay.load_name, sw_version=str(enpower.firmware_version), - via_device=(DOMAIN, self.envoy_serial_num), + via_device=(DOMAIN, enpower.serial_number), ) - self._attr_name = f"{self.data.dry_contact_settings[relay_id].load_name} Relay" @property def is_on(self) -> bool: diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index bab16bc6c58..0f292dfa8e3 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -34,6 +34,9 @@ }, "grid_status": { "name": "Grid status" + }, + "relay": { + "name": "Relay status" } }, "select": { From e2d2ec88178a5b1902dde15d41eddeb1d92ae64f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:30:20 +0200 Subject: [PATCH 0529/1151] Use asyncio.timeout [b-e] (#98448) --- homeassistant/components/blueprint/websocket_api.py | 4 ++-- homeassistant/components/canary/coordinator.py | 4 ++-- homeassistant/components/citybikes/sensor.py | 3 +-- .../components/color_extractor/__init__.py | 3 +-- .../components/comed_hourly_pricing/sensor.py | 3 +-- homeassistant/components/daikin/__init__.py | 3 +-- homeassistant/components/daikin/config_flow.py | 3 +-- homeassistant/components/deconz/config_flow.py | 7 +++---- homeassistant/components/deconz/gateway.py | 3 +-- .../components/devolo_home_network/__init__.py | 12 ++++++------ homeassistant/components/doorbird/camera.py | 3 +-- homeassistant/components/dsmr/config_flow.py | 3 +-- homeassistant/components/eafm/sensor.py | 4 ++-- .../components/electric_kiwi/coordinator.py | 4 ++-- homeassistant/components/elkm1/__init__.py | 3 +-- homeassistant/components/elmax/common.py | 4 ++-- homeassistant/components/emulated_hue/hue_api.py | 3 +-- homeassistant/components/escea/config_flow.py | 4 +--- .../components/esphome/bluetooth/client.py | 3 +-- homeassistant/components/esphome/voice_assistant.py | 13 ++++++------- .../components/evil_genius_labs/__init__.py | 8 ++++---- .../components/evil_genius_labs/config_flow.py | 3 +-- homeassistant/components/evil_genius_labs/light.py | 13 ++++++------- homeassistant/components/ezviz/coordinator.py | 4 ++-- tests/components/esphome/conftest.py | 4 ++-- tests/components/esphome/test_voice_assistant.py | 3 +-- 26 files changed, 53 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index a9bcf5ded1c..1732320c1e9 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -1,9 +1,9 @@ """Websocket API for blueprint.""" from __future__ import annotations +import asyncio from typing import Any, cast -import async_timeout import voluptuous as vol from homeassistant.components import websocket_api @@ -72,7 +72,7 @@ async def ws_import_blueprint( msg: dict[str, Any], ) -> None: """Import a blueprint.""" - async with async_timeout.timeout(10): + async with asyncio.timeout(10): imported_blueprint = await importer.fetch_blueprint_from_url(hass, msg["url"]) if imported_blueprint is None: diff --git a/homeassistant/components/canary/coordinator.py b/homeassistant/components/canary/coordinator.py index d81589020e3..1b47d6d70b7 100644 --- a/homeassistant/components/canary/coordinator.py +++ b/homeassistant/components/canary/coordinator.py @@ -1,11 +1,11 @@ """Provides the Canary DataUpdateCoordinator.""" from __future__ import annotations +import asyncio from collections.abc import ValuesView from datetime import timedelta import logging -from async_timeout import timeout from canary.api import Api from canary.model import Location, Reading from requests.exceptions import ConnectTimeout, HTTPError @@ -58,7 +58,7 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator[CanaryData]): """Fetch data from Canary.""" try: - async with timeout(15): + async with asyncio.timeout(15): return await self.hass.async_add_executor_job(self._update_data) except (ConnectTimeout, HTTPError) as error: raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index c87427e0e7e..fcd780dba7d 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -6,7 +6,6 @@ from datetime import timedelta import logging import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components.sensor import ( @@ -140,7 +139,7 @@ async def async_citybikes_request(hass, uri, schema): try: session = async_get_clientsession(hass) - async with async_timeout.timeout(REQUEST_TIMEOUT): + async with asyncio.timeout(REQUEST_TIMEOUT): req = await session.get(DEFAULT_ENDPOINT.format(uri=uri)) json_response = await req.json() diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index d0a6b53964b..0e27f396c6d 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -4,7 +4,6 @@ import io import logging import aiohttp -import async_timeout from colorthief import ColorThief from PIL import UnidentifiedImageError import voluptuous as vol @@ -120,7 +119,7 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: try: session = aiohttp_client.async_get_clientsession(hass) - async with async_timeout.timeout(10): + async with asyncio.timeout(10): response = await session.get(url) except (asyncio.TimeoutError, aiohttp.ClientError) as err: diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 3336f5b79f8..ef974b8f3ed 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -7,7 +7,6 @@ import json import logging import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components.sensor import ( @@ -112,7 +111,7 @@ class ComedHourlyPricingSensor(SensorEntity): else: url_string += "?type=currenthouraverage" - async with async_timeout.timeout(60): + async with asyncio.timeout(60): response = await self.websession.get(url_string) # The API responds with MIME type 'text/html' text = await response.text() diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index dcab26211c9..f6fd399f855 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -4,7 +4,6 @@ from datetime import timedelta import logging from aiohttp import ClientConnectionError -from async_timeout import timeout from pydaikin.daikin_base import Appliance from homeassistant.config_entries import ConfigEntry @@ -74,7 +73,7 @@ async def daikin_api_setup(hass: HomeAssistant, host, key, uuid, password): session = async_get_clientsession(hass) try: - async with timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT): device = await Appliance.factory( host, session, key=key, uuid=uuid, password=password ) diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index a64f2059972..2d5d1e12dfd 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -4,7 +4,6 @@ import logging from uuid import uuid4 from aiohttp import ClientError, web_exceptions -from async_timeout import timeout from pydaikin.daikin_base import Appliance, DaikinException from pydaikin.discovery import Discovery import voluptuous as vol @@ -70,7 +69,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): password = None try: - async with timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT): device = await Appliance.factory( host, async_get_clientsession(self.hass), diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 8eda93c2d46..c0361aa2bca 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -9,7 +9,6 @@ from pprint import pformat from typing import Any, cast from urllib.parse import urlparse -import async_timeout from pydeconz.errors import LinkButtonNotPressed, RequestError, ResponseError from pydeconz.gateway import DeconzSession from pydeconz.utils import ( @@ -101,7 +100,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): session = aiohttp_client.async_get_clientsession(self.hass) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): self.bridges = await deconz_discovery(session) except (asyncio.TimeoutError, ResponseError): @@ -159,7 +158,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): deconz_session = DeconzSession(session, self.host, self.port) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): api_key = await deconz_session.get_api_key() except LinkButtonNotPressed: @@ -180,7 +179,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): session = aiohttp_client.async_get_clientsession(self.hass) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): self.bridge_id = await deconz_get_bridge_id( session, self.host, self.port, self.api_key ) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index f4af7337427..156309c0903 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -7,7 +7,6 @@ from collections.abc import Callable from types import MappingProxyType from typing import TYPE_CHECKING, Any, cast -import async_timeout from pydeconz import DeconzSession, errors from pydeconz.interfaces import sensors from pydeconz.interfaces.api_handlers import APIHandler, GroupedAPIHandler @@ -353,7 +352,7 @@ async def get_deconz_session( config[CONF_API_KEY], ) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await deconz_session.refresh_state() return deconz_session diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index e70b28f9c3c..ed070abf0c8 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -1,10 +1,10 @@ """The devolo Home Network integration.""" from __future__ import annotations +import asyncio import logging from typing import Any -import async_timeout from devolo_plc_api import Device from devolo_plc_api.device_api import ( ConnectedStationInfo, @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from API endpoint.""" assert device.plcnet try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return await device.plcnet.async_get_network_overview() except DeviceUnavailable as err: raise UpdateFailed(err) from err @@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from API endpoint.""" assert device.device try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return await device.device.async_get_wifi_guest_access() except DeviceUnavailable as err: raise UpdateFailed(err) from err @@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from API endpoint.""" assert device.device try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return await device.device.async_get_led_setting() except DeviceUnavailable as err: raise UpdateFailed(err) from err @@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from API endpoint.""" assert device.device try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return await device.device.async_get_wifi_connected_station() except DeviceUnavailable as err: raise UpdateFailed(err) from err @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from API endpoint.""" assert device.device try: - async with async_timeout.timeout(30): + async with asyncio.timeout(30): return await device.device.async_get_wifi_neighbor_access_points() except DeviceUnavailable as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index c1c8a622af8..63eb646972d 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -6,7 +6,6 @@ import datetime import logging import aiohttp -import async_timeout from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.config_entries import ConfigEntry @@ -118,7 +117,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): try: websession = async_get_clientsession(self.hass) - async with async_timeout.timeout(_TIMEOUT): + async with asyncio.timeout(_TIMEOUT): response = await websession.get(self._url) self._last_image = await response.read() diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 6152a3756e3..c7b9ab4e380 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -6,7 +6,6 @@ from functools import partial import os from typing import Any -from async_timeout import timeout from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader from dsmr_parser.clients.rfxtrx_protocol import ( @@ -121,7 +120,7 @@ class DSMRConnection: if transport: try: - async with timeout(30): + async with asyncio.timeout(30): await protocol.wait_closed() except asyncio.TimeoutError: # Timeout (no data received), close transport and return True (if telegram is empty, will result in CannotCommunicate error) diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py index 8358887f7a2..2c7f8456a72 100644 --- a/homeassistant/components/eafm/sensor.py +++ b/homeassistant/components/eafm/sensor.py @@ -1,9 +1,9 @@ """Support for gauges from flood monitoring API.""" +import asyncio from datetime import timedelta import logging from aioeafm import get_station -import async_timeout from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry @@ -48,7 +48,7 @@ async def async_setup_entry( async def async_update_data(): # DataUpdateCoordinator will handle aiohttp ClientErrors and timeouts - async with async_timeout.timeout(30): + async with asyncio.timeout(30): data = await get_station(session, station_key) measures = get_measures(data) diff --git a/homeassistant/components/electric_kiwi/coordinator.py b/homeassistant/components/electric_kiwi/coordinator.py index 3e0ba997cd4..49611f9febd 100644 --- a/homeassistant/components/electric_kiwi/coordinator.py +++ b/homeassistant/components/electric_kiwi/coordinator.py @@ -1,9 +1,9 @@ """Electric Kiwi coordinators.""" +import asyncio from collections import OrderedDict from datetime import timedelta import logging -import async_timeout from electrickiwi_api import ElectricKiwiApi from electrickiwi_api.exceptions import ApiException, AuthException from electrickiwi_api.model import Hop, HopIntervals @@ -61,7 +61,7 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]): filters the intervals to remove ones that are not active """ try: - async with async_timeout.timeout(60): + async with asyncio.timeout(60): if self.hop_intervals is None: hop_intervals: HopIntervals = await self._ek_api.get_hop_intervals() hop_intervals.intervals = OrderedDict( diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 49e35a127fe..352c8419106 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -8,7 +8,6 @@ import re from types import MappingProxyType from typing import Any, cast -import async_timeout from elkm1_lib.elements import Element from elkm1_lib.elk import Elk from elkm1_lib.util import parse_url @@ -382,7 +381,7 @@ async def async_wait_for_elk_to_sync( ): _LOGGER.debug("Waiting for %s event for %s seconds", name, timeout) try: - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): await event.wait() except asyncio.TimeoutError: _LOGGER.debug("Timed out waiting for %s event", name) diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index fc08895ba4d..b593ae399f4 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -1,11 +1,11 @@ """Elmax integration common classes and utilities.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging from logging import Logger -import async_timeout from elmax_api.exceptions import ( ElmaxApiError, ElmaxBadLoginError, @@ -94,7 +94,7 @@ class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): async def _async_update_data(self): try: - async with async_timeout.timeout(DEFAULT_TIMEOUT): + async with asyncio.timeout(DEFAULT_TIMEOUT): # Retrieve the panel online status first panels = await self._client.list_control_panels() panel = next( diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index f0a54ba0ea9..566779671e8 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -11,7 +11,6 @@ import time from typing import Any from aiohttp import web -import async_timeout from homeassistant import core from homeassistant.components import ( @@ -898,7 +897,7 @@ async def wait_for_state_change_or_timeout( unsub = async_track_state_change_event(hass, [entity_id], _async_event_changed) try: - async with async_timeout.timeout(STATE_CHANGE_WAIT_TIMEOUT): + async with asyncio.timeout(STATE_CHANGE_WAIT_TIMEOUT): await ev.wait() except asyncio.TimeoutError: pass diff --git a/homeassistant/components/escea/config_flow.py b/homeassistant/components/escea/config_flow.py index 2a6e19343d9..8766c30c04a 100644 --- a/homeassistant/components/escea/config_flow.py +++ b/homeassistant/components/escea/config_flow.py @@ -3,8 +3,6 @@ import asyncio from contextlib import suppress import logging -from async_timeout import timeout - from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_entry_flow from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -34,7 +32,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: discovery_service = await async_start_discovery_service(hass) with suppress(asyncio.TimeoutError): - async with timeout(TIMEOUT_DISCOVERY): + async with asyncio.timeout(TIMEOUT_DISCOVERY): await controller_ready.wait() remove_handler() diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 748035bedac..4ce8909587e 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -22,7 +22,6 @@ from aioesphomeapi import ( from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError from aioesphomeapi.core import BluetoothGATTAPIError from async_interrupt import interrupt -import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.client import BaseBleakClient, NotifyCallback from bleak.backends.device import BLEDevice @@ -402,7 +401,7 @@ class ESPHomeClient(BaseBleakClient): self._ble_device.name, self._ble_device.address, ) - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): await bluetooth_device.wait_for_ble_connections_free() @property diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index 6b49549d812..f870f9e42f7 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -9,7 +9,6 @@ import socket from typing import cast from aioesphomeapi import VoiceAssistantEventType -import async_timeout from homeassistant.components import stt, tts from homeassistant.components.assist_pipeline import ( @@ -210,7 +209,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): Returns False if the connection was stopped gracefully (b"" put onto the queue). """ # Timeout if no audio comes in for a while. - async with async_timeout.timeout(self.audio_timeout): + async with asyncio.timeout(self.audio_timeout): chunk = await self.queue.get() while chunk: @@ -220,7 +219,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): if segmenter.in_command: return True - async with async_timeout.timeout(self.audio_timeout): + async with asyncio.timeout(self.audio_timeout): chunk = await self.queue.get() # If chunk is falsey, `stop()` was called @@ -240,7 +239,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): yield buffered_chunk # Timeout if no audio comes in for a while. - async with async_timeout.timeout(self.audio_timeout): + async with asyncio.timeout(self.audio_timeout): chunk = await self.queue.get() while chunk: @@ -250,7 +249,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): yield chunk - async with async_timeout.timeout(self.audio_timeout): + async with asyncio.timeout(self.audio_timeout): chunk = await self.queue.get() async def _iterate_packets_with_vad( @@ -259,7 +258,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): segmenter = VoiceCommandSegmenter(silence_seconds=silence_seconds) chunk_buffer: deque[bytes] = deque(maxlen=100) try: - async with async_timeout.timeout(pipeline_timeout): + async with asyncio.timeout(pipeline_timeout): speech_detected = await self._wait_for_speech(segmenter, chunk_buffer) if not speech_detected: _LOGGER.debug( @@ -326,7 +325,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): _LOGGER.debug("Starting pipeline") try: - async with async_timeout.timeout(pipeline_timeout): + async with asyncio.timeout(pipeline_timeout): await async_pipeline_from_audio_stream( self.hass, context=self.context, diff --git a/homeassistant/components/evil_genius_labs/__init__.py b/homeassistant/components/evil_genius_labs/__init__.py index 81a29b1432e..3d65a5516c7 100644 --- a/homeassistant/components/evil_genius_labs/__init__.py +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -1,12 +1,12 @@ """The Evil Genius Labs integration.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging from typing import cast from aiohttp import ContentTypeError -from async_timeout import timeout import pyevilgenius from homeassistant.config_entries import ConfigEntry @@ -85,18 +85,18 @@ class EvilGeniusUpdateCoordinator(DataUpdateCoordinator[dict]): async def _async_update_data(self) -> dict: """Update Evil Genius data.""" if not hasattr(self, "info"): - async with timeout(5): + async with asyncio.timeout(5): self.info = await self.client.get_info() if not hasattr(self, "product"): - async with timeout(5): + async with asyncio.timeout(5): try: self.product = await self.client.get_product() except ContentTypeError: # Older versions of the API don't support this self.product = None - async with timeout(5): + async with asyncio.timeout(5): return cast(dict, await self.client.get_all()) diff --git a/homeassistant/components/evil_genius_labs/config_flow.py b/homeassistant/components/evil_genius_labs/config_flow.py index 53303d738a5..beb16115bd7 100644 --- a/homeassistant/components/evil_genius_labs/config_flow.py +++ b/homeassistant/components/evil_genius_labs/config_flow.py @@ -6,7 +6,6 @@ import logging from typing import Any import aiohttp -import async_timeout import pyevilgenius import voluptuous as vol @@ -31,7 +30,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, ) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): data = await hub.get_all() info = await hub.get_info() except aiohttp.ClientError as err: diff --git a/homeassistant/components/evil_genius_labs/light.py b/homeassistant/components/evil_genius_labs/light.py index a915619b1b8..5612d0e8522 100644 --- a/homeassistant/components/evil_genius_labs/light.py +++ b/homeassistant/components/evil_genius_labs/light.py @@ -1,10 +1,9 @@ """Light platform for Evil Genius Light.""" from __future__ import annotations +import asyncio from typing import Any, cast -from async_timeout import timeout - from homeassistant.components import light from homeassistant.components.light import ColorMode, LightEntity, LightEntityFeature from homeassistant.config_entries import ConfigEntry @@ -89,27 +88,27 @@ class EvilGeniusLight(EvilGeniusEntity, LightEntity): ) -> None: """Turn light on.""" if (brightness := kwargs.get(light.ATTR_BRIGHTNESS)) is not None: - async with timeout(5): + async with asyncio.timeout(5): await self.coordinator.client.set_path_value("brightness", brightness) # Setting a color will change the effect to "Solid Color" so skip setting effect if (rgb_color := kwargs.get(light.ATTR_RGB_COLOR)) is not None: - async with timeout(5): + async with asyncio.timeout(5): await self.coordinator.client.set_rgb_color(*rgb_color) elif (effect := kwargs.get(light.ATTR_EFFECT)) is not None: if effect == HA_NO_EFFECT: effect = FIB_NO_EFFECT - async with timeout(5): + async with asyncio.timeout(5): await self.coordinator.client.set_path_value( "pattern", self.coordinator.data["pattern"]["options"].index(effect) ) - async with timeout(5): + async with asyncio.timeout(5): await self.coordinator.client.set_path_value("power", 1) @update_when_done async def async_turn_off(self, **kwargs: Any) -> None: """Turn light off.""" - async with timeout(5): + async with asyncio.timeout(5): await self.coordinator.client.set_path_value("power", 0) diff --git a/homeassistant/components/ezviz/coordinator.py b/homeassistant/components/ezviz/coordinator.py index ba8ed336a51..427e52f7dd0 100644 --- a/homeassistant/components/ezviz/coordinator.py +++ b/homeassistant/components/ezviz/coordinator.py @@ -1,8 +1,8 @@ """Provides the ezviz DataUpdateCoordinator.""" +import asyncio from datetime import timedelta import logging -from async_timeout import timeout from pyezviz.client import EzvizClient from pyezviz.exceptions import ( EzvizAuthTokenExpired, @@ -37,7 +37,7 @@ class EzvizDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict: """Fetch data from EZVIZ.""" try: - async with timeout(self._api_timeout): + async with asyncio.timeout(self._api_timeout): return await self.hass.async_add_executor_job( self.ezviz_client.load_cameras ) diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 4deae7f13fa..f0fe2d9ccb0 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -1,6 +1,7 @@ """esphome session fixtures.""" from __future__ import annotations +import asyncio from asyncio import Event from collections.abc import Awaitable, Callable from typing import Any @@ -15,7 +16,6 @@ from aioesphomeapi import ( ReconnectLogic, UserService, ) -import async_timeout import pytest from zeroconf import Zeroconf @@ -252,7 +252,7 @@ async def _mock_generic_device_entry( "homeassistant.components.esphome.manager.ReconnectLogic", MockReconnectLogic ): assert await hass.config_entries.async_setup(entry.entry_id) - async with async_timeout.timeout(2): + async with asyncio.timeout(2): await try_connect_done.wait() await hass.async_block_till_done() diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index 4188e375907..d6562651f0b 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -5,7 +5,6 @@ import socket from unittest.mock import Mock, patch from aioesphomeapi import VoiceAssistantEventType -import async_timeout import pytest from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType @@ -148,7 +147,7 @@ async def test_udp_server( sock.sendto(b"test", ("127.0.0.1", port)) # Give the socket some time to send/receive the data - async with async_timeout.timeout(1): + async with asyncio.timeout(1): while voice_assistant_udp_server_v1.queue.qsize() == 0: await asyncio.sleep(0.1) From a9ade1f84df04c272545e6f3014ab9f43aaae346 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:36:05 +0200 Subject: [PATCH 0530/1151] Use asyncio.timeout [core] (#98447) --- homeassistant/components/bluetooth/scanner.py | 3 +-- homeassistant/components/camera/__init__.py | 5 ++--- .../components/cloud/alexa_config.py | 3 +-- homeassistant/components/cloud/http_api.py | 15 +++++++------ .../components/cloud/subscription.py | 5 ++--- homeassistant/components/mqtt/client.py | 3 +-- homeassistant/components/mqtt/util.py | 3 +-- homeassistant/components/recorder/core.py | 3 +-- homeassistant/components/stream/core.py | 3 +-- .../components/system_health/__init__.py | 3 +-- .../components/websocket_api/http.py | 5 ++--- homeassistant/core.py | 3 +-- homeassistant/helpers/aiohttp_client.py | 5 ++--- .../helpers/config_entry_oauth2_flow.py | 5 ++--- homeassistant/helpers/script.py | 17 ++++++++------- homeassistant/helpers/template.py | 3 +-- tests/components/group/test_cover.py | 4 ++-- tests/components/group/test_fan.py | 4 ++-- tests/components/group/test_light.py | 4 ++-- tests/components/group/test_media_player.py | 4 ++-- tests/components/group/test_switch.py | 5 ++--- .../components/history/test_websocket_api.py | 21 +++++++++---------- .../components/websocket_api/test_commands.py | 8 +++---- .../helpers/test_config_entry_oauth2_flow.py | 4 ++-- tests/helpers/test_event.py | 9 ++++---- tests/helpers/test_script.py | 5 ++--- tests/test_core.py | 21 +++++++++---------- 27 files changed, 77 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 35efbdf3cbe..f0b7df528e1 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -8,7 +8,6 @@ import logging import platform from typing import Any -import async_timeout import bleak from bleak import BleakError from bleak.assigned_numbers import AdvertisementDataType @@ -220,7 +219,7 @@ class HaScanner(BaseHaScanner): START_ATTEMPTS, ) try: - async with async_timeout.timeout(START_TIMEOUT): + async with asyncio.timeout(START_TIMEOUT): await self.scanner.start() # type: ignore[no-untyped-call] except InvalidMessageError as ex: _LOGGER.debug( diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 277aa10075e..486c964bb45 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -15,7 +15,6 @@ from random import SystemRandom from typing import Any, Final, cast, final from aiohttp import hdrs, web -import async_timeout import attr import voluptuous as vol @@ -168,7 +167,7 @@ async def _async_get_image( are handled. """ with suppress(asyncio.CancelledError, asyncio.TimeoutError): - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): if image_bytes := await camera.async_camera_image( width=width, height=height ): @@ -525,7 +524,7 @@ class Camera(Entity): self._create_stream_lock = asyncio.Lock() async with self._create_stream_lock: if not self.stream: - async with async_timeout.timeout(CAMERA_STREAM_SOURCE_TIMEOUT): + async with asyncio.timeout(CAMERA_STREAM_SOURCE_TIMEOUT): source = await self.stream_source() if not source: return None diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 3ceb02972d1..e85c6dd277a 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -10,7 +10,6 @@ import logging from typing import TYPE_CHECKING, Any import aiohttp -import async_timeout from hass_nabucasa import Cloud, cloud_api from yarl import URL @@ -501,7 +500,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): ) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) return True diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 00ef4455f3b..e3b1b39f687 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -10,7 +10,6 @@ from typing import Any, Concatenate, ParamSpec, TypeVar import aiohttp from aiohttp import web -import async_timeout import attr from hass_nabucasa import Cloud, auth, thingtalk from hass_nabucasa.const import STATE_DISCONNECTED @@ -252,7 +251,7 @@ class CloudLogoutView(HomeAssistantView): hass = request.app["hass"] cloud = hass.data[DOMAIN] - async with async_timeout.timeout(REQUEST_TIMEOUT): + async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.logout() return self.json_message("ok") @@ -292,7 +291,7 @@ class CloudRegisterView(HomeAssistantView): if location_info.zip_code is not None: client_metadata["NC_ZIP_CODE"] = location_info.zip_code - async with async_timeout.timeout(REQUEST_TIMEOUT): + async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.auth.async_register( data["email"], data["password"], @@ -316,7 +315,7 @@ class CloudResendConfirmView(HomeAssistantView): hass = request.app["hass"] cloud = hass.data[DOMAIN] - async with async_timeout.timeout(REQUEST_TIMEOUT): + async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.auth.async_resend_email_confirm(data["email"]) return self.json_message("ok") @@ -336,7 +335,7 @@ class CloudForgotPasswordView(HomeAssistantView): hass = request.app["hass"] cloud = hass.data[DOMAIN] - async with async_timeout.timeout(REQUEST_TIMEOUT): + async with asyncio.timeout(REQUEST_TIMEOUT): await cloud.auth.async_forgot_password(data["email"]) return self.json_message("ok") @@ -439,7 +438,7 @@ async def websocket_update_prefs( if changes.get(PREF_ALEXA_REPORT_STATE): alexa_config = await cloud.client.get_alexa_config() try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await alexa_config.async_get_access_token() except asyncio.TimeoutError: connection.send_error( @@ -779,7 +778,7 @@ async def alexa_sync( cloud = hass.data[DOMAIN] alexa_config = await cloud.client.get_alexa_config() - async with async_timeout.timeout(10): + async with asyncio.timeout(10): try: success = await alexa_config.async_sync_entities() except alexa_errors.NoTokenAvailable: @@ -808,7 +807,7 @@ async def thingtalk_convert( """Convert a query.""" cloud = hass.data[DOMAIN] - async with async_timeout.timeout(10): + async with asyncio.timeout(10): try: connection.send_result( msg["id"], await thingtalk.async_convert(cloud, msg["query"]) diff --git a/homeassistant/components/cloud/subscription.py b/homeassistant/components/cloud/subscription.py index 633f0c95e1b..9a62f2d115c 100644 --- a/homeassistant/components/cloud/subscription.py +++ b/homeassistant/components/cloud/subscription.py @@ -6,7 +6,6 @@ import logging from typing import Any from aiohttp.client_exceptions import ClientError -import async_timeout from hass_nabucasa import Cloud, cloud_api from .client import CloudClient @@ -18,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) async def async_subscription_info(cloud: Cloud[CloudClient]) -> dict[str, Any] | None: """Fetch the subscription info.""" try: - async with async_timeout.timeout(REQUEST_TIMEOUT): + async with asyncio.timeout(REQUEST_TIMEOUT): return await cloud_api.async_subscription_info(cloud) except asyncio.TimeoutError: _LOGGER.error( @@ -39,7 +38,7 @@ async def async_migrate_paypal_agreement( ) -> dict[str, Any] | None: """Migrate a paypal agreement from legacy.""" try: - async with async_timeout.timeout(REQUEST_TIMEOUT): + async with asyncio.timeout(REQUEST_TIMEOUT): return await cloud_api.async_migrate_paypal_agreement(cloud) except asyncio.TimeoutError: _LOGGER.error( diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 07fbc0ca8c5..0c351e69bcf 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -12,7 +12,6 @@ import time from typing import TYPE_CHECKING, Any import uuid -import async_timeout import attr import certifi @@ -918,7 +917,7 @@ class MQTT: # may be executed first. await self._register_mid(mid) try: - async with async_timeout.timeout(TIMEOUT_ACK): + async with asyncio.timeout(TIMEOUT_ACK): await self._pending_operations[mid].wait() except asyncio.TimeoutError: _LOGGER.warning( diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 896ba21f802..02d9964bcd1 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -8,7 +8,6 @@ from pathlib import Path import tempfile from typing import Any -import async_timeout import voluptuous as vol from homeassistant.config_entries import ConfigEntryState @@ -71,7 +70,7 @@ async def async_wait_for_mqtt_client(hass: HomeAssistant) -> bool: return state_reached_future.result() try: - async with async_timeout.timeout(AVAILABILITY_TIMEOUT): + async with asyncio.timeout(AVAILABILITY_TIMEOUT): # Await the client setup or an error state was received return await state_reached_future except asyncio.TimeoutError: diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index d4a026cfefc..ffdc3807039 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -13,7 +13,6 @@ import threading import time from typing import Any, TypeVar, cast -import async_timeout import psutil_home_assistant as ha_psutil from sqlalchemy import create_engine, event as sqlalchemy_event, exc, select from sqlalchemy.engine import Engine @@ -1306,7 +1305,7 @@ class Recorder(threading.Thread): task = DatabaseLockTask(database_locked, threading.Event(), False) self.queue_task(task) try: - async with async_timeout.timeout(DB_LOCK_TIMEOUT): + async with asyncio.timeout(DB_LOCK_TIMEOUT): await database_locked.wait() except asyncio.TimeoutError as err: task.database_unlock.set() diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index cc3c0abb96c..f3591e7e5d7 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -10,7 +10,6 @@ import logging from typing import TYPE_CHECKING, Any from aiohttp import web -import async_timeout import attr import numpy as np @@ -332,7 +331,7 @@ class StreamOutput: async def part_recv(self, timeout: float | None = None) -> bool: """Wait for an event signalling the latest part segment.""" try: - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): await self._part_event.wait() except asyncio.TimeoutError: return False diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index 9a222d7096c..32970bc4fe5 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -9,7 +9,6 @@ import logging from typing import Any import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components import websocket_api @@ -73,7 +72,7 @@ async def get_integration_info( """Get integration system health.""" try: assert registration.info_callback - async with async_timeout.timeout(INFO_CALLBACK_TIMEOUT): + async with asyncio.timeout(INFO_CALLBACK_TIMEOUT): data = await registration.info_callback(hass) except asyncio.TimeoutError: data = {"error": {"type": "failed", "error": "timeout"}} diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index fcaa13ff8de..238cd6d7465 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -9,7 +9,6 @@ import logging from typing import TYPE_CHECKING, Any, Final from aiohttp import WSMsgType, web -import async_timeout from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -273,7 +272,7 @@ class WebSocketHandler: logging_debug = logging.DEBUG try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await wsock.prepare(request) except asyncio.TimeoutError: self._logger.warning("Timeout preparing request from %s", request.remote) @@ -302,7 +301,7 @@ class WebSocketHandler: # Auth Phase try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): msg = await wsock.receive() except asyncio.TimeoutError as err: disconnect_warn = "Did not receive auth message within 10 seconds" diff --git a/homeassistant/core.py b/homeassistant/core.py index 3b54358dc3d..a025eacd4bc 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -29,7 +29,6 @@ from time import monotonic from typing import TYPE_CHECKING, Any, Generic, ParamSpec, Self, TypeVar, cast, overload from urllib.parse import urlparse -import async_timeout import voluptuous as vol import yarl @@ -806,7 +805,7 @@ class HomeAssistant: ) task.cancel("Home Assistant stage 2 shutdown") try: - async with async_timeout.timeout(0.1): + async with asyncio.timeout(0.1): await task except asyncio.CancelledError: pass diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 8208c774887..ac253d49254 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -13,7 +13,6 @@ import aiohttp from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout -import async_timeout from homeassistant import config_entries from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__ @@ -170,7 +169,7 @@ async def async_aiohttp_proxy_web( ) -> web.StreamResponse | None: """Stream websession request to aiohttp web response.""" try: - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): req = await web_coro except asyncio.CancelledError: @@ -211,7 +210,7 @@ async def async_aiohttp_proxy_stream( # Suppressing something went wrong fetching data, closed connection with suppress(asyncio.TimeoutError, aiohttp.ClientError): while hass.is_running: - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): data = await stream.read(buffer_size) if not data: diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index fe4e5473092..4fd8948843e 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -16,7 +16,6 @@ import time from typing import Any, cast from aiohttp import client, web -import async_timeout import jwt import voluptuous as vol from yarl import URL @@ -287,7 +286,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): return self.async_external_step_done(next_step_id=next_step) try: - async with async_timeout.timeout(OAUTH_AUTHORIZE_URL_TIMEOUT_SEC): + async with asyncio.timeout(OAUTH_AUTHORIZE_URL_TIMEOUT_SEC): url = await self.async_generate_authorize_url() except asyncio.TimeoutError as err: _LOGGER.error("Timeout generating authorize url: %s", err) @@ -311,7 +310,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): _LOGGER.debug("Creating config entry from external data") try: - async with async_timeout.timeout(OAUTH_TOKEN_TIMEOUT_SEC): + async with asyncio.timeout(OAUTH_TOKEN_TIMEOUT_SEC): token = await self.flow_impl.async_resolve_external_data( self.external_data ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 0dacb90e318..4035d55b325 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -13,7 +13,6 @@ import logging from types import MappingProxyType from typing import Any, TypedDict, TypeVar, cast -import async_timeout import voluptuous as vol from homeassistant import exceptions @@ -574,7 +573,7 @@ class _ScriptRun: self._changed() trace_set_result(delay=delay, done=False) try: - async with async_timeout.timeout(delay): + async with asyncio.timeout(delay): await self._stop.wait() except asyncio.TimeoutError: trace_set_result(delay=delay, done=True) @@ -602,9 +601,10 @@ class _ScriptRun: @callback def async_script_wait(entity_id, from_s, to_s): """Handle script after template condition is true.""" + # pylint: disable=protected-access wait_var = self._variables["wait"] - if to_context and to_context.deadline: - wait_var["remaining"] = to_context.deadline - self._hass.loop.time() + if to_context and to_context._when: + wait_var["remaining"] = to_context._when - self._hass.loop.time() else: wait_var["remaining"] = timeout wait_var["completed"] = True @@ -621,7 +621,7 @@ class _ScriptRun: self._hass.async_create_task(flag.wait()) for flag in (self._stop, done) ] try: - async with async_timeout.timeout(timeout) as to_context: + async with asyncio.timeout(timeout) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.TimeoutError as ex: self._variables["wait"]["remaining"] = 0.0 @@ -971,9 +971,10 @@ class _ScriptRun: done = asyncio.Event() async def async_done(variables, context=None): + # pylint: disable=protected-access wait_var = self._variables["wait"] - if to_context and to_context.deadline: - wait_var["remaining"] = to_context.deadline - self._hass.loop.time() + if to_context and to_context._when: + wait_var["remaining"] = to_context._when - self._hass.loop.time() else: wait_var["remaining"] = timeout wait_var["trigger"] = variables["trigger"] @@ -1000,7 +1001,7 @@ class _ScriptRun: self._hass.async_create_task(flag.wait()) for flag in (self._stop, done) ] try: - async with async_timeout.timeout(timeout) as to_context: + async with asyncio.timeout(timeout) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.TimeoutError as ex: self._variables["wait"]["remaining"] = 0.0 diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 67c1a3ed52f..40d64ba37ae 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -34,7 +34,6 @@ from typing import ( from urllib.parse import urlencode as urllib_urlencode import weakref -import async_timeout from awesomeversion import AwesomeVersion import jinja2 from jinja2 import pass_context, pass_environment, pass_eval_context @@ -651,7 +650,7 @@ class Template: try: template_render_thread = ThreadWithException(target=_render_template) template_render_thread.start() - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): await finish_event.wait() if self._exc_info: raise TemplateError(self._exc_info[1].with_traceback(self._exc_info[2])) diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 863747369e1..84ccba2ff66 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -1,7 +1,7 @@ """The tests for the group cover platform.""" +import asyncio from datetime import timedelta -import async_timeout import pytest from homeassistant.components.cover import ( @@ -828,7 +828,7 @@ async def test_nested_group(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_ENTITY_ID) == ["cover.bedroom_group"] # Test controlling the nested group - async with async_timeout.timeout(0.5): + async with asyncio.timeout(0.5): await hass.services.async_call( DOMAIN, SERVICE_CLOSE_COVER, diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index cb980841266..6269df3fed7 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -1,7 +1,7 @@ """The tests for the group fan platform.""" +import asyncio from unittest.mock import patch -import async_timeout import pytest from homeassistant import config as hass_config @@ -576,7 +576,7 @@ async def test_nested_group(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_ENTITY_ID) == ["fan.bedroom_group"] # Test controlling the nested group - async with async_timeout.timeout(0.5): + async with asyncio.timeout(0.5): await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 539a8c61414..062cf161bb9 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,7 +1,7 @@ """The tests for the Group Light platform.""" +import asyncio from unittest.mock import MagicMock, patch -import async_timeout import pytest from homeassistant import config as hass_config @@ -1643,7 +1643,7 @@ async def test_nested_group(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_ENTITY_ID) == ["light.bedroom_group"] # Test controlling the nested group - async with async_timeout.timeout(0.5): + async with asyncio.timeout(0.5): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TOGGLE, diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index 2a1a2a05e4e..e1f269a947d 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -1,7 +1,7 @@ """The tests for the Media group platform.""" +import asyncio from unittest.mock import Mock, patch -import async_timeout import pytest from homeassistant.components.group import DOMAIN @@ -583,7 +583,7 @@ async def test_nested_group(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_ENTITY_ID) == ["media_player.group_1"] # Test controlling the nested group - async with async_timeout.timeout(0.5): + async with asyncio.timeout(0.5): await hass.services.async_call( MEDIA_DOMAIN, SERVICE_TURN_OFF, diff --git a/tests/components/group/test_switch.py b/tests/components/group/test_switch.py index 29cd389c233..bc9a05f4754 100644 --- a/tests/components/group/test_switch.py +++ b/tests/components/group/test_switch.py @@ -1,8 +1,7 @@ """The tests for the Group Switch platform.""" +import asyncio from unittest.mock import patch -import async_timeout - from homeassistant import config as hass_config from homeassistant.components.group import DOMAIN, SERVICE_RELOAD from homeassistant.components.switch import ( @@ -445,7 +444,7 @@ async def test_nested_group(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_ENTITY_ID) == ["switch.some_group"] # Test controlling the nested group - async with async_timeout.timeout(0.5): + async with asyncio.timeout(0.5): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TOGGLE, diff --git a/tests/components/history/test_websocket_api.py b/tests/components/history/test_websocket_api.py index 4f00e50def1..87489486614 100644 --- a/tests/components/history/test_websocket_api.py +++ b/tests/components/history/test_websocket_api.py @@ -4,7 +4,6 @@ import asyncio from datetime import timedelta from unittest.mock import patch -import async_timeout from freezegun import freeze_time import pytest @@ -560,12 +559,12 @@ async def test_history_stream_significant_domain_historical_only( "no_attributes": True, } ) - async with async_timeout.timeout(3): + async with asyncio.timeout(3): response = await client.receive_json() assert response["success"] assert response["id"] == 1 assert response["type"] == "result" - async with async_timeout.timeout(3): + async with asyncio.timeout(3): response = await client.receive_json() assert response == { "event": { @@ -591,13 +590,13 @@ async def test_history_stream_significant_domain_historical_only( "minimal_response": True, } ) - async with async_timeout.timeout(3): + async with asyncio.timeout(3): response = await client.receive_json() assert response["success"] assert response["id"] == 2 assert response["type"] == "result" - async with async_timeout.timeout(3): + async with asyncio.timeout(3): response = await client.receive_json() sensor_test_history = response["event"]["states"]["climate.test"] assert len(sensor_test_history) == 5 @@ -626,13 +625,13 @@ async def test_history_stream_significant_domain_historical_only( "no_attributes": False, } ) - async with async_timeout.timeout(3): + async with asyncio.timeout(3): response = await client.receive_json() assert response["success"] assert response["id"] == 3 assert response["type"] == "result" - async with async_timeout.timeout(3): + async with asyncio.timeout(3): response = await client.receive_json() sensor_test_history = response["event"]["states"]["climate.test"] @@ -663,13 +662,13 @@ async def test_history_stream_significant_domain_historical_only( "no_attributes": False, } ) - async with async_timeout.timeout(3): + async with asyncio.timeout(3): response = await client.receive_json() assert response["success"] assert response["id"] == 4 assert response["type"] == "result" - async with async_timeout.timeout(3): + async with asyncio.timeout(3): response = await client.receive_json() sensor_test_history = response["event"]["states"]["climate.test"] @@ -708,13 +707,13 @@ async def test_history_stream_significant_domain_historical_only( "no_attributes": False, } ) - async with async_timeout.timeout(3): + async with asyncio.timeout(3): response = await client.receive_json() assert response["success"] assert response["id"] == 5 assert response["type"] == "result" - async with async_timeout.timeout(3): + async with asyncio.timeout(3): response = await client.receive_json() sensor_test_history = response["event"]["states"]["climate.test"] diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 85c0ac62b25..73baa968ab6 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1,9 +1,9 @@ """Tests for WebSocket API commands.""" +import asyncio from copy import deepcopy import datetime from unittest.mock import ANY, AsyncMock, Mock, patch -from async_timeout import timeout import pytest import voluptuous as vol @@ -497,7 +497,7 @@ async def test_subscribe_unsubscribe_events( hass.bus.async_fire("test_event", {"hello": "world"}) hass.bus.async_fire("ignore_event") - async with timeout(3): + async with asyncio.timeout(3): msg = await websocket_client.receive_json() assert msg["id"] == 5 @@ -712,7 +712,7 @@ async def test_subscribe_unsubscribe_events_whitelist( hass.bus.async_fire("themes_updated") - async with timeout(3): + async with asyncio.timeout(3): msg = await websocket_client.receive_json() assert msg["id"] == 6 @@ -1611,7 +1611,7 @@ async def test_subscribe_trigger(hass: HomeAssistant, websocket_client) -> None: hass.bus.async_fire("test_event", {"hello": "world"}, context=context) hass.bus.async_fire("ignore_event") - async with timeout(3): + async with asyncio.timeout(3): msg = await websocket_client.receive_json() assert msg["id"] == 5 diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 3baaf7e7333..94cdf34cba3 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -140,7 +140,7 @@ async def test_abort_if_authorization_timeout( flow.hass = hass with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_timeout.timeout", + "homeassistant.helpers.config_entry_oauth2_flow.asyncio.timeout", side_effect=asyncio.TimeoutError, ): result = await flow.async_step_user() @@ -331,7 +331,7 @@ async def test_abort_on_oauth_timeout_error( assert resp.headers["content-type"] == "text/html; charset=utf-8" with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_timeout.timeout", + "homeassistant.helpers.config_entry_oauth2_flow.asyncio.timeout", side_effect=asyncio.TimeoutError, ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index b88f716a8ec..572a0d22e92 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -8,7 +8,6 @@ from unittest.mock import patch from astral import LocationInfo import astral.sun -import async_timeout from freezegun import freeze_time from freezegun.api import FrozenDateTimeFactory import jinja2 @@ -4361,7 +4360,7 @@ async def test_call_later(hass: HomeAssistant) -> None: async_fire_time_changed_exact(hass, dt_util.utcnow() + timedelta(seconds=delay)) - async with async_timeout.timeout(delay + delay_tolerance): + async with asyncio.timeout(delay + delay_tolerance): assert await future, "callback was called but the delay was wrong" @@ -4381,7 +4380,7 @@ async def test_async_call_later(hass: HomeAssistant) -> None: async_fire_time_changed_exact(hass, dt_util.utcnow() + timedelta(seconds=delay)) - async with async_timeout.timeout(delay + delay_tolerance): + async with asyncio.timeout(delay + delay_tolerance): assert await future, "callback was called but the delay was wrong" assert isinstance(remove, Callable) remove() @@ -4403,7 +4402,7 @@ async def test_async_call_later_timedelta(hass: HomeAssistant) -> None: async_fire_time_changed_exact(hass, dt_util.utcnow() + timedelta(seconds=delay)) - async with async_timeout.timeout(delay + delay_tolerance): + async with asyncio.timeout(delay + delay_tolerance): assert await future, "callback was called but the delay was wrong" assert isinstance(remove, Callable) remove() @@ -4430,7 +4429,7 @@ async def test_async_call_later_cancel(hass: HomeAssistant) -> None: async_fire_time_changed_exact(hass, dt_util.utcnow() + timedelta(seconds=delay)) with contextlib.suppress(asyncio.TimeoutError): - async with async_timeout.timeout(delay + delay_tolerance): + async with asyncio.timeout(delay + delay_tolerance): assert await future, "callback not canceled" diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 7f66ec25977..5163dd0ca6d 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -9,7 +9,6 @@ from types import MappingProxyType from unittest import mock from unittest.mock import AsyncMock, MagicMock, patch -from async_timeout import timeout import pytest import voluptuous as vol @@ -1000,7 +999,7 @@ async def test_wait_basic_times_out(hass: HomeAssistant, action_type) -> None: assert script_obj.last_action == wait_alias hass.states.async_set("switch.test", "not_on") - async with timeout(0.1): + async with asyncio.timeout(0.1): await hass.async_block_till_done() except asyncio.TimeoutError: timed_out = True @@ -1386,7 +1385,7 @@ async def test_wait_template_with_utcnow_no_match(hass: HomeAssistant) -> None: ): async_fire_time_changed(hass, second_non_matching_time) - async with timeout(0.1): + async with asyncio.timeout(0.1): await hass.async_block_till_done() except asyncio.TimeoutError: timed_out = True diff --git a/tests/test_core.py b/tests/test_core.py index 488975ef02f..9f6e5aeb2dd 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -14,7 +14,6 @@ import time from typing import Any from unittest.mock import MagicMock, Mock, PropertyMock, patch -import async_timeout import pytest import voluptuous as vol @@ -235,7 +234,7 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: assert can_call_async_get_hass() hass.async_create_task(_async_create_task(), "create_task") - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await task_finished.wait() task_finished.clear() @@ -246,7 +245,7 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: task_finished.set() hass.async_add_job(_add_job) - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await task_finished.wait() task_finished.clear() @@ -263,7 +262,7 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: hass.async_add_job(_callback) _schedule_callback_from_callback() - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await task_finished.wait() task_finished.clear() @@ -279,7 +278,7 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: hass.async_add_job(_coroutine()) _schedule_coroutine_from_callback() - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await task_finished.wait() task_finished.clear() @@ -295,7 +294,7 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: hass.async_add_job(_callback) await _schedule_callback_from_coroutine() - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await task_finished.wait() task_finished.clear() @@ -310,7 +309,7 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: await hass.async_create_task(_coroutine()) await _schedule_callback_from_coroutine() - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await task_finished.wait() task_finished.clear() @@ -326,7 +325,7 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: hass.add_job(_async_add_job) await hass.async_add_executor_job(_async_add_executor_job_add_job) - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await task_finished.wait() task_finished.clear() @@ -341,7 +340,7 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: hass.create_task(_async_create_task()) await hass.async_add_executor_job(_async_add_executor_job_create_task) - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await task_finished.wait() task_finished.clear() @@ -359,7 +358,7 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: my_job_add_job = MyJobAddJob() my_job_add_job.start() - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await task_finished.wait() task_finished.clear() my_job_add_job.join() @@ -377,7 +376,7 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: my_job_create_task = MyJobCreateTask() my_job_create_task.start() - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await task_finished.wait() task_finished.clear() my_job_create_task.join() From 5dd3f05db89d0171ad31605e9273aae9c709d0a6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:37:06 +0200 Subject: [PATCH 0531/1151] Use asyncio.timeout [f-h] (#98449) --- homeassistant/components/faa_delays/__init__.py | 4 ++-- homeassistant/components/flick_electric/config_flow.py | 3 +-- homeassistant/components/flick_electric/sensor.py | 4 ++-- homeassistant/components/flo/device.py | 4 ++-- homeassistant/components/flock/notify.py | 3 +-- homeassistant/components/forked_daapd/media_player.py | 5 ++--- homeassistant/components/freedns/__init__.py | 3 +-- homeassistant/components/fully_kiosk/config_flow.py | 3 +-- homeassistant/components/fully_kiosk/coordinator.py | 3 +-- homeassistant/components/garages_amsterdam/__init__.py | 4 ++-- homeassistant/components/generic/config_flow.py | 4 ++-- homeassistant/components/gios/__init__.py | 4 ++-- homeassistant/components/gios/config_flow.py | 3 +-- homeassistant/components/google_cloud/tts.py | 3 +-- homeassistant/components/google_domains/__init__.py | 3 +-- homeassistant/components/hlk_sw16/config_flow.py | 3 +-- homeassistant/components/home_plus_control/__init__.py | 4 ++-- .../components/homeassistant_yellow/config_flow.py | 6 +++--- homeassistant/components/hue/bridge.py | 3 +-- homeassistant/components/hue/config_flow.py | 3 +-- homeassistant/components/hue/v1/light.py | 4 ++-- homeassistant/components/hue/v1/sensor_base.py | 4 ++-- homeassistant/components/huisbaasje/__init__.py | 4 ++-- .../components/hunterdouglas_powerview/__init__.py | 10 +++++----- .../components/hunterdouglas_powerview/config_flow.py | 4 ++-- .../components/hunterdouglas_powerview/coordinator.py | 4 ++-- .../components/hunterdouglas_powerview/cover.py | 3 +-- .../components/hvv_departures/binary_sensor.py | 4 ++-- 28 files changed, 48 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index 10ddb13c228..b165492d076 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -1,9 +1,9 @@ """The FAA Delays integration.""" +import asyncio from datetime import timedelta import logging from aiohttp import ClientConnectionError -from async_timeout import timeout from faadelays import Airport from homeassistant.config_entries import ConfigEntry @@ -56,7 +56,7 @@ class FAADataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): try: - async with timeout(10): + async with asyncio.timeout(10): await self.data.update() except ClientConnectionError as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/flick_electric/config_flow.py b/homeassistant/components/flick_electric/config_flow.py index 5fac5cdb83a..557d0492320 100644 --- a/homeassistant/components/flick_electric/config_flow.py +++ b/homeassistant/components/flick_electric/config_flow.py @@ -2,7 +2,6 @@ import asyncio import logging -import async_timeout from pyflick.authentication import AuthException, SimpleFlickAuth from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET import voluptuous as vol @@ -45,7 +44,7 @@ class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) try: - async with async_timeout.timeout(60): + async with asyncio.timeout(60): token = await auth.async_get_access_token() except asyncio.TimeoutError as err: raise CannotConnect() from err diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index a0844fe6cdb..8280e7b2fe0 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -1,9 +1,9 @@ """Support for Flick Electric Pricing data.""" +import asyncio from datetime import timedelta import logging from typing import Any -import async_timeout from pyflick import FlickAPI, FlickPrice from homeassistant.components.sensor import SensorEntity @@ -58,7 +58,7 @@ class FlickPricingSensor(SensorEntity): if self._price and self._price.end_at >= utcnow(): return # Power price data is still valid - async with async_timeout.timeout(60): + async with asyncio.timeout(60): self._price = await self._api.getPricing() _LOGGER.debug("Pricing data: %s", self._price) diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index 1b28a2552a2..99e86d4b6b5 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -1,12 +1,12 @@ """Flo device object.""" from __future__ import annotations +import asyncio from datetime import datetime, timedelta from typing import Any from aioflo.api import API from aioflo.errors import RequestError -from async_timeout import timeout from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -39,7 +39,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Update data via library.""" try: - async with timeout(20): + async with asyncio.timeout(20): await self.send_presence_ping() await self._update_device() await self._update_consumption_data() diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index 5ac340400af..3fdd54dd40d 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -5,7 +5,6 @@ import asyncio from http import HTTPStatus import logging -import async_timeout import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService @@ -49,7 +48,7 @@ class FlockNotificationService(BaseNotificationService): _LOGGER.debug("Attempting to call Flock at %s", self._url) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): response = await self._session.post(self._url, json=payload) result = await response.json() diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 868ec8e1f9e..48c2be07c76 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -6,7 +6,6 @@ from collections import defaultdict import logging from typing import Any -import async_timeout from pyforked_daapd import ForkedDaapdAPI from pylibrespot_java import LibrespotJavaAPI @@ -667,7 +666,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): self._pause_requested = True await self.async_media_pause() try: - async with async_timeout.timeout(CALLBACK_TIMEOUT): + async with asyncio.timeout(CALLBACK_TIMEOUT): await self._paused_event.wait() # wait for paused except asyncio.TimeoutError: self._pause_requested = False @@ -762,7 +761,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): await sleep_future await self.api.add_to_queue(uris=media_id, playback="start", clear=True) try: - async with async_timeout.timeout(TTS_TIMEOUT): + async with asyncio.timeout(TTS_TIMEOUT): await self._tts_playing_event.wait() # we have started TTS, now wait for completion except asyncio.TimeoutError: diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index e6ac11889bc..e65856e03f4 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -4,7 +4,6 @@ from datetime import datetime, timedelta import logging import aiohttp -import async_timeout import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL, CONF_URL @@ -76,7 +75,7 @@ async def _update_freedns(hass, session, url, auth_token): params[auth_token] = "" try: - async with async_timeout.timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT): resp = await session.get(url, params=params) body = await resp.text() diff --git a/homeassistant/components/fully_kiosk/config_flow.py b/homeassistant/components/fully_kiosk/config_flow.py index cdd7c7b276b..7d744214d93 100644 --- a/homeassistant/components/fully_kiosk/config_flow.py +++ b/homeassistant/components/fully_kiosk/config_flow.py @@ -6,7 +6,6 @@ import json from typing import Any from aiohttp.client_exceptions import ClientConnectorError -from async_timeout import timeout from fullykiosk import FullyKiosk from fullykiosk.exceptions import FullyKioskError import voluptuous as vol @@ -42,7 +41,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) try: - async with timeout(15): + async with asyncio.timeout(15): device_info = await fully.getDeviceInfo() except ( ClientConnectorError, diff --git a/homeassistant/components/fully_kiosk/coordinator.py b/homeassistant/components/fully_kiosk/coordinator.py index 4e35d614587..0cfc15268b4 100644 --- a/homeassistant/components/fully_kiosk/coordinator.py +++ b/homeassistant/components/fully_kiosk/coordinator.py @@ -2,7 +2,6 @@ import asyncio from typing import Any, cast -from async_timeout import timeout from fullykiosk import FullyKiosk from fullykiosk.exceptions import FullyKioskError @@ -36,7 +35,7 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" try: - async with timeout(15): + async with asyncio.timeout(15): # Get device info and settings in parallel result = await asyncio.gather( self.fully.getDeviceInfo(), self.fully.getSettings() diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py index 63a17dbf285..2af4227391b 100644 --- a/homeassistant/components/garages_amsterdam/__init__.py +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -1,8 +1,8 @@ """The Garages Amsterdam integration.""" +import asyncio from datetime import timedelta import logging -import async_timeout from odp_amsterdam import ODPAmsterdam from homeassistant.config_entries import ConfigEntry @@ -40,7 +40,7 @@ async def get_coordinator( return hass.data[DOMAIN] async def async_get_garages(): - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return { garage.garage_name: garage for garage in await ODPAmsterdam( diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index eb2d109caeb..67ff5a84ed9 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -1,6 +1,7 @@ """Config flow for generic (IP Camera).""" from __future__ import annotations +import asyncio from collections.abc import Mapping import contextlib from datetime import datetime @@ -10,7 +11,6 @@ import logging from typing import Any from aiohttp import web -from async_timeout import timeout from httpx import HTTPStatusError, RequestError, TimeoutException import PIL.Image import voluptuous as vol @@ -171,7 +171,7 @@ async def async_test_still( auth = generate_auth(info) try: async_client = get_async_client(hass, verify_ssl=verify_ssl) - async with timeout(GET_IMAGE_TIMEOUT): + async with asyncio.timeout(GET_IMAGE_TIMEOUT): response = await async_client.get(url, auth=auth, timeout=GET_IMAGE_TIMEOUT) response.raise_for_status() image = response.content diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 2b56a9f6cbb..3cdf48944fd 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -1,11 +1,11 @@ """The GIOS component.""" from __future__ import annotations +import asyncio import logging from aiohttp import ClientSession from aiohttp.client_exceptions import ClientConnectorError -from async_timeout import timeout from gios import Gios from gios.exceptions import GiosError from gios.model import GiosSensors @@ -88,7 +88,7 @@ class GiosDataUpdateCoordinator(DataUpdateCoordinator[GiosSensors]): async def _async_update_data(self) -> GiosSensors: """Update data via library.""" try: - async with timeout(API_TIMEOUT): + async with asyncio.timeout(API_TIMEOUT): return await self.gios.async_update() except (GiosError, ClientConnectorError) as error: raise UpdateFailed(error) from error diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index a1b4abd2dc7..ffc34bd2b78 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -5,7 +5,6 @@ import asyncio from typing import Any from aiohttp.client_exceptions import ClientConnectorError -from async_timeout import timeout from gios import ApiError, Gios, InvalidSensorsDataError, NoStationError import voluptuous as vol @@ -37,7 +36,7 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): websession = async_get_clientsession(self.hass) - async with timeout(API_TIMEOUT): + async with asyncio.timeout(API_TIMEOUT): gios = Gios(user_input[CONF_STATION_ID], websession) await gios.async_update() diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index c8f6869f6e4..720c7d9aa2b 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -3,7 +3,6 @@ import asyncio import logging import os -import async_timeout from google.cloud import texttospeech import voluptuous as vol @@ -286,7 +285,7 @@ class GoogleCloudTTSProvider(Provider): "input": synthesis_input, } - async with async_timeout.timeout(10): + async with asyncio.timeout(10): assert self.hass response = await self.hass.async_add_executor_job( self._client.synthesize_speech, request diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index c7f7e632bd6..52dcdb61e8f 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -4,7 +4,6 @@ from datetime import timedelta import logging import aiohttp -import async_timeout import voluptuous as vol from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME @@ -69,7 +68,7 @@ async def _update_google_domains(hass, session, domain, user, password, timeout) params = {"hostname": domain} try: - async with async_timeout.timeout(timeout): + async with asyncio.timeout(timeout): resp = await session.get(url, params=params) body = await resp.text() diff --git a/homeassistant/components/hlk_sw16/config_flow.py b/homeassistant/components/hlk_sw16/config_flow.py index 4920e1542d5..01f695ad1a6 100644 --- a/homeassistant/components/hlk_sw16/config_flow.py +++ b/homeassistant/components/hlk_sw16/config_flow.py @@ -1,7 +1,6 @@ """Config flow for HLK-SW16.""" import asyncio -import async_timeout from hlk_sw16 import create_hlk_sw16_connection import voluptuous as vol @@ -36,7 +35,7 @@ async def connect_client(hass, user_input): reconnect_interval=DEFAULT_RECONNECT_INTERVAL, keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, ) - async with async_timeout.timeout(CONNECTION_TIMEOUT): + async with asyncio.timeout(CONNECTION_TIMEOUT): return await client_aw diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index 007f8895bf0..b6a1fc68a17 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -1,8 +1,8 @@ """The Legrand Home+ Control integration.""" +import asyncio from datetime import timedelta import logging -import async_timeout from homepluscontrol.homeplusapi import HomePlusControlApiError import voluptuous as vol @@ -100,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return await api.async_get_modules() except HomePlusControlApiError as err: raise UpdateFailed( diff --git a/homeassistant/components/homeassistant_yellow/config_flow.py b/homeassistant/components/homeassistant_yellow/config_flow.py index 3da67023abd..8be7b8a4ff7 100644 --- a/homeassistant/components/homeassistant_yellow/config_flow.py +++ b/homeassistant/components/homeassistant_yellow/config_flow.py @@ -1,11 +1,11 @@ """Config flow for the Home Assistant Yellow integration.""" from __future__ import annotations +import asyncio import logging from typing import Any import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components.hassio import ( @@ -80,7 +80,7 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl if self._hw_settings == user_input: return self.async_create_entry(data={}) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await async_set_yellow_settings(self.hass, user_input) except (aiohttp.ClientError, TimeoutError, HassioAPIError) as err: _LOGGER.warning("Failed to write hardware settings", exc_info=err) @@ -88,7 +88,7 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl return await self.async_step_confirm_reboot() try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): self._hw_settings: dict[str, bool] = await async_get_yellow_settings( self.hass ) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 0e1688221b3..04bd63e5b1f 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -10,7 +10,6 @@ import aiohttp from aiohttp import client_exceptions from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized from aiohue.errors import AiohueException, BridgeBusy -import async_timeout from homeassistant import core from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -73,7 +72,7 @@ class HueBridge: async def async_initialize_bridge(self) -> bool: """Initialize Connection with the Hue API.""" try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): await self.api.initialize() except (LinkButtonNotPressed, Unauthorized): diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 2b0ebdebcaa..9c8dda94c94 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -9,7 +9,6 @@ import aiohttp from aiohue import LinkButtonNotPressed, create_app_key from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp from aiohue.util import normalize_bridge_id -import async_timeout import slugify as unicode_slug import voluptuous as vol @@ -110,7 +109,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Find / discover bridges try: - async with async_timeout.timeout(5): + async with asyncio.timeout(5): bridges = await discover_nupnp( websession=aiohttp_client.async_get_clientsession(self.hass) ) diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index 8821c66a2cf..8ae09ef9d47 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -1,13 +1,13 @@ """Support for the Philips Hue lights.""" from __future__ import annotations +import asyncio from datetime import timedelta from functools import partial import logging import random import aiohue -import async_timeout from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -262,7 +262,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_safe_fetch(bridge, fetch_method): """Safely fetch data.""" try: - async with async_timeout.timeout(4): + async with asyncio.timeout(4): return await bridge.async_request_call(fetch_method) except aiohue.Unauthorized as err: await bridge.handle_unauthorized_error() diff --git a/homeassistant/components/hue/v1/sensor_base.py b/homeassistant/components/hue/v1/sensor_base.py index 84921707f2a..723ecfff451 100644 --- a/homeassistant/components/hue/v1/sensor_base.py +++ b/homeassistant/components/hue/v1/sensor_base.py @@ -1,13 +1,13 @@ """Support for the Philips Hue sensors as a platform.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging from typing import Any from aiohue import AiohueException, Unauthorized from aiohue.v1.sensors import TYPE_ZLL_PRESENCE -import async_timeout from homeassistant.components.sensor import SensorStateClass from homeassistant.core import callback @@ -61,7 +61,7 @@ class SensorManager: async def async_update_data(self): """Update sensor data.""" try: - async with async_timeout.timeout(4): + async with asyncio.timeout(4): return await self.bridge.async_request_call( self.bridge.api.sensors.update ) diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index 8559156379b..b1c2d865e0c 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -1,9 +1,9 @@ """The Huisbaasje integration.""" +import asyncio from datetime import timedelta import logging from typing import Any -import async_timeout from energyflip import EnergyFlip, EnergyFlipException from homeassistant.config_entries import ConfigEntry @@ -86,7 +86,7 @@ async def async_update_huisbaasje(energyflip: EnergyFlip) -> dict[str, dict[str, try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. - async with async_timeout.timeout(FETCH_TIMEOUT): + async with asyncio.timeout(FETCH_TIMEOUT): if not energyflip.is_authenticated(): _LOGGER.warning("Huisbaasje is unauthenticated. Reauthenticating") await energyflip.authenticate() diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 4b0d666d2ae..56ebbe6fb26 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -1,4 +1,5 @@ """The Hunter Douglas PowerView integration.""" +import asyncio import logging from aiopvapi.helpers.aiorequest import AioRequest @@ -8,7 +9,6 @@ from aiopvapi.rooms import Rooms from aiopvapi.scenes import Scenes from aiopvapi.shades import Shades from aiopvapi.userdata import UserData -import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform @@ -63,20 +63,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): device_info = await async_get_device_info(pv_request, hub_address) - async with async_timeout.timeout(10): + async with asyncio.timeout(10): rooms = Rooms(pv_request) room_data = async_map_data_by_id((await rooms.get_resources())[ROOM_DATA]) - async with async_timeout.timeout(10): + async with asyncio.timeout(10): scenes = Scenes(pv_request) scene_data = async_map_data_by_id( (await scenes.get_resources())[SCENE_DATA] ) - async with async_timeout.timeout(10): + async with asyncio.timeout(10): shades = Shades(pv_request) shade_entries = await shades.get_resources() shade_data = async_map_data_by_id(shade_entries[SHADE_DATA]) diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 7c9bdfcf244..8c6d0fc4dd3 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -1,10 +1,10 @@ """Config flow for Hunter Douglas PowerView integration.""" from __future__ import annotations +import asyncio import logging from aiopvapi.helpers.aiorequest import AioRequest -import async_timeout import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -34,7 +34,7 @@ async def validate_input(hass: core.HomeAssistant, hub_address: str) -> dict[str pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): device_info = await async_get_device_info(pv_request, hub_address) except HUB_EXCEPTIONS as err: raise CannotConnect from err diff --git a/homeassistant/components/hunterdouglas_powerview/coordinator.py b/homeassistant/components/hunterdouglas_powerview/coordinator.py index 203aea6c49f..4643536d56d 100644 --- a/homeassistant/components/hunterdouglas_powerview/coordinator.py +++ b/homeassistant/components/hunterdouglas_powerview/coordinator.py @@ -1,11 +1,11 @@ """Coordinate data for powerview devices.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging from aiopvapi.shades import Shades -import async_timeout from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -37,7 +37,7 @@ class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData]) async def _async_update_data(self) -> PowerviewShadeData: """Fetch data from shade endpoint.""" - async with async_timeout.timeout(10): + async with asyncio.timeout(10): shade_entries = await self.shades.get_resources() if isinstance(shade_entries, bool): diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 5cb84658c50..833c1812ddb 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -19,7 +19,6 @@ from aiopvapi.helpers.constants import ( MIN_POSITION, ) from aiopvapi.resources.shade import BaseShade, factory as PvShade -import async_timeout from homeassistant.components.cover import ( ATTR_POSITION, @@ -84,7 +83,7 @@ async def async_setup_entry( shade: BaseShade = PvShade(raw_shade, pv_entry.api) name_before_refresh = shade.name with suppress(asyncio.TimeoutError): - async with async_timeout.timeout(1): + async with asyncio.timeout(1): await shade.refresh() if ATTR_POSITION_DATA not in shade.raw_data: diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py index ac965285977..513c8dbd8b0 100644 --- a/homeassistant/components/hvv_departures/binary_sensor.py +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -1,12 +1,12 @@ """Binary sensor platform for hvv_departures.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging from typing import Any from aiohttp import ClientConnectorError -import async_timeout from pygti.exceptions import InvalidAuth from homeassistant.components.binary_sensor import ( @@ -90,7 +90,7 @@ async def async_setup_entry( payload = {"station": {"id": station["id"], "type": station["type"]}} try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return get_elevator_entities_from_station_information( station_name, await hub.gti.stationInformation(payload) ) From 3de402bd150472b66094b0940adac2b7700efdf4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Aug 2023 16:22:42 +0200 Subject: [PATCH 0532/1151] Fix AiohttpClientMockResponse.release (#98458) --- tests/test_util/aiohttp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 356240dc37a..5e7284eb9c2 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -255,7 +255,7 @@ class AiohttpClientMockResponse: """Return mock response as a json.""" return loads(self.response.decode(encoding)) - def release(self): + async def release(self): """Mock release.""" def raise_for_status(self): From e209f3723e61231867b3020774a8cb6b24591b8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Aug 2023 09:29:25 -0500 Subject: [PATCH 0533/1151] Restore sensorpush state when device becomes available (#98420) --- homeassistant/components/sensorpush/sensor.py | 4 +- tests/components/sensorpush/__init__.py | 11 ++++ tests/components/sensorpush/test_sensor.py | 60 ++++++++++++++++--- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index 479acd8ac1e..e12bf0e48c6 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -110,7 +110,9 @@ async def async_setup_entry( SensorPushBluetoothSensorEntity, async_add_entities ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + coordinator.async_register_processor(processor, SensorEntityDescription) + ) class SensorPushBluetoothSensorEntity( diff --git a/tests/components/sensorpush/__init__.py b/tests/components/sensorpush/__init__.py index 0fe9ced64df..c281d4dc086 100644 --- a/tests/components/sensorpush/__init__.py +++ b/tests/components/sensorpush/__init__.py @@ -32,3 +32,14 @@ HTPWX_SERVICE_INFO = BluetoothServiceInfo( service_uuids=["ef090000-11d6-42ba-93b8-9dd7ec090ab0"], source="local", ) + + +HTPWX_EMPTY_SERVICE_INFO = BluetoothServiceInfo( + name="SensorPush HTP.xw F4D", + address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + rssi=-56, + manufacturer_data={}, + service_data={}, + service_uuids=["ef090000-11d6-42ba-93b8-9dd7ec090ab0"], + source="local", +) diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py index f2d6cf6d1ac..e00b626b20b 100644 --- a/tests/components/sensorpush/test_sensor.py +++ b/tests/components/sensorpush/test_sensor.py @@ -1,17 +1,33 @@ """Test the SensorPush 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.sensor import ATTR_STATE_CLASS from homeassistant.components.sensorpush.const import DOMAIN -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 HTPWX_SERVICE_INFO +from . import HTPWX_EMPTY_SERVICE_INFO, HTPWX_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: """Test setting up creates the sensors.""" + start_monotonic = time.monotonic() entry = MockConfigEntry( domain=DOMAIN, unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", @@ -27,11 +43,39 @@ async def test_sensors(hass: HomeAssistant) -> None: assert len(hass.states.async_all()) == 3 temp_sensor = hass.states.get("sensor.htp_xw_f4d_temperature") - temp_sensor_attribtes = temp_sensor.attributes + temp_sensor_attributes = temp_sensor.attributes + assert temp_sensor.state == "20.11" + assert temp_sensor_attributes[ATTR_FRIENDLY_NAME] == "HTP.xw F4D Temperature" + assert temp_sensor_attributes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attributes[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() + + temp_sensor = hass.states.get("sensor.htp_xw_f4d_temperature") + assert temp_sensor.state == STATE_UNAVAILABLE + inject_bluetooth_service_info(hass, HTPWX_EMPTY_SERVICE_INFO) + await hass.async_block_till_done() + + temp_sensor = hass.states.get("sensor.htp_xw_f4d_temperature") assert temp_sensor.state == "20.11" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "HTP.xw F4D Temperature" - assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" - assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() From 92cf6ed2a0e4117819b59137031bf1c1ab26beaa Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 15 Aug 2023 16:50:17 +0200 Subject: [PATCH 0534/1151] Reolink 100% test coverage (#94763) --- tests/components/reolink/conftest.py | 12 ++--- tests/components/reolink/test_config_flow.py | 4 +- tests/components/reolink/test_init.py | 50 ++++++++++++++------ 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 1e6f9aa4902..25719c4cff7 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -58,18 +58,16 @@ def reolink_connect(mock_get_source_ip: None) -> Generator[MagicMock, None, None host_mock.is_admin = True host_mock.user_level = "admin" host_mock.sw_version_update_required = False + host_mock.hardware_version = "IPC_00000" + host_mock.sw_version = "v1.0.0.0.0.0000" + host_mock.manufacturer = "Reolink" + host_mock.model = "RLC-123" + host_mock.session_active = True host_mock.timeout = 60 host_mock.renewtimer.return_value = 600 yield host_mock -@pytest.fixture -def reolink_ONVIF_wait() -> Generator[None, None, None]: - """Mock reolink connection.""" - with patch("homeassistant.components.reolink.host.asyncio.Event.wait", AsyncMock()): - yield - - @pytest.fixture def reolink_platforms(mock_get_source_ip: None) -> Generator[None, None, None]: """Mock reolink entry setup.""" diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index b6e48cab7b2..048b48d9576 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -29,9 +29,7 @@ from .conftest import ( from tests.common import MockConfigEntry -pytestmark = pytest.mark.usefixtures( - "mock_setup_entry", "reolink_connect", "reolink_ONVIF_wait" -) +pytestmark = pytest.mark.usefixtures("mock_setup_entry", "reolink_connect") async def test_config_flow_manual_success(hass: HomeAssistant) -> None: diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 8558ff0e8a2..d649baeb937 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -1,18 +1,23 @@ """Test the Reolink init.""" +from datetime import timedelta from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from reolink_aio.exceptions import ReolinkError -from homeassistant.components.reolink import const +from homeassistant.components.reolink import FIRMWARE_UPDATE_INTERVAL, const from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry +from .conftest import TEST_NVR_NAME + +from tests.common import MockConfigEntry, async_fire_time_changed pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") @@ -45,17 +50,11 @@ pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") Mock(return_value=False), ConfigEntryState.LOADED, ), - ( - "check_new_firmware", - AsyncMock(side_effect=ReolinkError("Test error")), - ConfigEntryState.LOADED, - ), ], ) async def test_failures_parametrized( hass: HomeAssistant, reolink_connect: MagicMock, - reolink_ONVIF_wait: MagicMock, config_entry: MockConfigEntry, attr: str, value: Any, @@ -71,11 +70,36 @@ async def test_failures_parametrized( assert config_entry.state == expected +async def test_firmware_error_twice( + hass: HomeAssistant, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, +) -> None: + """Test when the firmware update fails 2 times.""" + reolink_connect.check_new_firmware = AsyncMock( + side_effect=ReolinkError("Test error") + ) + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.UPDATE]): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + + entity_id = f"{Platform.UPDATE}.{TEST_NVR_NAME}_update" + assert hass.states.is_state(entity_id, STATE_OFF) + + async_fire_time_changed( + hass, utcnow() + FIRMWARE_UPDATE_INTERVAL + timedelta(minutes=1) + ) + await hass.async_block_till_done() + + assert hass.states.is_state(entity_id, STATE_UNAVAILABLE) + + async def test_entry_reloading( hass: HomeAssistant, config_entry: MockConfigEntry, reolink_connect: MagicMock, - reolink_ONVIF_wait: MagicMock, ) -> None: """Test the entry is reloaded correctly when settings change.""" assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -92,7 +116,7 @@ async def test_entry_reloading( async def test_no_repair_issue( - hass: HomeAssistant, config_entry: MockConfigEntry, reolink_ONVIF_wait: MagicMock + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test no repairs issue is raised when http local url is used.""" await async_process_ha_core_config( @@ -111,7 +135,7 @@ async def test_no_repair_issue( async def test_https_repair_issue( - hass: HomeAssistant, config_entry: MockConfigEntry, reolink_ONVIF_wait: MagicMock + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test repairs issue is raised when https local url is used.""" await async_process_ha_core_config( @@ -133,7 +157,7 @@ async def test_https_repair_issue( async def test_ssl_repair_issue( - hass: HomeAssistant, config_entry: MockConfigEntry, reolink_ONVIF_wait: MagicMock + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test repairs issue is raised when global ssl certificate is used.""" assert await async_setup_component(hass, "webhook", {}) @@ -162,7 +186,6 @@ async def test_port_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, reolink_connect: MagicMock, - reolink_ONVIF_wait: MagicMock, protocol: str, ) -> None: """Test repairs issue is raised when auto enable of ports fails.""" @@ -200,7 +223,6 @@ async def test_firmware_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, reolink_connect: MagicMock, - reolink_ONVIF_wait: MagicMock, ) -> None: """Test firmware issue is raised when too old firmware is used.""" reolink_connect.sw_version_update_required = True From 496a975c582552e4b6dd048a02a10891aa3e6c7b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Aug 2023 17:17:35 +0200 Subject: [PATCH 0535/1151] Set _attr_condition in WeatherEntity (#98459) --- homeassistant/components/buienradar/weather.py | 1 - homeassistant/components/smhi/weather.py | 1 - homeassistant/components/weather/__init__.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index aedfcf82aea..cdb8adf1dac 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -136,7 +136,6 @@ class BrWeather(WeatherEntity): self._stationname = config.get(CONF_NAME, "Buienradar") self._attr_name = self._stationname or f"BR {'(unknown station)'}" - self._attr_condition = None self._attr_unique_id = "{:2.6f}{:2.6f}".format( coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE] ) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index e62d236c819..db5d7287ccd 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -149,7 +149,6 @@ class SmhiWeather(WeatherEntity): name=name, configuration_url="http://opendata.smhi.se/apidocs/metfcst/parameters.html", ) - self._attr_condition = None self._attr_native_temperature = None @property diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 635c4948285..74671f0c1df 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -227,7 +227,7 @@ class WeatherEntity(Entity, PostInit): """ABC for weather data.""" entity_description: WeatherEntityDescription - _attr_condition: str | None + _attr_condition: str | None = None # _attr_forecast is deprecated, implement async_forecast_daily, # async_forecast_hourly or async_forecast_twice daily instead _attr_forecast: list[Forecast] | None = None From 063ce9159df64bbc9e7149ac00fcab3a39e088c1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Aug 2023 17:21:49 +0200 Subject: [PATCH 0536/1151] Use asyncio.timeout [o-s] (#98451) --- .../components/openalpr_cloud/image_processing.py | 3 +-- homeassistant/components/openexchangerates/config_flow.py | 5 ++--- homeassistant/components/openexchangerates/coordinator.py | 4 ++-- homeassistant/components/opentherm_gw/__init__.py | 3 +-- homeassistant/components/opentherm_gw/config_flow.py | 3 +-- .../openweathermap/weather_update_coordinator.py | 4 ++-- homeassistant/components/ovo_energy/__init__.py | 4 ++-- homeassistant/components/picnic/coordinator.py | 4 ++-- homeassistant/components/ping/binary_sensor.py | 3 +-- homeassistant/components/point/config_flow.py | 3 +-- homeassistant/components/poolsense/__init__.py | 4 ++-- homeassistant/components/progettihwsw/binary_sensor.py | 4 ++-- homeassistant/components/progettihwsw/switch.py | 4 ++-- homeassistant/components/prowl/notify.py | 3 +-- homeassistant/components/prusalink/__init__.py | 4 ++-- homeassistant/components/prusalink/config_flow.py | 3 +-- homeassistant/components/push/camera.py | 3 +-- homeassistant/components/qnap_qsw/coordinator.py | 6 +++--- homeassistant/components/rainbird/config_flow.py | 3 +-- homeassistant/components/rainbird/coordinator.py | 4 ++-- homeassistant/components/rainforest_eagle/data.py | 6 +++--- homeassistant/components/renson/__init__.py | 4 ++-- homeassistant/components/reolink/__init__.py | 7 +++---- homeassistant/components/rest/switch.py | 5 ++--- homeassistant/components/rflink/__init__.py | 3 +-- homeassistant/components/rfxtrx/__init__.py | 3 +-- homeassistant/components/rfxtrx/config_flow.py | 5 ++--- homeassistant/components/roomba/__init__.py | 5 ++--- homeassistant/components/rtsp_to_webrtc/__init__.py | 6 +++--- homeassistant/components/samsungtv/media_player.py | 3 +-- homeassistant/components/sensibo/entity.py | 4 ++-- homeassistant/components/sensibo/util.py | 5 +++-- homeassistant/components/sharkiq/__init__.py | 5 ++--- homeassistant/components/sharkiq/config_flow.py | 3 +-- homeassistant/components/sharkiq/update_coordinator.py | 3 +-- homeassistant/components/shell_command/__init__.py | 3 +-- homeassistant/components/smarttub/controller.py | 3 +-- homeassistant/components/smarttub/switch.py | 4 ++-- homeassistant/components/smhi/weather.py | 3 +-- homeassistant/components/sms/__init__.py | 6 +++--- tests/components/rainbird/test_config_flow.py | 2 +- 41 files changed, 70 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index aa1e5ecbc0a..64b46a1da94 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -7,7 +7,6 @@ from http import HTTPStatus import logging import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components.image_processing import ( @@ -199,7 +198,7 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): body = {"image_bytes": str(b64encode(image), "utf-8")} try: - async with async_timeout.timeout(self.timeout): + async with asyncio.timeout(self.timeout): request = await websession.post( OPENALPR_API_URL, params=params, data=body ) diff --git a/homeassistant/components/openexchangerates/config_flow.py b/homeassistant/components/openexchangerates/config_flow.py index 13060e19718..a61264dbf41 100644 --- a/homeassistant/components/openexchangerates/config_flow.py +++ b/homeassistant/components/openexchangerates/config_flow.py @@ -10,7 +10,6 @@ from aioopenexchangerates import ( OpenExchangeRatesAuthError, OpenExchangeRatesClientError, ) -import async_timeout import voluptuous as vol from homeassistant import config_entries @@ -40,7 +39,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, """Validate the user input allows us to connect.""" client = Client(data[CONF_API_KEY], async_get_clientsession(hass)) - async with async_timeout.timeout(CLIENT_TIMEOUT): + async with asyncio.timeout(CLIENT_TIMEOUT): await client.get_latest(base=data[CONF_BASE]) return {"title": data[CONF_BASE]} @@ -119,7 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not self.currencies: client = Client("dummy-api-key", async_get_clientsession(self.hass)) try: - async with async_timeout.timeout(CLIENT_TIMEOUT): + async with asyncio.timeout(CLIENT_TIMEOUT): self.currencies = await client.get_currencies() except OpenExchangeRatesClientError as err: raise AbortFlow("cannot_connect") from err diff --git a/homeassistant/components/openexchangerates/coordinator.py b/homeassistant/components/openexchangerates/coordinator.py index 3795f33aec5..beb588c7ce6 100644 --- a/homeassistant/components/openexchangerates/coordinator.py +++ b/homeassistant/components/openexchangerates/coordinator.py @@ -1,6 +1,7 @@ """Provide an OpenExchangeRates data coordinator.""" from __future__ import annotations +import asyncio from datetime import timedelta from aiohttp import ClientSession @@ -10,7 +11,6 @@ from aioopenexchangerates import ( OpenExchangeRatesAuthError, OpenExchangeRatesClientError, ) -import async_timeout from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -40,7 +40,7 @@ class OpenexchangeratesCoordinator(DataUpdateCoordinator[Latest]): async def _async_update_data(self) -> Latest: """Update data from Open Exchange Rates.""" try: - async with async_timeout.timeout(CLIENT_TIMEOUT): + async with asyncio.timeout(CLIENT_TIMEOUT): latest = await self.client.get_latest(base=self.base) except OpenExchangeRatesAuthError as err: raise ConfigEntryAuthFailed(err) from err diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 3efe911b27f..0b8d4693cb8 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -3,7 +3,6 @@ import asyncio from datetime import date, datetime import logging -import async_timeout import pyotgw import pyotgw.vars as gw_vars from serial import SerialException @@ -113,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.add_update_listener(options_updated) try: - async with async_timeout.timeout(CONNECTION_TIMEOUT): + async with asyncio.timeout(CONNECTION_TIMEOUT): await gateway.connect_and_subscribe() except (asyncio.TimeoutError, ConnectionError, SerialException) as ex: await gateway.cleanup() diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 87a51021657..07187f3a2ec 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -import async_timeout import pyotgw from pyotgw import vars as gw_vars from serial import SerialException @@ -69,7 +68,7 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) try: - async with async_timeout.timeout(CONNECTION_TIMEOUT): + async with asyncio.timeout(CONNECTION_TIMEOUT): await test_connection() except asyncio.TimeoutError: return self._show_form({"base": "timeout_connect"}) diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 521c1f87ca2..732557363d8 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -1,8 +1,8 @@ """Weather data coordinator for the OpenWeatherMap (OWM) service.""" +import asyncio from datetime import timedelta import logging -import async_timeout from pyowm.commons.exceptions import APIRequestError, UnauthorizedError from homeassistant.components.weather import ( @@ -80,7 +80,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Update the data.""" data = {} - async with async_timeout.timeout(20): + async with asyncio.timeout(20): try: weather_response = await self._get_owm_weather() data = self._convert_weather_response(weather_response) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 1a871e99023..99dd02a36a1 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -1,11 +1,11 @@ """Support for OVO Energy.""" from __future__ import annotations +import asyncio from datetime import datetime, timedelta import logging import aiohttp -import async_timeout from ovoenergy import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_data() -> OVODailyUsage: """Fetch data from OVO Energy.""" - async with async_timeout.timeout(10): + async with asyncio.timeout(10): try: authenticated = await client.authenticate( entry.data[CONF_USERNAME], diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index 06f4efd944e..00a9f534852 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -1,10 +1,10 @@ """Coordinator to fetch data from the Picnic API.""" +import asyncio from contextlib import suppress import copy from datetime import timedelta import logging -import async_timeout from python_picnic_api import PicnicAPI from python_picnic_api.session import PicnicAuthError @@ -44,7 +44,7 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator): try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. - async with async_timeout.timeout(10): + async with asyncio.timeout(10): data = await self.hass.async_add_executor_job(self.fetch_data) # Update the auth token in the config entry if applicable diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 786012d466c..6a150b3dc4c 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -8,7 +8,6 @@ import logging import re from typing import TYPE_CHECKING, Any -import async_timeout from icmplib import NameLookupError, async_ping import voluptuous as vol @@ -218,7 +217,7 @@ class PingDataSubProcess(PingData): close_fds=False, # required for posix_spawn ) try: - async with async_timeout.timeout(self._count + PING_TIMEOUT): + async with asyncio.timeout(self._count + PING_TIMEOUT): out_data, out_error = await pinger.communicate() if out_data: diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index fad5b746252..201e397ba7d 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -3,7 +3,6 @@ import asyncio from collections import OrderedDict import logging -import async_timeout from pypoint import PointSession import voluptuous as vol @@ -94,7 +93,7 @@ class PointFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "follow_link" try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): url = await self._get_authorization_url() except asyncio.TimeoutError: return self.async_abort(reason="authorize_url_timeout") diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 312a3b4be58..56b7eaaac77 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -1,8 +1,8 @@ """The PoolSense integration.""" +import asyncio from datetime import timedelta import logging -import async_timeout from poolsense import PoolSense from poolsense.exceptions import PoolSenseError @@ -90,7 +90,7 @@ class PoolSenseDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Update data via library.""" data = {} - async with async_timeout.timeout(10): + async with asyncio.timeout(10): try: data = await self.poolsense.get_poolsense_data() except PoolSenseError as error: diff --git a/homeassistant/components/progettihwsw/binary_sensor.py b/homeassistant/components/progettihwsw/binary_sensor.py index b2019389fe3..e2d1025cc64 100644 --- a/homeassistant/components/progettihwsw/binary_sensor.py +++ b/homeassistant/components/progettihwsw/binary_sensor.py @@ -1,8 +1,8 @@ """Control binary sensor instances.""" +import asyncio from datetime import timedelta import logging -import async_timeout from ProgettiHWSW.input import Input from homeassistant.components.binary_sensor import BinarySensorEntity @@ -32,7 +32,7 @@ async def async_setup_entry( async def async_update_data(): """Fetch data from API endpoint of board.""" - async with async_timeout.timeout(5): + async with asyncio.timeout(5): return await board_api.get_inputs() coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/progettihwsw/switch.py b/homeassistant/components/progettihwsw/switch.py index dc7f838bcbc..77cfb6ba4d1 100644 --- a/homeassistant/components/progettihwsw/switch.py +++ b/homeassistant/components/progettihwsw/switch.py @@ -1,9 +1,9 @@ """Control switches.""" +import asyncio from datetime import timedelta import logging from typing import Any -import async_timeout from ProgettiHWSW.relay import Relay from homeassistant.components.switch import SwitchEntity @@ -33,7 +33,7 @@ async def async_setup_entry( async def async_update_data(): """Fetch data from API endpoint of board.""" - async with async_timeout.timeout(5): + async with asyncio.timeout(5): return await board_api.get_switches() coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index 02d4f61f4e4..d0b35aaf4b9 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -5,7 +5,6 @@ import asyncio from http import HTTPStatus import logging -import async_timeout import voluptuous as vol from homeassistant.components.notify import ( @@ -64,7 +63,7 @@ class ProwlNotificationService(BaseNotificationService): session = async_get_clientsession(self._hass) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): response = await session.post(url, data=payload) result = await response.text() diff --git a/homeassistant/components/prusalink/__init__.py b/homeassistant/components/prusalink/__init__.py index 59708d76097..e81901dad52 100644 --- a/homeassistant/components/prusalink/__init__.py +++ b/homeassistant/components/prusalink/__init__.py @@ -2,12 +2,12 @@ from __future__ import annotations from abc import ABC, abstractmethod +import asyncio from datetime import timedelta import logging from time import monotonic from typing import Generic, TypeVar -import async_timeout from pyprusalink import InvalidAuth, JobInfo, PrinterInfo, PrusaLink, PrusaLinkError from homeassistant.config_entries import ConfigEntry @@ -77,7 +77,7 @@ class PrusaLinkUpdateCoordinator(DataUpdateCoordinator, Generic[T], ABC): async def _async_update_data(self) -> T: """Update the data.""" try: - async with async_timeout.timeout(5): + async with asyncio.timeout(5): data = await self._fetch_data() except InvalidAuth: raise UpdateFailed("Invalid authentication") from None diff --git a/homeassistant/components/prusalink/config_flow.py b/homeassistant/components/prusalink/config_flow.py index cef2bdf2f6e..b1faad6e3ea 100644 --- a/homeassistant/components/prusalink/config_flow.py +++ b/homeassistant/components/prusalink/config_flow.py @@ -6,7 +6,6 @@ import logging from typing import Any from aiohttp import ClientError -import async_timeout from awesomeversion import AwesomeVersion, AwesomeVersionException from pyprusalink import InvalidAuth, PrusaLink import voluptuous as vol @@ -39,7 +38,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, api = PrusaLink(async_get_clientsession(hass), data[CONF_HOST], data[CONF_API_KEY]) try: - async with async_timeout.timeout(5): + async with asyncio.timeout(5): version = await api.get_version() except (asyncio.TimeoutError, ClientError) as err: diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 77bcf63e17e..a4fec1c3d4d 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -7,7 +7,6 @@ from datetime import timedelta import logging import aiohttp -import async_timeout import voluptuous as vol from homeassistant.components import webhook @@ -74,7 +73,7 @@ async def async_setup_platform( async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook POST with image files.""" try: - async with async_timeout.timeout(5): + async with asyncio.timeout(5): data = dict(await request.post()) except (asyncio.TimeoutError, aiohttp.web.HTTPException) as error: _LOGGER.error("Could not get information from POST <%s>", error) diff --git a/homeassistant/components/qnap_qsw/coordinator.py b/homeassistant/components/qnap_qsw/coordinator.py index eb4e60bf9bd..6451b525004 100644 --- a/homeassistant/components/qnap_qsw/coordinator.py +++ b/homeassistant/components/qnap_qsw/coordinator.py @@ -1,13 +1,13 @@ """The QNAP QSW coordinator.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging from typing import Any from aioqsw.exceptions import QswError from aioqsw.localapi import QnapQswApi -import async_timeout from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -36,7 +36,7 @@ class QswDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" - async with async_timeout.timeout(QSW_TIMEOUT_SEC): + async with asyncio.timeout(QSW_TIMEOUT_SEC): try: await self.qsw.update() except QswError as error: @@ -60,7 +60,7 @@ class QswFirmwareCoordinator(DataUpdateCoordinator[dict[str, Any]]): async def _async_update_data(self) -> dict[str, Any]: """Update firmware data via library.""" - async with async_timeout.timeout(QSW_TIMEOUT_SEC): + async with asyncio.timeout(QSW_TIMEOUT_SEC): try: await self.qsw.check_firmware() except QswError as error: diff --git a/homeassistant/components/rainbird/config_flow.py b/homeassistant/components/rainbird/config_flow.py index 0409d0ff564..a784e4623d6 100644 --- a/homeassistant/components/rainbird/config_flow.py +++ b/homeassistant/components/rainbird/config_flow.py @@ -6,7 +6,6 @@ import asyncio import logging from typing import Any -import async_timeout from pyrainbird.async_client import ( AsyncRainbirdClient, AsyncRainbirdController, @@ -106,7 +105,7 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) ) try: - async with async_timeout.timeout(TIMEOUT_SECONDS): + async with asyncio.timeout(TIMEOUT_SECONDS): return await controller.get_serial_number() except asyncio.TimeoutError as err: raise ConfigFlowError( diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index 91319b25e59..d81b942d669 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -2,12 +2,12 @@ from __future__ import annotations +import asyncio from dataclasses import dataclass import datetime import logging from typing import TypeVar -import async_timeout from pyrainbird.async_client import ( AsyncRainbirdController, RainbirdApiException, @@ -86,7 +86,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): async def _async_update_data(self) -> RainbirdDeviceState: """Fetch data from Rain Bird device.""" try: - async with async_timeout.timeout(TIMEOUT_SECONDS): + async with asyncio.timeout(TIMEOUT_SECONDS): return await self._fetch_data() except RainbirdDeviceBusyException as err: raise UpdateFailed("Rain Bird device is busy") from err diff --git a/homeassistant/components/rainforest_eagle/data.py b/homeassistant/components/rainforest_eagle/data.py index c7ef596bb61..f050e92f783 100644 --- a/homeassistant/components/rainforest_eagle/data.py +++ b/homeassistant/components/rainforest_eagle/data.py @@ -1,12 +1,12 @@ """Rainforest data.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging import aioeagle import aiohttp -import async_timeout from eagle100 import Eagle as Eagle100Reader from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout @@ -50,7 +50,7 @@ async def async_get_type(hass, cloud_id, install_code, host): ) try: - async with async_timeout.timeout(30): + async with asyncio.timeout(30): meters = await hub.get_device_list() except aioeagle.BadAuth as err: raise InvalidAuth from err @@ -150,7 +150,7 @@ class EagleDataCoordinator(DataUpdateCoordinator): else: is_connected = eagle200_meter.is_connected - async with async_timeout.timeout(30): + async with asyncio.timeout(30): data = await eagle200_meter.get_device_query() if self.eagle200_meter is None: diff --git a/homeassistant/components/renson/__init__.py b/homeassistant/components/renson/__init__.py index 211f7c88e40..bac9bafa8a5 100644 --- a/homeassistant/components/renson/__init__.py +++ b/homeassistant/components/renson/__init__.py @@ -1,12 +1,12 @@ """The Renson integration.""" from __future__ import annotations +import asyncio from dataclasses import dataclass from datetime import timedelta import logging from typing import Any -import async_timeout from renson_endura_delta.renson import RensonVentilation from homeassistant.config_entries import ConfigEntry @@ -84,5 +84,5 @@ class RensonCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Fetch data from API endpoint.""" - async with async_timeout.timeout(30): + async with asyncio.timeout(30): return await self.hass.async_add_executor_job(self.api.get_all_data) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 88eec9780a1..5cfb2ceecb7 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -8,7 +8,6 @@ from datetime import timedelta import logging from typing import Literal -import async_timeout from reolink_aio.api import RETRY_ATTEMPTS from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError @@ -78,13 +77,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_device_config_update() -> None: """Update the host state cache and renew the ONVIF-subscription.""" - async with async_timeout.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): + async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): try: await host.update_states() except ReolinkError as err: raise UpdateFailed(str(err)) from err - async with async_timeout.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): + async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): await host.renew() async def async_check_firmware_update() -> str | Literal[False]: @@ -92,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if not host.api.supported(None, "update"): return False - async with async_timeout.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): + async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): try: return await host.api.check_new_firmware() except (ReolinkError, asyncio.exceptions.CancelledError) as err: diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 0a220204997..22570c3a245 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -6,7 +6,6 @@ from http import HTTPStatus import logging from typing import Any -import async_timeout import httpx import voluptuous as vol @@ -203,7 +202,7 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_params = template.render_complex(self._params) - async with async_timeout.timeout(self._timeout): + async with asyncio.timeout(self._timeout): req: httpx.Response = await getattr(websession, self._method)( self._resource, auth=self._auth, @@ -234,7 +233,7 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity): rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_params = template.render_complex(self._params) - async with async_timeout.timeout(self._timeout): + async with asyncio.timeout(self._timeout): req = await websession.get( self._state_resource, auth=self._auth, diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 8df2d7ec343..60e2b0fef58 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -5,7 +5,6 @@ import asyncio from collections import defaultdict import logging -import async_timeout from rflink.protocol import ProtocolBase, create_rflink_connection from serial import SerialException import voluptuous as vol @@ -280,7 +279,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) try: - async with async_timeout.timeout(CONNECTION_TIMEOUT): + async with asyncio.timeout(CONNECTION_TIMEOUT): transport, protocol = await connection except ( diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index e8d20ef9c10..9c5ffa586cd 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -8,7 +8,6 @@ import copy import logging from typing import Any, NamedTuple, cast -import async_timeout import RFXtrx as rfxtrxmod import voluptuous as vol @@ -165,7 +164,7 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: config = entry.data # Initialize library - async with async_timeout.timeout(30): + async with asyncio.timeout(30): rfx_object = await hass.async_add_executor_job(_create_rfx, config) # Setup some per device config diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 8d55208cbb7..179dd04cfaa 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -8,7 +8,6 @@ import itertools import os from typing import Any, TypedDict, cast -from async_timeout import timeout import RFXtrx as rfxtrxmod import serial import serial.tools.list_ports @@ -374,7 +373,7 @@ class OptionsFlow(config_entries.OptionsFlow): # Wait for entities to finish cleanup with suppress(asyncio.TimeoutError): - async with timeout(10): + async with asyncio.timeout(10): await wait_for_entities.wait() remove_track_state_changes() @@ -409,7 +408,7 @@ class OptionsFlow(config_entries.OptionsFlow): # Wait for entities to finish renaming with suppress(asyncio.TimeoutError): - async with timeout(10): + async with asyncio.timeout(10): await wait_for_entities.wait() remove_track_state_changes() diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 641c814d122..85dbbe14cdc 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -3,7 +3,6 @@ import asyncio from functools import partial import logging -import async_timeout from roombapy import RoombaConnectionError, RoombaFactory from homeassistant import exceptions @@ -86,7 +85,7 @@ async def async_connect_or_timeout(hass, roomba): """Connect to vacuum.""" try: name = None - async with async_timeout.timeout(10): + async with asyncio.timeout(10): _LOGGER.debug("Initialize connection to vacuum") await hass.async_add_executor_job(roomba.connect) while not roomba.roomba_connected or name is None: @@ -110,7 +109,7 @@ async def async_connect_or_timeout(hass, roomba): async def async_disconnect_or_timeout(hass, roomba): """Disconnect to vacuum.""" _LOGGER.debug("Disconnect vacuum") - async with async_timeout.timeout(3): + async with asyncio.timeout(3): await hass.async_add_executor_job(roomba.disconnect) return True diff --git a/homeassistant/components/rtsp_to_webrtc/__init__.py b/homeassistant/components/rtsp_to_webrtc/__init__.py index f5f114bce9c..77bf7ffeb8f 100644 --- a/homeassistant/components/rtsp_to_webrtc/__init__.py +++ b/homeassistant/components/rtsp_to_webrtc/__init__.py @@ -18,10 +18,10 @@ Other integrations may use this integration with these steps: from __future__ import annotations +import asyncio import logging from typing import Any -import async_timeout from rtsp_to_webrtc.client import get_adaptive_client from rtsp_to_webrtc.exceptions import ClientError, ResponseError from rtsp_to_webrtc.interface import WebRTCClientInterface @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client: WebRTCClientInterface try: - async with async_timeout.timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT): client = await get_adaptive_client( async_get_clientsession(hass), entry.data[DATA_SERVER_URL] ) @@ -71,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: the stream itself happens directly between the client and proxy. """ try: - async with async_timeout.timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT): return await client.offer_stream_id(stream_id, offer_sdp, stream_source) except TimeoutError as err: raise HomeAssistantError("Timeout talking to RTSPtoWebRTC server") from err diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 2f82c979b94..06783314b4c 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -5,7 +5,6 @@ import asyncio from collections.abc import Coroutine, Sequence from typing import Any -import async_timeout from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester from async_upnp_client.client import UpnpDevice, UpnpService, UpnpStateVariable from async_upnp_client.client_factory import UpnpFactory @@ -217,7 +216,7 @@ class SamsungTVDevice(SamsungTVEntity, MediaPlayerEntity): # enter it unless we have to (Python 3.11 will have zero cost try) return try: - async with async_timeout.timeout(APP_LIST_DELAY): + async with asyncio.timeout(APP_LIST_DELAY): await self._app_list_event.wait() except asyncio.TimeoutError as err: # No need to try again diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index 9fdd1ef9f21..4eff1a011a5 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -1,10 +1,10 @@ """Base entity for Sensibo integration.""" from __future__ import annotations +import asyncio from collections.abc import Callable, Coroutine from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar -import async_timeout from pysensibo.model import MotionSensor, SensiboDevice from homeassistant.exceptions import HomeAssistantError @@ -27,7 +27,7 @@ def async_handle_api_call( """Wrap services for api calls.""" res: bool = False try: - async with async_timeout.timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT): res = await function(*args, **kwargs) except SENSIBO_ERRORS as err: raise HomeAssistantError from err diff --git a/homeassistant/components/sensibo/util.py b/homeassistant/components/sensibo/util.py index 9070be3412a..98b843a9dfc 100644 --- a/homeassistant/components/sensibo/util.py +++ b/homeassistant/components/sensibo/util.py @@ -1,7 +1,8 @@ """Utils for Sensibo integration.""" from __future__ import annotations -import async_timeout +import asyncio + from pysensibo import SensiboClient from pysensibo.exceptions import AuthenticationError @@ -20,7 +21,7 @@ async def async_validate_api(hass: HomeAssistant, api_key: str) -> str: ) try: - async with async_timeout.timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT): device_query = await client.async_get_devices() user_query = await client.async_get_me() except AuthenticationError as err: diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index b6cae8ad605..f80e7acf9a6 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -2,7 +2,6 @@ import asyncio from contextlib import suppress -import async_timeout from sharkiq import ( AylaApi, SharkIqAuthError, @@ -35,7 +34,7 @@ class CannotConnect(exceptions.HomeAssistantError): async def async_connect_or_timeout(ayla_api: AylaApi) -> bool: """Connect to vacuum.""" try: - async with async_timeout.timeout(API_TIMEOUT): + async with asyncio.timeout(API_TIMEOUT): LOGGER.debug("Initialize connection to Ayla networks API") await ayla_api.async_sign_in() except SharkIqAuthError: @@ -87,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_disconnect_or_timeout(coordinator: SharkIqUpdateCoordinator): """Disconnect to vacuum.""" LOGGER.debug("Disconnecting from Ayla Api") - async with async_timeout.timeout(5): + async with asyncio.timeout(5): with suppress( SharkIqAuthError, SharkIqAuthExpiringError, SharkIqNotAuthedError ): diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index 4161a5f5357..1957d12048f 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -6,7 +6,6 @@ from collections.abc import Mapping from typing import Any import aiohttp -import async_timeout from sharkiq import SharkIqAuthError, get_ayla_api import voluptuous as vol @@ -51,7 +50,7 @@ async def _validate_input( ) try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): LOGGER.debug("Initialize connection to Ayla networks API") await ayla_api.async_sign_in() except (asyncio.TimeoutError, aiohttp.ClientError, TypeError) as error: diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index 87f5aafe7a4..4cfbb033566 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta -from async_timeout import timeout from sharkiq import ( AylaApi, SharkIqAuthError, @@ -55,7 +54,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator[bool]): """Asynchronously update the data for a single vacuum.""" dsn = sharkiq.serial_number LOGGER.debug("Updating sharkiq data for device DSN %s", dsn) - async with timeout(API_TIMEOUT): + async with asyncio.timeout(API_TIMEOUT): await sharkiq.async_update() async def _async_update_data(self) -> bool: diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index b2f38f54b20..67258d701e9 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -6,7 +6,6 @@ from contextlib import suppress import logging import shlex -import async_timeout import voluptuous as vol from homeassistant.core import ( @@ -89,7 +88,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: process = await create_process try: - async with async_timeout.timeout(COMMAND_TIMEOUT): + async with asyncio.timeout(COMMAND_TIMEOUT): stdout_data, stderr_data = await process.communicate() except asyncio.TimeoutError: _LOGGER.error( diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 5d68b90145f..72157e086e3 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -5,7 +5,6 @@ from datetime import timedelta import logging from aiohttp import client_exceptions -import async_timeout from smarttub import APIError, LoginFailed, SmartTub from smarttub.api import Account @@ -85,7 +84,7 @@ class SmartTubController: data = {} try: - async with async_timeout.timeout(POLLING_TIMEOUT): + async with asyncio.timeout(POLLING_TIMEOUT): for spa in self.spas: data[spa.id] = await self._get_spa_data(spa) except APIError as err: diff --git a/homeassistant/components/smarttub/switch.py b/homeassistant/components/smarttub/switch.py index d01b92c2186..e105963bc01 100644 --- a/homeassistant/components/smarttub/switch.py +++ b/homeassistant/components/smarttub/switch.py @@ -1,7 +1,7 @@ """Platform for switch integration.""" +import asyncio from typing import Any -import async_timeout from smarttub import SpaPump from homeassistant.components.switch import SwitchEntity @@ -80,6 +80,6 @@ class SmartTubPump(SmartTubEntity, SwitchEntity): async def async_toggle(self, **kwargs: Any) -> None: """Toggle the pump on or off.""" - async with async_timeout.timeout(API_TIMEOUT): + async with asyncio.timeout(API_TIMEOUT): await self.pump.toggle() await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index db5d7287ccd..5b71d92b25f 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -8,7 +8,6 @@ import logging from typing import Any, Final import aiohttp -import async_timeout from smhi import Smhi from smhi.smhi_lib import SmhiForecast, SmhiForecastException @@ -164,7 +163,7 @@ class SmhiWeather(WeatherEntity): async def async_update(self) -> None: """Refresh the forecast data from SMHI weather API.""" try: - async with async_timeout.timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT): 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 diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 27cb7ac034d..5b4ecc3a141 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -1,8 +1,8 @@ """The sms component.""" +import asyncio from datetime import timedelta import logging -import async_timeout import gammu # pylint: disable=import-error import voluptuous as vol @@ -125,7 +125,7 @@ class SignalCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Fetch device signal quality.""" try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return await self._gateway.get_signal_quality_async() except gammu.GSMError as exc: raise UpdateFailed(f"Error communicating with device: {exc}") from exc @@ -147,7 +147,7 @@ class NetworkCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Fetch device network info.""" try: - async with async_timeout.timeout(10): + async with asyncio.timeout(10): return await self._gateway.get_network_info_async() except gammu.GSMError as exc: raise UpdateFailed(f"Error communicating with device: {exc}") from exc diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index 31650a0828a..f11eba4fed7 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -108,7 +108,7 @@ async def test_controller_timeout( """Test an error talking to the controller.""" with patch( - "homeassistant.components.rainbird.config_flow.async_timeout.timeout", + "homeassistant.components.rainbird.config_flow.asyncio.timeout", side_effect=asyncio.TimeoutError, ): result = await complete_flow(hass) From ffe3d7c25585fb147a4440acb32a3aeaf411af69 Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Tue, 15 Aug 2023 16:44:24 +0100 Subject: [PATCH 0537/1151] Replace "percents" -> "percentage" in flux_led option flow (#98059) --- homeassistant/components/flux_led/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/flux_led/strings.json b/homeassistant/components/flux_led/strings.json index d1d812cb210..aa56708c645 100644 --- a/homeassistant/components/flux_led/strings.json +++ b/homeassistant/components/flux_led/strings.json @@ -27,7 +27,7 @@ "data": { "mode": "The chosen brightness mode.", "custom_effect_colors": "Custom Effect: List of 1 to 16 [R,G,B] colors. Example: [255,0,255],[60,128,0]", - "custom_effect_speed_pct": "Custom Effect: Speed in percents for the effect that switch colors.", + "custom_effect_speed_pct": "Custom Effect: Speed in percentage for the effects that switch colors.", "custom_effect_transition": "Custom Effect: Type of transition between the colors." } } From 90413daa8a55839b21f1cd4fc59d32f822a29111 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Aug 2023 18:15:23 +0200 Subject: [PATCH 0538/1151] Update buienweather data before adding entities (#98455) * Update buienweather data before adding entities * Fix tests --- homeassistant/components/buienradar/const.py | 4 +-- homeassistant/components/buienradar/sensor.py | 11 +++--- homeassistant/components/buienradar/util.py | 30 +++++++++------- .../components/buienradar/weather.py | 36 +++++++------------ tests/components/buienradar/test_sensor.py | 5 +++ tests/components/buienradar/test_weather.py | 5 +++ 6 files changed, 49 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/buienradar/const.py b/homeassistant/components/buienradar/const.py index 8111f63c923..718812c5c73 100644 --- a/homeassistant/components/buienradar/const.py +++ b/homeassistant/components/buienradar/const.py @@ -14,10 +14,10 @@ CONF_TIMEFRAME = "timeframe" SUPPORTED_COUNTRY_CODES = ["NL", "BE"] DEFAULT_COUNTRY = "NL" -"""Schedule next call after (minutes).""" SCHEDULE_OK = 10 -"""When an error occurred, new call after (minutes).""" +"""Schedule next call after (minutes).""" SCHEDULE_NOK = 2 +"""When an error occurred, new call after (minutes).""" STATE_CONDITIONS = ["clear", "cloudy", "fog", "rainy", "snowy", "lightning"] diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 00740eb4801..fe3ce3164fe 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -714,17 +714,18 @@ async def async_setup_entry( timeframe, ) + # create weather entities: entities = [ BrSensor(config.get(CONF_NAME, "Buienradar"), coordinates, description) for description in SENSOR_TYPES ] - async_add_entities(entities) - + # create weather data: data = BrData(hass, coordinates, timeframe, entities) - # schedule the first update in 1 minute from now: - await data.schedule_update(1) hass.data[DOMAIN][entry.entry_id][Platform.SENSOR] = data + await data.async_update() + + async_add_entities(entities) class BrSensor(SensorEntity): @@ -755,7 +756,7 @@ class BrSensor(SensorEntity): @callback def data_updated(self, data: BrData): """Update data.""" - if self.hass and self._load_data(data.data): + if self._load_data(data.data) and self.hass: self.async_write_ha_state() @callback diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 9d0c2a575c9..3c50b3097cb 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -27,7 +27,7 @@ from buienradar.constants import ( from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -77,7 +77,8 @@ class BrData: for dev in self.devices: dev.data_updated(self) - async def schedule_update(self, minute=1): + @callback + def async_schedule_update(self, minute=1): """Schedule an update after minute minutes.""" _LOGGER.debug("Scheduling next update in %s minutes", minute) nxt = dt_util.utcnow() + timedelta(minutes=minute) @@ -110,7 +111,7 @@ class BrData: if resp is not None: await resp.release() - async def async_update(self, *_): + async def _async_update(self): """Update the data from buienradar.""" content = await self.get_data(JSON_FEED_URL) @@ -123,9 +124,7 @@ class BrData: content.get(MESSAGE), content.get(STATUS_CODE), ) - # schedule new call - await self.schedule_update(SCHEDULE_NOK) - return + return None self.load_error_count = 0 # rounding coordinates prevents unnecessary redirects/calls @@ -143,9 +142,7 @@ class BrData: raincontent.get(MESSAGE), raincontent.get(STATUS_CODE), ) - # schedule new call - await self.schedule_update(SCHEDULE_NOK) - return + return None self.rain_error_count = 0 result = parse_data( @@ -164,12 +161,21 @@ class BrData: "Unable to parse data from Buienradar. (Msg: %s)", result.get(MESSAGE), ) - await self.schedule_update(SCHEDULE_NOK) + return None + + return result[DATA] + + async def async_update(self, *_): + """Update the data from buienradar and schedule the next update.""" + data = await self._async_update() + + if data is None: + self.async_schedule_update(SCHEDULE_NOK) return - self.data = result.get(DATA) + self.data = data await self.update_devices() - await self.schedule_update(SCHEDULE_OK) + self.async_schedule_update(SCHEDULE_OK) @property def attribution(self): diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index cdb8adf1dac..66c3b23ec8b 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -82,6 +82,11 @@ CONDITION_CLASSES = { ATTR_CONDITION_WINDY_VARIANT: (), ATTR_CONDITION_EXCEPTIONAL: (), } +CONDITION_MAP = { + cond_code: cond_ha + for cond_ha, cond_codes in CONDITION_CLASSES.items() + for cond_code in cond_codes +} async def async_setup_entry( @@ -106,20 +111,10 @@ async def async_setup_entry( # create weather data: data = BrData(hass, coordinates, DEFAULT_TIMEFRAME, entities) hass.data[DOMAIN][entry.entry_id][Platform.WEATHER] = data - - # create condition helper - if DATA_CONDITION not in hass.data[DOMAIN]: - cond_keys = [str(chr(x)) for x in range(97, 123)] - hass.data[DOMAIN][DATA_CONDITION] = dict.fromkeys(cond_keys) - for cond, condlst in CONDITION_CLASSES.items(): - for condi in condlst: - hass.data[DOMAIN][DATA_CONDITION][condi] = cond + await data.async_update() async_add_entities(entities) - # schedule the first update in 1 minute from now: - await data.schedule_update(1) - class BrWeather(WeatherEntity): """Representation of a weather condition.""" @@ -143,9 +138,6 @@ class BrWeather(WeatherEntity): @callback def data_updated(self, data: BrData) -> None: """Update data.""" - if not self.hass: - return - self._attr_attribution = data.attribution self._attr_condition = self._calc_condition(data) self._attr_forecast = self._calc_forecast(data) @@ -158,22 +150,20 @@ class BrWeather(WeatherEntity): self._attr_native_visibility = data.visibility self._attr_native_wind_speed = data.wind_speed self._attr_wind_bearing = data.wind_bearing + + if not self.hass: + return self.async_write_ha_state() def _calc_condition(self, data: BrData): """Return the current condition.""" - if ( - data.condition - and (ccode := data.condition.get(CONDCODE)) - and (conditions := self.hass.data[DOMAIN].get(DATA_CONDITION)) - ): - return conditions.get(ccode) + if data.condition and (ccode := data.condition.get(CONDCODE)): + return CONDITION_MAP.get(ccode) return None def _calc_forecast(self, data: BrData): """Return the forecast array.""" fcdata_out = [] - cond = self.hass.data[DOMAIN][DATA_CONDITION] if not data.forecast: return None @@ -181,10 +171,10 @@ class BrWeather(WeatherEntity): for data_in in data.forecast: # remap keys from external library to # keys understood by the weather component: - condcode = data_in.get(CONDITION, []).get(CONDCODE) + condcode = data_in.get(CONDITION, {}).get(CONDCODE) data_out = { ATTR_FORECAST_TIME: data_in.get(DATETIME).isoformat(), - ATTR_FORECAST_CONDITION: cond[condcode], + ATTR_FORECAST_CONDITION: CONDITION_MAP.get(condcode), ATTR_FORECAST_NATIVE_TEMP_LOW: data_in.get(MIN_TEMP), ATTR_FORECAST_NATIVE_TEMP: data_in.get(MAX_TEMP), ATTR_FORECAST_NATIVE_PRECIPITATION: data_in.get(RAIN), diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py index 725b03a6cc5..fb83d7a13db 100644 --- a/tests/components/buienradar/test_sensor.py +++ b/tests/components/buienradar/test_sensor.py @@ -1,4 +1,6 @@ """The tests for the Buienradar sensor platform.""" +from http import HTTPStatus + from homeassistant.components.buienradar.const import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant @@ -18,6 +20,9 @@ async def test_smoke_test_setup_component( aioclient_mock: AiohttpClientMocker, hass: HomeAssistant ) -> None: """Smoke test for successfully set-up with default config.""" + aioclient_mock.get( + "https://data.buienradar.nl/2.0/feed/json", status=HTTPStatus.NOT_FOUND + ) mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) mock_entry.add_to_hass(hass) diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py index c8b0d459b78..d4c4af5f62a 100644 --- a/tests/components/buienradar/test_weather.py +++ b/tests/components/buienradar/test_weather.py @@ -1,4 +1,6 @@ """The tests for the buienradar weather component.""" +from http import HTTPStatus + from homeassistant.components.buienradar.const import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant @@ -13,6 +15,9 @@ async def test_smoke_test_setup_component( aioclient_mock: AiohttpClientMocker, hass: HomeAssistant ) -> None: """Smoke test for successfully set-up with default config.""" + aioclient_mock.get( + "https://data.buienradar.nl/2.0/feed/json", status=HTTPStatus.NOT_FOUND + ) mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) mock_entry.add_to_hass(hass) From 5a69f9ed04b8a5074c489955b745eb27533a6313 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Aug 2023 12:37:16 -0500 Subject: [PATCH 0539/1151] Remove unused code in enphase_envoy (#98474) --- homeassistant/components/enphase_envoy/binary_sensor.py | 2 -- homeassistant/components/enphase_envoy/select.py | 5 ----- homeassistant/components/enphase_envoy/sensor.py | 2 -- homeassistant/components/enphase_envoy/switch.py | 2 -- 4 files changed, 11 deletions(-) diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index 0e70a9fe98b..eae8c8628d5 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -112,8 +112,6 @@ async def async_setup_entry( 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( diff --git a/homeassistant/components/enphase_envoy/select.py b/homeassistant/components/enphase_envoy/select.py index 75c9ce0cf7c..59f2a16e7cf 100644 --- a/homeassistant/components/enphase_envoy/select.py +++ b/homeassistant/components/enphase_envoy/select.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -import logging from typing import Any from pyenphase import EnvoyDryContactSettings @@ -19,8 +18,6 @@ from .const import DOMAIN from .coordinator import EnphaseUpdateCoordinator from .entity import EnvoyBaseEntity -_LOGGER = logging.getLogger(__name__) - @dataclass class EnvoyRelayRequiredKeysMixin: @@ -113,8 +110,6 @@ async def async_setup_entry( 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[SelectEntity] = [] if envoy_data.dry_contact_settings: entities.extend( diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 0e4a9b71232..0e0a2aacfd7 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -350,8 +350,6 @@ async def async_setup_entry( 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 _LOGGER.debug("Envoy data: %s", envoy_data) entities: list[Entity] = [ diff --git a/homeassistant/components/enphase_envoy/switch.py b/homeassistant/components/enphase_envoy/switch.py index 820b904e070..e0f211a1019 100644 --- a/homeassistant/components/enphase_envoy/switch.py +++ b/homeassistant/components/enphase_envoy/switch.py @@ -55,8 +55,6 @@ async def async_setup_entry( 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( From 92535277be30a52022933a73921c3cc312092135 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 15 Aug 2023 14:08:11 -0400 Subject: [PATCH 0540/1151] Add number platform & battery setpoint entities to Enphase integration (#98427) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../components/enphase_envoy/const.py | 8 +- .../components/enphase_envoy/number.py | 116 ++++++++++++++++++ .../components/enphase_envoy/strings.json | 8 ++ 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/enphase_envoy/number.py diff --git a/.coveragerc b/.coveragerc index 014dc2f0f39..9930cbaf0b5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -305,6 +305,7 @@ omit = homeassistant/components/enphase_envoy/binary_sensor.py homeassistant/components/enphase_envoy/coordinator.py homeassistant/components/enphase_envoy/entity.py + homeassistant/components/enphase_envoy/number.py homeassistant/components/enphase_envoy/select.py homeassistant/components/enphase_envoy/sensor.py homeassistant/components/enphase_envoy/switch.py diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index d1c6618502e..c5656a65b6f 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -5,6 +5,12 @@ from homeassistant.const import Platform DOMAIN = "enphase_envoy" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SELECT, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] INVALID_AUTH_ERRORS = (EnvoyAuthenticationError, EnvoyAuthenticationRequired) diff --git a/homeassistant/components/enphase_envoy/number.py b/homeassistant/components/enphase_envoy/number.py new file mode 100644 index 00000000000..50d4de18f12 --- /dev/null +++ b/homeassistant/components/enphase_envoy/number.py @@ -0,0 +1,116 @@ +"""Number platform for Enphase Envoy solar energy monitor.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from pyenphase import EnvoyDryContactSettings + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, +) +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.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import EnphaseUpdateCoordinator +from .entity import EnvoyBaseEntity + + +@dataclass +class EnvoyRelayRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyDryContactSettings], float] + + +@dataclass +class EnvoyRelayNumberEntityDescription( + NumberEntityDescription, EnvoyRelayRequiredKeysMixin +): + """Describes an Envoy Dry Contact Relay number entity.""" + + +RELAY_ENTITIES = ( + EnvoyRelayNumberEntityDescription( + key="soc_low", + translation_key="cutoff_battery_level", + device_class=NumberDeviceClass.BATTERY, + entity_category=EntityCategory.CONFIG, + value_fn=lambda relay: relay.soc_low, + ), + EnvoyRelayNumberEntityDescription( + key="soc_high", + translation_key="restore_battery_level", + device_class=NumberDeviceClass.BATTERY, + entity_category=EntityCategory.CONFIG, + value_fn=lambda relay: relay.soc_high, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Enphase Envoy number platform.""" + coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + envoy_data = coordinator.envoy.data + assert envoy_data is not None + entities: list[NumberEntity] = [] + if envoy_data.dry_contact_settings: + entities.extend( + EnvoyRelayNumberEntity(coordinator, entity, relay) + for entity in RELAY_ENTITIES + for relay in envoy_data.dry_contact_settings + ) + async_add_entities(entities) + + +class EnvoyRelayNumberEntity(EnvoyBaseEntity, NumberEntity): + """Representation of an Enphase Enpower number entity.""" + + entity_description: EnvoyRelayNumberEntityDescription + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyRelayNumberEntityDescription, + relay_id: str, + ) -> None: + """Initialize the Enphase relay number entity.""" + super().__init__(coordinator, description) + self.envoy = coordinator.envoy + enpower = self.data.enpower + assert enpower is not None + serial_number = enpower.serial_number + self._relay_id = relay_id + self._attr_unique_id = f"{serial_number}_relay_{relay_id}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, relay_id)}, + manufacturer="Enphase", + model="Dry contact relay", + name=self.data.dry_contact_settings[relay_id].load_name, + sw_version=str(enpower.firmware_version), + via_device=(DOMAIN, serial_number), + ) + + @property + def native_value(self) -> float: + """Return the state of the relay entity.""" + return self.entity_description.value_fn( + self.data.dry_contact_settings[self._relay_id] + ) + + async def async_set_native_value(self, value: float) -> None: + """Update the relay.""" + await self.envoy.update_dry_contact( + {"id": self._relay_id, self.entity_description.key: int(value)} + ) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 0f292dfa8e3..f023bc7d114 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -39,6 +39,14 @@ "name": "Relay status" } }, + "number": { + "cutoff_battery_level": { + "name": "Cutoff battery level" + }, + "restore_battery_level": { + "name": "Restore battery level" + } + }, "select": { "relay_mode": { "name": "Mode", From 73f882bf36f94b1207869bfec63e3897b7ef11de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Aug 2023 13:08:55 -0500 Subject: [PATCH 0541/1151] Small cleanups to enphase_envoy select platform (#98476) --- .../components/enphase_envoy/select.py | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/enphase_envoy/select.py b/homeassistant/components/enphase_envoy/select.py index 59f2a16e7cf..5ae73a315f2 100644 --- a/homeassistant/components/enphase_envoy/select.py +++ b/homeassistant/components/enphase_envoy/select.py @@ -1,11 +1,11 @@ """Select platform for Enphase Envoy solar energy monitor.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any -from pyenphase import EnvoyDryContactSettings +from pyenphase import Envoy, EnvoyDryContactSettings from pyenphase.models.dry_contacts import DryContactAction, DryContactMode from homeassistant.components.select import SelectEntity, SelectEntityDescription @@ -24,7 +24,9 @@ class EnvoyRelayRequiredKeysMixin: """Mixin for required keys.""" value_fn: Callable[[EnvoyDryContactSettings], str] - update_fn: Callable[[Any, Any, Any], Any] + update_fn: Callable[ + [Envoy, EnvoyDryContactSettings, str], Coroutine[Any, Any, dict[str, Any]] + ] @dataclass @@ -129,36 +131,34 @@ class EnvoyRelaySelectEntity(EnvoyBaseEntity, SelectEntity): self, coordinator: EnphaseUpdateCoordinator, description: EnvoyRelaySelectEntityDescription, - relay: str, + relay_id: str, ) -> None: """Initialize the Enphase relay select entity.""" super().__init__(coordinator, description) self.envoy = coordinator.envoy - assert self.envoy is not None - assert self.data is not None - self.enpower = self.data.enpower - assert self.enpower is not None - self._serial_number = self.enpower.serial_number - self.relay = self.data.dry_contact_settings[relay] - self.relay_id = relay - self._attr_unique_id = ( - f"{self._serial_number}_relay_{relay}_{self.entity_description.key}" - ) + enpower = self.data.enpower + assert enpower is not None + serial_number = enpower.serial_number + self._relay_id = relay_id + self._attr_unique_id = f"{serial_number}_relay_{relay_id}_{description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, relay)}, + identifiers={(DOMAIN, relay_id)}, manufacturer="Enphase", model="Dry contact relay", name=self.relay.load_name, - sw_version=str(self.enpower.firmware_version), - via_device=(DOMAIN, self._serial_number), + sw_version=str(enpower.firmware_version), + via_device=(DOMAIN, serial_number), ) + @property + def relay(self) -> EnvoyDryContactSettings: + """Return the relay object.""" + return self.data.dry_contact_settings[self._relay_id] + @property def current_option(self) -> str: """Return the state of the Enpower switch.""" - return self.entity_description.value_fn( - self.data.dry_contact_settings[self.relay_id] - ) + return self.entity_description.value_fn(self.relay) async def async_select_option(self, option: str) -> None: """Update the relay.""" From 80d608bb5b4a84e5b7230706c34d190a865301ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Aug 2023 13:16:05 -0500 Subject: [PATCH 0542/1151] Remove some bound attributes from enphase_envoy binary_sensor (#98477) Some of these were never used --- .../components/enphase_envoy/binary_sensor.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index eae8c8628d5..77d41ccf375 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -186,13 +186,12 @@ class EnvoyEnpowerBinarySensorEntity(EnvoyBaseBinarySensorEntity): 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_unique_id = f"{enpower.serial_number}_{description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._serial_number)}, + identifiers={(DOMAIN, enpower.serial_number)}, manufacturer="Enphase", model="Enpower", - name=f"Enpower {self._serial_number}", + name=f"Enpower {enpower.serial_number}", sw_version=str(enpower.firmware_version), via_device=(DOMAIN, self.envoy_serial_num), ) @@ -218,15 +217,13 @@ class EnvoyRelayBinarySensorEntity(EnvoyBaseBinarySensorEntity): super().__init__(coordinator, description) enpower = self.data.enpower assert enpower is not None - self.relay_id = relay_id - self.relay = self.data.dry_contact_settings[self.relay_id] - self._serial_number = enpower.serial_number - self._attr_unique_id = f"{self._serial_number}_relay_{relay_id}" + self._relay_id = relay_id + self._attr_unique_id = f"{enpower.serial_number}_relay_{relay_id}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, relay_id)}, manufacturer="Enphase", model="Dry contact relay", - name=self.relay.load_name, + name=self.data.dry_contact_settings[relay_id].load_name, sw_version=str(enpower.firmware_version), via_device=(DOMAIN, enpower.serial_number), ) @@ -234,5 +231,5 @@ class EnvoyRelayBinarySensorEntity(EnvoyBaseBinarySensorEntity): @property def is_on(self) -> bool: """Return the state of the Enpower binary_sensor.""" - relay = self.data.dry_contact_status[self.relay_id] + relay = self.data.dry_contact_status[self._relay_id] return relay.status == DryContactStatus.CLOSED From 857369625a82c74f8e13ea396cab27bbd470f2f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Aug 2023 13:29:22 -0500 Subject: [PATCH 0543/1151] Remove some bound attributes from enphase_envoy sensor (#98479) --- homeassistant/components/enphase_envoy/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 0e0a2aacfd7..33b9e3a64df 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -563,16 +563,14 @@ class EnvoyEnpowerEntity(EnvoySensorBaseEntity): ) -> None: """Initialize Enpower entity.""" super().__init__(coordinator, description) - assert coordinator.envoy.data is not None - enpower_data = coordinator.envoy.data.enpower + enpower_data = self.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_unique_id = f"{enpower_data.serial_number}_{description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._serial_number)}, + identifiers={(DOMAIN, enpower_data.serial_number)}, manufacturer="Enphase", model="Enpower", - name=f"Enpower {self._serial_number}", + name=f"Enpower {enpower_data.serial_number}", sw_version=str(enpower_data.firmware_version), via_device=(DOMAIN, self.envoy_serial_num), ) From caeb20f9c9bec2a2a4b4f90f4653aac4170ac725 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Aug 2023 20:55:16 +0200 Subject: [PATCH 0544/1151] Modernize aemet weather (#97969) * Modernize aemet weather * Improve test coverage * Only create a single entity for new config entries --- homeassistant/components/aemet/weather.py | 77 +- .../aemet/snapshots/test_weather.ambr | 1238 +++++++++++++++++ tests/components/aemet/test_weather.py | 145 +- 3 files changed, 1445 insertions(+), 15 deletions(-) create mode 100644 tests/components/aemet/snapshots/test_weather.ambr diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index aba5a2781d0..f9b0f7ef6ca 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,4 +1,6 @@ """Support for the AEMET OpenData service.""" +from typing import cast + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_NATIVE_PRECIPITATION, @@ -8,7 +10,10 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + DOMAIN as WEATHER_DOMAIN, + Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -17,7 +22,8 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -79,10 +85,28 @@ async def async_setup_entry( weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] entities = [] - for mode in FORECAST_MODES: - name = f"{domain_data[ENTRY_NAME]} {mode}" - unique_id = f"{config_entry.unique_id} {mode}" - entities.append(AemetWeather(name, unique_id, weather_coordinator, mode)) + entity_registry = er.async_get(hass) + + # Add daily + hourly entity for legacy config entries, only add daily for new + # config entries. This can be removed in HA Core 2024.3 + if entity_registry.async_get_entity_id( + WEATHER_DOMAIN, + DOMAIN, + f"{config_entry.unique_id} {FORECAST_MODE_HOURLY}", + ): + for mode in FORECAST_MODES: + name = f"{domain_data[ENTRY_NAME]} {mode}" + unique_id = f"{config_entry.unique_id} {mode}" + entities.append(AemetWeather(name, unique_id, weather_coordinator, mode)) + else: + entities.append( + AemetWeather( + domain_data[ENTRY_NAME], + config_entry.unique_id, + weather_coordinator, + FORECAST_MODE_DAILY, + ) + ) async_add_entities(entities, False) @@ -95,6 +119,9 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], 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, @@ -112,20 +139,44 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): self._attr_name = name self._attr_unique_id = unique_id + @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): """Return the current condition.""" return self.coordinator.data[ATTR_API_CONDITION] - @property - def forecast(self): + def _forecast(self, forecast_mode: str) -> list[Forecast]: """Return the forecast array.""" - forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] - forecast_map = FORECAST_MAP[self._forecast_mode] - return [ - {ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()} - for forecast in forecasts - ] + forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[forecast_mode]] + forecast_map = FORECAST_MAP[forecast_mode] + return cast( + list[Forecast], + [ + {ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()} + for forecast in forecasts + ], + ) + + @property + def forecast(self) -> list[Forecast]: + """Return the forecast array.""" + return self._forecast(self._forecast_mode) + + async def async_forecast_daily(self) -> list[Forecast]: + """Return the daily forecast in native units.""" + return self._forecast(FORECAST_MODE_DAILY) + + async def async_forecast_hourly(self) -> list[Forecast]: + """Return the hourly forecast in native units.""" + return self._forecast(FORECAST_MODE_HOURLY) @property def humidity(self): diff --git a/tests/components/aemet/snapshots/test_weather.ambr b/tests/components/aemet/snapshots/test_weather.ambr new file mode 100644 index 00000000000..e9c922f041e --- /dev/null +++ b/tests/components/aemet/snapshots/test_weather.ambr @@ -0,0 +1,1238 @@ +# serializer version: 1 +# name: test_forecast_service + dict({ + 'forecast': list([ + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T00:00:00+00:00', + 'precipitation_probability': 30, + 'temperature': 4.0, + 'templow': -4.0, + 'wind_bearing': 45.0, + 'wind_speed': 20.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-11T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 3.0, + 'templow': -7.0, + 'wind_bearing': 0.0, + 'wind_speed': 5.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-12T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -1.0, + 'templow': -13.0, + 'wind_bearing': None, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-01-13T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 6.0, + 'templow': -11.0, + 'wind_bearing': None, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-14T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 6.0, + 'templow': -7.0, + 'wind_bearing': None, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-15T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 5.0, + 'templow': -4.0, + 'wind_bearing': None, + }), + ]), + }) +# --- +# name: test_forecast_service.1 + dict({ + 'forecast': list([ + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T12:00:00+00:00', + 'precipitation': 3.6, + 'precipitation_probability': 100, + 'temperature': 0.0, + 'wind_bearing': 90.0, + 'wind_speed': 15.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T13:00:00+00:00', + 'precipitation': 2.7, + 'precipitation_probability': 100, + 'temperature': 0.0, + 'wind_bearing': 135.0, + 'wind_speed': 15.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T14:00:00+00:00', + 'precipitation': 0.6, + 'precipitation_probability': 100, + 'temperature': 0.0, + 'wind_bearing': 135.0, + 'wind_speed': 14.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T15:00:00+00:00', + 'precipitation': 0.8, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 10.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T16:00:00+00:00', + 'precipitation': 1.4, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T17:00:00+00:00', + 'precipitation': 1.2, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 9.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T18:00:00+00:00', + 'precipitation': 0.4, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 90.0, + 'wind_speed': 7.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T19:00:00+00:00', + 'precipitation': 0.3, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T20:00:00+00:00', + 'precipitation': 0.1, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-09T21:00:00+00:00', + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 90.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-09T22:00:00+00:00', + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-09T23:00:00+00:00', + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 90.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'fog', + 'datetime': '2021-01-10T01:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 5.0, + }), + dict({ + 'condition': 'fog', + 'datetime': '2021-01-10T02:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': 0.0, + 'wind_bearing': 0.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'fog', + 'datetime': '2021-01-10T03:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': 0.0, + 'wind_bearing': 0.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T04:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T05:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': -1.0, + 'wind_bearing': 0.0, + 'wind_speed': 5.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T06:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': -1.0, + 'wind_bearing': 0.0, + 'wind_speed': 9.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T07:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': -2.0, + 'wind_bearing': 45.0, + 'wind_speed': 13.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T08:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T09:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T10:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T11:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': 2.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T12:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 22.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T13:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 20.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T14:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 19.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T15:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 4.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T16:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T17:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 2.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T18:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T19:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T20:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T21:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T22:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 19.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T23:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-11T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 19.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-11T01:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T02:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -2.0, + 'wind_bearing': 45.0, + 'wind_speed': 12.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T03:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -2.0, + 'wind_bearing': 45.0, + 'wind_speed': 10.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T04:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -3.0, + 'wind_bearing': 45.0, + 'wind_speed': 11.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T05:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -4.0, + 'wind_bearing': 45.0, + 'wind_speed': 10.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T06:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -4.0, + 'wind_bearing': 0.0, + 'wind_speed': 10.0, + }), + ]), + }) +# --- +# name: test_forecast_subscription[daily] + list([ + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T00:00:00+00:00', + 'precipitation_probability': 30, + 'temperature': 4.0, + 'templow': -4.0, + 'wind_bearing': 45.0, + 'wind_speed': 20.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-11T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 3.0, + 'templow': -7.0, + 'wind_bearing': 0.0, + 'wind_speed': 5.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-12T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -1.0, + 'templow': -13.0, + 'wind_bearing': None, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-01-13T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 6.0, + 'templow': -11.0, + 'wind_bearing': None, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-14T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 6.0, + 'templow': -7.0, + 'wind_bearing': None, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-15T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 5.0, + 'templow': -4.0, + 'wind_bearing': None, + }), + ]) +# --- +# name: test_forecast_subscription[daily].1 + list([ + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T00:00:00+00:00', + 'precipitation_probability': 30, + 'temperature': 4.0, + 'templow': -4.0, + 'wind_bearing': 45.0, + 'wind_speed': 20.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-11T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 3.0, + 'templow': -7.0, + 'wind_bearing': 0.0, + 'wind_speed': 5.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-12T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -1.0, + 'templow': -13.0, + 'wind_bearing': None, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-01-13T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 6.0, + 'templow': -11.0, + 'wind_bearing': None, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-14T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 6.0, + 'templow': -7.0, + 'wind_bearing': None, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-15T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 5.0, + 'templow': -4.0, + 'wind_bearing': None, + }), + ]) +# --- +# name: test_forecast_subscription[hourly] + list([ + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T12:00:00+00:00', + 'precipitation': 3.6, + 'precipitation_probability': 100, + 'temperature': 0.0, + 'wind_bearing': 90.0, + 'wind_speed': 15.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T13:00:00+00:00', + 'precipitation': 2.7, + 'precipitation_probability': 100, + 'temperature': 0.0, + 'wind_bearing': 135.0, + 'wind_speed': 15.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T14:00:00+00:00', + 'precipitation': 0.6, + 'precipitation_probability': 100, + 'temperature': 0.0, + 'wind_bearing': 135.0, + 'wind_speed': 14.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T15:00:00+00:00', + 'precipitation': 0.8, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 10.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T16:00:00+00:00', + 'precipitation': 1.4, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T17:00:00+00:00', + 'precipitation': 1.2, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 9.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T18:00:00+00:00', + 'precipitation': 0.4, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 90.0, + 'wind_speed': 7.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T19:00:00+00:00', + 'precipitation': 0.3, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T20:00:00+00:00', + 'precipitation': 0.1, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-09T21:00:00+00:00', + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 90.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-09T22:00:00+00:00', + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-09T23:00:00+00:00', + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 90.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'fog', + 'datetime': '2021-01-10T01:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 5.0, + }), + dict({ + 'condition': 'fog', + 'datetime': '2021-01-10T02:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': 0.0, + 'wind_bearing': 0.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'fog', + 'datetime': '2021-01-10T03:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': 0.0, + 'wind_bearing': 0.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T04:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T05:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': -1.0, + 'wind_bearing': 0.0, + 'wind_speed': 5.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T06:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': -1.0, + 'wind_bearing': 0.0, + 'wind_speed': 9.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T07:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': -2.0, + 'wind_bearing': 45.0, + 'wind_speed': 13.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T08:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T09:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T10:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T11:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': 2.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T12:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 22.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T13:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 20.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T14:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 19.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T15:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 4.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T16:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T17:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 2.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T18:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T19:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T20:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T21:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T22:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 19.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T23:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-11T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 19.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-11T01:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T02:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -2.0, + 'wind_bearing': 45.0, + 'wind_speed': 12.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T03:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -2.0, + 'wind_bearing': 45.0, + 'wind_speed': 10.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T04:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -3.0, + 'wind_bearing': 45.0, + 'wind_speed': 11.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T05:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -4.0, + 'wind_bearing': 45.0, + 'wind_speed': 10.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T06:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -4.0, + 'wind_bearing': 0.0, + 'wind_speed': 10.0, + }), + ]) +# --- +# name: test_forecast_subscription[hourly].1 + list([ + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T12:00:00+00:00', + 'precipitation': 3.6, + 'precipitation_probability': 100, + 'temperature': 0.0, + 'wind_bearing': 90.0, + 'wind_speed': 15.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T13:00:00+00:00', + 'precipitation': 2.7, + 'precipitation_probability': 100, + 'temperature': 0.0, + 'wind_bearing': 135.0, + 'wind_speed': 15.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T14:00:00+00:00', + 'precipitation': 0.6, + 'precipitation_probability': 100, + 'temperature': 0.0, + 'wind_bearing': 135.0, + 'wind_speed': 14.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T15:00:00+00:00', + 'precipitation': 0.8, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 10.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T16:00:00+00:00', + 'precipitation': 1.4, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T17:00:00+00:00', + 'precipitation': 1.2, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 9.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-01-09T18:00:00+00:00', + 'precipitation': 0.4, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 90.0, + 'wind_speed': 7.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T19:00:00+00:00', + 'precipitation': 0.3, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-01-09T20:00:00+00:00', + 'precipitation': 0.1, + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 135.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-09T21:00:00+00:00', + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 90.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-09T22:00:00+00:00', + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-09T23:00:00+00:00', + 'precipitation_probability': 100, + 'temperature': 1.0, + 'wind_bearing': 90.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'fog', + 'datetime': '2021-01-10T01:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 5.0, + }), + dict({ + 'condition': 'fog', + 'datetime': '2021-01-10T02:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': 0.0, + 'wind_bearing': 0.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'fog', + 'datetime': '2021-01-10T03:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': 0.0, + 'wind_bearing': 0.0, + 'wind_speed': 6.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T04:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 8.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T05:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': -1.0, + 'wind_bearing': 0.0, + 'wind_speed': 5.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T06:00:00+00:00', + 'precipitation_probability': 10, + 'temperature': -1.0, + 'wind_bearing': 0.0, + 'wind_speed': 9.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T07:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': -2.0, + 'wind_bearing': 45.0, + 'wind_speed': 13.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T08:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T09:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T10:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T11:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': 2.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T12:00:00+00:00', + 'precipitation_probability': 15, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 22.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T13:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 20.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T14:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 19.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T15:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 4.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T16:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 3.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T17:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 2.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T18:00:00+00:00', + 'precipitation_probability': 5, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-10T19:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T20:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T21:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 1.0, + 'wind_bearing': 45.0, + 'wind_speed': 17.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T22:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 19.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-10T23:00:00+00:00', + 'precipitation_probability': None, + 'temperature': 0.0, + 'wind_bearing': 45.0, + 'wind_speed': 21.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-01-11T00:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 19.0, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-01-11T01:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -1.0, + 'wind_bearing': 45.0, + 'wind_speed': 16.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T02:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -2.0, + 'wind_bearing': 45.0, + 'wind_speed': 12.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T03:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -2.0, + 'wind_bearing': 45.0, + 'wind_speed': 10.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T04:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -3.0, + 'wind_bearing': 45.0, + 'wind_speed': 11.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T05:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -4.0, + 'wind_bearing': 45.0, + 'wind_speed': 10.0, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-01-11T06:00:00+00:00', + 'precipitation_probability': None, + 'temperature': -4.0, + 'wind_bearing': 0.0, + 'wind_speed': 10.0, + }), + ]) +# --- diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py index 30b11876e74..c64e824e18d 100644 --- a/tests/components/aemet/test_weather.py +++ b/tests/components/aemet/test_weather.py @@ -1,7 +1,16 @@ """The sensor tests for the AEMET OpenData platform.""" +import datetime from unittest.mock import patch -from homeassistant.components.aemet.const import ATTRIBUTION +from freezegun.api import FrozenDateTimeFactory +import pytest +import requests_mock +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.aemet.const import ATTRIBUTION, DOMAIN +from homeassistant.components.aemet.weather_update_coordinator import ( + WEATHER_UPDATE_INTERVAL, +) from homeassistant.components.weather import ( ATTR_CONDITION_PARTLYCLOUDY, ATTR_CONDITION_SNOWY, @@ -19,17 +28,65 @@ from homeassistant.components.weather import ( ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, + SERVICE_GET_FORECAST, ) from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util -from .util import async_init_integration +from .util import aemet_requests_mock, async_init_integration + +from tests.typing import WebSocketGenerator async def test_aemet_weather(hass: HomeAssistant) -> None: """Test states of the weather.""" + hass.config.set_time_zone("UTC") + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ): + await async_init_integration(hass) + + state = hass.states.get("weather.aemet") + assert state + assert state.state == ATTR_CONDITION_SNOWY + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 99.0 + assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1004.4 # 100440.0 Pa -> hPa + assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == -0.7 + assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 90.0 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 15.0 # 4.17 m/s -> km/h + forecast = state.attributes.get(ATTR_FORECAST)[0] + assert forecast.get(ATTR_FORECAST_CONDITION) == ATTR_CONDITION_PARTLYCLOUDY + assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None + assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 30 + assert forecast.get(ATTR_FORECAST_TEMP) == 4 + assert forecast.get(ATTR_FORECAST_TEMP_LOW) == -4 + assert ( + forecast.get(ATTR_FORECAST_TIME) + == dt_util.parse_datetime("2021-01-10 00:00:00+00:00").isoformat() + ) + assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 45.0 + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 20.0 # 5.56 m/s -> km/h + + state = hass.states.get("weather.aemet_hourly") + assert state is None + + +async def test_aemet_weather_legacy(hass: HomeAssistant) -> None: + """Test states of the weather.""" + + registry = er.async_get(hass) + registry.async_get_or_create( + WEATHER_DOMAIN, + DOMAIN, + "None hourly", + ) + hass.config.set_time_zone("UTC") now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( @@ -61,3 +118,87 @@ async def test_aemet_weather(hass: HomeAssistant) -> None: state = hass.states.get("weather.aemet_hourly") assert state is None + + +async def test_forecast_service( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test multiple forecast.""" + hass.config.set_time_zone("UTC") + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ): + await async_init_integration(hass) + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": "weather.aemet", + "type": "daily", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": "weather.aemet", + "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, + snapshot: SnapshotAssertion, + forecast_type: str, +) -> None: + """Test multiple forecast.""" + client = await hass_ws_client(hass) + + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_init_integration(hass) + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": forecast_type, + "entity_id": "weather.aemet", + } + ) + 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 + + with requests_mock.mock() as _m: + aemet_requests_mock(_m) + freezer.tick(WEATHER_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 3cf86d5d1faead577d66f72ef890bca22e0a25ef Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Aug 2023 20:56:19 +0200 Subject: [PATCH 0545/1151] Create a single entity for new met_eireann config entries (#98100) --- .../components/met_eireann/weather.py | 43 ++++++++++++------- tests/components/met_eireann/test_weather.py | 26 +++++++++++ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index e31951ea8a2..c40c89892c9 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -1,10 +1,12 @@ """Support for Met Éireann weather service.""" import logging -from typing import cast +from types import MappingProxyType +from typing import Any, cast from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TIME, + DOMAIN as WEATHER_DOMAIN, Forecast, WeatherEntity, WeatherEntityFeature, @@ -20,6 +22,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -50,12 +53,28 @@ async def async_setup_entry( ) -> None: """Add a weather entity from a config_entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - [ - MetEireannWeather(coordinator, config_entry.data, False), - MetEireannWeather(coordinator, config_entry.data, True), - ] - ) + entity_registry = er.async_get(hass) + + entities = [MetEireannWeather(coordinator, config_entry.data, False)] + + # Add hourly entity to legacy config entries + if entity_registry.async_get_entity_id( + WEATHER_DOMAIN, + DOMAIN, + _calculate_unique_id(config_entry.data, True), + ): + entities.append(MetEireannWeather(coordinator, config_entry.data, True)) + + async_add_entities(entities) + + +def _calculate_unique_id(config: MappingProxyType[str, Any], hourly: bool) -> str: + """Calculate unique ID.""" + name_appendix = "" + if hourly: + name_appendix = "-hourly" + + return f"{config[CONF_LATITUDE]}-{config[CONF_LONGITUDE]}{name_appendix}" class MetEireannWeather( @@ -75,6 +94,7 @@ class MetEireannWeather( def __init__(self, coordinator, config, hourly): """Initialise the platform with a data instance and site.""" super().__init__(coordinator) + self._attr_unique_id = _calculate_unique_id(config, hourly) self._config = config self._hourly = hourly @@ -87,15 +107,6 @@ class MetEireannWeather( self.hass, self.async_update_listeners(("daily", "hourly")) ) - @property - def unique_id(self): - """Return unique ID.""" - name_appendix = "" - if self._hourly: - name_appendix = "-hourly" - - return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{name_appendix}" - @property def name(self): """Return the name of the sensor.""" diff --git a/tests/components/met_eireann/test_weather.py b/tests/components/met_eireann/test_weather.py index e14cd485cc6..a3ca1fd55f7 100644 --- a/tests/components/met_eireann/test_weather.py +++ b/tests/components/met_eireann/test_weather.py @@ -13,6 +13,7 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry from tests.typing import WebSocketGenerator @@ -31,6 +32,31 @@ async def setup_config_entry(hass: HomeAssistant) -> ConfigEntry: return mock_data +async def test_new_config_entry(hass: HomeAssistant, mock_weather) -> None: + """Test the expected entities are created.""" + registry = er.async_get(hass) + await setup_config_entry(hass) + assert len(hass.states.async_entity_ids("weather")) == 1 + + entry = hass.config_entries.async_entries()[0] + assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 1 + + +async def test_legacy_config_entry(hass: HomeAssistant, mock_weather) -> None: + """Test the expected entities are created.""" + registry = er.async_get(hass) + registry.async_get_or_create( + WEATHER_DOMAIN, + DOMAIN, + "10-20-hourly", + ) + await setup_config_entry(hass) + assert len(hass.states.async_entity_ids("weather")) == 2 + + entry = hass.config_entries.async_entries()[0] + assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 2 + + async def test_weather(hass: HomeAssistant, mock_weather) -> None: """Test weather entity.""" await setup_config_entry(hass) From c671b1069e8ce637b7cb00e7617f319b3f28e67e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Aug 2023 18:52:18 -0500 Subject: [PATCH 0546/1151] Bump protobuf to 4.24.0 (#98468) --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 37ec683aff0..389729fa4c2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -148,7 +148,7 @@ pyOpenSSL>=23.1.0 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==4.23.3 +protobuf==4.24.0 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b2954dc777b..5a683660efe 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -149,7 +149,7 @@ pyOpenSSL>=23.1.0 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==4.23.3 +protobuf==4.24.0 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder From 45966069b49e4146fef7d776381b5a3a54145685 Mon Sep 17 00:00:00 2001 From: dalinicus Date: Wed, 16 Aug 2023 01:15:28 -0500 Subject: [PATCH 0547/1151] Bump aiolyric to 1.1.0 (#98113) version bump aiolyric to include new features for additional room sensors --- homeassistant/components/lyric/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index e517ce5118e..a55f9c1d7cb 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -22,5 +22,5 @@ "iot_class": "cloud_polling", "loggers": ["aiolyric"], "quality_scale": "silver", - "requirements": ["aiolyric==1.0.9"] + "requirements": ["aiolyric==1.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index f86b6afc2e2..5a3df4e60c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -280,7 +280,7 @@ aiolivisi==0.0.19 aiolookin==1.0.0 # homeassistant.components.lyric -aiolyric==1.0.9 +aiolyric==1.1.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bc5a8b53ed..e5dda6a7715 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -255,7 +255,7 @@ aiolivisi==0.0.19 aiolookin==1.0.0 # homeassistant.components.lyric -aiolyric==1.0.9 +aiolyric==1.1.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 From c010b97abf11d253f0634905aacd720a6625fffc Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 16 Aug 2023 09:07:14 +0200 Subject: [PATCH 0548/1151] Improve test recovery MQTT certificate files (#98223) * Improve test recovery MQTT certificate files * Spelling --- tests/components/mqtt/test_util.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py index 96577bd3fa4..e93a5e376bb 100644 --- a/tests/components/mqtt/test_util.py +++ b/tests/components/mqtt/test_util.py @@ -37,7 +37,7 @@ def mock_temp_dir(): async def test_async_create_certificate_temp_files( hass: HomeAssistant, mock_temp_dir, option, content, file_created ) -> None: - """Test creating and reading certificate files.""" + """Test creating and reading and recovery certificate files.""" config = {option: content} await mqtt.util.async_create_certificate_temp_files(hass, config) @@ -47,6 +47,22 @@ async def test_async_create_certificate_temp_files( mqtt.util.migrate_certificate_file_to_content(file_path or content) == content ) + # Make sure certificate temp files are recovered + if file_path: + # Overwrite content of file (except for auto option) + file = open(file_path, "wb") + file.write(b"invalid") + file.close() + + await mqtt.util.async_create_certificate_temp_files(hass, config) + file_path2 = mqtt.util.get_file_path(option) + assert bool(file_path2) is file_created + assert ( + mqtt.util.migrate_certificate_file_to_content(file_path2 or content) == content + ) + + assert file_path == file_path2 + async def test_reading_non_exitisting_certificate_file() -> None: """Test reading a non existing certificate file.""" From 8efb9dad7e749ca93a5f8a2bee18117f656a904d Mon Sep 17 00:00:00 2001 From: Emma Vanbrabant Date: Wed, 16 Aug 2023 08:42:38 +0100 Subject: [PATCH 0549/1151] Add device_class to Renault charging remaining time (#98393) * Add device_class on charging remaining time Set `device_class to `duration` on the `charging_remaining_time` entity in the Renault integration. I had some issues showing this property on my dashboard, and setting this fixed it. The recommendation to open an issue against the original integration in these kinds of cases came from [here](https://community.home-assistant.io/t/how-to-format-a-card-to-show-hours-instead-of-seconds/425473/7). * Update test const to add duration * fix other cars * Update test_sensor.ambr * add duration in ambr --- homeassistant/components/renault/sensor.py | 1 + tests/components/renault/const.py | 3 +++ .../renault/snapshots/test_sensor.ambr | 18 ++++++++++++------ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 050c5a930f6..92deb3438de 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -190,6 +190,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( key="charging_remaining_time", coordinator="battery", data_key="chargingRemainingTime", + device_class=SensorDeviceClass.DURATION, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon="mdi:timer", native_unit_of_measurement=UnitOfTime.MINUTES, diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 4b2a7dfc72b..342ab803f33 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -198,6 +198,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, }, { + ATTR_DEVICE_CLASS: SensorDeviceClass.DURATION, ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", ATTR_ICON: "mdi:timer", ATTR_STATE: "145", @@ -433,6 +434,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, }, { + ATTR_DEVICE_CLASS: SensorDeviceClass.DURATION, ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", ATTR_ICON: "mdi:timer", ATTR_STATE: STATE_UNKNOWN, @@ -668,6 +670,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, }, { + ATTR_DEVICE_CLASS: SensorDeviceClass.DURATION, ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", ATTR_ICON: "mdi:timer", ATTR_STATE: "145", diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index b4e2f105b3b..46b231ac7ef 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -404,7 +404,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': 'mdi:timer', 'original_name': 'Charging remaining time', 'platform': 'renault', @@ -811,6 +811,7 @@ }), StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', 'icon': 'mdi:timer', 'state_class': , @@ -1100,7 +1101,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': 'mdi:timer', 'original_name': 'Charging remaining time', 'platform': 'renault', @@ -1505,6 +1506,7 @@ }), StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', 'icon': 'mdi:timer', 'state_class': , @@ -1790,7 +1792,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': 'mdi:timer', 'original_name': 'Charging remaining time', 'platform': 'renault', @@ -2223,6 +2225,7 @@ }), StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', 'icon': 'mdi:timer', 'state_class': , @@ -2803,7 +2806,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': 'mdi:timer', 'original_name': 'Charging remaining time', 'platform': 'renault', @@ -3210,6 +3213,7 @@ }), StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', 'icon': 'mdi:timer', 'state_class': , @@ -3499,7 +3503,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': 'mdi:timer', 'original_name': 'Charging remaining time', 'platform': 'renault', @@ -3904,6 +3908,7 @@ }), StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', 'icon': 'mdi:timer', 'state_class': , @@ -4189,7 +4194,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': 'mdi:timer', 'original_name': 'Charging remaining time', 'platform': 'renault', @@ -4622,6 +4627,7 @@ }), StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', 'icon': 'mdi:timer', 'state_class': , From 6c573953e332c615fdca28963c60a2fb260b6a3b Mon Sep 17 00:00:00 2001 From: Andy Barratt Date: Wed, 16 Aug 2023 09:06:37 +0100 Subject: [PATCH 0550/1151] Update Light flash description (#98252) * Update Light flash description `light.turn_on` service description for the `flash` option gave the impression of a boolean value being required when in fact a string of either `short` or `long` was required. Updated this to match the documentation found at https://www.home-assistant.io/integrations/light `light.turn_off` also described the existence of a `flash` option when none exists. I've removed this, which matches the aforementioned documentation too. * Revert removal of flash from turn-off As discussed in feedback, turn-off does indeed seem to support flash. --- 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 80e2ca54562..8be954f4653 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -308,7 +308,7 @@ }, "flash": { "name": "Flash", - "description": "If the light should flash." + "description": "Tell light to flash, can be either value short or long." }, "effect": { "name": "Effect", From a0ea6e6a0c4a480f3ddf1964505f2623aee7e7ea Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 16 Aug 2023 02:10:02 -0700 Subject: [PATCH 0551/1151] Bump opower to 0.0.29 (#98503) --- 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 97a605676e1..14720106f74 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.0.26"] + "requirements": ["opower==0.0.29"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5a3df4e60c1..99c0bb81432 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.26 +opower==0.0.29 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5dda6a7715..a82b48a4e29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1031,7 +1031,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.26 +opower==0.0.29 # homeassistant.components.oralb oralb-ble==0.17.6 From b680bca5e9cdb81abbf5285a1670d2e089a65ff4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Aug 2023 04:30:47 -0500 Subject: [PATCH 0552/1151] Bump aiohomekit to 2.6.16 (#98490) --- 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 52a91d42e67..5096544ba05 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.15"], + "requirements": ["aiohomekit==2.6.16"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 99c0bb81432..2d8205e7ff9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -246,7 +246,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.15 +aiohomekit==2.6.16 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a82b48a4e29..89044c9b368 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -224,7 +224,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==2.6.15 +aiohomekit==2.6.16 # homeassistant.components.emulated_hue # homeassistant.components.http From b083f5bf89779a1db2b9accd356b6a9d000cdf53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Aug 2023 04:33:25 -0500 Subject: [PATCH 0553/1151] Add some typing to doorbird (#98483) --- .coveragerc | 3 +- homeassistant/components/doorbird/__init__.py | 130 +---------------- homeassistant/components/doorbird/const.py | 2 + homeassistant/components/doorbird/device.py | 137 ++++++++++++++++++ homeassistant/components/doorbird/entity.py | 7 +- homeassistant/components/doorbird/util.py | 17 ++- 6 files changed, 166 insertions(+), 130 deletions(-) create mode 100644 homeassistant/components/doorbird/device.py diff --git a/.coveragerc b/.coveragerc index 9930cbaf0b5..4dd8dea258d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -212,8 +212,9 @@ omit = homeassistant/components/dominos/* homeassistant/components/doods/* homeassistant/components/doorbird/__init__.py - homeassistant/components/doorbird/button.py homeassistant/components/doorbird/camera.py + homeassistant/components/doorbird/button.py + homeassistant/components/doorbird/device.py homeassistant/components/doorbird/entity.py homeassistant/components/doorbird/util.py homeassistant/components/dormakaba_dkey/__init__.py diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index deb37c1bfe3..8651f7de6de 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -24,11 +24,10 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.network import get_url from homeassistant.helpers.typing import ConfigType -from homeassistant.util import dt as dt_util, slugify from .const import ( + API_URL, CONF_EVENTS, DOMAIN, DOOR_STATION, @@ -37,12 +36,11 @@ from .const import ( PLATFORMS, UNDO_UPDATE_LISTENER, ) +from .device import ConfiguredDoorBird from .util import get_doorstation_by_token _LOGGER = logging.getLogger(__name__) -API_URL = f"/api/{DOMAIN}" - CONF_CUSTOM_URL = "hass_url_override" RESET_DEVICE_FAVORITES = "doorbird_reset_favorites" @@ -128,9 +126,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) raise ConfigEntryNotReady - token = doorstation_config.get(CONF_TOKEN, config_entry_id) - custom_url = doorstation_config.get(CONF_CUSTOM_URL) - name = doorstation_config.get(CONF_NAME) + token: str = doorstation_config.get(CONF_TOKEN, config_entry_id) + custom_url: str | None = doorstation_config.get(CONF_CUSTOM_URL) + name: str | None = doorstation_config.get(CONF_NAME) events = doorstation_options.get(CONF_EVENTS, []) doorstation = ConfiguredDoorBird(device, name, custom_url, token) doorstation.update_events(events) @@ -151,7 +149,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -def _init_doorbird_device(device): +def _init_doorbird_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]: return device.ready(), device.info() @@ -211,122 +209,6 @@ def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: Confi hass.config_entries.async_update_entry(entry, options=options) -class ConfiguredDoorBird: - """Attach additional information to pass along with configured device.""" - - def __init__(self, device, name, custom_url, token): - """Initialize configured device.""" - self._name = name - self._device = device - self._custom_url = custom_url - self.events = None - self.doorstation_events = None - self._token = token - - def update_events(self, events): - """Update the doorbird events.""" - self.events = events - self.doorstation_events = [self._get_event_name(event) for event in self.events] - - @property - def name(self): - """Get custom device name.""" - return self._name - - @property - def device(self): - """Get the configured device.""" - return self._device - - @property - def custom_url(self): - """Get custom url for device.""" - return self._custom_url - - @property - def token(self): - """Get token for device.""" - return self._token - - def register_events(self, hass: HomeAssistant) -> None: - """Register events on device.""" - # Get the URL of this server - hass_url = get_url(hass, prefer_external=False) - - # Override url if another is specified in the configuration - if self.custom_url is not None: - hass_url = self.custom_url - - if not self.doorstation_events: - # User may not have permission to get the favorites - return - - favorites = self.device.favorites() - for event in self.doorstation_events: - if self._register_event(hass_url, event, favs=favorites): - _LOGGER.info( - "Successfully registered URL for %s on %s", event, self.name - ) - - @property - def slug(self): - """Get device slug.""" - return slugify(self._name) - - def _get_event_name(self, event): - return f"{self.slug}_{event}" - - def _register_event( - self, hass_url: str, event: str, favs: dict[str, Any] | None = None - ) -> bool: - """Add a schedule entry in the device for a sensor.""" - url = f"{hass_url}{API_URL}/{event}?token={self._token}" - - # Register HA URL as webhook if not already, then get the ID - if self.webhook_is_registered(url, favs=favs): - return True - - self.device.change_favorite("http", f"Home Assistant ({event})", url) - if not self.webhook_is_registered(url): - _LOGGER.warning( - 'Unable to set favorite URL "%s". Event "%s" will not fire', - url, - event, - ) - return False - return True - - def webhook_is_registered(self, url, favs=None) -> bool: - """Return whether the given URL is registered as a device favorite.""" - return self.get_webhook_id(url, favs) is not None - - def get_webhook_id(self, url, favs=None) -> str | None: - """Return the device favorite ID for the given URL. - - The favorite must exist or there will be problems. - """ - favs = favs if favs else self.device.favorites() - - if "http" not in favs: - return None - - for fav_id in favs["http"]: - if favs["http"][fav_id]["value"] == url: - return fav_id - - return None - - def get_event_data(self): - """Get data to pass along with HA event.""" - return { - "timestamp": dt_util.utcnow().isoformat(), - "live_video_url": self._device.live_video_url, - "live_image_url": self._device.live_image_url, - "rtsp_live_video_url": self._device.rtsp_live_video_url, - "html5_viewer_url": self._device.html5_viewer_url, - } - - class DoorBirdRequestView(HomeAssistantView): """Provide a page for the device to call.""" diff --git a/homeassistant/components/doorbird/const.py b/homeassistant/components/doorbird/const.py index 767366af734..416603a312c 100644 --- a/homeassistant/components/doorbird/const.py +++ b/homeassistant/components/doorbird/const.py @@ -19,3 +19,5 @@ DOORBIRD_INFO_KEY_PRIMARY_MAC_ADDR = "PRIMARY_MAC_ADDR" DOORBIRD_INFO_KEY_WIFI_MAC_ADDR = "WIFI_MAC_ADDR" UNDO_UPDATE_LISTENER = "undo_update_listener" + +API_URL = f"/api/{DOMAIN}" diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py new file mode 100644 index 00000000000..1c787feb934 --- /dev/null +++ b/homeassistant/components/doorbird/device.py @@ -0,0 +1,137 @@ +"""Support for DoorBird devices.""" +from __future__ import annotations + +import logging +from typing import Any + +from doorbirdpy import DoorBird + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.network import get_url +from homeassistant.util import dt as dt_util, slugify + +from .const import API_URL + +_LOGGER = logging.getLogger(__name__) + + +class ConfiguredDoorBird: + """Attach additional information to pass along with configured device.""" + + def __init__( + self, device: DoorBird, name: str | None, custom_url: str | None, token: str + ) -> None: + """Initialize configured device.""" + self._name = name + self._device = device + self._custom_url = custom_url + self.events = None + self.doorstation_events = None + self._token = token + + def update_events(self, events): + """Update the doorbird events.""" + self.events = events + self.doorstation_events = [self._get_event_name(event) for event in self.events] + + @property + def name(self) -> str | None: + """Get custom device name.""" + return self._name + + @property + def device(self) -> DoorBird: + """Get the configured device.""" + return self._device + + @property + def custom_url(self) -> str | None: + """Get custom url for device.""" + return self._custom_url + + @property + def token(self) -> str: + """Get token for device.""" + return self._token + + def register_events(self, hass: HomeAssistant) -> None: + """Register events on device.""" + # Get the URL of this server + hass_url = get_url(hass, prefer_external=False) + + # Override url if another is specified in the configuration + if self.custom_url is not None: + hass_url = self.custom_url + + if not self.doorstation_events: + # User may not have permission to get the favorites + return + + favorites = self.device.favorites() + for event in self.doorstation_events: + if self._register_event(hass_url, event, favs=favorites): + _LOGGER.info( + "Successfully registered URL for %s on %s", event, self.name + ) + + @property + def slug(self) -> str: + """Get device slug.""" + return slugify(self._name) + + def _get_event_name(self, event: str) -> str: + return f"{self.slug}_{event}" + + def _register_event( + self, hass_url: str, event: str, favs: dict[str, Any] | None = None + ) -> bool: + """Add a schedule entry in the device for a sensor.""" + url = f"{hass_url}{API_URL}/{event}?token={self._token}" + + # Register HA URL as webhook if not already, then get the ID + if self.webhook_is_registered(url, favs=favs): + return True + + self.device.change_favorite("http", f"Home Assistant ({event})", url) + if not self.webhook_is_registered(url): + _LOGGER.warning( + 'Unable to set favorite URL "%s". Event "%s" will not fire', + url, + event, + ) + return False + return True + + def webhook_is_registered( + self, url: str, favs: dict[str, Any] | None = None + ) -> bool: + """Return whether the given URL is registered as a device favorite.""" + return self.get_webhook_id(url, favs) is not None + + def get_webhook_id( + self, url: str, favs: dict[str, Any] | None = None + ) -> str | None: + """Return the device favorite ID for the given URL. + + The favorite must exist or there will be problems. + """ + favs = favs if favs else self.device.favorites() + + if "http" not in favs: + return None + + for fav_id in favs["http"]: + if favs["http"][fav_id]["value"] == url: + return fav_id + + return None + + def get_event_data(self) -> dict[str, str]: + """Get data to pass along with HA event.""" + return { + "timestamp": dt_util.utcnow().isoformat(), + "live_video_url": self._device.live_video_url, + "live_image_url": self._device.live_image_url, + "rtsp_live_video_url": self._device.rtsp_live_video_url, + "html5_viewer_url": self._device.html5_viewer_url, + } diff --git a/homeassistant/components/doorbird/entity.py b/homeassistant/components/doorbird/entity.py index 65431e38be1..32c9cfff784 100644 --- a/homeassistant/components/doorbird/entity.py +++ b/homeassistant/components/doorbird/entity.py @@ -1,5 +1,7 @@ """The DoorBird integration base entity.""" +from typing import Any + from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity @@ -10,6 +12,7 @@ from .const import ( DOORBIRD_INFO_KEY_FIRMWARE, MANUFACTURER, ) +from .device import ConfiguredDoorBird from .util import get_mac_address_from_doorstation_info @@ -18,7 +21,9 @@ class DoorBirdEntity(Entity): _attr_has_entity_name = True - def __init__(self, doorstation, doorstation_info): + def __init__( + self, doorstation: ConfiguredDoorBird, doorstation_info: dict[str, Any] + ) -> None: """Initialize the entity.""" super().__init__() self._doorstation = doorstation diff --git a/homeassistant/components/doorbird/util.py b/homeassistant/components/doorbird/util.py index 55974bc1866..7b406bc07fa 100644 --- a/homeassistant/components/doorbird/util.py +++ b/homeassistant/components/doorbird/util.py @@ -1,6 +1,9 @@ """DoorBird integration utils.""" +from homeassistant.core import HomeAssistant + from .const import DOMAIN, DOOR_STATION +from .device import ConfiguredDoorBird def get_mac_address_from_doorstation_info(doorstation_info): @@ -10,17 +13,23 @@ def get_mac_address_from_doorstation_info(doorstation_info): return doorstation_info["WIFI_MAC_ADDR"] -def get_doorstation_by_token(hass, token): +def get_doorstation_by_token( + hass: HomeAssistant, token: str +) -> ConfiguredDoorBird | None: """Get doorstation by token.""" return _get_doorstation_by_attr(hass, "token", token) -def get_doorstation_by_slug(hass, slug): +def get_doorstation_by_slug( + hass: HomeAssistant, slug: str +) -> ConfiguredDoorBird | None: """Get doorstation by slug.""" return _get_doorstation_by_attr(hass, "slug", slug) -def _get_doorstation_by_attr(hass, attr, val): +def _get_doorstation_by_attr( + hass: HomeAssistant, attr: str, val: str +) -> ConfiguredDoorBird | None: for entry in hass.data[DOMAIN].values(): if DOOR_STATION not in entry: continue @@ -33,7 +42,7 @@ def _get_doorstation_by_attr(hass, attr, val): return None -def get_all_doorstations(hass): +def get_all_doorstations(hass: HomeAssistant) -> list[ConfiguredDoorBird]: """Get all doorstations.""" return [ entry[DOOR_STATION] From 0bcc02e9086e954fec2c45cab678014d0380645a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:36:52 +0200 Subject: [PATCH 0554/1151] Skip writing pyc files [ci] (#98423) --- .github/workflows/ci.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6d41c4e1e7f..5cb51a30dda 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -735,6 +735,8 @@ jobs: if: needs.info.outputs.test_full_suite == 'true' timeout-minutes: 60 id: pytest-full + env: + PYTHONDONTWRITEBYTECODE: 1 run: | . venv/bin/activate python --version @@ -759,6 +761,8 @@ jobs: timeout-minutes: 10 id: pytest-partial shell: bash + env: + PYTHONDONTWRITEBYTECODE: 1 run: | . venv/bin/activate python --version @@ -877,6 +881,8 @@ jobs: timeout-minutes: 20 id: pytest-partial shell: bash + env: + PYTHONDONTWRITEBYTECODE: 1 run: | . venv/bin/activate python --version @@ -994,6 +1000,8 @@ jobs: timeout-minutes: 20 id: pytest-partial shell: bash + env: + PYTHONDONTWRITEBYTECODE: 1 run: | . venv/bin/activate python --version From e69090b943d7d25689ca794917b6fe9bdcc3da05 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 11:41:11 +0200 Subject: [PATCH 0555/1151] Map meteo_france weather condition codes once (#98513) --- homeassistant/components/meteo_france/const.py | 5 +++++ homeassistant/components/meteo_france/weather.py | 9 +++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index fad1a33e25c..f1e6ae8d0eb 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -89,3 +89,8 @@ CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_EXCEPTIONAL: [], } +CONDITION_MAP = { + cond_code: cond_ha + for cond_ha, cond_codes in CONDITION_CLASSES.items() + for cond_code in cond_codes +} diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index a2e9dc30c53..6459827b601 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -34,7 +34,7 @@ from homeassistant.util import dt as dt_util from .const import ( ATTRIBUTION, - CONDITION_CLASSES, + CONDITION_MAP, COORDINATOR_FORECAST, DOMAIN, FORECAST_MODE_DAILY, @@ -47,11 +47,8 @@ _LOGGER = logging.getLogger(__name__) def format_condition(condition: str): - """Return condition from dict CONDITION_CLASSES.""" - for key, value in CONDITION_CLASSES.items(): - if condition in value: - return key - return condition + """Return condition from dict CONDITION_MAP.""" + return CONDITION_MAP.get(condition, condition) async def async_setup_entry( From 636cb6279d076512fbba1d9fd7656324f35ec3db Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 11:59:59 +0200 Subject: [PATCH 0556/1151] Push updated ecobee weather forecast to listeners (#98511) --- homeassistant/components/ecobee/weather.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 729ab463fb3..3e71b05af1d 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -197,6 +197,7 @@ class EcobeeWeather(WeatherEntity): await self.data.update() thermostat = self.data.ecobee.get_thermostat(self._index) self.weather = thermostat.get("weather") + await self.async_update_listeners(("daily",)) def _process_forecast(json): From ed2f067c5294ef56e6821f7e4f4c6dfc6ae7c184 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Aug 2023 05:03:40 -0500 Subject: [PATCH 0557/1151] Bump zeroconf to 0.80.0 (#98416) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 6f3020244fa..0d63e87db17 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.78.0"] + "requirements": ["zeroconf==0.80.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 389729fa4c2..bac607545e6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.78.0 +zeroconf==0.80.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 2d8205e7ff9..db56052eb82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2752,7 +2752,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.78.0 +zeroconf==0.80.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89044c9b368..d69c88eb7a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2025,7 +2025,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.78.0 +zeroconf==0.80.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From abf065ed7672f90f92aa183fd3122d4ae75ce680 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 16 Aug 2023 11:56:47 +0100 Subject: [PATCH 0558/1151] Fix checks for duplicated config entries in IPMA (#98319) * fix unique_id * old unique id detection * update tests * match entry not unique_id * address review --- homeassistant/components/ipma/config_flow.py | 14 +- homeassistant/components/ipma/strings.json | 4 +- tests/components/ipma/conftest.py | 36 +++++ tests/components/ipma/test_config_flow.py | 145 ++++++------------- 4 files changed, 93 insertions(+), 106 deletions(-) create mode 100644 tests/components/ipma/conftest.py diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index 9434aed3097..d7b8b8cc003 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -22,14 +22,14 @@ class IpmaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._errors = {} if user_input is not None: - if user_input[CONF_NAME] not in self.hass.config_entries.async_entries( - DOMAIN - ): - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) + self._async_abort_entries_match( + { + CONF_LATITUDE: user_input[CONF_LATITUDE], + CONF_LONGITUDE: user_input[CONF_LONGITUDE], + } + ) - self._errors[CONF_NAME] = "name_exists" + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) # default location is set hass configuration return await self._show_config_form( diff --git a/homeassistant/components/ipma/strings.json b/homeassistant/components/ipma/strings.json index b9f50c66f9e..012550d8bd1 100644 --- a/homeassistant/components/ipma/strings.json +++ b/homeassistant/components/ipma/strings.json @@ -12,7 +12,9 @@ } } }, - "error": { "name_exists": "Name already exists" } + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } }, "system_health": { "info": { diff --git a/tests/components/ipma/conftest.py b/tests/components/ipma/conftest.py new file mode 100644 index 00000000000..dda0e69d118 --- /dev/null +++ b/tests/components/ipma/conftest.py @@ -0,0 +1,36 @@ +"""Define test fixtures for IPMA.""" + +import pytest + +from homeassistant.components.ipma import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(hass, config): + """Define a config entry fixture.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=config, + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture(name="config") +def config_fixture(): + """Define a config entry data fixture.""" + return { + CONF_NAME: "Home", + CONF_LATITUDE: 0, + CONF_LONGITUDE: 0, + } + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry): + """Define a fixture to set up ipma.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index 5bb1d8b8364..18b68a5a44d 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,116 +1,65 @@ """Tests for IPMA config flow.""" -from unittest.mock import Mock, patch +from unittest.mock import patch -from homeassistant.components.ipma import config_flow -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.ipma.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -async def test_show_config_form() -> None: - """Test show configuration form.""" - hass = Mock() - flow = config_flow.IpmaFlowHandler() - flow.hass = hass +@pytest.fixture(name="ipma_setup", autouse=True) +def ipma_setup_fixture(request): + """Patch ipma setup entry.""" + with patch("homeassistant.components.ipma.async_setup_entry", return_value=True): + yield - result = await flow._show_config_form() + +async def test_config_flow(hass: HomeAssistant) -> None: + """Test configuration form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result["type"] == "form" assert result["step_id"] == "user" + test_data = { + CONF_LONGITUDE: 0, + CONF_LATITUDE: 0, + } -async def test_show_config_form_default_values() -> None: - """Test show configuration form.""" - hass = Mock() - flow = config_flow.IpmaFlowHandler() - flow.hass = hass + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + test_data, + ) - result = await flow._show_config_form(name="test", latitude="0", longitude="0") - - assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Home" + assert result["data"] == { + CONF_NAME: "Home", + CONF_LONGITUDE: 0, + CONF_LATITUDE: 0, + } -async def test_flow_with_home_location(hass: HomeAssistant) -> None: - """Test config flow . +async def test_flow_entry_already_exists(hass: HomeAssistant, config_entry) -> None: + """Test user input for config_entry that already exists. - Tests the flow when a default location is configured - then it should return a form with default values + Test when the form should show when user puts existing location + in the config gui. Then the form should show with error. """ - flow = config_flow.IpmaFlowHandler() - flow.hass = hass + test_data = { + CONF_NAME: "Home", + CONF_LONGITUDE: 0, + CONF_LATITUDE: 0, + } - hass.config.location_name = "Home" - hass.config.latitude = 1 - hass.config.longitude = 1 + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data + ) + await hass.async_block_till_done() - result = await flow.async_step_user() - assert result["type"] == "form" - assert result["step_id"] == "user" - - -async def test_flow_show_form() -> None: - """Test show form scenarios first time. - - Test when the form should show when no configurations exists - """ - hass = Mock() - flow = config_flow.IpmaFlowHandler() - flow.hass = hass - - with patch( - "homeassistant.components.ipma.config_flow.IpmaFlowHandler._show_config_form" - ) as config_form: - await flow.async_step_user() - assert len(config_form.mock_calls) == 1 - - -async def test_flow_entry_created_from_user_input() -> None: - """Test that create data from user input. - - Test when the form should show when no configurations exists - """ - hass = Mock() - flow = config_flow.IpmaFlowHandler() - flow.hass = hass - - test_data = {"name": "home", CONF_LONGITUDE: "0", CONF_LATITUDE: "0"} - - # Test that entry created when user_input name not exists - with patch( - "homeassistant.components.ipma.config_flow.IpmaFlowHandler._show_config_form" - ) as config_form, patch.object( - flow.hass.config_entries, - "async_entries", - return_value=[], - ) as config_entries: - result = await flow.async_step_user(user_input=test_data) - - assert result["type"] == "create_entry" - assert result["data"] == test_data - assert len(config_entries.mock_calls) == 1 - assert not config_form.mock_calls - - -async def test_flow_entry_config_entry_already_exists() -> None: - """Test that create data from user input and config_entry already exists. - - Test when the form should show when user puts existing name - in the config gui. Then the form should show with error - """ - hass = Mock() - flow = config_flow.IpmaFlowHandler() - flow.hass = hass - - test_data = {"name": "home", CONF_LONGITUDE: "0", CONF_LATITUDE: "0"} - - # Test that entry created when user_input name not exists - with patch( - "homeassistant.components.ipma.config_flow.IpmaFlowHandler._show_config_form" - ) as config_form, patch.object( - flow.hass.config_entries, "async_entries", return_value={"home": test_data} - ) as config_entries: - await flow.async_step_user(user_input=test_data) - - assert len(config_form.mock_calls) == 1 - assert len(config_entries.mock_calls) == 1 - assert len(flow._errors) == 1 + assert result["type"] == "abort" + assert result["reason"] == "already_configured" From 5ed3e906070c516e8bbfc61f466e3afc01874393 Mon Sep 17 00:00:00 2001 From: VidFerris <29590790+VidFerris@users.noreply.github.com> Date: Wed, 16 Aug 2023 20:57:16 +1000 Subject: [PATCH 0559/1151] Use Local Timezone for Withings Integration (#98137) --- homeassistant/components/withings/common.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 9282e3977c1..ef3b6456d20 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -438,22 +438,16 @@ class DataManager: async def async_get_sleep_summary(self) -> dict[Measurement, Any]: """Get the sleep summary data.""" _LOGGER.debug("Updating withing sleep summary") - now = dt_util.utcnow() + now = dt_util.now() yesterday = now - datetime.timedelta(days=1) - yesterday_noon = datetime.datetime( - yesterday.year, - yesterday.month, - yesterday.day, - 12, - 0, - 0, - 0, - datetime.UTC, + yesterday_noon = dt_util.start_of_local_day(yesterday) + datetime.timedelta( + hours=12 ) + yesterday_noon_utc = dt_util.as_utc(yesterday_noon) def get_sleep_summary() -> SleepGetSummaryResponse: return self._api.sleep_get_summary( - lastupdate=yesterday_noon, + lastupdate=yesterday_noon_utc, data_fields=[ GetSleepSummaryField.BREATHING_DISTURBANCES_INTENSITY, GetSleepSummaryField.DEEP_SLEEP_DURATION, From 91faa5384370e5df661bf93865f13637005d2948 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 13:00:14 +0200 Subject: [PATCH 0560/1151] Don't allow hass.config.config_dir to be None (#98442) --- homeassistant/bootstrap.py | 6 ++--- homeassistant/components/cloud/client.py | 1 - homeassistant/components/file/notify.py | 3 --- .../components/homematicip_cloud/services.py | 2 +- .../components/system_log/__init__.py | 1 - homeassistant/components/verisure/camera.py | 1 - homeassistant/components/zha/core/gateway.py | 1 - homeassistant/config.py | 6 +---- homeassistant/core.py | 12 ++++----- homeassistant/helpers/check_config.py | 2 -- homeassistant/loader.py | 6 +---- homeassistant/scripts/auth.py | 3 +-- homeassistant/scripts/benchmark/__init__.py | 2 +- homeassistant/scripts/check_config.py | 3 +-- homeassistant/scripts/ensure_config.py | 3 +-- tests/common.py | 3 +-- tests/conftest.py | 4 +-- tests/test_core.py | 25 ++++++++----------- 18 files changed, 28 insertions(+), 56 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 196a00dda7c..81ae4eb6e18 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -110,8 +110,7 @@ async def async_setup_hass( runtime_config: RuntimeConfig, ) -> core.HomeAssistant | None: """Set up Home Assistant.""" - hass = core.HomeAssistant() - hass.config.config_dir = runtime_config.config_dir + hass = core.HomeAssistant(runtime_config.config_dir) async_enable_logging( hass, @@ -178,14 +177,13 @@ async def async_setup_hass( old_config = hass.config old_logging = hass.data.get(DATA_LOGGING) - hass = core.HomeAssistant() + hass = core.HomeAssistant(old_config.config_dir) if old_logging: hass.data[DATA_LOGGING] = old_logging hass.config.skip_pip = old_config.skip_pip hass.config.skip_pip_packages = old_config.skip_pip_packages hass.config.internal_url = old_config.internal_url hass.config.external_url = old_config.external_url - hass.config.config_dir = old_config.config_dir # Setup loader cache after the config dir has been set loader.async_setup(hass) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 7bd80000ca4..6fbcfc30f69 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -54,7 +54,6 @@ class CloudClient(Interface): @property def base_path(self) -> Path: """Return path to base dir.""" - assert self._hass.config.config_dir is not None return Path(self._hass.config.config_dir) @property diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index 3238fe91102..ca0deb89c7b 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -51,9 +51,6 @@ class FileNotificationService(BaseNotificationService): def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a file.""" file: TextIO - if not self.hass.config.config_dir: - return - filepath: str = os.path.join(self.hass.config.config_dir, self.filename) with open(filepath, "a", encoding="utf8") as file: if os.stat(filepath).st_size == 0: diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index a8393ff88ac..09457ce0792 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -286,7 +286,7 @@ async def _set_active_climate_profile( async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> None: """Service to dump the configuration of a Homematic IP Access Point.""" config_path: str = ( - service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir or "." + service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir ) config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] anonymize = service.data[ATTR_ANONYMIZE] diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index f025013cc2b..cba8082d23c 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -234,7 +234,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass_path: str = HOMEASSISTANT_PATH[0] config_dir = hass.config.config_dir - assert config_dir is not None paths_re = re.compile( r"(?:{})/(.*)".format("|".join([re.escape(x) for x in (hass_path, config_dir)])) ) diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index c9d98041a2c..a240d45cf7e 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -36,7 +36,6 @@ async def async_setup_entry( VerisureSmartcam.capture_smartcam.__name__, ) - assert hass.config.config_dir async_add_entities( VerisureSmartcam(coordinator, serial_number, hass.config.config_dir) for serial_number in coordinator.data["cameras"] diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 1f3a71f4cbf..1320e77ba3c 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -802,7 +802,6 @@ class LogRelayHandler(logging.Handler): hass_path: str = HOMEASSISTANT_PATH[0] config_dir = self.hass.config.config_dir - assert config_dir is not None paths_re = re.compile( r"(?:{})/(.*)".format( "|".join([re.escape(x) for x in (hass_path, config_dir)]) diff --git a/homeassistant/config.py b/homeassistant/config.py index eed296baf0e..0d9e1d9034e 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -337,7 +337,6 @@ async def async_create_default_config(hass: HomeAssistant) -> bool: Return if creation was successful. """ - assert hass.config.config_dir return await hass.async_add_executor_job( _write_default_config, hass.config.config_dir ) @@ -390,10 +389,7 @@ async def async_hass_config_yaml(hass: HomeAssistant) -> dict: This function allow a component inside the asyncio loop to reload its configuration by itself. Include package merge. """ - if hass.config.config_dir is None: - secrets = None - else: - secrets = Secrets(Path(hass.config.config_dir)) + secrets = Secrets(Path(hass.config.config_dir)) # Not using async_add_executor_job because this is an internal method. config = await hass.loop.run_in_executor( diff --git a/homeassistant/core.py b/homeassistant/core.py index a025eacd4bc..140cf203e70 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -288,13 +288,13 @@ class HomeAssistant: http: HomeAssistantHTTP = None # type: ignore[assignment] config_entries: ConfigEntries = None # type: ignore[assignment] - def __new__(cls) -> HomeAssistant: + def __new__(cls, config_dir: str) -> HomeAssistant: """Set the _hass thread local data.""" hass = super().__new__(cls) _hass.hass = hass return hass - def __init__(self) -> None: + def __init__(self, config_dir: str) -> None: """Initialize new Home Assistant object.""" self.loop = asyncio.get_running_loop() self._tasks: set[asyncio.Future[Any]] = set() @@ -302,7 +302,7 @@ class HomeAssistant: self.bus = EventBus(self) self.services = ServiceRegistry(self) self.states = StateMachine(self.bus, self.loop) - self.config = Config(self) + self.config = Config(self, config_dir) self.components = loader.Components(self) self.helpers = loader.Helpers(self) # This is a dictionary that any component can store any data on. @@ -2011,7 +2011,7 @@ class ServiceRegistry: class Config: """Configuration settings for Home Assistant.""" - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, hass: HomeAssistant, config_dir: str) -> None: """Initialize a new config object.""" self.hass = hass @@ -2047,7 +2047,7 @@ class Config: self.api: ApiConfig | None = None # Directory that holds the configuration - self.config_dir: str | None = None + self.config_dir: str = config_dir # List of allowed external dirs to access self.allowlist_external_dirs: set[str] = set() @@ -2078,8 +2078,6 @@ class Config: Async friendly. """ - if self.config_dir is None: - raise HomeAssistantError("config_dir is not set") return os.path.join(self.config_dir, *path) def is_allowed_external_url(self, url: str) -> bool: diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index a580c013cd0..1e1cac050f1 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -94,8 +94,6 @@ async def async_check_ha_config_file( # noqa: C901 if not await hass.async_add_executor_job(os.path.isfile, config_path): return result.add_error("File configuration.yaml not found.") - assert hass.config.config_dir is not None - config = await hass.async_add_executor_job( load_yaml_config_file, config_path, diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 340888a2f7a..697e47187ce 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -1148,17 +1148,13 @@ async def _async_component_dependencies( return loaded -def _async_mount_config_dir(hass: HomeAssistant) -> bool: +def _async_mount_config_dir(hass: HomeAssistant) -> None: """Mount config dir in order to load custom_component. Async friendly but not a coroutine. """ - if hass.config.config_dir is None: - _LOGGER.error("Can't load integrations - configuration directory is not set") - return False if hass.config.config_dir not in sys.path: sys.path.insert(0, hass.config.config_dir) - return True def _lookup_path(hass: HomeAssistant) -> list[str]: diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py index 11ab6aadfbf..5714e5814a4 100644 --- a/homeassistant/scripts/auth.py +++ b/homeassistant/scripts/auth.py @@ -50,8 +50,7 @@ def run(args): async def run_command(args): """Run the command.""" - hass = HomeAssistant() - hass.config.config_dir = os.path.join(os.getcwd(), args.config) + hass = HomeAssistant(os.path.join(os.getcwd(), args.config)) hass.auth = await auth_manager_from_config(hass, [{"type": "homeassistant"}], []) provider = hass.auth.auth_providers[0] await provider.async_initialize() diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 3627e4096d3..a04493a8935 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -49,7 +49,7 @@ def run(args): async def run_benchmark(bench): """Run a benchmark.""" - hass = core.HomeAssistant() + hass = core.HomeAssistant("") runtime = await bench(hass) print(f"Benchmark {bench.__name__} done in {runtime}s") await hass.async_stop() diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 7c4a200bbc5..5c81c4664da 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -231,9 +231,8 @@ def check(config_dir, secrets=False): async def async_check_config(config_dir): """Check the HA config.""" - hass = core.HomeAssistant() + hass = core.HomeAssistant(config_dir) loader.async_setup(hass) - hass.config.config_dir = config_dir hass.config_entries = ConfigEntries(hass, {}) await ar.async_load(hass) await dr.async_load(hass) diff --git a/homeassistant/scripts/ensure_config.py b/homeassistant/scripts/ensure_config.py index 6dbda59522f..786b16ca923 100644 --- a/homeassistant/scripts/ensure_config.py +++ b/homeassistant/scripts/ensure_config.py @@ -39,8 +39,7 @@ def run(args): async def async_run(config_dir): """Make sure config exists.""" - hass = HomeAssistant() - hass.config.config_dir = config_dir + hass = HomeAssistant(config_dir) path = await config_util.async_ensure_config_exists(hass) await hass.async_stop(force=True) return path diff --git a/tests/common.py b/tests/common.py index 95947719ef4..6f2209276ce 100644 --- a/tests/common.py +++ b/tests/common.py @@ -179,7 +179,7 @@ def get_test_home_assistant(): async def async_test_home_assistant(event_loop, load_registries=True): """Return a Home Assistant object pointing at test config dir.""" - hass = HomeAssistant() + hass = HomeAssistant(get_test_config_dir()) store = auth_store.AuthStore(hass) hass.auth = auth.AuthManager(hass, store, {}, {}) ensure_auth_manager_loaded(hass.auth) @@ -231,7 +231,6 @@ async def async_test_home_assistant(event_loop, load_registries=True): hass.data[loader.DATA_CUSTOM_COMPONENTS] = {} hass.config.location_name = "test home" - hass.config.config_dir = get_test_config_dir() hass.config.latitude = 32.87336 hass.config.longitude = -117.22743 hass.config.elevation = 0 diff --git a/tests/conftest.py b/tests/conftest.py index 31900dff6de..f90984e1c7b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -552,8 +552,8 @@ async def stop_hass( created = [] - def mock_hass(): - hass_inst = orig_hass() + def mock_hass(*args): + hass_inst = orig_hass(*args) created.append(hass_inst) return hass_inst diff --git a/tests/test_core.py b/tests/test_core.py index 9f6e5aeb2dd..4f7916e757b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1428,7 +1428,7 @@ async def test_serviceregistry_return_response_optional( async def test_config_defaults() -> None: """Test config defaults.""" hass = Mock() - config = ha.Config(hass) + config = ha.Config(hass, "/test/ha-config") assert config.hass is hass assert config.latitude == 0 assert config.longitude == 0 @@ -1442,7 +1442,7 @@ async def test_config_defaults() -> None: assert config.skip_pip_packages == [] assert config.components == set() assert config.api is None - assert config.config_dir is None + assert config.config_dir == "/test/ha-config" assert config.allowlist_external_dirs == set() assert config.allowlist_external_urls == set() assert config.media_dirs == {} @@ -1455,22 +1455,19 @@ async def test_config_defaults() -> None: async def test_config_path_with_file() -> None: """Test get_config_path method.""" - config = ha.Config(None) - config.config_dir = "/test/ha-config" + config = ha.Config(None, "/test/ha-config") assert config.path("test.conf") == "/test/ha-config/test.conf" async def test_config_path_with_dir_and_file() -> None: """Test get_config_path method.""" - config = ha.Config(None) - config.config_dir = "/test/ha-config" + config = ha.Config(None, "/test/ha-config") assert config.path("dir", "test.conf") == "/test/ha-config/dir/test.conf" async def test_config_as_dict() -> None: """Test as dict.""" - config = ha.Config(None) - config.config_dir = "/test/ha-config" + config = ha.Config(None, "/test/ha-config") config.hass = MagicMock() type(config.hass.state).value = PropertyMock(return_value="RUNNING") expected = { @@ -1501,7 +1498,7 @@ async def test_config_as_dict() -> None: async def test_config_is_allowed_path() -> None: """Test is_allowed_path method.""" - config = ha.Config(None) + config = ha.Config(None, "/test/ha-config") with TemporaryDirectory() as tmp_dir: # The created dir is in /tmp. This is a symlink on OS X # causing this test to fail unless we resolve path first. @@ -1533,7 +1530,7 @@ async def test_config_is_allowed_path() -> None: async def test_config_is_allowed_external_url() -> None: """Test is_allowed_external_url method.""" - config = ha.Config(None) + config = ha.Config(None, "/test/ha-config") config.allowlist_external_urls = [ "http://x.com/", "https://y.com/bla/", @@ -1584,7 +1581,7 @@ async def test_start_taking_too_long( event_loop, caplog: pytest.LogCaptureFixture ) -> None: """Test when async_start takes too long.""" - hass = ha.HomeAssistant() + hass = ha.HomeAssistant("/test/ha-config") caplog.set_level(logging.WARNING) hass.async_create_task(asyncio.sleep(0)) @@ -1751,7 +1748,7 @@ async def test_additional_data_in_core_config( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test that we can handle additional data in core configuration.""" - config = ha.Config(hass) + config = ha.Config(hass, "/test/ha-config") hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, "data": {"location_name": "Test Name", "additional_valid_key": "value"}, @@ -1764,7 +1761,7 @@ async def test_incorrect_internal_external_url( hass: HomeAssistant, hass_storage: dict[str, Any], caplog: pytest.LogCaptureFixture ) -> None: """Test that we warn when detecting invalid internal/external url.""" - config = ha.Config(hass) + config = ha.Config(hass, "/test/ha-config") hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, @@ -1777,7 +1774,7 @@ async def test_incorrect_internal_external_url( assert "Invalid external_url set" not in caplog.text assert "Invalid internal_url set" not in caplog.text - config = ha.Config(hass) + config = ha.Config(hass, "/test/ha-config") hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, From 732dac6f051794c4df84806f2caa2687d3c3cb79 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 16 Aug 2023 13:24:41 +0200 Subject: [PATCH 0561/1151] Create abstraction for Generic YeeLight (#97939) * Create abstraction for Generic YeeLight * Update light.py --- homeassistant/components/yeelight/light.py | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index f5f39e9997d..a442540109a 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -238,7 +238,7 @@ def _parse_custom_effects(effects_config) -> dict[str, dict[str, Any]]: def _async_cmd(func): """Define a wrapper to catch exceptions from the bulb.""" - async def _async_wrap(self: YeelightGenericLight, *args, **kwargs): + async def _async_wrap(self: YeelightBaseLight, *args, **kwargs): for attempts in range(2): try: _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) @@ -403,8 +403,8 @@ def _async_setup_services(hass: HomeAssistant): ) -class YeelightGenericLight(YeelightEntity, LightEntity): - """Representation of a Yeelight generic light.""" +class YeelightBaseLight(YeelightEntity, LightEntity): + """Abstract Yeelight light.""" _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} @@ -861,7 +861,13 @@ class YeelightGenericLight(YeelightEntity, LightEntity): await self._bulb.async_set_scene(scene_class, *args) -class YeelightColorLightSupport(YeelightGenericLight): +class YeelightGenericLight(YeelightBaseLight): + """Representation of a generic Yeelight.""" + + _attr_name = None + + +class YeelightColorLightSupport(YeelightBaseLight): """Representation of a Color Yeelight light support.""" _attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS, ColorMode.RGB} @@ -884,7 +890,7 @@ class YeelightColorLightSupport(YeelightGenericLight): return YEELIGHT_COLOR_EFFECT_LIST -class YeelightWhiteTempLightSupport(YeelightGenericLight): +class YeelightWhiteTempLightSupport(YeelightBaseLight): """Representation of a White temp Yeelight light.""" _attr_name = None @@ -904,7 +910,7 @@ class YeelightNightLightSupport: return PowerMode.NORMAL -class YeelightWithoutNightlightSwitchMixIn(YeelightGenericLight): +class YeelightWithoutNightlightSwitchMixIn(YeelightBaseLight): """A mix-in for yeelights without a nightlight switch.""" @property @@ -940,7 +946,7 @@ class YeelightColorLightWithoutNightlightSwitchLight( class YeelightColorLightWithNightlightSwitch( - YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight + YeelightNightLightSupport, YeelightColorLightSupport, YeelightBaseLight ): """Representation of a Yeelight with rgb support and nightlight. @@ -964,7 +970,7 @@ class YeelightWhiteTempWithoutNightlightSwitch( class YeelightWithNightLight( - YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightGenericLight + YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightBaseLight ): """Representation of a Yeelight with temp only support and nightlight. @@ -979,7 +985,7 @@ class YeelightWithNightLight( return super().is_on and not self.device.is_nightlight_enabled -class YeelightNightLightMode(YeelightGenericLight): +class YeelightNightLightMode(YeelightBaseLight): """Representation of a Yeelight when in nightlight mode.""" _attr_color_mode = ColorMode.BRIGHTNESS From cf8c9ad184fa9695ce22218910b7fed3095543f1 Mon Sep 17 00:00:00 2001 From: Mike Heath Date: Wed, 16 Aug 2023 05:38:53 -0600 Subject: [PATCH 0562/1151] Add PoE switch tests (#95087) * Add PoE switch tests * Update tests/components/tplink_omada/test_switch.py Co-authored-by: Erik Montnemery * Remove files covered by tests from exclusion * Rename entity_name to entity_id * Fix test, use snapshot, other improvements --------- Co-authored-by: Erik Montnemery --- .coveragerc | 3 - tests/components/tplink_omada/conftest.py | 87 ++ .../fixtures/switch-TL-SG3210XHP-M2.json | 683 ++++++++++++ .../switch-ports-TL-SG3210XHP-M2.json | 974 ++++++++++++++++++ .../tplink_omada/snapshots/test_switch.ambr | 345 +++++++ tests/components/tplink_omada/test_switch.py | 122 +++ 6 files changed, 2211 insertions(+), 3 deletions(-) create mode 100644 tests/components/tplink_omada/conftest.py create mode 100644 tests/components/tplink_omada/fixtures/switch-TL-SG3210XHP-M2.json create mode 100644 tests/components/tplink_omada/fixtures/switch-ports-TL-SG3210XHP-M2.json create mode 100644 tests/components/tplink_omada/snapshots/test_switch.ambr create mode 100644 tests/components/tplink_omada/test_switch.py diff --git a/.coveragerc b/.coveragerc index 4dd8dea258d..71542ebad3a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1328,9 +1328,6 @@ omit = homeassistant/components/tplink_omada/__init__.py homeassistant/components/tplink_omada/binary_sensor.py homeassistant/components/tplink_omada/controller.py - homeassistant/components/tplink_omada/coordinator.py - homeassistant/components/tplink_omada/entity.py - homeassistant/components/tplink_omada/switch.py homeassistant/components/tplink_omada/update.py homeassistant/components/traccar/device_tracker.py homeassistant/components/tractive/__init__.py diff --git a/tests/components/tplink_omada/conftest.py b/tests/components/tplink_omada/conftest.py new file mode 100644 index 00000000000..8f977de588c --- /dev/null +++ b/tests/components/tplink_omada/conftest.py @@ -0,0 +1,87 @@ +"""Test fixtures for TP-Link Omada integration.""" +from collections.abc import Generator +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from tplink_omada_client.devices import OmadaSwitch, OmadaSwitchPortDetails + +from homeassistant.components.tplink_omada.config_flow import CONF_SITE +from homeassistant.components.tplink_omada.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Test Omada Controller", + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "mocked-password", + CONF_USERNAME: "mocked-user", + CONF_VERIFY_SSL: False, + CONF_SITE: "Default", + }, + unique_id="12345", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.tplink_omada.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_omada_site_client() -> Generator[AsyncMock, None, None]: + """Mock Omada site client.""" + site_client = AsyncMock() + switch1_data = json.loads(load_fixture("switch-TL-SG3210XHP-M2.json", DOMAIN)) + switch1 = OmadaSwitch(switch1_data) + site_client.get_switches.return_value = [switch1] + + switch1_ports_data = json.loads( + load_fixture("switch-ports-TL-SG3210XHP-M2.json", DOMAIN) + ) + switch1_ports = [OmadaSwitchPortDetails(p) for p in switch1_ports_data] + site_client.get_switch_ports.return_value = switch1_ports + + return site_client + + +@pytest.fixture +def mock_omada_client( + mock_omada_site_client: AsyncMock, +) -> Generator[MagicMock, None, None]: + """Mock Omada client.""" + with patch( + "homeassistant.components.tplink_omada.create_omada_client", + autospec=True, + ) as client_mock: + client = client_mock.return_value + + client.get_site_client.return_value = mock_omada_site_client + yield client + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_omada_client: MagicMock, +) -> MockConfigEntry: + """Set up the TP-Link Omada integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/tplink_omada/fixtures/switch-TL-SG3210XHP-M2.json b/tests/components/tplink_omada/fixtures/switch-TL-SG3210XHP-M2.json new file mode 100644 index 00000000000..2e3f21406b0 --- /dev/null +++ b/tests/components/tplink_omada/fixtures/switch-TL-SG3210XHP-M2.json @@ -0,0 +1,683 @@ +{ + "type": "switch", + "mac": "54-AF-97-00-00-01", + "name": "Test PoE Switch", + "model": "TL-SG3210XHP-M2", + "modelVersion": "1.0", + "compoundModel": "TL-SG3210XHP-M2 v1.0", + "showModel": "TL-SG3210XHP-M2 v1.0", + "firmwareVersion": "1.0.12 Build 20230203 Rel.36545", + "version": "1.0.12", + "hwVersion": "1.0", + "status": 14, + "statusCategory": 1, + "site": "000000000000000000000000", + "omadacId": "00000000000000000000000000000000", + "compatible": 0, + "sn": "Y220000000001", + "addedInAdvanced": false, + "deviceMisc": { + "portNum": 10 + }, + "devCap": { + "maxMirrorGroup": 1, + "maxMirroredPort": 9, + "maxLagNum": 8, + "maxLagMember": 8, + "poePortNum": 8, + "poeSupport": true, + "supportBt": false, + "jumboSupport": true, + "jumboOddSupport": false, + "lagCap": { + "lacpModSupport": true, + "lagHashAlgSupport": true, + "lagHashAlgs": [0, 1, 2, 3, 4, 5] + }, + "eeeSupport": true, + "flowControlSupport": true, + "loopbackVlanBasedSupport": true, + "dhcpL2RelaySupport": true, + "sfpBeginNum": 9, + "sfpNum": 2 + }, + "ledSetting": 2, + "mvlanNetworkId": "000000000000000000000000", + "ipSetting": { + "mode": "dhcp", + "fallback": true, + "fallbackIp": "192.168.0.1", + "fallbackMask": "255.255.255.0" + }, + "loopbackDetectEnable": true, + "stp": 0, + "priority": 32768, + "snmp": { + "location": "", + "contact": "" + }, + "ports": [ + { + "id": "000000000000000000000001", + "port": 1, + "name": "Port1", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "operation": "switching", + "portStatus": { + "port": 1, + "linkStatus": 1, + "linkSpeed": 2, + "duplex": 2, + "poe": true, + "poePower": 2.7, + "tx": 22048870335, + "rx": 6155774646, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ] + }, + { + "id": "000000000000000000000002", + "port": 2, + "name": "Port2", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "operation": "switching", + "portStatus": { + "port": 2, + "linkStatus": 1, + "linkSpeed": 3, + "duplex": 2, + "poe": false, + "poePower": 0.0, + "tx": 2111818481511, + "rx": 297809855535, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ] + }, + { + "id": "000000000000000000000003", + "port": 3, + "name": "Primary AP", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "operation": "switching", + "portStatus": { + "port": 3, + "linkStatus": 1, + "linkSpeed": 4, + "duplex": 2, + "poe": true, + "poePower": 9.8, + "tx": 2118915311852, + "rx": 1222744181939, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ] + }, + { + "id": "000000000000000000000004", + "port": 4, + "name": "Port4", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "operation": "switching", + "portStatus": { + "port": 4, + "linkStatus": 0, + "linkSpeed": 0, + "duplex": 0, + "poe": false, + "poePower": 0.0, + "tx": 0, + "rx": 0, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ] + }, + { + "id": "000000000000000000000005", + "port": 5, + "name": "Port5", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "operation": "switching", + "portStatus": { + "port": 5, + "linkStatus": 1, + "linkSpeed": 3, + "duplex": 2, + "poe": true, + "poePower": 7.2, + "tx": 357059477760, + "rx": 59530432926, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ] + }, + { + "id": "000000000000000000000006", + "port": 6, + "name": "Port6", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "operation": "switching", + "portStatus": { + "port": 6, + "linkStatus": 1, + "linkSpeed": 3, + "duplex": 2, + "poe": false, + "poePower": 0.0, + "tx": 20729276425, + "rx": 1260359882, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ] + }, + { + "id": "000000000000000000000007", + "port": 7, + "name": "Port7", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "operation": "switching", + "portStatus": { + "port": 7, + "linkStatus": 0, + "linkSpeed": 0, + "duplex": 0, + "poe": false, + "poePower": 0.0, + "tx": 6884938116575, + "rx": 3015211000000, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ] + }, + { + "id": "000000000000000000000008", + "port": 8, + "name": "Family Room Kiosk", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "operation": "switching", + "portStatus": { + "port": 8, + "linkStatus": 1, + "linkSpeed": 3, + "duplex": 2, + "poe": true, + "poePower": 1.9, + "tx": 17735212467, + "rx": 2751725454, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ] + }, + { + "id": "000000000000000000000009", + "port": 9, + "name": "Port9", + "disable": false, + "type": 3, + "maxSpeed": 5, + "profileId": "00000000", + "profileName": "All", + "operation": "switching", + "portStatus": { + "port": 9, + "linkStatus": 0, + "linkSpeed": 0, + "duplex": 0, + "poe": false, + "tx": 0, + "rx": 0, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 5, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ] + }, + { + "id": "00000000000000000000000a", + "port": 10, + "name": "Uplink", + "disable": false, + "type": 3, + "maxSpeed": 5, + "profileId": "00000000", + "profileName": "All", + "operation": "switching", + "portStatus": { + "port": 10, + "linkStatus": 1, + "linkSpeed": 5, + "duplex": 2, + "poe": false, + "tx": 4599788287992, + "rx": 11431810000000, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 5, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ] + } + ], + "lags": [], + "tagIds": [], + "ip": "192.168.0.12", + "lastSeen": 1687385981898, + "needUpgrade": false, + "uptime": "97day(s) 23h 57m 34s", + "uptimeLong": 8467054, + "cpuUtil": 18, + "memUtil": 72, + "poeRemain": 218.399994, + "poeRemainPercent": 91.0, + "fanStatus": 0, + "downlinkList": [ + { + "port": 3, + "model": "EAP660 HD", + "hwVersion": "1.0", + "modelVersion": "1.0", + "mac": "B4-B0-24-00-00-01", + "name": "Access Point 1", + "linkSpeed": 4, + "duplex": 2 + }, + { + "port": 5, + "model": "EAP653", + "hwVersion": "1.0", + "modelVersion": "1.0", + "mac": "34-60-F9-00-00-01E", + "name": "Access Point 2", + "linkSpeed": 3, + "duplex": 2 + } + ], + "download": 16037273330382, + "upload": 16133033034917, + "supportVlanIf": true, + "jumbo": 1518, + "lagHashAlg": 2, + "speeds": [2, 3, 4, 5] +} diff --git a/tests/components/tplink_omada/fixtures/switch-ports-TL-SG3210XHP-M2.json b/tests/components/tplink_omada/fixtures/switch-ports-TL-SG3210XHP-M2.json new file mode 100644 index 00000000000..b079b2d2fb7 --- /dev/null +++ b/tests/components/tplink_omada/fixtures/switch-ports-TL-SG3210XHP-M2.json @@ -0,0 +1,974 @@ +[ + { + "id": "000000000000000000000001", + "port": 1, + "switchId": "640934810000000000000000", + "switchMac": "54-AF-97-00-00-01", + "site": "000000000000000000000000", + "name": "Port1", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "profileOverrideEnable": false, + "operation": "switching", + "linkSpeed": 0, + "duplex": 0, + "dot1x": 2, + "poe": 1, + "bandWidthCtrlType": 0, + "bandCtrl": { + "egressEnable": false, + "egressLimit": 0, + "egressUnit": 1, + "ingressEnable": false, + "ingressLimit": 0, + "ingressUnit": 1 + }, + "stormCtrl": { + "unknownUnicastEnable": false, + "unknownUnicast": 0, + "multicastEnable": false, + "multicast": 0, + "broadcastEnable": false, + "broadcast": 0, + "action": 0, + "recoverTime": 3600 + }, + "lldpMedEnable": true, + "topoNotifyEnable": false, + "spanningTreeEnable": false, + "loopbackDetectEnable": true, + "loopbackDetectVlanBasedEnable": false, + "portIsolationEnable": false, + "portStatus": { + "port": 1, + "linkStatus": 1, + "linkSpeed": 2, + "duplex": 2, + "poe": true, + "poePower": 2.7, + "tx": 22265663705, + "rx": 6202420396, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ], + "eeeEnable": false, + "flowControlEnable": false, + "dhcpL2RelaySettings": { + "enable": false, + "format": 0 + } + }, + { + "id": "000000000000000000000002", + "port": 2, + "switchId": "640934810000000000000000", + "switchMac": "54-AF-97-00-00-01", + "site": "000000000000000000000000", + "name": "Port2", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "profileOverrideEnable": true, + "operation": "switching", + "linkSpeed": 0, + "duplex": 0, + "dot1x": 1, + "poe": 1, + "bandWidthCtrlType": 0, + "bandCtrl": { + "egressEnable": false, + "egressLimit": 0, + "egressUnit": 1, + "ingressEnable": false, + "ingressLimit": 0, + "ingressUnit": 1 + }, + "stormCtrl": { + "unknownUnicastEnable": false, + "unknownUnicast": 0, + "multicastEnable": false, + "multicast": 0, + "broadcastEnable": false, + "broadcast": 0, + "action": 0, + "recoverTime": 3600 + }, + "lldpMedEnable": true, + "topoNotifyEnable": false, + "spanningTreeEnable": false, + "loopbackDetectEnable": true, + "loopbackDetectVlanBasedEnable": false, + "portIsolationEnable": false, + "portStatus": { + "port": 2, + "linkStatus": 1, + "linkSpeed": 3, + "duplex": 2, + "poe": false, + "poePower": 0.0, + "tx": 2136778000000, + "rx": 298419647322, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ], + "eeeEnable": false, + "flowControlEnable": false, + "dhcpL2RelaySettings": { + "enable": false, + "format": 0 + } + }, + { + "id": "000000000000000000000003", + "port": 3, + "switchId": "640934810000000000000000", + "switchMac": "54-AF-97-00-00-01", + "site": "000000000000000000000000", + "name": "Port3", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "profileOverrideEnable": false, + "operation": "switching", + "linkSpeed": 0, + "duplex": 0, + "dot1x": 2, + "poe": 1, + "bandWidthCtrlType": 0, + "bandCtrl": { + "egressEnable": false, + "egressLimit": 0, + "egressUnit": 1, + "ingressEnable": false, + "ingressLimit": 0, + "ingressUnit": 1 + }, + "stormCtrl": { + "unknownUnicastEnable": false, + "unknownUnicast": 0, + "multicastEnable": false, + "multicast": 0, + "broadcastEnable": false, + "broadcast": 0, + "action": 0, + "recoverTime": 3600 + }, + "lldpMedEnable": true, + "topoNotifyEnable": false, + "spanningTreeEnable": false, + "loopbackDetectEnable": true, + "loopbackDetectVlanBasedEnable": false, + "portIsolationEnable": false, + "portStatus": { + "port": 3, + "linkStatus": 1, + "linkSpeed": 4, + "duplex": 2, + "poe": true, + "poePower": 10.0, + "tx": 2139129000000, + "rx": 1241262105432, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ], + "eeeEnable": false, + "flowControlEnable": false, + "dhcpL2RelaySettings": { + "enable": false, + "format": 0 + } + }, + { + "id": "000000000000000000000004", + "port": 4, + "switchId": "640934810000000000000000", + "switchMac": "54-AF-97-00-00-01", + "site": "000000000000000000000000", + "name": "Port4", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "profileOverrideEnable": false, + "operation": "switching", + "linkSpeed": 0, + "duplex": 0, + "dot1x": 2, + "poe": 1, + "bandWidthCtrlType": 0, + "bandCtrl": { + "egressEnable": false, + "egressLimit": 0, + "egressUnit": 1, + "ingressEnable": false, + "ingressLimit": 0, + "ingressUnit": 1 + }, + "stormCtrl": { + "unknownUnicastEnable": false, + "unknownUnicast": 0, + "multicastEnable": false, + "multicast": 0, + "broadcastEnable": false, + "broadcast": 0, + "action": 0, + "recoverTime": 3600 + }, + "lldpMedEnable": true, + "topoNotifyEnable": false, + "spanningTreeEnable": false, + "loopbackDetectEnable": false, + "loopbackDetectVlanBasedEnable": false, + "portIsolationEnable": false, + "portStatus": { + "port": 4, + "linkStatus": 0, + "linkSpeed": 0, + "duplex": 0, + "poe": false, + "poePower": 0.0, + "tx": 0, + "rx": 0, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ], + "eeeEnable": false, + "flowControlEnable": false, + "dhcpL2RelaySettings": { + "enable": false, + "format": 0 + } + }, + { + "id": "000000000000000000000005", + "port": 5, + "switchId": "640934810000000000000000", + "switchMac": "54-AF-97-00-00-01", + "site": "000000000000000000000000", + "name": "Port5", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "profileOverrideEnable": false, + "operation": "switching", + "linkSpeed": 0, + "duplex": 0, + "dot1x": 2, + "poe": 1, + "bandWidthCtrlType": 0, + "bandCtrl": { + "egressEnable": false, + "egressLimit": 0, + "egressUnit": 1, + "ingressEnable": false, + "ingressLimit": 0, + "ingressUnit": 1 + }, + "stormCtrl": { + "unknownUnicastEnable": false, + "unknownUnicast": 0, + "multicastEnable": false, + "multicast": 0, + "broadcastEnable": false, + "broadcast": 0, + "action": 0, + "recoverTime": 3600 + }, + "lldpMedEnable": true, + "topoNotifyEnable": false, + "spanningTreeEnable": false, + "loopbackDetectEnable": true, + "loopbackDetectVlanBasedEnable": false, + "portIsolationEnable": false, + "portStatus": { + "port": 5, + "linkStatus": 1, + "linkSpeed": 3, + "duplex": 2, + "poe": true, + "poePower": 7.7, + "tx": 358431854723, + "rx": 62202058965, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ], + "eeeEnable": false, + "flowControlEnable": false, + "dhcpL2RelaySettings": { + "enable": false, + "format": 0 + } + }, + { + "id": "000000000000000000000006", + "port": 6, + "switchId": "640934810000000000000000", + "switchMac": "54-AF-97-00-00-01", + "site": "000000000000000000000000", + "name": "Port6", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "profileOverrideEnable": false, + "operation": "switching", + "linkSpeed": 0, + "duplex": 0, + "dot1x": 2, + "poe": 1, + "bandWidthCtrlType": 0, + "bandCtrl": { + "egressEnable": false, + "egressLimit": 0, + "egressUnit": 1, + "ingressEnable": false, + "ingressLimit": 0, + "ingressUnit": 1 + }, + "stormCtrl": { + "unknownUnicastEnable": false, + "unknownUnicast": 0, + "multicastEnable": false, + "multicast": 0, + "broadcastEnable": false, + "broadcast": 0, + "action": 0, + "recoverTime": 3600 + }, + "lldpMedEnable": true, + "topoNotifyEnable": false, + "spanningTreeEnable": false, + "loopbackDetectEnable": false, + "loopbackDetectVlanBasedEnable": false, + "portIsolationEnable": false, + "portStatus": { + "port": 6, + "linkStatus": 1, + "linkSpeed": 3, + "duplex": 2, + "poe": false, + "poePower": 0.0, + "tx": 21045680895, + "rx": 1266702649, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ], + "eeeEnable": false, + "flowControlEnable": false, + "dhcpL2RelaySettings": { + "enable": false, + "format": 0 + } + }, + { + "id": "000000000000000000000007", + "port": 7, + "switchId": "640934810000000000000000", + "switchMac": "54-AF-97-00-00-01", + "site": "000000000000000000000000", + "name": "Port7", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "profileOverrideEnable": false, + "operation": "switching", + "linkSpeed": 0, + "duplex": 0, + "dot1x": 2, + "poe": 1, + "bandWidthCtrlType": 0, + "bandCtrl": { + "egressEnable": false, + "egressLimit": 0, + "egressUnit": 1, + "ingressEnable": false, + "ingressLimit": 0, + "ingressUnit": 1 + }, + "stormCtrl": { + "unknownUnicastEnable": false, + "unknownUnicast": 0, + "multicastEnable": false, + "multicast": 0, + "broadcastEnable": false, + "broadcast": 0, + "action": 0, + "recoverTime": 3600 + }, + "lldpMedEnable": true, + "topoNotifyEnable": false, + "spanningTreeEnable": false, + "loopbackDetectEnable": false, + "loopbackDetectVlanBasedEnable": false, + "portIsolationEnable": false, + "portStatus": { + "port": 7, + "linkStatus": 0, + "linkSpeed": 0, + "duplex": 0, + "poe": false, + "poePower": 0.0, + "tx": 6884938116575, + "rx": 3015211000000, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ], + "eeeEnable": false, + "flowControlEnable": false, + "dhcpL2RelaySettings": { + "enable": false, + "format": 0 + } + }, + { + "id": "000000000000000000000008", + "port": 8, + "switchId": "640934810000000000000000", + "switchMac": "54-AF-97-00-00-01", + "site": "000000000000000000000000", + "name": "Port8", + "disable": false, + "type": 1, + "maxSpeed": 4, + "profileId": "00000000", + "profileName": "All", + "profileOverrideEnable": false, + "operation": "switching", + "linkSpeed": 0, + "duplex": 0, + "dot1x": 2, + "poe": 1, + "bandWidthCtrlType": 0, + "bandCtrl": { + "egressEnable": false, + "egressLimit": 0, + "egressUnit": 1, + "ingressEnable": false, + "ingressLimit": 0, + "ingressUnit": 1 + }, + "stormCtrl": { + "unknownUnicastEnable": false, + "unknownUnicast": 0, + "multicastEnable": false, + "multicast": 0, + "broadcastEnable": false, + "broadcast": 0, + "action": 0, + "recoverTime": 3600 + }, + "lldpMedEnable": true, + "topoNotifyEnable": false, + "spanningTreeEnable": false, + "loopbackDetectEnable": false, + "loopbackDetectVlanBasedEnable": false, + "portIsolationEnable": false, + "portStatus": { + "port": 8, + "linkStatus": 1, + "linkSpeed": 3, + "duplex": 2, + "poe": true, + "poePower": 1.9, + "tx": 17983115259, + "rx": 2764463784, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 4, + "duplex": 0 + }, + { + "linkSpeed": 4, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 1 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 0, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 2 + }, + { + "linkSpeed": 2, + "duplex": 0 + } + ], + "eeeEnable": false, + "flowControlEnable": false, + "dhcpL2RelaySettings": { + "enable": false, + "format": 0 + } + }, + { + "id": "000000000000000000000009", + "port": 9, + "switchId": "640934810000000000000000", + "switchMac": "54-AF-97-00-00-01", + "site": "000000000000000000000000", + "name": "Port9", + "disable": false, + "type": 3, + "maxSpeed": 5, + "profileId": "00000000", + "profileName": "All", + "profileOverrideEnable": false, + "operation": "switching", + "linkSpeed": 5, + "duplex": 2, + "dot1x": 2, + "poe": 1, + "bandWidthCtrlType": 0, + "bandCtrl": { + "egressEnable": false, + "egressLimit": 0, + "egressUnit": 1, + "ingressEnable": false, + "ingressLimit": 0, + "ingressUnit": 1 + }, + "stormCtrl": { + "unknownUnicastEnable": false, + "unknownUnicast": 0, + "multicastEnable": false, + "multicast": 0, + "broadcastEnable": false, + "broadcast": 0, + "action": 0, + "recoverTime": 3600 + }, + "lldpMedEnable": true, + "topoNotifyEnable": false, + "spanningTreeEnable": false, + "loopbackDetectEnable": true, + "loopbackDetectVlanBasedEnable": false, + "portIsolationEnable": false, + "portStatus": { + "port": 9, + "linkStatus": 0, + "linkSpeed": 0, + "duplex": 0, + "poe": false, + "tx": 0, + "rx": 0, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 5, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "eeeEnable": false, + "flowControlEnable": false, + "dhcpL2RelaySettings": { + "enable": false, + "format": 0 + } + }, + { + "id": "00000000000000000000000a", + "port": 10, + "switchId": "640934810000000000000000", + "switchMac": "54-AF-97-00-00-01", + "site": "000000000000000000000000", + "name": "Port10", + "disable": false, + "type": 3, + "maxSpeed": 5, + "profileId": "00000000", + "profileName": "All", + "profileOverrideEnable": false, + "operation": "switching", + "linkSpeed": 5, + "duplex": 2, + "dot1x": 2, + "poe": 1, + "bandWidthCtrlType": 0, + "bandCtrl": { + "egressEnable": false, + "egressLimit": 0, + "egressUnit": 1, + "ingressEnable": false, + "ingressLimit": 0, + "ingressUnit": 1 + }, + "stormCtrl": { + "unknownUnicastEnable": false, + "unknownUnicast": 0, + "multicastEnable": false, + "multicast": 0, + "broadcastEnable": false, + "broadcast": 0, + "action": 0, + "recoverTime": 3600 + }, + "lldpMedEnable": true, + "topoNotifyEnable": false, + "spanningTreeEnable": false, + "loopbackDetectEnable": true, + "loopbackDetectVlanBasedEnable": false, + "portIsolationEnable": false, + "portStatus": { + "port": 10, + "linkStatus": 1, + "linkSpeed": 5, + "duplex": 2, + "poe": false, + "tx": 4621489812572, + "rx": 11477190000000, + "stpDiscarding": false + }, + "portCap": [ + { + "linkSpeed": 5, + "duplex": 2 + }, + { + "linkSpeed": 3, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "eeeEnable": false, + "flowControlEnable": false, + "dhcpL2RelaySettings": { + "enable": false, + "format": 0 + } + } +] diff --git a/tests/components/tplink_omada/snapshots/test_switch.ambr b/tests/components/tplink_omada/snapshots/test_switch.ambr new file mode 100644 index 00000000000..b48f6a5e749 --- /dev/null +++ b/tests/components/tplink_omada/snapshots/test_switch.ambr @@ -0,0 +1,345 @@ +# serializer version: 1 +# name: test_poe_switches + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test PoE Switch Port 1 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.test_poe_switch_port_1_poe', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_poe_switches.1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_poe_switch_port_1_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 1 PoE', + 'platform': 'tplink_omada', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '54-AF-97-00-00-01_000000000000000000000001_poe', + 'unit_of_measurement': None, + }) +# --- +# name: test_poe_switches.10 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test PoE Switch Port 6 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.test_poe_switch_port_6_poe', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_poe_switches.11 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_poe_switch_port_6_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 6 PoE', + 'platform': 'tplink_omada', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '54-AF-97-00-00-01_000000000000000000000006_poe', + 'unit_of_measurement': None, + }) +# --- +# name: test_poe_switches.12 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test PoE Switch Port 7 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.test_poe_switch_port_7_poe', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_poe_switches.13 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_poe_switch_port_7_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 7 PoE', + 'platform': 'tplink_omada', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '54-AF-97-00-00-01_000000000000000000000007_poe', + 'unit_of_measurement': None, + }) +# --- +# name: test_poe_switches.14 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test PoE Switch Port 8 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.test_poe_switch_port_8_poe', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_poe_switches.15 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_poe_switch_port_8_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 8 PoE', + 'platform': 'tplink_omada', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '54-AF-97-00-00-01_000000000000000000000008_poe', + 'unit_of_measurement': None, + }) +# --- +# name: test_poe_switches.2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test PoE Switch Port 2 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.test_poe_switch_port_2_poe', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_poe_switches.3 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_poe_switch_port_2_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 2 PoE', + 'platform': 'tplink_omada', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '54-AF-97-00-00-01_000000000000000000000002_poe', + 'unit_of_measurement': None, + }) +# --- +# name: test_poe_switches.4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test PoE Switch Port 3 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.test_poe_switch_port_3_poe', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_poe_switches.5 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_poe_switch_port_3_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 3 PoE', + 'platform': 'tplink_omada', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '54-AF-97-00-00-01_000000000000000000000003_poe', + 'unit_of_measurement': None, + }) +# --- +# name: test_poe_switches.6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test PoE Switch Port 4 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.test_poe_switch_port_4_poe', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_poe_switches.7 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_poe_switch_port_4_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 4 PoE', + 'platform': 'tplink_omada', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '54-AF-97-00-00-01_000000000000000000000004_poe', + 'unit_of_measurement': None, + }) +# --- +# name: test_poe_switches.8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test PoE Switch Port 5 PoE', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.test_poe_switch_port_5_poe', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_poe_switches.9 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_poe_switch_port_5_poe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:ethernet', + 'original_name': 'Port 5 PoE', + 'platform': 'tplink_omada', + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '54-AF-97-00-00-01_000000000000000000000005_poe', + 'unit_of_measurement': None, + }) +# --- diff --git a/tests/components/tplink_omada/test_switch.py b/tests/components/tplink_omada/test_switch.py new file mode 100644 index 00000000000..dd8b520e0a8 --- /dev/null +++ b/tests/components/tplink_omada/test_switch.py @@ -0,0 +1,122 @@ +"""Tests for TP-Link Omada switch entities.""" +from unittest.mock import MagicMock + +from syrupy.assertion import SnapshotAssertion +from tplink_omada_client.definitions import PoEMode +from tplink_omada_client.devices import OmadaSwitch, OmadaSwitchPortDetails +from tplink_omada_client.omadasiteclient import SwitchPortOverrides + +from homeassistant.components import switch +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_poe_switches( + hass: HomeAssistant, + mock_omada_site_client: MagicMock, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test PoE switch.""" + poe_switch_mac = "54-AF-97-00-00-01" + for i in range(1, 9): + await _test_poe_switch( + hass, + mock_omada_site_client, + f"switch.test_poe_switch_port_{i}_poe", + poe_switch_mac, + i, + snapshot, + ) + + +async def _test_poe_switch( + hass: HomeAssistant, + mock_omada_site_client: MagicMock, + entity_id: str, + network_switch_mac: str, + port_num: int, + snapshot: SnapshotAssertion, +) -> None: + entity_registry = er.async_get(hass) + + def assert_update_switch_port( + device: OmadaSwitch, + switch_port_details: OmadaSwitchPortDetails, + poe_enabled: bool, + overrides: SwitchPortOverrides = None, + ) -> None: + assert device + assert device.mac == network_switch_mac + assert switch_port_details + assert switch_port_details.port == port_num + assert overrides + assert overrides.enable_poe == poe_enabled + + entity = hass.states.get(entity_id) + assert entity == snapshot + entry = entity_registry.async_get(entity_id) + assert entry == snapshot + + mock_omada_site_client.update_switch_port.reset_mock() + mock_omada_site_client.update_switch_port.return_value = await _update_port_details( + mock_omada_site_client, port_num, False + ) + await call_service(hass, "turn_off", entity_id) + mock_omada_site_client.update_switch_port.assert_called_once() + ( + device, + switch_port_details, + ) = mock_omada_site_client.update_switch_port.call_args.args + assert_update_switch_port( + device, + switch_port_details, + False, + **mock_omada_site_client.update_switch_port.call_args.kwargs, + ) + entity = hass.states.get(entity_id) + assert entity.state == "off" + + mock_omada_site_client.update_switch_port.reset_mock() + mock_omada_site_client.update_switch_port.return_value = await _update_port_details( + mock_omada_site_client, port_num, True + ) + await call_service(hass, "turn_on", entity_id) + mock_omada_site_client.update_switch_port.assert_called_once() + device, switch_port = mock_omada_site_client.update_switch_port.call_args.args + assert_update_switch_port( + device, + switch_port, + True, + **mock_omada_site_client.update_switch_port.call_args.kwargs, + ) + entity = hass.states.get(entity_id) + assert entity.state == "on" + + +async def _update_port_details( + mock_omada_site_client: MagicMock, + port_num: int, + poe_enabled: bool, +) -> OmadaSwitchPortDetails: + switch_ports = await mock_omada_site_client.get_switch_ports() + port_details: OmadaSwitchPortDetails = None + for details in switch_ports: + if details.port == port_num: + port_details = details + break + + assert port_details is not None + raw_data = port_details.raw_data.copy() + raw_data["poe"] = PoEMode.ENABLED if poe_enabled else PoEMode.DISABLED + return OmadaSwitchPortDetails(raw_data) + + +def call_service(hass: HomeAssistant, service: str, entity_id: str): + """Call any service on entity.""" + return hass.services.async_call( + switch.DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) From 2c48f0e4167d37126bfbb9141752c6733b83d4ca Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Wed, 16 Aug 2023 21:56:52 +1000 Subject: [PATCH 0563/1151] Fix ness alarm armed_home state appearing as disarmed/armed_away (#94351) * Fix nessclient arm home appearing as arm away * patch arming mode enum and use dynamic access * Revert "patch arming mode enum and use dynamic access" This reverts commit b9cca8e92bcb382abe364381a8cb1674c32d1d2a. * Remove mock enums --- .../components/ness_alarm/__init__.py | 8 ++-- .../ness_alarm/alarm_control_panel.py | 22 ++++++++-- .../components/ness_alarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ness_alarm/test_init.py | 44 +++++++------------ 6 files changed, 43 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index c1d97f781af..b5d30219550 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -3,7 +3,7 @@ from collections import namedtuple import datetime import logging -from nessclient import ArmingState, Client +from nessclient import ArmingMode, ArmingState, Client import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -136,9 +136,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, SIGNAL_ZONE_CHANGED, ZoneChangedData(zone_id=zone_id, state=state) ) - def on_state_change(arming_state: ArmingState): + def on_state_change(arming_state: ArmingState, arming_mode: ArmingMode | None): """Receives and propagates arming state updates.""" - async_dispatcher_send(hass, SIGNAL_ARMING_STATE_CHANGED, arming_state) + async_dispatcher_send( + hass, SIGNAL_ARMING_STATE_CHANGED, arming_state, arming_mode + ) client.on_zone_change(on_zone_change) client.on_state_change(on_state_change) diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index 2f54b3abde6..92feaba13aa 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -3,12 +3,15 @@ from __future__ import annotations import logging -from nessclient import ArmingState, Client +from nessclient import ArmingMode, ArmingState, Client import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -23,6 +26,15 @@ from . import DATA_NESS, SIGNAL_ARMING_STATE_CHANGED _LOGGER = logging.getLogger(__name__) +ARMING_MODE_TO_STATE = { + ArmingMode.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + ArmingMode.ARMED_HOME: STATE_ALARM_ARMED_HOME, + ArmingMode.ARMED_DAY: STATE_ALARM_ARMED_AWAY, # no applicable state, fallback to away + ArmingMode.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, + ArmingMode.ARMED_VACATION: STATE_ALARM_ARMED_VACATION, + ArmingMode.ARMED_HIGHEST: STATE_ALARM_ARMED_AWAY, # no applicable state, fallback to away +} + async def async_setup_platform( hass: HomeAssistant, @@ -79,7 +91,9 @@ class NessAlarmPanel(alarm.AlarmControlPanelEntity): await self._client.panic(code) @callback - def _handle_arming_state_change(self, arming_state: ArmingState) -> None: + def _handle_arming_state_change( + self, arming_state: ArmingState, arming_mode: ArmingMode | None + ) -> None: """Handle arming state update.""" if arming_state == ArmingState.UNKNOWN: @@ -91,7 +105,9 @@ class NessAlarmPanel(alarm.AlarmControlPanelEntity): elif arming_state == ArmingState.EXIT_DELAY: self._attr_state = STATE_ALARM_ARMING elif arming_state == ArmingState.ARMED: - self._attr_state = STATE_ALARM_ARMED_AWAY + self._attr_state = ARMING_MODE_TO_STATE.get( + arming_mode, STATE_ALARM_ARMED_AWAY + ) elif arming_state == ArmingState.ENTRY_DELAY: self._attr_state = STATE_ALARM_PENDING elif arming_state == ArmingState.TRIGGERED: diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index d92a3d02c7a..e4c5b5fb344 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/ness_alarm", "iot_class": "local_push", "loggers": ["nessclient"], - "requirements": ["nessclient==0.10.0"] + "requirements": ["nessclient==1.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index db56052eb82..ec345859233 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1240,7 +1240,7 @@ nad-receiver==0.3.0 ndms2-client==0.1.2 # homeassistant.components.ness_alarm -nessclient==0.10.0 +nessclient==1.0.0 # homeassistant.components.netdata netdata==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d69c88eb7a3..d65f0676a65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -951,7 +951,7 @@ mutesync==0.0.1 ndms2-client==0.1.2 # homeassistant.components.ness_alarm -nessclient==0.10.0 +nessclient==1.0.0 # homeassistant.components.nmap_tracker netmap==0.7.0.2 diff --git a/tests/components/ness_alarm/test_init.py b/tests/components/ness_alarm/test_init.py index 908e23ec795..5bf48e0667e 100644 --- a/tests/components/ness_alarm/test_init.py +++ b/tests/components/ness_alarm/test_init.py @@ -1,7 +1,7 @@ """Tests for the ness_alarm component.""" -from enum import Enum from unittest.mock import MagicMock, patch +from nessclient import ArmingMode, ArmingState import pytest from homeassistant.components import alarm_control_panel @@ -24,6 +24,8 @@ from homeassistant.const import ( SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -84,7 +86,7 @@ async def test_dispatch_state_change(hass: HomeAssistant, mock_nessclient) -> No await hass.async_block_till_done() on_state_change = mock_nessclient.on_state_change.call_args[0][0] - on_state_change(MockArmingState.ARMING) + on_state_change(ArmingState.ARMING, None) await hass.async_block_till_done() assert hass.states.is_state("alarm_control_panel.alarm_panel", STATE_ALARM_ARMING) @@ -174,13 +176,16 @@ async def test_dispatch_zone_change(hass: HomeAssistant, mock_nessclient) -> Non async def test_arming_state_change(hass: HomeAssistant, mock_nessclient) -> None: """Test arming state change handing.""" states = [ - (MockArmingState.UNKNOWN, STATE_UNKNOWN), - (MockArmingState.DISARMED, STATE_ALARM_DISARMED), - (MockArmingState.ARMING, STATE_ALARM_ARMING), - (MockArmingState.EXIT_DELAY, STATE_ALARM_ARMING), - (MockArmingState.ARMED, STATE_ALARM_ARMED_AWAY), - (MockArmingState.ENTRY_DELAY, STATE_ALARM_PENDING), - (MockArmingState.TRIGGERED, STATE_ALARM_TRIGGERED), + (ArmingState.UNKNOWN, None, STATE_UNKNOWN), + (ArmingState.DISARMED, None, STATE_ALARM_DISARMED), + (ArmingState.ARMING, None, STATE_ALARM_ARMING), + (ArmingState.EXIT_DELAY, None, STATE_ALARM_ARMING), + (ArmingState.ARMED, None, STATE_ALARM_ARMED_AWAY), + (ArmingState.ARMED, ArmingMode.ARMED_AWAY, STATE_ALARM_ARMED_AWAY), + (ArmingState.ARMED, ArmingMode.ARMED_HOME, STATE_ALARM_ARMED_HOME), + (ArmingState.ARMED, ArmingMode.ARMED_NIGHT, STATE_ALARM_ARMED_NIGHT), + (ArmingState.ENTRY_DELAY, None, STATE_ALARM_PENDING), + (ArmingState.TRIGGERED, None, STATE_ALARM_TRIGGERED), ] await async_setup_component(hass, DOMAIN, VALID_CONFIG) @@ -188,24 +193,12 @@ async def test_arming_state_change(hass: HomeAssistant, mock_nessclient) -> None assert hass.states.is_state("alarm_control_panel.alarm_panel", STATE_UNKNOWN) on_state_change = mock_nessclient.on_state_change.call_args[0][0] - for arming_state, expected_state in states: - on_state_change(arming_state) + for arming_state, arming_mode, expected_state in states: + on_state_change(arming_state, arming_mode) await hass.async_block_till_done() assert hass.states.is_state("alarm_control_panel.alarm_panel", expected_state) -class MockArmingState(Enum): - """Mock nessclient.ArmingState enum.""" - - UNKNOWN = "UNKNOWN" - DISARMED = "DISARMED" - ARMING = "ARMING" - EXIT_DELAY = "EXIT_DELAY" - ARMED = "ARMED" - ENTRY_DELAY = "ENTRY_DELAY" - TRIGGERED = "TRIGGERED" - - class MockClient: """Mock nessclient.Client stub.""" @@ -253,10 +246,5 @@ def mock_nessclient(): with patch( "homeassistant.components.ness_alarm.Client", new=_mock_factory, create=True - ), patch( - "homeassistant.components.ness_alarm.ArmingState", new=MockArmingState - ), patch( - "homeassistant.components.ness_alarm.alarm_control_panel.ArmingState", - new=MockArmingState, ): yield _mock_instance From a2e619155a25864c0860a03a4c58881db01e7f26 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 15:01:54 +0200 Subject: [PATCH 0564/1151] Map ipma weather condition codes once (#98512) --- homeassistant/components/ipma/const.py | 9 ++++++++- homeassistant/components/ipma/weather.py | 7 ++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py index c7482770f48..26fdee779b6 100644 --- a/homeassistant/components/ipma/const.py +++ b/homeassistant/components/ipma/const.py @@ -1,4 +1,6 @@ """Constants for IPMA component.""" +from __future__ import annotations + from datetime import timedelta from homeassistant.components.weather import ( @@ -31,7 +33,7 @@ ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.ipma_{HOME_LOCATION_NAME}" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[int]] = { ATTR_CONDITION_CLOUDY: [4, 5, 24, 25, 27], ATTR_CONDITION_FOG: [16, 17, 26], ATTR_CONDITION_HAIL: [21, 22], @@ -48,5 +50,10 @@ CONDITION_CLASSES = { ATTR_CONDITION_EXCEPTIONAL: [], ATTR_CONDITION_CLEAR_NIGHT: [-1], } +CONDITION_MAP = { + cond_code: cond_ha + for cond_ha, cond_codes in CONDITION_CLASSES.items() + for cond_code in cond_codes +} ATTRIBUTION = "Instituto Português do Mar e Atmosfera" diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index d4d11aa26e8..1f948bcc4e1 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -37,7 +37,7 @@ from homeassistant.util import Throttle from .const import ( ATTRIBUTION, - CONDITION_CLASSES, + CONDITION_MAP, DATA_API, DATA_LOCATION, DOMAIN, @@ -135,10 +135,7 @@ class IPMAWeather(WeatherEntity, IPMADevice): if identifier == 1 and not is_up(self.hass, forecast_dt): identifier = -identifier - return next( - (k for k, v in CONDITION_CLASSES.items() if identifier in v), - None, - ) + return CONDITION_MAP.get(identifier) @property def condition(self): From 4180e2e4771b14f69d6d6abc10a9de695fe2c599 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 15:22:48 +0200 Subject: [PATCH 0565/1151] Make EnOceanSensor a RestoreSensor (#98527) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/enocean/sensor.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 5d1c0027791..db386a2d9fc 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -9,8 +9,8 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + RestoreSensor, SensorDeviceClass, - SensorEntity, SensorEntityDescription, SensorStateClass, ) @@ -27,7 +27,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .device import EnOceanEntity @@ -151,9 +150,8 @@ def setup_platform( add_entities(entities) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class EnOceanSensor(EnOceanEntity, RestoreEntity, SensorEntity): - """Representation of an EnOcean sensor device such as a power meter.""" +class EnOceanSensor(EnOceanEntity, RestoreSensor): + """Representation of an EnOcean sensor device such as a power meter.""" def __init__( self, @@ -174,14 +172,13 @@ class EnOceanSensor(EnOceanEntity, RestoreEntity, SensorEntity): if self._attr_native_value is not None: return - if (state := await self.async_get_last_state()) is not None: - self._attr_native_value = state.state + if (sensor_data := await self.async_get_last_sensor_data()) is not None: + self._attr_native_value = sensor_data.native_value def value_changed(self, packet): """Update the internal state of the sensor.""" -# pylint: disable-next=hass-invalid-inheritance # needs fixing class EnOceanPowerSensor(EnOceanSensor): """Representation of an EnOcean power sensor. @@ -202,7 +199,6 @@ class EnOceanPowerSensor(EnOceanSensor): self.schedule_update_ha_state() -# pylint: disable-next=hass-invalid-inheritance # needs fixing class EnOceanTemperatureSensor(EnOceanSensor): """Representation of an EnOcean temperature sensor device. @@ -252,7 +248,6 @@ class EnOceanTemperatureSensor(EnOceanSensor): self.schedule_update_ha_state() -# pylint: disable-next=hass-invalid-inheritance # needs fixing class EnOceanHumiditySensor(EnOceanSensor): """Representation of an EnOcean humidity sensor device. @@ -271,7 +266,6 @@ class EnOceanHumiditySensor(EnOceanSensor): self.schedule_update_ha_state() -# pylint: disable-next=hass-invalid-inheritance # needs fixing class EnOceanWindowHandle(EnOceanSensor): """Representation of an EnOcean window handle device. From 5bf80a0f6d0a2630851aaf29bb01ad9a5399e1e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Aug 2023 11:05:22 -0500 Subject: [PATCH 0566/1151] Make ESPHome deep sleep tests more robust (#98535) --- tests/components/esphome/test_entity.py | 50 ++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index ac121a93eff..fdc57b2dc24 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -8,10 +8,12 @@ from aioesphomeapi import ( BinarySensorState, EntityInfo, EntityState, + SensorInfo, + SensorState, UserService, ) -from homeassistant.const import ATTR_RESTORED, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ATTR_RESTORED, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from .conftest import MockESPHomeDevice @@ -149,10 +151,17 @@ async def test_deep_sleep_device( name="my binary_sensor", unique_id="my_binary_sensor", ), + SensorInfo( + object_id="my_sensor", + key=3, + name="my sensor", + unique_id="my_sensor", + ), ] states = [ BinarySensorState(key=1, state=True, missing_state=False), BinarySensorState(key=2, state=True, missing_state=False), + SensorState(key=3, state=123.0, missing_state=False), ] user_service = [] mock_device = await mock_esphome_device( @@ -165,12 +174,18 @@ async def test_deep_sleep_device( state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON + state = hass.states.get("sensor.test_my_sensor") + assert state is not None + assert state.state == "123" await mock_device.mock_disconnect(False) await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_UNAVAILABLE + state = hass.states.get("sensor.test_my_sensor") + assert state is not None + assert state.state == STATE_UNAVAILABLE await mock_device.mock_connect() await hass.async_block_till_done() @@ -178,12 +193,43 @@ async def test_deep_sleep_device( state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None assert state.state == STATE_ON + state = hass.states.get("sensor.test_my_sensor") + assert state is not None + assert state.state == "123" + + await mock_device.mock_disconnect(True) + await hass.async_block_till_done() + await mock_device.mock_connect() + await hass.async_block_till_done() + mock_device.set_state(BinarySensorState(key=1, state=False, missing_state=False)) + mock_device.set_state(SensorState(key=3, state=56, missing_state=False)) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test_mybinary_sensor") + assert state is not None + assert state.state == STATE_OFF + state = hass.states.get("sensor.test_my_sensor") + assert state is not None + assert state.state == "56" await mock_device.mock_disconnect(True) await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_mybinary_sensor") assert state is not None - assert state.state == STATE_ON + assert state.state == STATE_OFF + state = hass.states.get("sensor.test_my_sensor") + assert state is not None + assert state.state == "56" + + await mock_device.mock_connect() + await hass.async_block_till_done() + await mock_device.mock_disconnect(False) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test_mybinary_sensor") + assert state is not None + assert state.state == STATE_UNAVAILABLE + state = hass.states.get("sensor.test_my_sensor") + assert state is not None + assert state.state == STATE_UNAVAILABLE async def test_esphome_device_without_friendly_name( From 3e1d2a10009a77eefc4bd50728aaed63adff346c Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 16 Aug 2023 12:59:34 -0400 Subject: [PATCH 0567/1151] Handle missing keys in Honeywell (#98392) --- homeassistant/components/honeywell/climate.py | 6 +++--- homeassistant/components/honeywell/sensor.py | 3 ++- tests/components/honeywell/test_climate.py | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 19eb5c649d7..6bfefcf3a8c 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -146,13 +146,13 @@ class HoneywellUSThermostat(ClimateEntity): | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) - if device._data["canControlHumidification"]: + if device._data.get("canControlHumidification"): self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY - if device.raw_ui_data["SwitchEmergencyHeatAllowed"]: + if device.raw_ui_data.get("SwitchEmergencyHeatAllowed"): self._attr_supported_features |= ClimateEntityFeature.AUX_HEAT - if not device._data["hasFan"]: + if not device._data.get("hasFan"): return # not all honeywell fans support all modes diff --git a/homeassistant/components/honeywell/sensor.py b/homeassistant/components/honeywell/sensor.py index 8c25216b2ff..c1f70bbdd1f 100644 --- a/homeassistant/components/honeywell/sensor.py +++ b/homeassistant/components/honeywell/sensor.py @@ -20,6 +20,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from . import HoneywellData from .const import DOMAIN, HUMIDITY_STATUS_KEY, TEMPERATURE_STATUS_KEY @@ -71,7 +72,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Honeywell thermostat.""" - data = hass.data[DOMAIN][config_entry.entry_id] + data: HoneywellData = hass.data[DOMAIN][config_entry.entry_id] sensors = [] for device in data.devices.values(): diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index afb49cbffca..4d6989d79e8 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -48,13 +48,13 @@ FAN_ACTION = "fan_action" PRESET_HOLD = "Hold" -async def test_no_thermostats( +async def test_no_thermostat_options( hass: HomeAssistant, device: MagicMock, config_entry: MagicMock ) -> None: - """Test the setup of the climate entities when there are no appliances available.""" + """Test the setup of the climate entities when there are no additional options available.""" device._data = {} await init_integration(hass, config_entry) - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all()) == 1 async def test_static_attributes( From b9203cbeaffa32351519507e9e93271b95555667 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 16 Aug 2023 19:18:46 +0200 Subject: [PATCH 0568/1151] Add base entity for Dexcom (#98158) --- homeassistant/components/dexcom/sensor.py | 35 +++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dexcom/sensor.py b/homeassistant/components/dexcom/sensor.py index b9958dc7309..cbe24088378 100644 --- a/homeassistant/components/dexcom/sensor.py +++ b/homeassistant/components/dexcom/sensor.py @@ -6,7 +6,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import COORDINATOR, DOMAIN, GLUCOSE_TREND_ICON, GLUCOSE_VALUE_ICON, MG_DL @@ -29,18 +32,33 @@ async def async_setup_entry( ) -class DexcomGlucoseValueSensor(CoordinatorEntity, SensorEntity): +class DexcomSensorEntity(CoordinatorEntity, SensorEntity): + """Base Dexcom sensor entity.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, username: str, key: str + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{username}-{key}" + + +class DexcomGlucoseValueSensor(DexcomSensorEntity): """Representation of a Dexcom glucose value sensor.""" _attr_icon = GLUCOSE_VALUE_ICON - def __init__(self, coordinator, username, unit_of_measurement): + def __init__( + self, + coordinator: DataUpdateCoordinator, + username: str, + unit_of_measurement: str, + ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, username, "value") self._attr_native_unit_of_measurement = unit_of_measurement self._key = "mg_dl" if unit_of_measurement == MG_DL else "mmol_l" self._attr_name = f"{DOMAIN}_{username}_glucose_value" - self._attr_unique_id = f"{username}-value" @property def native_value(self): @@ -50,14 +68,13 @@ class DexcomGlucoseValueSensor(CoordinatorEntity, SensorEntity): return None -class DexcomGlucoseTrendSensor(CoordinatorEntity, SensorEntity): +class DexcomGlucoseTrendSensor(DexcomSensorEntity): """Representation of a Dexcom glucose trend sensor.""" - def __init__(self, coordinator, username): + def __init__(self, coordinator: DataUpdateCoordinator, username: str) -> None: """Initialize the sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, username, "trend") self._attr_name = f"{DOMAIN}_{username}_glucose_trend" - self._attr_unique_id = f"{username}-trend" @property def icon(self): From 31f5932fe4467c8d0db4b8d5c4df457f9fbe9a89 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 20:14:49 +0200 Subject: [PATCH 0569/1151] Log events with no listeners (#98540) * Log events with no listeners * Unconditionally create the Event object * Reformat code --- homeassistant/core.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 140cf203e70..49c288188f3 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1025,6 +1025,11 @@ class EventBus: listeners = self._listeners.get(event_type, []) match_all_listeners = self._match_all_listeners + event = Event(event_type, event_data, origin, time_fired, context) + + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("Bus:Handling %s", event) + if not listeners and not match_all_listeners: return @@ -1032,11 +1037,6 @@ class EventBus: if event_type != EVENT_HOMEASSISTANT_CLOSE: listeners = match_all_listeners + listeners - event = Event(event_type, event_data, origin, time_fired, context) - - if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("Bus:Handling %s", event) - for job, event_filter, run_immediately in listeners: if event_filter is not None: try: From 4eb0f1cf373dc2a76aa96fbeb363402f6a309f5d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 20:15:47 +0200 Subject: [PATCH 0570/1151] Make eufylife_ble sensors inherit RestoreSensor (#98528) --- .../components/eufylife_ble/sensor.py | 57 ++++++------------- 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/eufylife_ble/sensor.py b/homeassistant/components/eufylife_ble/sensor.py index 7bc732b911e..3278f1c1387 100644 --- a/homeassistant/components/eufylife_ble/sensor.py +++ b/homeassistant/components/eufylife_ble/sensor.py @@ -7,19 +7,16 @@ from eufylife_ble_client import MODEL_TO_NAME from homeassistant import config_entries from homeassistant.components.bluetooth import async_address_present -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - STATE_UNAVAILABLE, - STATE_UNKNOWN, - UnitOfMass, +from homeassistant.components.sensor import ( + RestoreSensor, + SensorDeviceClass, + SensorEntity, ) +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfMass from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr 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 from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from .const import DOMAIN @@ -111,16 +108,13 @@ class EufyLifeRealTimeWeightSensorEntity(EufyLifeSensorEntity): return UnitOfMass.KILOGRAMS -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class EufyLifeWeightSensorEntity(RestoreEntity, EufyLifeSensorEntity): +class EufyLifeWeightSensorEntity(RestoreSensor, EufyLifeSensorEntity): """Representation of an EufyLife weight sensor.""" _attr_translation_key = "weight" _attr_native_unit_of_measurement = UnitOfMass.KILOGRAMS _attr_device_class = SensorDeviceClass.WEIGHT - _weight_kg: float | None = None - def __init__(self, data: EufyLifeData) -> None: """Initialize the weight sensor entity.""" super().__init__(data) @@ -131,11 +125,6 @@ class EufyLifeWeightSensorEntity(RestoreEntity, EufyLifeSensorEntity): """Determine if the entity is available.""" return True - @property - def native_value(self) -> float | None: - """Return the native value.""" - return self._weight_kg - @property def suggested_unit_of_measurement(self) -> str | None: """Set the suggested unit based on the unit system.""" @@ -149,7 +138,7 @@ class EufyLifeWeightSensorEntity(RestoreEntity, EufyLifeSensorEntity): """Handle state update.""" state = self._data.client.state if state is not None and state.final_weight_kg is not None: - self._weight_kg = state.final_weight_kg + self._attr_native_value = state.final_weight_kg super()._handle_state_update(args) @@ -158,30 +147,21 @@ class EufyLifeWeightSensorEntity(RestoreEntity, EufyLifeSensorEntity): await super().async_added_to_hass() last_state = await self.async_get_last_state() - if not last_state or last_state.state in IGNORED_STATES: + last_sensor_data = await self.async_get_last_sensor_data() + + if not last_state or not last_sensor_data or last_state.state in IGNORED_STATES: return - last_weight = float(last_state.state) - last_weight_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - - # Since the RestoreEntity stores the state using the displayed unit, - # not the native unit, we need to convert the state back to the native - # unit. - self._weight_kg = MassConverter.convert( - last_weight, last_weight_unit, self.native_unit_of_measurement - ) + self._attr_native_value = last_sensor_data.native_value -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class EufyLifeHeartRateSensorEntity(RestoreEntity, EufyLifeSensorEntity): +class EufyLifeHeartRateSensorEntity(RestoreSensor, EufyLifeSensorEntity): """Representation of an EufyLife heart rate sensor.""" _attr_translation_key = "heart_rate" _attr_icon = "mdi:heart-pulse" _attr_native_unit_of_measurement = "bpm" - _heart_rate: int | None = None - def __init__(self, data: EufyLifeData) -> None: """Initialize the heart rate sensor entity.""" super().__init__(data) @@ -192,17 +172,12 @@ class EufyLifeHeartRateSensorEntity(RestoreEntity, EufyLifeSensorEntity): """Determine if the entity is available.""" return True - @property - def native_value(self) -> float | None: - """Return the native value.""" - return self._heart_rate - @callback def _handle_state_update(self, *args: Any) -> None: """Handle state update.""" state = self._data.client.state if state is not None and state.heart_rate is not None: - self._heart_rate = state.heart_rate + self._attr_native_value = state.heart_rate super()._handle_state_update(args) @@ -211,7 +186,9 @@ class EufyLifeHeartRateSensorEntity(RestoreEntity, EufyLifeSensorEntity): await super().async_added_to_hass() last_state = await self.async_get_last_state() - if not last_state or last_state.state in IGNORED_STATES: + last_sensor_data = await self.async_get_last_sensor_data() + + if not last_state or not last_sensor_data or last_state.state in IGNORED_STATES: return - self._heart_rate = int(last_state.state) + self._attr_native_value = last_sensor_data.native_value From 8ed7d2dd3e27080b6be157b8603382bd338cad99 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 20:20:14 +0200 Subject: [PATCH 0571/1151] Don't create certain start.ca sensors for unlimited plans (#98525) Don't create certain startca sensors for unlimited setups --- homeassistant/components/startca/sensor.py | 20 +++++++++++--------- tests/components/startca/test_sensor.py | 15 ++++----------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 50224944849..ab53b039756 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -157,6 +157,13 @@ async def async_setup_platform( name = config[CONF_NAME] monitored_variables = config[CONF_MONITORED_VARIABLES] + if bandwidthcap <= 0: + monitored_variables = list( + filter( + lambda itm: itm not in {"limit", "usage", "used_remaining"}, + monitored_variables, + ) + ) entities = [ StartcaSensor(ts_data, name, description) for description in SENSOR_TYPES @@ -193,11 +200,9 @@ class StartcaData: self.api_key = api_key self.bandwidth_cap = bandwidth_cap # Set unlimited users to infinite, otherwise the cap. - self.data = ( - {"limit": self.bandwidth_cap} - if self.bandwidth_cap > 0 - else {"limit": float("inf")} - ) + self.data = {} + if self.bandwidth_cap > 0: + self.data["limit"] = self.bandwidth_cap @staticmethod def bytes_to_gb(value): @@ -232,11 +237,9 @@ class StartcaData: total_dl = self.bytes_to_gb(xml_data["usage"]["total"]["download"]) total_ul = self.bytes_to_gb(xml_data["usage"]["total"]["upload"]) - limit = self.data["limit"] if self.bandwidth_cap > 0: self.data["usage"] = 100 * used_dl / self.bandwidth_cap - else: - self.data["usage"] = 0 + self.data["used_remaining"] = self.data["limit"] - used_dl self.data["usage_gb"] = used_dl self.data["used_download"] = used_dl self.data["used_upload"] = used_ul @@ -246,6 +249,5 @@ class StartcaData: self.data["grace_total"] = grace_dl + grace_ul self.data["total_download"] = total_dl self.data["total_upload"] = total_ul - self.data["used_remaining"] = limit - used_dl return True diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index 3907427bbd3..7b691410907 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -157,18 +157,15 @@ async def test_unlimited_setup( await async_setup_component(hass, "sensor", {"sensor": config}) await hass.async_block_till_done() - state = hass.states.get("sensor.start_ca_usage_ratio") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.state == "0" + # These sensors should not be created for unlimited setups + assert hass.states.get("sensor.start_ca_usage_ratio") is None + assert hass.states.get("sensor.start_ca_data_limit") is None + assert hass.states.get("sensor.start_ca_remaining") is None state = hass.states.get("sensor.start_ca_usage") assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "0.0" - state = hass.states.get("sensor.start_ca_data_limit") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES - assert state.state == "inf" - state = hass.states.get("sensor.start_ca_used_download") assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "0.0" @@ -201,10 +198,6 @@ async def test_unlimited_setup( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "6.48" - state = hass.states.get("sensor.start_ca_remaining") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES - assert state.state == "inf" - async def test_bad_return_code( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker From b1053e8077291527402191d2ca5418631b725c50 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 20:20:47 +0200 Subject: [PATCH 0572/1151] Map accuweather weather condition codes once (#98509) Map accuweather condition codes once --- homeassistant/components/accuweather/const.py | 5 +++++ homeassistant/components/accuweather/weather.py | 15 +++------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 87bc8eaef89..2e18977d112 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -50,3 +50,8 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_SUNNY: [1, 2, 5], ATTR_CONDITION_WINDY: [32], } +CONDITION_MAP = { + cond_code: cond_ha + for cond_ha, cond_codes in CONDITION_CLASSES.items() + for cond_code in cond_codes +} diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index c2889bae102..518714b3874 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -40,7 +40,7 @@ from .const import ( ATTR_SPEED, ATTR_VALUE, ATTRIBUTION, - CONDITION_CLASSES, + CONDITION_MAP, DOMAIN, ) @@ -80,14 +80,7 @@ class AccuWeatherEntity( @property def condition(self) -> str | None: """Return the current condition.""" - try: - return [ - k - for k, v in CONDITION_CLASSES.items() - if self.coordinator.data["WeatherIcon"] in v - ][0] - except IndexError: - return None + return CONDITION_MAP.get(self.coordinator.data["WeatherIcon"]) @property def cloud_coverage(self) -> float: @@ -177,9 +170,7 @@ class AccuWeatherEntity( ], ATTR_FORECAST_UV_INDEX: item["UVIndex"][ATTR_VALUE], ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"], - ATTR_FORECAST_CONDITION: [ - k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v - ][0], + ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]), } for item in self.coordinator.data[ATTR_FORECAST] ] From 827e06a5c88deefe706d066ddd2a4a41299a5e1e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 20:21:07 +0200 Subject: [PATCH 0573/1151] Improve typing of nws (#98485) * Improve typing of nws * Address review comments --- homeassistant/components/nws/__init__.py | 33 +++++++++++++----------- homeassistant/components/nws/const.py | 5 ---- homeassistant/components/nws/sensor.py | 23 +++++------------ homeassistant/components/nws/weather.py | 22 +++++++--------- 4 files changed, 34 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 54c239664dc..f0f2a12cfec 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable +from dataclasses import dataclass import datetime import logging from typing import TYPE_CHECKING @@ -18,15 +19,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow -from .const import ( - CONF_STATION, - COORDINATOR_FORECAST, - COORDINATOR_FORECAST_HOURLY, - COORDINATOR_OBSERVATION, - DOMAIN, - NWS_DATA, - UPDATE_TIME_PERIOD, -) +from .const import CONF_STATION, DOMAIN, UPDATE_TIME_PERIOD _LOGGER = logging.getLogger(__name__) @@ -42,6 +35,16 @@ def base_unique_id(latitude: float, longitude: float) -> str: return f"{latitude}_{longitude}" +@dataclass +class NWSData: + """Data for the National Weather Service integration.""" + + api: SimpleNWS + coordinator_observation: NwsDataUpdateCoordinator + coordinator_forecast: NwsDataUpdateCoordinator + coordinator_forecast_hourly: NwsDataUpdateCoordinator + + class NwsDataUpdateCoordinator(DataUpdateCoordinator[None]): """NWS data update coordinator. @@ -150,12 +153,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), ) nws_hass_data = hass.data.setdefault(DOMAIN, {}) - nws_hass_data[entry.entry_id] = { - NWS_DATA: nws_data, - COORDINATOR_OBSERVATION: coordinator_observation, - COORDINATOR_FORECAST: coordinator_forecast, - COORDINATOR_FORECAST_HOURLY: coordinator_forecast_hourly, - } + nws_hass_data[entry.entry_id] = NWSData( + nws_data, + coordinator_observation, + coordinator_forecast, + coordinator_forecast_hourly, + ) # Fetch initial data so we have data when entities subscribe await coordinator_observation.async_refresh() diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index e5718d5132f..5db541106b9 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -74,11 +74,6 @@ CONDITION_CLASSES: dict[str, list[str]] = { DAYNIGHT = "daynight" HOURLY = "hourly" -NWS_DATA = "nws data" -COORDINATOR_OBSERVATION = "coordinator_observation" -COORDINATOR_FORECAST = "coordinator_forecast" -COORDINATOR_FORECAST_HOURLY = "coordinator_forecast_hourly" - OBSERVATION_VALID_TIME = timedelta(minutes=20) FORECAST_VALID_TIME = timedelta(minutes=45) # A lot of stations update once hourly plus some wiggle room diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 71eeda0d8cf..7c49ca278a7 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -5,8 +5,6 @@ from dataclasses import dataclass from types import MappingProxyType from typing import Any -from pynws import SimpleNWS - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -36,15 +34,8 @@ from homeassistant.util.unit_conversion import ( ) from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from . import NwsDataUpdateCoordinator, base_unique_id, device_info -from .const import ( - ATTRIBUTION, - CONF_STATION, - COORDINATOR_OBSERVATION, - DOMAIN, - NWS_DATA, - OBSERVATION_VALID_TIME, -) +from . import NWSData, NwsDataUpdateCoordinator, base_unique_id, device_info +from .const import ATTRIBUTION, CONF_STATION, DOMAIN, OBSERVATION_VALID_TIME PARALLEL_UPDATES = 0 @@ -152,14 +143,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the NWS weather platform.""" - hass_data = hass.data[DOMAIN][entry.entry_id] + nws_data: NWSData = hass.data[DOMAIN][entry.entry_id] station = entry.data[CONF_STATION] async_add_entities( NWSSensor( hass=hass, entry_data=entry.data, - hass_data=hass_data, + nws_data=nws_data, description=description, station=station, ) @@ -177,13 +168,13 @@ class NWSSensor(CoordinatorEntity[NwsDataUpdateCoordinator], SensorEntity): self, hass: HomeAssistant, entry_data: MappingProxyType[str, Any], - hass_data: dict[str, Any], + nws_data: NWSData, description: NWSSensorEntityDescription, station: str, ) -> None: """Initialise the platform with a data instance.""" - super().__init__(hass_data[COORDINATOR_OBSERVATION]) - self._nws: SimpleNWS = hass_data[NWS_DATA] + super().__init__(nws_data.coordinator_observation) + self._nws = nws_data.api self._latitude = entry_data[CONF_LATITUDE] self._longitude = entry_data[CONF_LONGITUDE] self.entity_description = description diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 0c491723117..0e5fd412e0c 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -35,19 +35,15 @@ from homeassistant.util.dt import utcnow from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter from homeassistant.util.unit_system import UnitSystem -from . import base_unique_id, device_info +from . import NWSData, base_unique_id, device_info from .const import ( ATTR_FORECAST_DETAILED_DESCRIPTION, ATTRIBUTION, CONDITION_CLASSES, - COORDINATOR_FORECAST, - COORDINATOR_FORECAST_HOURLY, - COORDINATOR_OBSERVATION, DAYNIGHT, DOMAIN, FORECAST_VALID_TIME, HOURLY, - NWS_DATA, OBSERVATION_VALID_TIME, ) @@ -84,12 +80,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the NWS weather platform.""" - hass_data = hass.data[DOMAIN][entry.entry_id] + nws_data: NWSData = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - NWSWeather(entry.data, hass_data, DAYNIGHT, hass.config.units), - NWSWeather(entry.data, hass_data, HOURLY, hass.config.units), + NWSWeather(entry.data, nws_data, DAYNIGHT, hass.config.units), + NWSWeather(entry.data, nws_data, HOURLY, hass.config.units), ], False, ) @@ -112,19 +108,19 @@ class NWSWeather(WeatherEntity): def __init__( self, entry_data: MappingProxyType[str, Any], - hass_data: dict[str, Any], + nws_data: NWSData, mode: str, units: UnitSystem, ) -> None: """Initialise the platform with a data instance and station name.""" - self.nws = hass_data[NWS_DATA] + self.nws = nws_data.api self.latitude = entry_data[CONF_LATITUDE] self.longitude = entry_data[CONF_LONGITUDE] - self.coordinator_observation = hass_data[COORDINATOR_OBSERVATION] + self.coordinator_observation = nws_data.coordinator_observation if mode == DAYNIGHT: - self.coordinator_forecast = hass_data[COORDINATOR_FORECAST] + self.coordinator_forecast = nws_data.coordinator_forecast else: - self.coordinator_forecast = hass_data[COORDINATOR_FORECAST_HOURLY] + self.coordinator_forecast = nws_data.coordinator_forecast_hourly self.station = self.nws.station self.mode = mode From 5c1c8dc682a13b6aa0a0a71a400d4ec82ff7ab52 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 20:22:38 +0200 Subject: [PATCH 0574/1151] Modernize tomorrowio weather (#98466) * Modernize tomorrowio weather * Add test snapshot * Update snapshots * Address review comments * Improve test coverage --- .../components/tomorrowio/weather.py | 88 +- .../tomorrowio/snapshots/test_weather.ambr | 1097 +++++++++++++++++ tests/components/tomorrowio/test_weather.py | 216 +++- 3 files changed, 1368 insertions(+), 33 deletions(-) create mode 100644 tests/components/tomorrowio/snapshots/test_weather.ambr diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index 86b84ec3ca6..333aa0cd472 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime -from typing import Any from pytomorrowio.const import DAILY, FORECASTS, HOURLY, NOWCAST, WeatherCode @@ -15,7 +14,10 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + DOMAIN as WEATHER_DOMAIN, + Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -27,7 +29,8 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import is_up from homeassistant.util import dt as dt_util @@ -63,14 +66,30 @@ async def async_setup_entry( ) -> None: """Set up a config entry.""" coordinator = hass.data[DOMAIN][config_entry.data[CONF_API_KEY]] + entity_registry = er.async_get(hass) + + entities = [TomorrowioWeatherEntity(config_entry, coordinator, 4, DAILY)] + + # Add hourly and nowcast entities to legacy config entries + for forecast_type in (HOURLY, NOWCAST): + if not entity_registry.async_get_entity_id( + WEATHER_DOMAIN, + DOMAIN, + _calculate_unique_id(config_entry.unique_id, forecast_type), + ): + continue + entities.append( + TomorrowioWeatherEntity(config_entry, coordinator, 4, forecast_type) + ) - entities = [ - TomorrowioWeatherEntity(config_entry, coordinator, 4, forecast_type) - for forecast_type in (DAILY, HOURLY, NOWCAST) - ] async_add_entities(entities) +def _calculate_unique_id(config_entry_unique_id: str | None, forecast_type: str) -> str: + """Calculate unique ID.""" + return f"{config_entry_unique_id}_{forecast_type}" + + class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): """Entity that talks to Tomorrow.io v4 API to retrieve weather data.""" @@ -79,6 +98,9 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_visibility_unit = UnitOfLength.KILOMETERS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) def __init__( self, @@ -94,7 +116,18 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): forecast_type == DEFAULT_FORECAST_TYPE ) self._attr_name = f"{config_entry.data[CONF_NAME]} - {forecast_type.title()}" - self._attr_unique_id = f"{config_entry.unique_id}_{forecast_type}" + self._attr_unique_id = _calculate_unique_id( + config_entry.unique_id, forecast_type + ) + + @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")) + ) def _forecast_dict( self, @@ -102,12 +135,12 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): use_datetime: bool, condition: int, precipitation: float | None, - precipitation_probability: float | None, + precipitation_probability: int | None, temp: float | None, temp_low: float | None, wind_direction: float | None, wind_speed: float | None, - ) -> dict[str, Any]: + ) -> Forecast: """Return formatted Forecast dict from Tomorrow.io forecast data.""" if use_datetime: translated_condition = self._translate_condition( @@ -116,7 +149,7 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): else: translated_condition = self._translate_condition(condition, True) - data = { + return { ATTR_FORECAST_TIME: forecast_dt.isoformat(), ATTR_FORECAST_CONDITION: translated_condition, ATTR_FORECAST_NATIVE_PRECIPITATION: precipitation, @@ -127,8 +160,6 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed, } - return {k: v for k, v in data.items() if v is not None} - @staticmethod def _translate_condition( condition: int | None, sun_is_up: bool = True @@ -187,20 +218,19 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): """Return the raw visibility.""" return self._get_current_property(TMRW_ATTR_VISIBILITY) - @property - def forecast(self): + def _forecast(self, forecast_type: str) -> list[Forecast] | None: """Return the forecast.""" # Check if forecasts are available raw_forecasts = ( self.coordinator.data.get(self._config_entry.entry_id, {}) .get(FORECASTS, {}) - .get(self.forecast_type) + .get(forecast_type) ) if not raw_forecasts: return None - forecasts = [] - max_forecasts = MAX_FORECASTS[self.forecast_type] + forecasts: list[Forecast] = [] + max_forecasts = MAX_FORECASTS[forecast_type] forecast_count = 0 # Convert utcnow to local to be compatible with tests @@ -212,7 +242,7 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): forecast_dt = dt_util.parse_datetime(forecast[TMRW_ATTR_TIMESTAMP]) # Throw out past data - if dt_util.as_local(forecast_dt).date() < today: + if forecast_dt is None or dt_util.as_local(forecast_dt).date() < today: continue values = forecast["values"] @@ -222,18 +252,23 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): precipitation = values.get(TMRW_ATTR_PRECIPITATION) precipitation_probability = values.get(TMRW_ATTR_PRECIPITATION_PROBABILITY) + try: + precipitation_probability = round(precipitation_probability) + except TypeError: + precipitation_probability = None + temp = values.get(TMRW_ATTR_TEMPERATURE_HIGH) temp_low = None wind_direction = values.get(TMRW_ATTR_WIND_DIRECTION) wind_speed = values.get(TMRW_ATTR_WIND_SPEED) - if self.forecast_type == DAILY: + if forecast_type == DAILY: use_datetime = False temp_low = values.get(TMRW_ATTR_TEMPERATURE_LOW) if precipitation: precipitation = precipitation * 24 - elif self.forecast_type == NOWCAST: + elif forecast_type == NOWCAST: # Precipitation is forecasted in CONF_TIMESTEP increments but in a # per hour rate, so value needs to be converted to an amount. if precipitation: @@ -260,3 +295,16 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): break return forecasts + + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast array.""" + return self._forecast(self.forecast_type) + + async def async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + return self._forecast(DAILY) + + async def async_forecast_hourly(self) -> list[Forecast] | None: + """Return the hourly forecast in native units.""" + return self._forecast(HOURLY) diff --git a/tests/components/tomorrowio/snapshots/test_weather.ambr b/tests/components/tomorrowio/snapshots/test_weather.ambr new file mode 100644 index 00000000000..40ff18658c6 --- /dev/null +++ b/tests/components/tomorrowio/snapshots/test_weather.ambr @@ -0,0 +1,1097 @@ +# serializer version: 1 +# name: test_forecast_subscription[daily] + list([ + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 45.9, + 'templow': 26.1, + 'wind_bearing': 239.6, + 'wind_speed': 34.16, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-08T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 49.4, + 'templow': 26.3, + 'wind_bearing': 262.82, + 'wind_speed': 26.06, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-09T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 67.0, + 'templow': 31.5, + 'wind_bearing': 229.3, + 'wind_speed': 25.38, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-10T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 65.3, + 'templow': 37.3, + 'wind_bearing': 149.91, + 'wind_speed': 38.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-11T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 66.2, + 'templow': 48.3, + 'wind_bearing': 210.45, + 'wind_speed': 56.48, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-03-12T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'temperature': 67.9, + 'templow': 53.8, + 'wind_bearing': 217.98, + 'wind_speed': 44.28, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-13T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'temperature': 54.5, + 'templow': 42.9, + 'wind_bearing': 58.79, + 'wind_speed': 34.99, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-03-14T10:00:00+00:00', + 'precipitation': 0.94, + 'precipitation_probability': 95, + 'temperature': 42.9, + 'templow': 33.4, + 'wind_bearing': 70.25, + 'wind_speed': 58.5, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-03-15T10:00:00+00:00', + 'precipitation': 0.06, + 'precipitation_probability': 55, + 'temperature': 43.7, + 'templow': 29.4, + 'wind_bearing': 84.47, + 'wind_speed': 57.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-16T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 43.0, + 'templow': 29.1, + 'wind_bearing': 103.85, + 'wind_speed': 24.16, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-17T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 52.4, + 'templow': 34.3, + 'wind_bearing': 145.41, + 'wind_speed': 26.17, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-18T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'temperature': 54.1, + 'templow': 41.3, + 'wind_bearing': 62.99, + 'wind_speed': 23.69, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-03-19T10:00:00+00:00', + 'precipitation': 0.12, + 'precipitation_probability': 55, + 'temperature': 48.9, + 'templow': 39.4, + 'wind_bearing': 68.54, + 'wind_speed': 50.08, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-03-20T10:00:00+00:00', + 'precipitation': 0.05, + 'precipitation_probability': 33, + 'temperature': 40.1, + 'templow': 35.1, + 'wind_bearing': 56.98, + 'wind_speed': 62.46, + }), + ]) +# --- +# name: test_forecast_subscription[daily].1 + list([ + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 45.9, + 'templow': 26.1, + 'wind_bearing': 239.6, + 'wind_speed': 34.16, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-08T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 49.4, + 'templow': 26.3, + 'wind_bearing': 262.82, + 'wind_speed': 26.06, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-09T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 67.0, + 'templow': 31.5, + 'wind_bearing': 229.3, + 'wind_speed': 25.38, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-10T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 65.3, + 'templow': 37.3, + 'wind_bearing': 149.91, + 'wind_speed': 38.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-11T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 66.2, + 'templow': 48.3, + 'wind_bearing': 210.45, + 'wind_speed': 56.48, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-03-12T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'temperature': 67.9, + 'templow': 53.8, + 'wind_bearing': 217.98, + 'wind_speed': 44.28, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-13T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'temperature': 54.5, + 'templow': 42.9, + 'wind_bearing': 58.79, + 'wind_speed': 34.99, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-03-14T10:00:00+00:00', + 'precipitation': 0.94, + 'precipitation_probability': 95, + 'temperature': 42.9, + 'templow': 33.4, + 'wind_bearing': 70.25, + 'wind_speed': 58.5, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-03-15T10:00:00+00:00', + 'precipitation': 0.06, + 'precipitation_probability': 55, + 'temperature': 43.7, + 'templow': 29.4, + 'wind_bearing': 84.47, + 'wind_speed': 57.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-16T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 43.0, + 'templow': 29.1, + 'wind_bearing': 103.85, + 'wind_speed': 24.16, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-17T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 52.4, + 'templow': 34.3, + 'wind_bearing': 145.41, + 'wind_speed': 26.17, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-18T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'temperature': 54.1, + 'templow': 41.3, + 'wind_bearing': 62.99, + 'wind_speed': 23.69, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-03-19T10:00:00+00:00', + 'precipitation': 0.12, + 'precipitation_probability': 55, + 'temperature': 48.9, + 'templow': 39.4, + 'wind_bearing': 68.54, + 'wind_speed': 50.08, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-03-20T10:00:00+00:00', + 'precipitation': 0.05, + 'precipitation_probability': 33, + 'temperature': 40.1, + 'templow': 35.1, + 'wind_bearing': 56.98, + 'wind_speed': 62.46, + }), + ]) +# --- +# name: test_forecast_subscription[hourly] + list([ + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T17:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 44.1, + 'wind_bearing': 315.14, + 'wind_speed': 33.59, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T18:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 44.8, + 'wind_bearing': 321.71, + 'wind_speed': 31.82, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T19:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 45.8, + 'wind_bearing': 323.38, + 'wind_speed': 32.04, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T20:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 45.3, + 'wind_bearing': 318.43, + 'wind_speed': 33.73, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T21:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 44.6, + 'wind_bearing': 320.9, + 'wind_speed': 28.98, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T22:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 41.9, + 'wind_bearing': 322.11, + 'wind_speed': 15.7, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T23:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 38.9, + 'wind_bearing': 295.94, + 'wind_speed': 17.78, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-08T00:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 36.2, + 'wind_bearing': 11.94, + 'wind_speed': 20.12, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-08T01:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 34.3, + 'wind_bearing': 13.68, + 'wind_speed': 20.05, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T02:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 32.9, + 'wind_bearing': 14.93, + 'wind_speed': 19.48, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T03:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 31.9, + 'wind_bearing': 26.07, + 'wind_speed': 16.6, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T04:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 29.2, + 'wind_bearing': 51.27, + 'wind_speed': 9.32, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T05:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 27.4, + 'wind_bearing': 343.25, + 'wind_speed': 11.92, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T06:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 26.7, + 'wind_bearing': 341.46, + 'wind_speed': 15.37, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T07:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 26.4, + 'wind_bearing': 322.34, + 'wind_speed': 12.71, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T08:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 26.1, + 'wind_bearing': 294.69, + 'wind_speed': 13.14, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T09:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 30.1, + 'wind_bearing': 325.32, + 'wind_speed': 11.52, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T10:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 31.0, + 'wind_bearing': 322.27, + 'wind_speed': 10.22, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T11:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 27.2, + 'wind_bearing': 310.14, + 'wind_speed': 20.12, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T12:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 29.2, + 'wind_bearing': 324.8, + 'wind_speed': 25.38, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-03-08T13:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 33.2, + 'wind_bearing': 335.16, + 'wind_speed': 23.26, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-08T14:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 37.0, + 'wind_bearing': 324.49, + 'wind_speed': 21.17, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-08T15:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 40.0, + 'wind_bearing': 310.68, + 'wind_speed': 19.98, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-03-08T16:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 42.4, + 'wind_bearing': 304.18, + 'wind_speed': 19.66, + }), + ]) +# --- +# name: test_forecast_subscription[hourly].1 + list([ + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T17:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 44.1, + 'wind_bearing': 315.14, + 'wind_speed': 33.59, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T18:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 44.8, + 'wind_bearing': 321.71, + 'wind_speed': 31.82, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T19:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 45.8, + 'wind_bearing': 323.38, + 'wind_speed': 32.04, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T20:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 45.3, + 'wind_bearing': 318.43, + 'wind_speed': 33.73, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T21:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 44.6, + 'wind_bearing': 320.9, + 'wind_speed': 28.98, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T22:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 41.9, + 'wind_bearing': 322.11, + 'wind_speed': 15.7, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T23:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 38.9, + 'wind_bearing': 295.94, + 'wind_speed': 17.78, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-08T00:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 36.2, + 'wind_bearing': 11.94, + 'wind_speed': 20.12, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-08T01:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 34.3, + 'wind_bearing': 13.68, + 'wind_speed': 20.05, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T02:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 32.9, + 'wind_bearing': 14.93, + 'wind_speed': 19.48, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T03:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 31.9, + 'wind_bearing': 26.07, + 'wind_speed': 16.6, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T04:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 29.2, + 'wind_bearing': 51.27, + 'wind_speed': 9.32, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T05:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 27.4, + 'wind_bearing': 343.25, + 'wind_speed': 11.92, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T06:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 26.7, + 'wind_bearing': 341.46, + 'wind_speed': 15.37, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T07:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 26.4, + 'wind_bearing': 322.34, + 'wind_speed': 12.71, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T08:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 26.1, + 'wind_bearing': 294.69, + 'wind_speed': 13.14, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T09:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 30.1, + 'wind_bearing': 325.32, + 'wind_speed': 11.52, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T10:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 31.0, + 'wind_bearing': 322.27, + 'wind_speed': 10.22, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T11:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 27.2, + 'wind_bearing': 310.14, + 'wind_speed': 20.12, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T12:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 29.2, + 'wind_bearing': 324.8, + 'wind_speed': 25.38, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-03-08T13:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 33.2, + 'wind_bearing': 335.16, + 'wind_speed': 23.26, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-08T14:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 37.0, + 'wind_bearing': 324.49, + 'wind_speed': 21.17, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-08T15:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 40.0, + 'wind_bearing': 310.68, + 'wind_speed': 19.98, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-03-08T16:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 42.4, + 'wind_bearing': 304.18, + 'wind_speed': 19.66, + }), + ]) +# --- +# name: test_v4_forecast_service + dict({ + 'forecast': list([ + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 45.9, + 'templow': 26.1, + 'wind_bearing': 239.6, + 'wind_speed': 34.16, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-08T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 49.4, + 'templow': 26.3, + 'wind_bearing': 262.82, + 'wind_speed': 26.06, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-09T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 67.0, + 'templow': 31.5, + 'wind_bearing': 229.3, + 'wind_speed': 25.38, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-10T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 65.3, + 'templow': 37.3, + 'wind_bearing': 149.91, + 'wind_speed': 38.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-11T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 66.2, + 'templow': 48.3, + 'wind_bearing': 210.45, + 'wind_speed': 56.48, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-03-12T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'temperature': 67.9, + 'templow': 53.8, + 'wind_bearing': 217.98, + 'wind_speed': 44.28, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-13T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'temperature': 54.5, + 'templow': 42.9, + 'wind_bearing': 58.79, + 'wind_speed': 34.99, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-03-14T10:00:00+00:00', + 'precipitation': 0.94, + 'precipitation_probability': 95, + 'temperature': 42.9, + 'templow': 33.4, + 'wind_bearing': 70.25, + 'wind_speed': 58.5, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-03-15T10:00:00+00:00', + 'precipitation': 0.06, + 'precipitation_probability': 55, + 'temperature': 43.7, + 'templow': 29.4, + 'wind_bearing': 84.47, + 'wind_speed': 57.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-16T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 43.0, + 'templow': 29.1, + 'wind_bearing': 103.85, + 'wind_speed': 24.16, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-17T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 52.4, + 'templow': 34.3, + 'wind_bearing': 145.41, + 'wind_speed': 26.17, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-18T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'temperature': 54.1, + 'templow': 41.3, + 'wind_bearing': 62.99, + 'wind_speed': 23.69, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-03-19T10:00:00+00:00', + 'precipitation': 0.12, + 'precipitation_probability': 55, + 'temperature': 48.9, + 'templow': 39.4, + 'wind_bearing': 68.54, + 'wind_speed': 50.08, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-03-20T10:00:00+00:00', + 'precipitation': 0.05, + 'precipitation_probability': 33, + 'temperature': 40.1, + 'templow': 35.1, + 'wind_bearing': 56.98, + 'wind_speed': 62.46, + }), + ]), + }) +# --- +# name: test_v4_forecast_service.1 + dict({ + 'forecast': list([ + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T17:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 44.1, + 'wind_bearing': 315.14, + 'wind_speed': 33.59, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T18:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 44.8, + 'wind_bearing': 321.71, + 'wind_speed': 31.82, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T19:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 45.8, + 'wind_bearing': 323.38, + 'wind_speed': 32.04, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T20:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 45.3, + 'wind_bearing': 318.43, + 'wind_speed': 33.73, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T21:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 44.6, + 'wind_bearing': 320.9, + 'wind_speed': 28.98, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T22:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 41.9, + 'wind_bearing': 322.11, + 'wind_speed': 15.7, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-07T23:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 38.9, + 'wind_bearing': 295.94, + 'wind_speed': 17.78, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-08T00:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 36.2, + 'wind_bearing': 11.94, + 'wind_speed': 20.12, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-03-08T01:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 34.3, + 'wind_bearing': 13.68, + 'wind_speed': 20.05, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T02:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 32.9, + 'wind_bearing': 14.93, + 'wind_speed': 19.48, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T03:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 31.9, + 'wind_bearing': 26.07, + 'wind_speed': 16.6, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T04:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 29.2, + 'wind_bearing': 51.27, + 'wind_speed': 9.32, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T05:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 27.4, + 'wind_bearing': 343.25, + 'wind_speed': 11.92, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T06:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 26.7, + 'wind_bearing': 341.46, + 'wind_speed': 15.37, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T07:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 26.4, + 'wind_bearing': 322.34, + 'wind_speed': 12.71, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T08:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 26.1, + 'wind_bearing': 294.69, + 'wind_speed': 13.14, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T09:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 30.1, + 'wind_bearing': 325.32, + 'wind_speed': 11.52, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T10:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 31.0, + 'wind_bearing': 322.27, + 'wind_speed': 10.22, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T11:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 27.2, + 'wind_bearing': 310.14, + 'wind_speed': 20.12, + }), + dict({ + 'condition': 'clear-night', + 'datetime': '2021-03-08T12:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 29.2, + 'wind_bearing': 324.8, + 'wind_speed': 25.38, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-03-08T13:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 33.2, + 'wind_bearing': 335.16, + 'wind_speed': 23.26, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-08T14:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 37.0, + 'wind_bearing': 324.49, + 'wind_speed': 21.17, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-03-08T15:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 40.0, + 'wind_bearing': 310.68, + 'wind_speed': 19.98, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-03-08T16:48:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 0, + 'temperature': 42.4, + 'wind_bearing': 304.18, + 'wind_speed': 19.66, + }), + ]), + }) +# --- diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index 586fd87f681..8490b94a7f9 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -1,10 +1,13 @@ """Tests for Tomorrow.io weather entity.""" from __future__ import annotations -from datetime import datetime +from datetime import datetime, timedelta from typing import Any from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.tomorrowio.config_flow import ( _get_config_schema, @@ -41,16 +44,19 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED_UNIT, DOMAIN as WEATHER_DOMAIN, + SERVICE_GET_FORECAST, ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, SOURCE_USER from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME, CONF_NAME from homeassistant.core import HomeAssistant, State, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util from .const import API_V4_ENTRY_DATA from tests.common import MockConfigEntry, async_fire_time_changed +from tests.typing import WebSocketGenerator @callback @@ -65,23 +71,47 @@ def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: assert updated_entry.disabled is False +async def _setup_config_entry(hass: HomeAssistant, config: dict[str, Any]) -> State: + """Set up entry and return entity state.""" + data = _get_config_schema(hass, SOURCE_USER)(config) + data[CONF_NAME] = DEFAULT_NAME + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, + unique_id=_get_unique_id(hass, data), + version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: """Set up entry and return entity state.""" + with freeze_time(datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC)): + await _setup_config_entry(hass, config) + + return hass.states.get("weather.tomorrow_io_daily") + + +async def _setup_legacy(hass: HomeAssistant, config: dict[str, Any]) -> State: + """Set up entry and return entity state.""" + registry = er.async_get(hass) + data = _get_config_schema(hass, SOURCE_USER)(config) + for entity_name in ("hourly", "nowcast"): + registry.async_get_or_create( + WEATHER_DOMAIN, + DOMAIN, + f"{_get_unique_id(hass, data)}_{entity_name}", + disabled_by=er.RegistryEntryDisabler.INTEGRATION, + suggested_object_id=f"tomorrow_io_{entity_name}", + ) + with freeze_time( datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC) ) as frozen_time: - data = _get_config_schema(hass, SOURCE_USER)(config) - data[CONF_NAME] = DEFAULT_NAME - config_entry = MockConfigEntry( - domain=DOMAIN, - data=data, - options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, - unique_id=_get_unique_id(hass, data), - version=1, - ) - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await _setup_config_entry(hass, config) for entity_name in ("hourly", "nowcast"): _enable_entity(hass, f"weather.tomorrow_io_{entity_name}") await hass.async_block_till_done() @@ -94,6 +124,33 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: return hass.states.get("weather.tomorrow_io_daily") +async def test_new_config_entry(hass: HomeAssistant) -> None: + """Test the expected entities are created.""" + registry = er.async_get(hass) + await _setup(hass, API_V4_ENTRY_DATA) + assert len(hass.states.async_entity_ids("weather")) == 1 + + entry = hass.config_entries.async_entries()[0] + assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 28 + + +async def test_legacy_config_entry(hass: HomeAssistant) -> None: + """Test the expected entities are created.""" + registry = er.async_get(hass) + data = _get_config_schema(hass, SOURCE_USER)(API_V4_ENTRY_DATA) + for entity_name in ("hourly", "nowcast"): + registry.async_get_or_create( + WEATHER_DOMAIN, + DOMAIN, + f"{_get_unique_id(hass, data)}_{entity_name}", + ) + await _setup(hass, API_V4_ENTRY_DATA) + assert len(hass.states.async_entity_ids("weather")) == 3 + + entry = hass.config_entries.async_entries()[0] + assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 30 + + async def test_v4_weather(hass: HomeAssistant) -> None: """Test v4 weather data.""" weather_state = await _setup(hass, API_V4_ENTRY_DATA) @@ -123,3 +180,136 @@ async def test_v4_weather(hass: HomeAssistant) -> None: assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 33.59 # 9.33 m/s ->km/h assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == "km/h" + + +async def test_v4_weather_legacy_entities(hass: HomeAssistant) -> None: + """Test v4 weather data.""" + weather_state = await _setup_legacy(hass, API_V4_ENTRY_DATA) + assert weather_state.state == ATTR_CONDITION_SUNNY + assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION + assert len(weather_state.attributes[ATTR_FORECAST]) == 14 + assert weather_state.attributes[ATTR_FORECAST][0] == { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, + ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 45.9, + ATTR_FORECAST_TEMP_LOW: 26.1, + ATTR_FORECAST_WIND_BEARING: 239.6, + ATTR_FORECAST_WIND_SPEED: 34.16, # 9.49 m/s -> km/h + } + assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "Tomorrow.io - Daily" + assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 + assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 + assert weather_state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == "mm" + assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 30.35 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == "hPa" + assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 44.1 + assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == "°C" + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 8.15 + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == "km" + assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 33.59 # 9.33 m/s ->km/h + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == "km/h" + + +@freeze_time(datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC)) +async def test_v4_forecast_service( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test multiple forecast.""" + weather_state = await _setup(hass, API_V4_ENTRY_DATA) + entity_id = weather_state.entity_id + + for forecast_type in ("daily", "hourly"): + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": entity_id, + "type": forecast_type, + }, + blocking=True, + return_response=True, + ) + assert response["forecast"] != [] + assert response == snapshot + + +async def test_v4_bad_forecast( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + tomorrowio_config_entry_update, + snapshot: SnapshotAssertion, +) -> None: + """Test bad forecast data.""" + freezer.move_to(datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC)) + + weather_state = await _setup(hass, API_V4_ENTRY_DATA) + entity_id = weather_state.entity_id + hourly_forecast = tomorrowio_config_entry_update.return_value["forecasts"]["hourly"] + hourly_forecast[0]["values"]["precipitationProbability"] = "blah" + + # Trigger data refetch + freezer.tick(timedelta(minutes=32) + timedelta(seconds=1)) + await hass.async_block_till_done() + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": entity_id, + "type": "hourly", + }, + blocking=True, + return_response=True, + ) + assert response["forecast"][0]["precipitation_probability"] is None + + +@pytest.mark.parametrize("forecast_type", ["daily", "hourly"]) +async def test_forecast_subscription( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, + forecast_type: str, +) -> None: + """Test multiple forecast.""" + client = await hass_ws_client(hass) + freezer.move_to(datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC)) + + weather_state = await _setup(hass, API_V4_ENTRY_DATA) + entity_id = weather_state.entity_id + + 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 != [] + assert forecast1 == snapshot + + freezer.tick(timedelta(minutes=32) + 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 != [] + assert forecast2 == snapshot From f643d2de46de31ab2542de25638b2688c068a13b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 22:07:12 +0200 Subject: [PATCH 0575/1151] Map SMHI weather condition codes once (#98517) --- homeassistant/components/smhi/weather.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 5b71d92b25f..c8ff9127ba8 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -81,6 +81,11 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_EXCEPTIONAL: [], } +CONDITION_MAP = { + cond_code: cond_ha + for cond_ha, cond_codes in CONDITION_CLASSES.items() + for cond_code in cond_codes +} TIMEOUT = 10 # 5 minutes between retrying connect to API again @@ -148,7 +153,6 @@ class SmhiWeather(WeatherEntity): name=name, configuration_url="http://opendata.smhi.se/apidocs/metfcst/parameters.html", ) - self._attr_native_temperature = None @property def extra_state_attributes(self) -> Mapping[str, Any] | None: @@ -183,14 +187,7 @@ class SmhiWeather(WeatherEntity): 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._forecast_daily[0].symbol in v - ), - None, - ) + self._attr_condition = CONDITION_MAP.get(self._forecast_daily[0].symbol) await self.async_update_listeners(("daily", "hourly")) async def retry_update(self, _: datetime) -> None: @@ -208,9 +205,7 @@ class SmhiWeather(WeatherEntity): data: list[Forecast] = [] for forecast in self._forecast_daily[1:]: - condition = next( - (k for k, v in CONDITION_CLASSES.items() if forecast.symbol in v), None - ) + condition = CONDITION_MAP.get(forecast.symbol) data.append( { @@ -240,9 +235,7 @@ class SmhiWeather(WeatherEntity): 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 - ) + condition = CONDITION_MAP.get(forecast.symbol) data.append( { From f135c42524587c7c311f968711a1616b9924720b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 22:08:17 +0200 Subject: [PATCH 0576/1151] Map openweathermap weather condition codes once (#98516) --- homeassistant/components/openweathermap/const.py | 5 +++++ .../components/openweathermap/weather_update_coordinator.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index d53fbc136b2..1420b1170ca 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -157,3 +157,8 @@ CONDITION_CLASSES = { 904, ], } +CONDITION_MAP = { + cond_code: cond_ha + for cond_ha, cond_codes in CONDITION_CLASSES.items() + for cond_code in cond_codes +} diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 732557363d8..cf0c941f0df 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -46,7 +46,7 @@ from .const import ( ATTR_API_WIND_BEARING, ATTR_API_WIND_GUST, ATTR_API_WIND_SPEED, - CONDITION_CLASSES, + CONDITION_MAP, DOMAIN, FORECAST_MODE_DAILY, FORECAST_MODE_HOURLY, @@ -267,7 +267,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return ATTR_CONDITION_SUNNY return ATTR_CONDITION_CLEAR_NIGHT - return [k for k, v in CONDITION_CLASSES.items() if weather_code in v][0] + return CONDITION_MAP.get(weather_code) class LegacyWeather: From 227d4a590da30780c32f0fa73f2a5ac9d9af8c49 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 22:09:06 +0200 Subject: [PATCH 0577/1151] Map metoffice weather condition codes once (#98515) --- homeassistant/components/metoffice/const.py | 5 +++++ homeassistant/components/metoffice/sensor.py | 8 ++------ homeassistant/components/metoffice/weather.py | 13 +++---------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index e4843d1235e..8b86784b70b 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -52,6 +52,11 @@ CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_EXCEPTIONAL: [], } +CONDITION_MAP = { + cond_code: cond_ha + for cond_ha, cond_codes in CONDITION_CLASSES.items() + for cond_code in cond_codes +} VISIBILITY_CLASSES = { "VP": "Very Poor", diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index fcb8e5b134e..371c396a829 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -29,7 +29,7 @@ from homeassistant.helpers.update_coordinator import ( from . import get_device_info from .const import ( ATTRIBUTION, - CONDITION_CLASSES, + CONDITION_MAP, DOMAIN, METOFFICE_COORDINATES, METOFFICE_DAILY_COORDINATOR, @@ -221,11 +221,7 @@ class MetOfficeCurrentSensor( elif self.entity_description.key == "weather" and hasattr( self.coordinator.data.now, self.entity_description.key ): - value = [ - k - for k, v in CONDITION_CLASSES.items() - if self.coordinator.data.now.weather.value in v - ][0] + value = CONDITION_MAP.get(self.coordinator.data.now.weather.value) elif hasattr(self.coordinator.data.now, self.entity_description.key): value = getattr(self.coordinator.data.now, self.entity_description.key) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 8257c8a3c35..0b4672ddec8 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -26,7 +26,7 @@ from homeassistant.helpers.update_coordinator import ( from . import get_device_info from .const import ( ATTRIBUTION, - CONDITION_CLASSES, + CONDITION_MAP, DOMAIN, METOFFICE_COORDINATES, METOFFICE_DAILY_COORDINATOR, @@ -55,7 +55,7 @@ async def async_setup_entry( def _build_forecast_data(timestep: Timestep) -> Forecast: data = Forecast(datetime=timestep.date.isoformat()) if timestep.weather: - data[ATTR_FORECAST_CONDITION] = _get_weather_condition(timestep.weather.value) + data[ATTR_FORECAST_CONDITION] = CONDITION_MAP.get(timestep.weather.value) if timestep.precipitation: data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = timestep.precipitation.value if timestep.temperature: @@ -67,13 +67,6 @@ def _build_forecast_data(timestep: Timestep) -> Forecast: return data -def _get_weather_condition(metoffice_code: str) -> str | None: - for hass_name, metoffice_codes in CONDITION_CLASSES.items(): - if metoffice_code in metoffice_codes: - return hass_name - return None - - class MetOfficeWeather( CoordinatorEntity[DataUpdateCoordinator[MetOfficeData]], WeatherEntity ): @@ -107,7 +100,7 @@ class MetOfficeWeather( def condition(self) -> str | None: """Return the current condition.""" if self.coordinator.data.now: - return _get_weather_condition(self.coordinator.data.now.weather.value) + return CONDITION_MAP.get(self.coordinator.data.now.weather.value) return None @property From f85c2e5a92d3ca4c37b7353da29c1c101f86e4b8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 22:10:48 +0200 Subject: [PATCH 0578/1151] Modernize environment_canada weather (#98502) --- .../components/environment_canada/weather.py | 57 ++++++++++++++++--- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index a9f79907b54..bdc300dc9a3 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -21,7 +21,10 @@ from homeassistant.components.weather import ( ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, + DOMAIN as WEATHER_DOMAIN, + Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -30,7 +33,8 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util @@ -63,7 +67,24 @@ async def async_setup_entry( ) -> None: """Add a weather entity from a config_entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id]["weather_coordinator"] - async_add_entities([ECWeather(coordinator, False), ECWeather(coordinator, True)]) + entity_registry = er.async_get(hass) + + entities = [ECWeather(coordinator, False)] + + # Add hourly entity to legacy config entries + if entity_registry.async_get_entity_id( + WEATHER_DOMAIN, + DOMAIN, + _calculate_unique_id(config_entry.unique_id, True), + ): + entities.append(ECWeather(coordinator, True)) + + async_add_entities(entities) + + +def _calculate_unique_id(config_entry_unique_id: str | None, hourly: bool) -> str: + """Calculate unique ID.""" + return f"{config_entry_unique_id}{'-hourly' if hourly else '-daily'}" class ECWeather(CoordinatorEntity, WeatherEntity): @@ -74,6 +95,9 @@ class ECWeather(CoordinatorEntity, WeatherEntity): _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_visibility_unit = UnitOfLength.KILOMETERS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) def __init__(self, coordinator, hourly): """Initialize Environment Canada weather.""" @@ -81,13 +105,22 @@ class ECWeather(CoordinatorEntity, WeatherEntity): self.ec_data = coordinator.ec_data self._attr_attribution = self.ec_data.metadata["attribution"] self._attr_translation_key = "hourly_forecast" if hourly else "forecast" - self._attr_unique_id = ( - f"{coordinator.config_entry.unique_id}{'-hourly' if hourly else '-daily'}" + self._attr_unique_id = _calculate_unique_id( + coordinator.config_entry.unique_id, hourly ) self._attr_entity_registry_enabled_default = not hourly self._hourly = hourly self._attr_device_info = device_info(coordinator.config_entry) + @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 native_temperature(self): """Return the temperature.""" @@ -155,20 +188,28 @@ class ECWeather(CoordinatorEntity, WeatherEntity): return "" @property - def forecast(self): + def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" return get_forecast(self.ec_data, self._hourly) + async def async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + return get_forecast(self.ec_data, False) -def get_forecast(ec_data, hourly): + async def async_forecast_hourly(self) -> list[Forecast] | None: + """Return the hourly forecast in native units.""" + return get_forecast(self.ec_data, True) + + +def get_forecast(ec_data, hourly) -> list[Forecast] | None: """Build the forecast array.""" - forecast_array = [] + forecast_array: list[Forecast] = [] if not hourly: if not (half_days := ec_data.daily_forecasts): return None - today = { + today: Forecast = { ATTR_FORECAST_TIME: dt_util.now().isoformat(), ATTR_FORECAST_CONDITION: icon_code_to_condition( int(half_days[0]["icon_code"]) From 614d6e929d9e4a2cb4efc56c2ff577ca530a3957 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 22:11:27 +0200 Subject: [PATCH 0579/1151] Map meteoclimatic weather condition codes once (#98514) --- homeassistant/components/meteoclimatic/const.py | 5 +++++ homeassistant/components/meteoclimatic/weather.py | 9 ++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/meteoclimatic/const.py b/homeassistant/components/meteoclimatic/const.py index 4de299f1cf7..4a7276d4e42 100644 --- a/homeassistant/components/meteoclimatic/const.py +++ b/homeassistant/components/meteoclimatic/const.py @@ -54,3 +54,8 @@ CONDITION_CLASSES = { ATTR_CONDITION_WINDY: [], ATTR_CONDITION_WINDY_VARIANT: [], } +CONDITION_MAP = { + cond_code: cond_ha + for cond_ha, cond_codes in CONDITION_CLASSES.items() + for cond_code in cond_codes +} diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index d275707488b..f9b341cf114 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -12,14 +12,13 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ATTRIBUTION, CONDITION_CLASSES, DOMAIN, MANUFACTURER, MODEL +from .const import ATTRIBUTION, CONDITION_MAP, DOMAIN, MANUFACTURER, MODEL def format_condition(condition): - """Return condition from dict CONDITION_CLASSES.""" - for key, value in CONDITION_CLASSES.items(): - if condition in value: - return key + """Return condition from dict CONDITION_MAP.""" + if condition in CONDITION_MAP: + return CONDITION_MAP[condition] if isinstance(condition, Condition): return condition.value return condition From 1897be146751e077307d119a47503958738c297b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 22:12:22 +0200 Subject: [PATCH 0580/1151] Map demo and kitchen_sink weather condition codes once (#98510) Map demo and kitchen_sink condition codes once --- homeassistant/components/demo/weather.py | 9 ++++++--- homeassistant/components/kitchen_sink/weather.py | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index 887a9212335..758b5075041 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -46,6 +46,11 @@ CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_EXCEPTIONAL: [], } +CONDITION_MAP = { + cond_code: cond_ha + for cond_ha, cond_codes in CONDITION_CLASSES.items() + for cond_code in cond_codes +} WEATHER_UPDATE_INTERVAL = timedelta(minutes=30) @@ -237,9 +242,7 @@ class DemoWeather(WeatherEntity): @property def condition(self) -> str: """Return the weather condition.""" - return [ - k for k, v in CONDITION_CLASSES.items() if self._condition.lower() in v - ][0] + return CONDITION_MAP[self._condition.lower()] async def async_forecast_daily(self) -> list[Forecast]: """Return the daily forecast.""" diff --git a/homeassistant/components/kitchen_sink/weather.py b/homeassistant/components/kitchen_sink/weather.py index aba30013746..8449b68b460 100644 --- a/homeassistant/components/kitchen_sink/weather.py +++ b/homeassistant/components/kitchen_sink/weather.py @@ -45,6 +45,11 @@ CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_EXCEPTIONAL: [], } +CONDITION_MAP = { + cond_code: cond_ha + for cond_ha, cond_codes in CONDITION_CLASSES.items() + for cond_code in cond_codes +} async def async_setup_entry( @@ -352,9 +357,7 @@ class DemoWeather(WeatherEntity): @property def condition(self) -> str: """Return the weather condition.""" - return [ - k for k, v in CONDITION_CLASSES.items() if self._condition.lower() in v - ][0] + return CONDITION_MAP[self._condition.lower()] @property def forecast(self) -> list[Forecast]: From 992cc56c7ea2a762006b94b1a5315b170ec1a50c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Aug 2023 22:19:22 +0200 Subject: [PATCH 0581/1151] Modernize buienradar weather (#98473) --- homeassistant/components/buienradar/weather.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index 66c3b23ec8b..de00faadd64 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -34,7 +34,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 ( @@ -125,6 +127,7 @@ class BrWeather(WeatherEntity): _attr_native_visibility_unit = UnitOfLength.METERS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND _attr_should_poll = False + _attr_supported_features = WeatherEntityFeature.FORECAST_DAILY def __init__(self, config, coordinates): """Initialize the platform with a data instance and station name.""" @@ -154,6 +157,10 @@ class BrWeather(WeatherEntity): if not self.hass: return self.async_write_ha_state() + assert self.platform.config_entry + self.platform.config_entry.async_create_task( + self.hass, self.async_update_listeners(("daily",)) + ) def _calc_condition(self, data: BrData): """Return the current condition.""" @@ -185,3 +192,7 @@ class BrWeather(WeatherEntity): fcdata_out.append(data_out) return fcdata_out + + async def async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + return self._attr_forecast From a776ecddb72e7451a8095ed0aa0a9de1ab4c0bec Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 17 Aug 2023 01:44:02 +0200 Subject: [PATCH 0582/1151] Update mypy to 1.5.1 (#98554) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index acb70d5fb8c..de135e4a997 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ astroid==2.15.4 coverage==7.3.0 freezegun==1.2.2 mock-open==1.4.0 -mypy==1.5.0 +mypy==1.5.1 pre-commit==3.3.3 pydantic==1.10.12 pylint==2.17.4 From 52a8f0109620029f6876a07a276101f3652c9fb7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 16 Aug 2023 21:15:35 -0400 Subject: [PATCH 0583/1151] Make IKEA fan sensors diagnostic in ZHA (#97747) --- homeassistant/components/zha/binary_sensor.py | 1 + homeassistant/components/zha/sensor.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 48fbf1f0bb2..50cfb783370 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -265,6 +265,7 @@ class ReplaceFilter(BinarySensor, id_suffix="replace_filter"): SENSOR_ATTR = "replace_filter" _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM + _attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC _attr_name: str = "Replace filter" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 49ba46038f9..0e520d98b52 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -968,6 +968,7 @@ class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): _attr_icon = "mdi:timer" _attr_name: str = "Device run time" _attr_native_unit_of_measurement = UnitOfTime.MINUTES + _attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC @MULTI_MATCH(cluster_handler_names="ikea_airpurifier") @@ -980,6 +981,7 @@ class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"): _attr_icon = "mdi:timer" _attr_name: str = "Filter run time" _attr_native_unit_of_measurement = UnitOfTime.MINUTES + _attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC class AqaraFeedingSource(types.enum8): From fde498586ed95ac987e85d169adc78b6f4ccef7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= Date: Thu, 17 Aug 2023 08:45:23 +0300 Subject: [PATCH 0584/1151] Expose dew point in Met.no (#98543) --- homeassistant/components/met/const.py | 2 ++ homeassistant/components/met/manifest.json | 2 +- homeassistant/components/met/weather.py | 8 ++++++++ homeassistant/components/norway_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/met/conftest.py | 1 + 7 files changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index dcc493570ba..b690f1b6723 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -22,6 +22,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, ATTR_WEATHER_CLOUD_COVERAGE, + ATTR_WEATHER_DEW_POINT, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -199,4 +200,5 @@ ATTR_MAP = { ATTR_WEATHER_WIND_SPEED: "wind_speed", ATTR_WEATHER_WIND_GUST_SPEED: "wind_gust", ATTR_WEATHER_CLOUD_COVERAGE: "cloudiness", + ATTR_WEATHER_DEW_POINT: "dew_point", } diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 5c476b10665..d6466bb64c4 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/met", "iot_class": "cloud_polling", "loggers": ["metno"], - "requirements": ["PyMetno==0.10.0"] + "requirements": ["PyMetno==0.11.0"] } diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 500cb3c5716..2fcde1e05f0 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -8,6 +8,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TIME, ATTR_WEATHER_CLOUD_COVERAGE, + ATTR_WEATHER_DEW_POINT, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -202,6 +203,13 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): ATTR_MAP[ATTR_WEATHER_CLOUD_COVERAGE] ) + @property + def native_dew_point(self) -> float | None: + """Return the dew point.""" + return self.coordinator.data.current_weather_data.get( + ATTR_MAP[ATTR_WEATHER_DEW_POINT] + ) + def _forecast(self, hourly: bool) -> list[Forecast] | None: """Return the forecast array.""" if hourly: diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 4a3fc7cee96..84af1313cf5 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/norway_air", "iot_class": "cloud_polling", "loggers": ["metno"], - "requirements": ["PyMetno==0.10.0"] + "requirements": ["PyMetno==0.11.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ec345859233..37f4404f48c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -73,7 +73,7 @@ PyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air -PyMetno==0.10.0 +PyMetno==0.11.0 # homeassistant.components.keymitt_ble PyMicroBot==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d65f0676a65..c5164ac72f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -63,7 +63,7 @@ PyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air -PyMetno==0.10.0 +PyMetno==0.11.0 # homeassistant.components.keymitt_ble PyMicroBot==0.0.9 diff --git a/tests/components/met/conftest.py b/tests/components/met/conftest.py index e6b975023d1..a007620988f 100644 --- a/tests/components/met/conftest.py +++ b/tests/components/met/conftest.py @@ -17,6 +17,7 @@ def mock_weather(): "humidity": 50, "wind_speed": 10, "wind_bearing": "NE", + "dew_point": 12.1, } mock_data.get_forecast.return_value = {} yield mock_data From 6faa9abc756344a67732979ce3cf5bedeefc7f70 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 17 Aug 2023 08:51:59 +0200 Subject: [PATCH 0585/1151] Fix Verisure config entry migration (#98546) --- homeassistant/components/verisure/__init__.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 302bd23b66f..62f41913862 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -83,21 +83,22 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.debug("Migrating from version %s", entry.version) if entry.version == 1: - config_entry_default_code = entry.options.get(CONF_LOCK_DEFAULT_CODE) - entity_reg = er.async_get(hass) - entries = er.async_entries_for_config_entry(entity_reg, entry.entry_id) - for entity in entries: - if entity.entity_id.startswith("lock"): - entity_reg.async_update_entity_options( - entity.entity_id, - LOCK_DOMAIN, - {CONF_DEFAULT_CODE: config_entry_default_code}, - ) - new_options = entry.options.copy() - del new_options[CONF_LOCK_DEFAULT_CODE] + if config_entry_default_code := entry.options.get(CONF_LOCK_DEFAULT_CODE): + entity_reg = er.async_get(hass) + entries = er.async_entries_for_config_entry(entity_reg, entry.entry_id) + for entity in entries: + if entity.entity_id.startswith("lock"): + entity_reg.async_update_entity_options( + entity.entity_id, + LOCK_DOMAIN, + {CONF_DEFAULT_CODE: config_entry_default_code}, + ) + new_options = entry.options.copy() + del new_options[CONF_LOCK_DEFAULT_CODE] + + hass.config_entries.async_update_entry(entry, options=new_options) entry.version = 2 - hass.config_entries.async_update_entry(entry, options=new_options) LOGGER.info("Migration to version %s successful", entry.version) From 8b4937f627126f5b6a16776e6def1a5070124641 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 17 Aug 2023 10:24:58 +0200 Subject: [PATCH 0586/1151] Bump odp-amsterdam to v5.3.0 (#98555) * Bump package to v5.3.0 * Load only the garages for cars --- homeassistant/components/garages_amsterdam/__init__.py | 2 +- homeassistant/components/garages_amsterdam/config_flow.py | 2 +- homeassistant/components/garages_amsterdam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py index 2af4227391b..35d177b2cca 100644 --- a/homeassistant/components/garages_amsterdam/__init__.py +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -45,7 +45,7 @@ async def get_coordinator( garage.garage_name: garage for garage in await ODPAmsterdam( session=aiohttp_client.async_get_clientsession(hass) - ).all_garages() + ).all_garages(vehicle="car") } coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/garages_amsterdam/config_flow.py b/homeassistant/components/garages_amsterdam/config_flow.py index cd1591c9bc0..7799630ddee 100644 --- a/homeassistant/components/garages_amsterdam/config_flow.py +++ b/homeassistant/components/garages_amsterdam/config_flow.py @@ -32,7 +32,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: api_data = await ODPAmsterdam( session=aiohttp_client.async_get_clientsession(self.hass) - ).all_garages() + ).all_garages(vehicle="car") except ClientResponseError: _LOGGER.error("Unexpected response from server") return self.async_abort(reason="cannot_connect") diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json index e2f068b961c..e67bdaa04d0 100644 --- a/homeassistant/components/garages_amsterdam/manifest.json +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", "iot_class": "cloud_polling", - "requirements": ["odp-amsterdam==5.1.0"] + "requirements": ["odp-amsterdam==5.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 37f4404f48c..23e85a5bd25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1314,7 +1314,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==5.1.0 +odp-amsterdam==5.3.0 # homeassistant.components.oem oemthermostat==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5164ac72f3..d1234e1e1a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==5.1.0 +odp-amsterdam==5.3.0 # homeassistant.components.omnilogic omnilogic==0.4.5 From 1954539e6534252c146295923782bd16b40bec59 Mon Sep 17 00:00:00 2001 From: Dennis Date: Thu, 17 Aug 2023 11:13:11 +0200 Subject: [PATCH 0587/1151] Add state_class to tomorrowio UV Index (#98541) * Added state_class to UV Index Forgot to add a state_class as other sensors got their state_class from their device class. As there is no UV Index device class I left it out. * Forgotten a comma, whoops * Changed measurement to string. * Changed from "measurement" to SensorStateClass --- homeassistant/components/tomorrowio/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index aba5b44f284..7ccb4f673cd 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -18,6 +18,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -295,6 +296,7 @@ SENSOR_TYPES = ( TomorrowioSensorEntityDescription( key=TMRW_ATTR_UV_INDEX, name="UV Index", + state_class=SensorStateClass.MEASUREMENT, icon="mdi:sun-wireless", ), TomorrowioSensorEntityDescription( From d6a7127b845e420280971082cbdb655d64ed1c76 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 17 Aug 2023 10:15:36 +0000 Subject: [PATCH 0588/1151] Improve availability of Tractive entities (#97091) Co-authored-by: Robert Resch --- homeassistant/components/tractive/__init__.py | 10 +- .../components/tractive/binary_sensor.py | 57 +++----- .../components/tractive/device_tracker.py | 24 ++-- homeassistant/components/tractive/entity.py | 49 ++++++- homeassistant/components/tractive/sensor.py | 127 ++++-------------- homeassistant/components/tractive/switch.py | 50 ++----- 6 files changed, 123 insertions(+), 194 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 351b39f61e7..e08ea954e21 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -89,7 +89,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from error tractive = TractiveClient(hass, client, creds["user_id"], entry) - tractive.subscribe() try: trackable_objects = await client.trackable_objects() @@ -97,7 +96,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: *(_generate_trackables(client, item) for item in trackable_objects) ) except aiotractive.exceptions.TractiveError as error: - await tractive.unsubscribe() raise ConfigEntryNotReady from error # When the pet defined in Tractive has no tracker linked we get None as `trackable`. @@ -173,6 +171,14 @@ class TractiveClient: """Return user id.""" return self._user_id + @property + def subscribed(self) -> bool: + """Return True if subscribed.""" + if self._listen_task is None: + return False + + return not self._listen_task.cancelled() + async def trackable_objects( self, ) -> list[aiotractive.trackable_object.TrackableObject]: diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index d7968f15bf8..940ff82687e 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -11,17 +11,10 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_CHARGING, EntityCategory from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import Trackables -from .const import ( - CLIENT, - DOMAIN, - SERVER_UNAVAILABLE, - TRACKABLES, - TRACKER_HARDWARE_STATUS_UPDATED, -) +from . import Trackables, TractiveClient +from .const import CLIENT, DOMAIN, TRACKABLES, TRACKER_HARDWARE_STATUS_UPDATED from .entity import TractiveEntity @@ -29,45 +22,29 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): """Tractive sensor.""" def __init__( - self, user_id: str, item: Trackables, description: BinarySensorEntityDescription + self, + client: TractiveClient, + item: Trackables, + description: BinarySensorEntityDescription, ) -> None: """Initialize sensor entity.""" - super().__init__(user_id, item.trackable, item.tracker_details) + super().__init__( + client, + item.trackable, + item.tracker_details, + f"{TRACKER_HARDWARE_STATUS_UPDATED}-{item.tracker_details['_id']}", + ) self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" + self._attr_available = False self.entity_description = description @callback - def handle_server_unavailable(self) -> None: - """Handle server unavailable.""" - self._attr_available = False - self.async_write_ha_state() - - @callback - def handle_hardware_status_update(self, event: dict[str, Any]) -> None: - """Handle hardware status update.""" + def handle_status_update(self, event: dict[str, Any]) -> None: + """Handle status update.""" self._attr_is_on = event[self.entity_description.key] - self._attr_available = True - self.async_write_ha_state() - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{TRACKER_HARDWARE_STATUS_UPDATED}-{self._tracker_id}", - self.handle_hardware_status_update, - ) - ) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SERVER_UNAVAILABLE}-{self._user_id}", - self.handle_server_unavailable, - ) - ) + super().handle_status_update(event) SENSOR_TYPE = BinarySensorEntityDescription( @@ -86,7 +63,7 @@ async def async_setup_entry( trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] entities = [ - TractiveBinarySensor(client.user_id, item, SENSOR_TYPE) + TractiveBinarySensor(client, item, SENSOR_TYPE) for item in trackables if item.tracker_details.get("charging_state") is not None ] diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index a97ea963362..0e373e1a44f 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import Trackables +from . import Trackables, TractiveClient from .const import ( CLIENT, DOMAIN, @@ -28,7 +28,7 @@ async def async_setup_entry( client = hass.data[DOMAIN][entry.entry_id][CLIENT] trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] - entities = [TractiveDeviceTracker(client.user_id, item) for item in trackables] + entities = [TractiveDeviceTracker(client, item) for item in trackables] async_add_entities(entities) @@ -39,9 +39,14 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): _attr_icon = "mdi:paw" _attr_translation_key = "tracker" - def __init__(self, user_id: str, item: Trackables) -> None: + def __init__(self, client: TractiveClient, item: Trackables) -> None: """Initialize tracker entity.""" - super().__init__(user_id, item.trackable, item.tracker_details) + super().__init__( + client, + item.trackable, + item.tracker_details, + f"{TRACKER_HARDWARE_STATUS_UPDATED}-{item.tracker_details['_id']}", + ) self._battery_level: int | None = item.hw_info.get("battery_level") self._latitude: float = item.pos_report["latlong"][0] @@ -94,18 +99,15 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): self._attr_available = True self.async_write_ha_state() - @callback - def _handle_server_unavailable(self) -> None: - self._attr_available = False - self.async_write_ha_state() - async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" + if not self._client.subscribed: + self._client.subscribe() self.async_on_remove( async_dispatcher_connect( self.hass, - f"{TRACKER_HARDWARE_STATUS_UPDATED}-{self._tracker_id}", + self._dispatcher_signal, self._handle_hardware_status_update, ) ) @@ -122,6 +124,6 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): async_dispatcher_connect( self.hass, f"{SERVER_UNAVAILABLE}-{self._user_id}", - self._handle_server_unavailable, + self.handle_server_unavailable, ) ) diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py index d142fe69db5..da7beb8bcdd 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -3,10 +3,13 @@ from __future__ import annotations from typing import Any +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 Entity -from .const import DOMAIN +from . import TractiveClient +from .const import DOMAIN, SERVER_UNAVAILABLE class TractiveEntity(Entity): @@ -15,7 +18,11 @@ class TractiveEntity(Entity): _attr_has_entity_name = True def __init__( - self, user_id: str, trackable: dict[str, Any], tracker_details: dict[str, Any] + self, + client: TractiveClient, + trackable: dict[str, Any], + tracker_details: dict[str, Any], + dispatcher_signal: str, ) -> None: """Initialize tracker entity.""" self._attr_device_info = DeviceInfo( @@ -26,6 +33,40 @@ class TractiveEntity(Entity): sw_version=tracker_details["fw_version"], model=tracker_details["model_number"], ) - self._user_id = user_id + self._user_id = client.user_id self._tracker_id = tracker_details["_id"] - self._trackable = trackable + self._client = client + self._dispatcher_signal = dispatcher_signal + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + if not self._client.subscribed: + self._client.subscribe() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._dispatcher_signal, + self.handle_status_update, + ) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SERVER_UNAVAILABLE}-{self._user_id}", + self.handle_server_unavailable, + ) + ) + + @callback + def handle_status_update(self, event: dict[str, Any]) -> None: + """Handle status update.""" + self._attr_available = event[self.entity_description.key] is not None + self.async_write_ha_state() + + @callback + def handle_server_unavailable(self) -> None: + """Handle server unavailable.""" + self._attr_available = False + self.async_write_ha_state() diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 493b627f9b4..6891b74d31b 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -18,10 +18,9 @@ from homeassistant.const import ( UnitOfTime, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import Trackables +from . import Trackables, TractiveClient from .const import ( ATTR_CALORIES, ATTR_DAILY_GOAL, @@ -32,7 +31,6 @@ from .const import ( ATTR_TRACKER_STATE, CLIENT, DOMAIN, - SERVER_UNAVAILABLE, TRACKABLES, TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, @@ -45,7 +43,7 @@ from .entity import TractiveEntity class TractiveRequiredKeysMixin: """Mixin for required keys.""" - entity_class: type[TractiveSensor] + signal_prefix: str @dataclass @@ -54,112 +52,39 @@ class TractiveSensorEntityDescription( ): """Class describing Tractive sensor entities.""" + hardware_sensor: bool = False + class TractiveSensor(TractiveEntity, SensorEntity): """Tractive sensor.""" def __init__( self, - user_id: str, + client: TractiveClient, item: Trackables, description: TractiveSensorEntityDescription, ) -> None: """Initialize sensor entity.""" - super().__init__(user_id, item.trackable, item.tracker_details) + if description.hardware_sensor: + dispatcher_signal = ( + f"{description.signal_prefix}-{item.tracker_details['_id']}" + ) + else: + dispatcher_signal = f"{description.signal_prefix}-{item.trackable['_id']}" + super().__init__( + client, item.trackable, item.tracker_details, dispatcher_signal + ) self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" - self.entity_description = description - - @callback - def handle_server_unavailable(self) -> None: - """Handle server unavailable.""" self._attr_available = False - self.async_write_ha_state() - - -class TractiveHardwareSensor(TractiveSensor): - """Tractive hardware sensor.""" - - @callback - def handle_hardware_status_update(self, event: dict[str, Any]) -> None: - """Handle hardware status update.""" - if (_state := event[self.entity_description.key]) is None: - return - self._attr_native_value = _state - self._attr_available = True - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{TRACKER_HARDWARE_STATUS_UPDATED}-{self._tracker_id}", - self.handle_hardware_status_update, - ) - ) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SERVER_UNAVAILABLE}-{self._user_id}", - self.handle_server_unavailable, - ) - ) - - -class TractiveActivitySensor(TractiveSensor): - """Tractive active sensor.""" + self.entity_description = description @callback def handle_status_update(self, event: dict[str, Any]) -> None: """Handle status update.""" self._attr_native_value = event[self.entity_description.key] - self._attr_available = True - self.async_write_ha_state() - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{TRACKER_ACTIVITY_STATUS_UPDATED}-{self._trackable['_id']}", - self.handle_status_update, - ) - ) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SERVER_UNAVAILABLE}-{self._user_id}", - self.handle_server_unavailable, - ) - ) - - -class TractiveWellnessSensor(TractiveActivitySensor): - """Tractive wellness sensor.""" - - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{TRACKER_WELLNESS_STATUS_UPDATED}-{self._trackable['_id']}", - self.handle_status_update, - ) - ) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SERVER_UNAVAILABLE}-{self._user_id}", - self.handle_server_unavailable, - ) - ) + super().handle_status_update(event) SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( @@ -168,13 +93,15 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( translation_key="tracker_battery_level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, - entity_class=TractiveHardwareSensor, + signal_prefix=TRACKER_HARDWARE_STATUS_UPDATED, + hardware_sensor=True, entity_category=EntityCategory.DIAGNOSTIC, ), TractiveSensorEntityDescription( key=ATTR_TRACKER_STATE, translation_key="tracker_state", - entity_class=TractiveHardwareSensor, + signal_prefix=TRACKER_HARDWARE_STATUS_UPDATED, + hardware_sensor=True, icon="mdi:radar", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, @@ -190,7 +117,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( translation_key="activity_time", icon="mdi:clock-time-eight-outline", native_unit_of_measurement=UnitOfTime.MINUTES, - entity_class=TractiveActivitySensor, + signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( @@ -198,7 +125,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( translation_key="rest_time", icon="mdi:clock-time-eight-outline", native_unit_of_measurement=UnitOfTime.MINUTES, - entity_class=TractiveWellnessSensor, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( @@ -206,7 +133,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( translation_key="calories", icon="mdi:fire", native_unit_of_measurement="kcal", - entity_class=TractiveWellnessSensor, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( @@ -214,14 +141,14 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( translation_key="daily_goal", icon="mdi:flag-checkered", native_unit_of_measurement=UnitOfTime.MINUTES, - entity_class=TractiveActivitySensor, + signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, ), TractiveSensorEntityDescription( key=ATTR_MINUTES_DAY_SLEEP, translation_key="minutes_day_sleep", icon="mdi:sleep", native_unit_of_measurement=UnitOfTime.MINUTES, - entity_class=TractiveWellnessSensor, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( @@ -229,7 +156,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( translation_key="minutes_night_sleep", icon="mdi:sleep", native_unit_of_measurement=UnitOfTime.MINUTES, - entity_class=TractiveWellnessSensor, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), ) @@ -243,7 +170,7 @@ async def async_setup_entry( trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] entities = [ - description.entity_class(client.user_id, item, description) + TractiveSensor(client, item, description) for description in SENSOR_TYPES for item in trackables ] diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index 6d8274df253..55acdb9bdcd 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -11,17 +11,15 @@ 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.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import Trackables +from . import Trackables, TractiveClient from .const import ( ATTR_BUZZER, ATTR_LED, ATTR_LIVE_TRACKING, CLIENT, DOMAIN, - SERVER_UNAVAILABLE, TRACKABLES, TRACKER_HARDWARE_STATUS_UPDATED, ) @@ -77,7 +75,7 @@ async def async_setup_entry( trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] entities = [ - TractiveSwitch(client.user_id, item, description) + TractiveSwitch(client, item, description) for description in SWITCH_TYPES for item in trackables ] @@ -92,12 +90,17 @@ class TractiveSwitch(TractiveEntity, SwitchEntity): def __init__( self, - user_id: str, + client: TractiveClient, item: Trackables, description: TractiveSwitchEntityDescription, ) -> None: """Initialize switch entity.""" - super().__init__(user_id, item.trackable, item.tracker_details) + super().__init__( + client, + item.trackable, + item.tracker_details, + f"{TRACKER_HARDWARE_STATUS_UPDATED}-{item.tracker_details['_id']}", + ) self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self._attr_available = False @@ -106,38 +109,11 @@ class TractiveSwitch(TractiveEntity, SwitchEntity): self.entity_description = description @callback - def handle_server_unavailable(self) -> None: - """Handle server unavailable.""" - self._attr_available = False - self.async_write_ha_state() + def handle_status_update(self, event: dict[str, Any]) -> None: + """Handle status update.""" + self._attr_is_on = event[self.entity_description.key] - @callback - def handle_hardware_status_update(self, event: dict[str, Any]) -> None: - """Handle hardware status update.""" - if (state := event[self.entity_description.key]) is None: - return - self._attr_is_on = state - self._attr_available = True - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{TRACKER_HARDWARE_STATUS_UPDATED}-{self._tracker_id}", - self.handle_hardware_status_update, - ) - ) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{SERVER_UNAVAILABLE}-{self._user_id}", - self.handle_server_unavailable, - ) - ) + super().handle_status_update(event) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on a switch.""" From cb4917f8805efd9c75f245dc22e29b240d8ba559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Thu, 17 Aug 2023 15:12:35 +0200 Subject: [PATCH 0589/1151] Fix GoGoGate2 configuration URL when remote access is disabled (#98387) --- homeassistant/components/gogogate2/common.py | 7 ++++--- tests/components/gogogate2/__init__.py | 2 +- tests/components/gogogate2/test_cover.py | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 4a811373cb1..ba1426e1201 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -113,9 +113,10 @@ class GoGoGate2Entity(CoordinatorEntity[DeviceDataUpdateCoordinator]): def device_info(self) -> DeviceInfo: """Device info for the controller.""" data = self.coordinator.data - configuration_url = ( - f"https://{data.remoteaccess}" if data.remoteaccess else None - ) + if data.remoteaccessenabled: + configuration_url = f"https://{data.remoteaccess}" + else: + configuration_url = f"http://{self._config_entry.data[CONF_IP_ADDRESS]}" return DeviceInfo( configuration_url=configuration_url, identifiers={(DOMAIN, str(self._config_entry.unique_id))}, diff --git a/tests/components/gogogate2/__init__.py b/tests/components/gogogate2/__init__.py index f7e3d40a44b..08675c58709 100644 --- a/tests/components/gogogate2/__init__.py +++ b/tests/components/gogogate2/__init__.py @@ -77,7 +77,7 @@ def _mocked_ismartgate_closed_door_response(): ismartgatename="ismartgatename0", model="ismartgatePRO", apiversion="", - remoteaccessenabled=False, + remoteaccessenabled=True, remoteaccess="abc321.blah.blah", firmwareversion="555", pin=123, diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 00cc0057d7c..ca6509d53b9 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -340,6 +340,7 @@ async def test_device_info_ismartgate( assert device.name == "mycontroller" assert device.model == "ismartgatePRO" assert device.sw_version == "555" + assert device.configuration_url == "https://abc321.blah.blah" @patch("homeassistant.components.gogogate2.common.GogoGate2Api") @@ -375,3 +376,4 @@ async def test_device_info_gogogate2( assert device.name == "mycontroller" assert device.model == "gogogate2" assert device.sw_version == "222" + assert device.configuration_url == "http://127.0.0.1" From ea5272ba62f8457d6903481c869fa6a770b66170 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Thu, 17 Aug 2023 15:44:23 +0200 Subject: [PATCH 0590/1151] Revert "Fix fanSpeed issue in Tado" (#98506) Revert "Fix fanSpeed issue (#98293)" This reverts commit d6498aa39e40f5aa34707533d440736f7b19b963. --- homeassistant/components/tado/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 0ef6dc17934..b57d384124c 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -327,7 +327,7 @@ class TadoConnector: device_type, "ON", mode, - fan_speed=fan_speed, + fanSpeed=fan_speed, swing=swing, ) From 6f4294dc62019b928868a49620cacf1a7c3821b0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 17 Aug 2023 16:02:22 +0200 Subject: [PATCH 0591/1151] Migrate IPMA to has entity name (#98572) * Migrate IPMA to has entity name * Migrate IPMA to has entity name --- homeassistant/components/ipma/entity.py | 12 +++++++++--- homeassistant/components/ipma/sensor.py | 3 +-- homeassistant/components/ipma/weather.py | 16 ++++++++-------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/ipma/entity.py b/homeassistant/components/ipma/entity.py index 6424084c533..7eb8e2fe1a7 100644 --- a/homeassistant/components/ipma/entity.py +++ b/homeassistant/components/ipma/entity.py @@ -1,6 +1,9 @@ """Base Entity for IPMA.""" from __future__ import annotations +from pyipma.api import IPMA_API +from pyipma.location import Location + from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import Entity @@ -10,17 +13,20 @@ from .const import DOMAIN class IPMADevice(Entity): """Common IPMA Device Information.""" - def __init__(self, location) -> None: + _attr_has_entity_name = True + + def __init__(self, api: IPMA_API, location: Location) -> None: """Initialize device information.""" + self._api = api self._location = location self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={ ( DOMAIN, - f"{self._location.station_latitude}, {self._location.station_longitude}", + f"{location.station_latitude}, {location.station_longitude}", ) }, manufacturer=DOMAIN, - name=self._location.name, + name=location.name, ) diff --git a/homeassistant/components/ipma/sensor.py b/homeassistant/components/ipma/sensor.py index 1bd257a3994..7f5782f3f89 100644 --- a/homeassistant/components/ipma/sensor.py +++ b/homeassistant/components/ipma/sensor.py @@ -75,9 +75,8 @@ class IPMASensor(SensorEntity, IPMADevice): description: IPMASensorEntityDescription, ) -> None: """Initialize the IPMA Sensor.""" - IPMADevice.__init__(self, location) + IPMADevice.__init__(self, api, location) self.entity_description = description - self._api = api self._attr_unique_id = f"{self._location.station_latitude}, {self._location.station_longitude}, {self.entity_description.key}" @Throttle(MIN_TIME_BETWEEN_UPDATES) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 1f948bcc4e1..a5bb3981575 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -25,7 +25,6 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_MODE, - CONF_NAME, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, @@ -56,13 +55,14 @@ async def async_setup_entry( """Add a weather entity from a config_entry.""" api = hass.data[DOMAIN][config_entry.entry_id][DATA_API] location = hass.data[DOMAIN][config_entry.entry_id][DATA_LOCATION] - async_add_entities([IPMAWeather(location, api, config_entry.data)], True) + async_add_entities([IPMAWeather(api, location, config_entry)], True) class IPMAWeather(WeatherEntity, IPMADevice): """Representation of a weather condition.""" _attr_attribution = ATTRIBUTION + _attr_name = None _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR @@ -70,13 +70,13 @@ class IPMAWeather(WeatherEntity, IPMADevice): WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY ) - def __init__(self, location: Location, api: IPMA_API, config) -> None: + def __init__( + self, api: IPMA_API, location: Location, config_entry: ConfigEntry + ) -> None: """Initialise the platform with a data instance and station name.""" - IPMADevice.__init__(self, location) - self._api = api - self._attr_name = config.get(CONF_NAME, location.name) - self._mode = config.get(CONF_MODE) - self._period = 1 if config.get(CONF_MODE) == "hourly" else 24 + IPMADevice.__init__(self, api, location) + self._mode = config_entry.data.get(CONF_MODE) + self._period = 1 if config_entry.data.get(CONF_MODE) == "hourly" else 24 self._observation = None self._daily_forecast: list[IPMAForecast] | None = None self._hourly_forecast: list[IPMAForecast] | None = None From 2d4decc9b1626416fcaaca6bbe9b98b5631ed9cf Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Thu, 17 Aug 2023 16:16:47 +0200 Subject: [PATCH 0592/1151] Revert "Integration tado bump" (#98505) Revert "Integration tado bump (#97791)" This reverts commit 65365d1db57a5e8cdf58d925c6e52871eb75f6be. --- homeassistant/components/tado/__init__.py | 39 +++++++++++---------- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index b57d384124c..1cd21634c8e 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -163,11 +163,12 @@ class TadoConnector: def setup(self): """Connect to Tado and fetch the zones.""" - self.tado = Tado(self._username, self._password, None, True) + self.tado = Tado(self._username, self._password) + self.tado.setDebugging(True) # Load zones and devices - self.zones = self.tado.get_zones() - self.devices = self.tado.get_devices() - tado_home = self.tado.get_me()["homes"][0] + self.zones = self.tado.getZones() + self.devices = self.tado.getDevices() + tado_home = self.tado.getMe()["homes"][0] self.home_id = tado_home["id"] self.home_name = tado_home["name"] @@ -180,7 +181,7 @@ class TadoConnector: def update_devices(self): """Update the device data from Tado.""" - devices = self.tado.get_devices() + devices = self.tado.getDevices() for device in devices: device_short_serial_no = device["shortSerialNo"] _LOGGER.debug("Updating device %s", device_short_serial_no) @@ -189,7 +190,7 @@ class TadoConnector: INSIDE_TEMPERATURE_MEASUREMENT in device["characteristics"]["capabilities"] ): - device[TEMP_OFFSET] = self.tado.get_device_info( + device[TEMP_OFFSET] = self.tado.getDeviceInfo( device_short_serial_no, TEMP_OFFSET ) except RuntimeError: @@ -217,7 +218,7 @@ class TadoConnector: def update_zones(self): """Update the zone data from Tado.""" try: - zone_states = self.tado.get_zone_states()["zoneStates"] + zone_states = self.tado.getZoneStates()["zoneStates"] except RuntimeError: _LOGGER.error("Unable to connect to Tado while updating zones") return @@ -229,7 +230,7 @@ class TadoConnector: """Update the internal data from Tado.""" _LOGGER.debug("Updating zone %s", zone_id) try: - data = self.tado.get_zone_state(zone_id) + data = self.tado.getZoneState(zone_id) except RuntimeError: _LOGGER.error("Unable to connect to Tado while updating zone %s", zone_id) return @@ -250,8 +251,8 @@ class TadoConnector: def update_home(self): """Update the home data from Tado.""" try: - self.data["weather"] = self.tado.get_weather() - self.data["geofence"] = self.tado.get_home_state() + self.data["weather"] = self.tado.getWeather() + self.data["geofence"] = self.tado.getHomeState() dispatcher_send( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "home", "data"), @@ -264,15 +265,15 @@ class TadoConnector: def get_capabilities(self, zone_id): """Return the capabilities of the devices.""" - return self.tado.get_capabilities(zone_id) + return self.tado.getCapabilities(zone_id) def get_auto_geofencing_supported(self): """Return whether the Tado Home supports auto geofencing.""" - return self.tado.get_auto_geofencing_supported() + return self.tado.getAutoGeofencingSupported() def reset_zone_overlay(self, zone_id): """Reset the zone back to the default operation.""" - self.tado.reset_zone_overlay(zone_id) + self.tado.resetZoneOverlay(zone_id) self.update_zone(zone_id) def set_presence( @@ -281,11 +282,11 @@ class TadoConnector: ): """Set the presence to home, away or auto.""" if presence == PRESET_AWAY: - self.tado.set_away() + self.tado.setAway() elif presence == PRESET_HOME: - self.tado.set_home() + self.tado.setHome() elif presence == PRESET_AUTO: - self.tado.set_auto() + self.tado.setAuto() # Update everything when changing modes self.update_zones() @@ -319,7 +320,7 @@ class TadoConnector: ) try: - self.tado.set_zone_overlay( + self.tado.setZoneOverlay( zone_id, overlay_mode, temperature, @@ -339,7 +340,7 @@ class TadoConnector: def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): """Set a zone to off.""" try: - self.tado.set_zone_overlay( + self.tado.setZoneOverlay( zone_id, overlay_mode, None, None, device_type, "OFF" ) except RequestException as exc: @@ -350,6 +351,6 @@ class TadoConnector: def set_temperature_offset(self, device_id, offset): """Set temperature offset of device.""" try: - self.tado.set_temp_offset(device_id, offset) + self.tado.setTempOffset(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 bea608514bd..62f7a377239 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.16.0"] + "requirements": ["python-tado==0.15.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 23e85a5bd25..d91e6c58b0c 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.16.0 +python-tado==0.15.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d1234e1e1a9..ccf963c1b3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1588,7 +1588,7 @@ python-smarttub==0.0.33 python-songpal==0.15.2 # homeassistant.components.tado -python-tado==0.16.0 +python-tado==0.15.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 From 30a88e9e61201368dec3f41c238cad096a8f60d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Aug 2023 09:37:54 -0500 Subject: [PATCH 0593/1151] Additional doorbird cleanups to prepare for event entities (#98542) --- homeassistant/components/doorbird/__init__.py | 113 +++++++----------- homeassistant/components/doorbird/button.py | 25 ++-- homeassistant/components/doorbird/camera.py | 67 ++++------- .../components/doorbird/config_flow.py | 11 +- homeassistant/components/doorbird/device.py | 26 ++-- homeassistant/components/doorbird/entity.py | 24 ++-- homeassistant/components/doorbird/logbook.py | 26 ++-- homeassistant/components/doorbird/models.py | 26 ++++ homeassistant/components/doorbird/util.py | 49 ++------ 9 files changed, 158 insertions(+), 209 deletions(-) create mode 100644 homeassistant/components/doorbird/models.py diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 8651f7de6de..d1ad91bbb2c 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -14,30 +14,21 @@ from homeassistant.components import persistent_notification from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import ( - API_URL, - CONF_EVENTS, - DOMAIN, - DOOR_STATION, - DOOR_STATION_EVENT_ENTITY_IDS, - DOOR_STATION_INFO, - PLATFORMS, - UNDO_UPDATE_LISTENER, -) +from .const import API_URL, CONF_EVENTS, DOMAIN, PLATFORMS from .device import ConfiguredDoorBird -from .util import get_doorstation_by_token +from .models import DoorBirdData +from .util import get_door_station_by_token _LOGGER = logging.getLogger(__name__) @@ -64,26 +55,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the DoorBird component.""" hass.data.setdefault(DOMAIN, {}) - # Provide an endpoint for the doorstations to call to trigger events + # Provide an endpoint for the door stations to call to trigger events hass.http.register_view(DoorBirdRequestView) - def _reset_device_favorites_handler(event): + def _reset_device_favorites_handler(event: Event) -> None: """Handle clearing favorites on device.""" if (token := event.data.get("token")) is None: return - doorstation = get_doorstation_by_token(hass, token) + door_station = get_door_station_by_token(hass, token) - if doorstation is None: + if door_station is None: _LOGGER.error("Device not found for provided token") return # Clear webhooks - favorites = doorstation.device.favorites() - - for favorite_type in favorites: - for favorite_id in favorites[favorite_type]: - doorstation.device.delete_favorite(favorite_type, favorite_id) + favorites: dict[str, list[str]] = door_station.device.favorites() + for favorite_type, favorite_ids in favorites.items(): + for favorite_id in favorite_ids: + door_station.device.delete_favorite(favorite_type, favorite_id) hass.bus.async_listen(RESET_DEVICE_FAVORITES, _reset_device_favorites_handler) @@ -95,17 +85,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _async_import_options_from_data_if_missing(hass, entry) - doorstation_config = entry.data - doorstation_options = entry.options + door_station_config = entry.data config_entry_id = entry.entry_id - device_ip = doorstation_config[CONF_HOST] - username = doorstation_config[CONF_USERNAME] - password = doorstation_config[CONF_PASSWORD] + device_ip = door_station_config[CONF_HOST] + username = door_station_config[CONF_USERNAME] + password = door_station_config[CONF_PASSWORD] device = DoorBird(device_ip, username, password) try: - status, info = await hass.async_add_executor_job(_init_doorbird_device, device) + status, info = await hass.async_add_executor_job(_init_door_bird_device, device) except requests.exceptions.HTTPError as err: if err.response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error( @@ -126,50 +115,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) raise ConfigEntryNotReady - token: str = doorstation_config.get(CONF_TOKEN, config_entry_id) - custom_url: str | None = doorstation_config.get(CONF_CUSTOM_URL) - name: str | None = doorstation_config.get(CONF_NAME) - events = doorstation_options.get(CONF_EVENTS, []) - doorstation = ConfiguredDoorBird(device, name, custom_url, token) - doorstation.update_events(events) + token: str = door_station_config.get(CONF_TOKEN, config_entry_id) + custom_url: str | None = door_station_config.get(CONF_CUSTOM_URL) + name: str | None = door_station_config.get(CONF_NAME) + events = entry.options.get(CONF_EVENTS, []) + event_entity_ids: dict[str, str] = {} + door_station = ConfiguredDoorBird(device, name, custom_url, token, event_entity_ids) + door_bird_data = DoorBirdData(door_station, info, event_entity_ids) + door_station.update_events(events) # Subscribe to doorbell or motion events - if not await _async_register_events(hass, doorstation): + if not await _async_register_events(hass, door_station): raise ConfigEntryNotReady - undo_listener = entry.add_update_listener(_update_listener) - - hass.data[DOMAIN][config_entry_id] = { - DOOR_STATION: doorstation, - DOOR_STATION_INFO: info, - UNDO_UPDATE_LISTENER: undo_listener, - } - + entry.async_on_unload(entry.add_update_listener(_update_listener)) + hass.data[DOMAIN][config_entry_id] = door_bird_data await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -def _init_doorbird_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]: +def _init_door_bird_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]: + """Verify we can connect to the device and return the status.""" return device.ready(), device.info() async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - + data: dict[str, DoorBirdData] = hass.data[DOMAIN] + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + data.pop(entry.entry_id) return unload_ok async def _async_register_events( - hass: HomeAssistant, doorstation: ConfiguredDoorBird + hass: HomeAssistant, door_station: ConfiguredDoorBird ) -> bool: try: - await hass.async_add_executor_job(doorstation.register_events, hass) + await hass.async_add_executor_job(door_station.register_events, hass) except requests.exceptions.HTTPError: persistent_notification.async_create( hass, @@ -190,10 +172,11 @@ async def _async_register_events( async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" config_entry_id = entry.entry_id - doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION] - doorstation.update_events(entry.options[CONF_EVENTS]) + data: DoorBirdData = hass.data[DOMAIN][config_entry_id] + door_station = data.door_station + door_station.update_events(entry.options[CONF_EVENTS]) # Subscribe to doorbell or motion events - await _async_register_events(hass, doorstation) + await _async_register_events(hass, door_station) @callback @@ -217,21 +200,17 @@ class DoorBirdRequestView(HomeAssistantView): name = API_URL[1:].replace("/", ":") extra_urls = [API_URL + "/{event}"] - async def get(self, request, event): + async def get(self, request: web.Request, event: str) -> web.Response: """Respond to requests from the device.""" - hass = request.app["hass"] - - token = request.query.get("token") - - device = get_doorstation_by_token(hass, token) - - if device is None: + hass: HomeAssistant = request.app["hass"] + token: str | None = request.query.get("token") + if token is None or (device := get_door_station_by_token(hass, token)) is None: return web.Response( status=HTTPStatus.UNAUTHORIZED, text="Invalid token provided." ) if device: - event_data = device.get_event_data() + event_data = device.get_event_data(event) else: event_data = {} @@ -241,10 +220,6 @@ class DoorBirdRequestView(HomeAssistantView): message = f"HTTP Favorites cleared for {device.slug}" return web.Response(text=message) - event_data[ATTR_ENTITY_ID] = hass.data[DOMAIN][ - DOOR_STATION_EVENT_ENTITY_IDS - ].get(event) - hass.bus.async_fire(f"{DOMAIN}_{event}", event_data) return web.Response(text="OK") diff --git a/homeassistant/components/doorbird/button.py b/homeassistant/components/doorbird/button.py index fb13a6f5be3..1c69429d3c7 100644 --- a/homeassistant/components/doorbird/button.py +++ b/homeassistant/components/doorbird/button.py @@ -10,8 +10,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, DOOR_STATION, DOOR_STATION_INFO +from .const import DOMAIN from .entity import DoorBirdEntity +from .models import DoorBirdData IR_RELAY = "__ir_light__" @@ -49,20 +50,14 @@ async def async_setup_entry( ) -> None: """Set up the DoorBird button platform.""" config_entry_id = config_entry.entry_id - - data = hass.data[DOMAIN][config_entry_id] - doorstation = data[DOOR_STATION] - doorstation_info = data[DOOR_STATION_INFO] - - relays = doorstation_info["RELAYS"] + door_bird_data: DoorBirdData = hass.data[DOMAIN][config_entry_id] + relays = door_bird_data.door_station_info["RELAYS"] entities = [ - DoorBirdButton(doorstation, doorstation_info, relay, RELAY_ENTITY_DESCRIPTION) + DoorBirdButton(door_bird_data, relay, RELAY_ENTITY_DESCRIPTION) for relay in relays ] - entities.append( - DoorBirdButton(doorstation, doorstation_info, IR_RELAY, IR_ENTITY_DESCRIPTION) - ) + entities.append(DoorBirdButton(door_bird_data, IR_RELAY, IR_ENTITY_DESCRIPTION)) async_add_entities(entities) @@ -74,16 +69,14 @@ class DoorBirdButton(DoorBirdEntity, ButtonEntity): def __init__( self, - doorstation: DoorBird, - doorstation_info, + door_bird_data: DoorBirdData, relay: str, entity_description: DoorbirdButtonEntityDescription, ) -> None: """Initialize a relay in a DoorBird device.""" - super().__init__(doorstation, doorstation_info) + super().__init__(door_bird_data) self._relay = relay self.entity_description = entity_description - if self._relay == IR_RELAY: self._attr_name = "IR" else: @@ -92,4 +85,4 @@ class DoorBirdButton(DoorBirdEntity, ButtonEntity): def press(self) -> None: """Power the relay.""" - self.entity_description.press_action(self._doorstation.device, self._relay) + self.entity_description.press_action(self._door_station.device, self._relay) diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 63eb646972d..06bdb494463 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -14,13 +14,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from .const import ( - DOMAIN, - DOOR_STATION, - DOOR_STATION_EVENT_ENTITY_IDS, - DOOR_STATION_INFO, -) +from .const import DOMAIN from .entity import DoorBirdEntity +from .models import DoorBirdData _LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=2) _LAST_MOTION_INTERVAL = datetime.timedelta(seconds=30) @@ -36,39 +32,31 @@ async def async_setup_entry( ) -> None: """Set up the DoorBird camera platform.""" config_entry_id = config_entry.entry_id - config_data = hass.data[DOMAIN][config_entry_id] - doorstation = config_data[DOOR_STATION] - doorstation_info = config_data[DOOR_STATION_INFO] - device = doorstation.device + door_bird_data: DoorBirdData = hass.data[DOMAIN][config_entry_id] + device = door_bird_data.door_station.device async_add_entities( [ DoorBirdCamera( - doorstation, - doorstation_info, + door_bird_data, device.live_image_url, "live", "live", - doorstation.doorstation_events, _LIVE_INTERVAL, device.rtsp_live_video_url, ), DoorBirdCamera( - doorstation, - doorstation_info, + door_bird_data, device.history_image_url(1, "doorbell"), "last_ring", "last_ring", - [], _LAST_VISITOR_INTERVAL, ), DoorBirdCamera( - doorstation, - doorstation_info, + door_bird_data, device.history_image_url(1, "motionsensor"), "last_motion", "last_motion", - [], _LAST_MOTION_INTERVAL, ), ] @@ -80,17 +68,15 @@ class DoorBirdCamera(DoorBirdEntity, Camera): def __init__( self, - doorstation, - doorstation_info, - url, - camera_id, - translation_key, - doorstation_events, - interval, - stream_url=None, + door_bird_data: DoorBirdData, + url: str, + camera_id: str, + translation_key: str, + interval: datetime.timedelta, + stream_url: str | None = None, ) -> None: """Initialize the camera on a DoorBird device.""" - super().__init__(doorstation, doorstation_info) + super().__init__(door_bird_data) self._url = url self._stream_url = stream_url self._attr_translation_key = translation_key @@ -100,7 +86,6 @@ class DoorBirdCamera(DoorBirdEntity, Camera): self._interval = interval self._last_update = datetime.datetime.min self._attr_unique_id = f"{self._mac_addr}_{camera_id}" - self._doorstation_events = doorstation_events async def stream_source(self): """Return the stream source.""" @@ -133,19 +118,15 @@ class DoorBirdCamera(DoorBirdEntity, Camera): return self._last_image async def async_added_to_hass(self) -> None: - """Add callback after being added to hass. - - Registers entity_id map for the logbook - """ - event_to_entity_id = self.hass.data[DOMAIN].setdefault( - DOOR_STATION_EVENT_ENTITY_IDS, {} - ) - for event in self._doorstation_events: + """Subscribe to events.""" + await super().async_added_to_hass() + event_to_entity_id = self._door_bird_data.event_entity_ids + for event in self._door_station.events: event_to_entity_id[event] = self.entity_id - async def will_remove_from_hass(self): - """Unregister entity_id map for the logbook.""" - event_to_entity_id = self.hass.data[DOMAIN][DOOR_STATION_EVENT_ENTITY_IDS] - for event in self._doorstation_events: - if event in event_to_entity_id: - del event_to_entity_id[event] + async def async_will_remove_from_hass(self) -> None: + """Unsubscribe from events.""" + event_to_entity_id = self._door_bird_data.event_entity_ids + for event in self._door_station.events: + del event_to_entity_id[event] + await super().async_will_remove_from_hass() diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 4ad5e24247e..d2197de93c9 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations from http import HTTPStatus from ipaddress import ip_address import logging +from typing import Any from doorbirdpy import DoorBird import requests @@ -12,12 +13,12 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.util.network import is_ipv4_address, is_link_local from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI -from .util import get_mac_address_from_doorstation_info +from .util import get_mac_address_from_door_station_info _LOGGER = logging.getLogger(__name__) @@ -33,7 +34,7 @@ def _schema_with_defaults(host=None, name=None): ) -def _check_device(device): +def _check_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]: """Verify we can connect to the device and return the status.""" return device.ready(), device.info() @@ -53,13 +54,13 @@ async def validate_input(hass: core.HomeAssistant, data): if not status[0]: raise CannotConnect - mac_addr = get_mac_address_from_doorstation_info(info) + mac_addr = get_mac_address_from_door_station_info(info) # Return info that you want to store in the config entry. return {"title": data[CONF_HOST], "mac_addr": mac_addr} -async def async_verify_supported_device(hass, host): +async def async_verify_supported_device(hass: HomeAssistant, host: str) -> bool: """Verify the doorbell state endpoint returns a 401.""" device = DoorBird(host, "", "") try: diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index 1c787feb934..aced0d8723f 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -6,6 +6,7 @@ from typing import Any from doorbirdpy import DoorBird +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers.network import get_url from homeassistant.util import dt as dt_util, slugify @@ -19,20 +20,28 @@ class ConfiguredDoorBird: """Attach additional information to pass along with configured device.""" def __init__( - self, device: DoorBird, name: str | None, custom_url: str | None, token: str + self, + device: DoorBird, + name: str | None, + custom_url: str | None, + token: str, + event_entity_ids: dict[str, str], ) -> None: """Initialize configured device.""" self._name = name self._device = device self._custom_url = custom_url - self.events = None - self.doorstation_events = None self._token = token + self._event_entity_ids = event_entity_ids + self.events: list[str] = [] + self.door_station_events: list[str] = [] - def update_events(self, events): + def update_events(self, events: list[str]) -> None: """Update the doorbird events.""" self.events = events - self.doorstation_events = [self._get_event_name(event) for event in self.events] + self.door_station_events = [ + self._get_event_name(event) for event in self.events + ] @property def name(self) -> str | None: @@ -63,12 +72,12 @@ class ConfiguredDoorBird: if self.custom_url is not None: hass_url = self.custom_url - if not self.doorstation_events: + if not self.door_station_events: # User may not have permission to get the favorites return favorites = self.device.favorites() - for event in self.doorstation_events: + for event in self.door_station_events: if self._register_event(hass_url, event, favs=favorites): _LOGGER.info( "Successfully registered URL for %s on %s", event, self.name @@ -126,7 +135,7 @@ class ConfiguredDoorBird: return None - def get_event_data(self) -> dict[str, str]: + def get_event_data(self, event: str) -> dict[str, str | None]: """Get data to pass along with HA event.""" return { "timestamp": dt_util.utcnow().isoformat(), @@ -134,4 +143,5 @@ class ConfiguredDoorBird: "live_image_url": self._device.live_image_url, "rtsp_live_video_url": self._device.rtsp_live_video_url, "html5_viewer_url": self._device.html5_viewer_url, + ATTR_ENTITY_ID: self._event_entity_ids.get(event), } diff --git a/homeassistant/components/doorbird/entity.py b/homeassistant/components/doorbird/entity.py index 32c9cfff784..4360a8ff490 100644 --- a/homeassistant/components/doorbird/entity.py +++ b/homeassistant/components/doorbird/entity.py @@ -1,6 +1,5 @@ """The DoorBird integration base entity.""" -from typing import Any from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo @@ -12,8 +11,8 @@ from .const import ( DOORBIRD_INFO_KEY_FIRMWARE, MANUFACTURER, ) -from .device import ConfiguredDoorBird -from .util import get_mac_address_from_doorstation_info +from .models import DoorBirdData +from .util import get_mac_address_from_door_station_info class DoorBirdEntity(Entity): @@ -21,21 +20,20 @@ class DoorBirdEntity(Entity): _attr_has_entity_name = True - def __init__( - self, doorstation: ConfiguredDoorBird, doorstation_info: dict[str, Any] - ) -> None: + def __init__(self, door_bird_data: DoorBirdData) -> None: """Initialize the entity.""" super().__init__() - self._doorstation = doorstation - self._mac_addr = get_mac_address_from_doorstation_info(doorstation_info) - - firmware = doorstation_info[DOORBIRD_INFO_KEY_FIRMWARE] - firmware_build = doorstation_info[DOORBIRD_INFO_KEY_BUILD_NUMBER] + self._door_bird_data = door_bird_data + self._door_station = door_bird_data.door_station + door_station_info = door_bird_data.door_station_info + self._mac_addr = get_mac_address_from_door_station_info(door_station_info) + firmware = door_station_info[DOORBIRD_INFO_KEY_FIRMWARE] + firmware_build = door_station_info[DOORBIRD_INFO_KEY_BUILD_NUMBER] self._attr_device_info = DeviceInfo( configuration_url="https://webadmin.doorbird.com/", connections={(dr.CONNECTION_NETWORK_MAC, self._mac_addr)}, manufacturer=MANUFACTURER, - model=doorstation_info[DOORBIRD_INFO_KEY_DEVICE_TYPE], - name=self._doorstation.name, + model=door_station_info[DOORBIRD_INFO_KEY_DEVICE_TYPE], + name=self._door_station.name, sw_version=f"{firmware} {firmware_build}", ) diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index f3beebe6971..7c8e3cd3c51 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -1,43 +1,35 @@ """Describe logbook events.""" from __future__ import annotations -from typing import Any - from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, ) from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import callback +from homeassistant.core import Event, HomeAssistant, callback -from .const import DOMAIN, DOOR_STATION, DOOR_STATION_EVENT_ENTITY_IDS +from .const import DOMAIN +from .models import DoorBirdData @callback -def async_describe_events(hass, async_describe_event): +def async_describe_events(hass: HomeAssistant, async_describe_event): """Describe logbook events.""" @callback - def async_describe_logbook_event(event): + def async_describe_logbook_event(event: Event): """Describe a logbook event.""" - doorbird_event = event.event_type.split("_", 1)[1] - return { LOGBOOK_ENTRY_NAME: "Doorbird", LOGBOOK_ENTRY_MESSAGE: f"Event {event.event_type} was fired", - LOGBOOK_ENTRY_ENTITY_ID: hass.data[DOMAIN][ - DOOR_STATION_EVENT_ENTITY_IDS - ].get(doorbird_event, event.data.get(ATTR_ENTITY_ID)), + # Database entries before Jun 25th 2020 will not have an entity ID + LOGBOOK_ENTRY_ENTITY_ID: event.data.get(ATTR_ENTITY_ID), } - domain_data: dict[str, Any] = hass.data[DOMAIN] - + domain_data: dict[str, DoorBirdData] = hass.data[DOMAIN] for data in domain_data.values(): - if DOOR_STATION not in data: - # We need to skip door_station_event_entity_ids - continue - for event in data[DOOR_STATION].doorstation_events: + for event in data.door_station.door_station_events: async_describe_event( DOMAIN, f"{DOMAIN}_{event}", async_describe_logbook_event ) diff --git a/homeassistant/components/doorbird/models.py b/homeassistant/components/doorbird/models.py new file mode 100644 index 00000000000..f8fb8687e59 --- /dev/null +++ b/homeassistant/components/doorbird/models.py @@ -0,0 +1,26 @@ +"""The doorbird integration models.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from .device import ConfiguredDoorBird + + +@dataclass +class DoorBirdData: + """Data for the doorbird integration.""" + + door_station: ConfiguredDoorBird + door_station_info: dict[str, Any] + + # + # This integration uses a different event for + # each entity id. It would be a major breaking + # change to change this to a single event at this + # point. + # + # Do not copy this pattern in the future + # for any new integrations. + # + event_entity_ids: dict[str, str] diff --git a/homeassistant/components/doorbird/util.py b/homeassistant/components/doorbird/util.py index 7b406bc07fa..52c1417a67c 100644 --- a/homeassistant/components/doorbird/util.py +++ b/homeassistant/components/doorbird/util.py @@ -2,50 +2,23 @@ from homeassistant.core import HomeAssistant -from .const import DOMAIN, DOOR_STATION +from .const import DOMAIN from .device import ConfiguredDoorBird +from .models import DoorBirdData -def get_mac_address_from_doorstation_info(doorstation_info): +def get_mac_address_from_door_station_info(door_station_info): """Get the mac address depending on the device type.""" - if "PRIMARY_MAC_ADDR" in doorstation_info: - return doorstation_info["PRIMARY_MAC_ADDR"] - return doorstation_info["WIFI_MAC_ADDR"] + return door_station_info.get("PRIMARY_MAC_ADDR", door_station_info["WIFI_MAC_ADDR"]) -def get_doorstation_by_token( +def get_door_station_by_token( hass: HomeAssistant, token: str ) -> ConfiguredDoorBird | None: - """Get doorstation by token.""" - return _get_doorstation_by_attr(hass, "token", token) - - -def get_doorstation_by_slug( - hass: HomeAssistant, slug: str -) -> ConfiguredDoorBird | None: - """Get doorstation by slug.""" - return _get_doorstation_by_attr(hass, "slug", slug) - - -def _get_doorstation_by_attr( - hass: HomeAssistant, attr: str, val: str -) -> ConfiguredDoorBird | None: - for entry in hass.data[DOMAIN].values(): - if DOOR_STATION not in entry: - continue - - doorstation = entry[DOOR_STATION] - - if getattr(doorstation, attr) == val: - return doorstation - + """Get door station by token.""" + domain_data: dict[str, DoorBirdData] = hass.data[DOMAIN] + for data in domain_data.values(): + door_station = data.door_station + if door_station.token == token: + return door_station return None - - -def get_all_doorstations(hass: HomeAssistant) -> list[ConfiguredDoorBird]: - """Get all doorstations.""" - return [ - entry[DOOR_STATION] - for entry in hass.data[DOMAIN].values() - if DOOR_STATION in entry - ] From d44847bb239e180f86c0979bee443bd4c966ecdc Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 17 Aug 2023 15:09:16 +0000 Subject: [PATCH 0594/1151] Log Tractive events on debug level (#98539) --- homeassistant/components/tractive/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index e08ea954e21..043e074270e 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -207,6 +207,7 @@ class TractiveClient: while True: try: async for event in self._client.events(): + _LOGGER.debug("Received event: %s", event) if server_was_unavailable: _LOGGER.debug("Tractive is back online") server_was_unavailable = False From 740cabc21e034684b91a0f8a2ca644f1588d7b1f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 17 Aug 2023 17:36:22 +0200 Subject: [PATCH 0595/1151] Pin setuptools to 68.0.0 (#98582) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3003e3a29ca..4e477440cde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools~=68.0", "wheel~=0.40.0"] +requires = ["setuptools==68.0.0", "wheel~=0.40.0"] build-backend = "setuptools.build_meta" [project] From e95979e9af235691ae4437ecd02c5f8999b9023c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Aug 2023 10:39:35 -0500 Subject: [PATCH 0596/1151] Bump ESPHome recommended BLE version to 2023.8.0 (#98586) --- homeassistant/components/esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/const.py b/homeassistant/components/esphome/const.py index f0e3972f197..575c57c8672 100644 --- a/homeassistant/components/esphome/const.py +++ b/homeassistant/components/esphome/const.py @@ -11,7 +11,7 @@ DEFAULT_ALLOW_SERVICE_CALLS = True DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False -STABLE_BLE_VERSION_STR = "2023.6.0" +STABLE_BLE_VERSION_STR = "2023.8.0" STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) PROJECT_URLS = { "esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", From 529bc507a07003ee57f05215ccc9ffb961a20a01 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:42:20 +0200 Subject: [PATCH 0597/1151] Fix aiohttp test RuntimeWarning (#98568) --- homeassistant/components/buienradar/util.py | 2 +- tests/test_util/aiohttp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 3c50b3097cb..63e0004dc43 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -109,7 +109,7 @@ class BrData: return result finally: if resp is not None: - await resp.release() + resp.release() async def _async_update(self): """Update the data from buienradar.""" diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 5e7284eb9c2..356240dc37a 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -255,7 +255,7 @@ class AiohttpClientMockResponse: """Return mock response as a json.""" return loads(self.response.decode(encoding)) - async def release(self): + def release(self): """Mock release.""" def raise_for_status(self): From 3e14e5acbad1b9141fd8bc9cedc1f9143f1121d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Aug 2023 10:46:21 -0500 Subject: [PATCH 0598/1151] Bump aioesphomeapi to 16.0.1 (#98536) --- homeassistant/components/esphome/manager.py | 6 ++++-- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index a0f49340c1a..35939dc9b1f 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -18,6 +18,7 @@ from aioesphomeapi import ( UserServiceArgType, VoiceAssistantEventType, ) +from aioesphomeapi.model import VoiceAssistantCommandFlag from awesomeversion import AwesomeVersion import voluptuous as vol @@ -319,7 +320,7 @@ class ESPHomeManager: self.voice_assistant_udp_server = None async def _handle_pipeline_start( - self, conversation_id: str, use_vad: bool + self, conversation_id: str, use_vad: int ) -> int | None: """Start a voice assistant pipeline.""" if self.voice_assistant_udp_server is not None: @@ -339,7 +340,8 @@ class ESPHomeManager: voice_assistant_udp_server.run_pipeline( device_id=self.device_id, conversation_id=conversation_id or None, - use_vad=use_vad, + use_vad=VoiceAssistantCommandFlag(use_vad) + == VoiceAssistantCommandFlag.USE_VAD, ), "esphome.voice_assistant_udp_server.run_pipeline", ) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index c44c8b3e28d..313ba5355bb 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async_interrupt==1.1.1", - "aioesphomeapi==15.1.15", + "aioesphomeapi==16.0.1", "bluetooth-data-tools==1.8.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index d91e6c58b0c..21aba7da4e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -228,7 +228,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.15 +aioesphomeapi==16.0.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccf963c1b3c..93994110627 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -209,7 +209,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==15.1.15 +aioesphomeapi==16.0.1 # homeassistant.components.flo aioflo==2021.11.0 From 49995a4667600a7d0adb7c86266d26432a6e4c6c Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Thu, 17 Aug 2023 08:58:52 -0700 Subject: [PATCH 0599/1151] Add tests for device tracker in Prometheus (#98054) --- tests/components/prometheus/test_init.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 09c8a37dc2a..446666c4a6a 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -509,6 +509,23 @@ async def test_cover(client, cover_entities) -> None: assert tilt_position_metric in body +@pytest.mark.parametrize("namespace", [""]) +async def test_device_tracker(client, device_tracker_entities) -> None: + """Test prometheus metrics for device_tracker.""" + body = await generate_latest_metrics(client) + + assert ( + 'device_tracker_state{domain="device_tracker",' + 'entity="device_tracker.phone",' + 'friendly_name="Phone"} 1.0' in body + ) + assert ( + 'device_tracker_state{domain="device_tracker",' + 'entity="device_tracker.watch",' + 'friendly_name="Watch"} 0.0' in body + ) + + @pytest.mark.parametrize("namespace", [""]) async def test_counter(client, counter_entities) -> None: """Test prometheus metrics for counter.""" From a9b1f23b7ffc03f16bdefa37ade5d314d6d6f4f6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 17 Aug 2023 18:16:32 +0200 Subject: [PATCH 0600/1151] Bump renault-api to 0.2.0 (#98587) --- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 5f2670fb170..e5470259aa4 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "platinum", - "requirements": ["renault-api==0.1.13"] + "requirements": ["renault-api==0.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 21aba7da4e7..65a6ab08bb2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2278,7 +2278,7 @@ raspyrfm-client==1.2.8 regenmaschine==2023.06.0 # homeassistant.components.renault -renault-api==0.1.13 +renault-api==0.2.0 # homeassistant.components.renson renson-endura-delta==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93994110627..7df3a7172b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1671,7 +1671,7 @@ rapt-ble==0.1.2 regenmaschine==2023.06.0 # homeassistant.components.renault -renault-api==0.1.13 +renault-api==0.2.0 # homeassistant.components.renson renson-endura-delta==1.5.0 From dd69ba31366e9baf0612fc537d326f6f26953101 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 17 Aug 2023 18:29:20 +0200 Subject: [PATCH 0601/1151] Migrate Cert Expiry to has entity name (#98160) * Migrate Cert Expiry to has entity name * Migrate Cert Expiry to has entity name * Fix entity name --- homeassistant/components/cert_expiry/sensor.py | 3 ++- .../components/cert_expiry/strings.json | 7 +++++++ tests/components/cert_expiry/test_init.py | 10 +++++----- tests/components/cert_expiry/test_sensors.py | 18 +++++++++--------- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index aeae8a5afe9..645642067e6 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -77,6 +77,7 @@ class CertExpiryEntity(CoordinatorEntity[CertExpiryDataUpdateCoordinator]): """Defines a base Cert Expiry entity.""" _attr_icon = "mdi:certificate" + _attr_has_entity_name = True @property def extra_state_attributes(self): @@ -91,6 +92,7 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): """Implementation of the Cert Expiry timestamp sensor.""" _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_translation_key = "certificate_expiry" def __init__( self, @@ -98,7 +100,6 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): ) -> None: """Initialize a Cert Expiry timestamp sensor.""" super().__init__(coordinator) - self._attr_name = f"Cert Expiry Timestamp ({coordinator.name})" self._attr_unique_id = f"{coordinator.host}:{coordinator.port}-timestamp" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{coordinator.host}:{coordinator.port}")}, diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json index 5c8af4df931..b8c7ffe037f 100644 --- a/homeassistant/components/cert_expiry/strings.json +++ b/homeassistant/components/cert_expiry/strings.json @@ -20,5 +20,12 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "import_failed": "Import from config failed" } + }, + "entity": { + "sensor": { + "certificate_expiry": { + "name": "Cert expiry" + } + } } } diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index 2113ff5cc42..29fbf372ec4 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -99,7 +99,7 @@ async def test_unload_config_entry(mock_now, hass: HomeAssistant) -> None: await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state.state == timestamp.isoformat() assert state.attributes.get("error") == "None" assert state.attributes.get("is_valid") @@ -107,12 +107,12 @@ async def test_unload_config_entry(mock_now, hass: HomeAssistant) -> None: await hass.config_entries.async_unload(entry.entry_id) assert entry.state is ConfigEntryState.NOT_LOADED - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state.state == STATE_UNAVAILABLE await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state is None @@ -129,7 +129,7 @@ async def test_delay_load_during_startup(hass: HomeAssistant) -> None: assert hass.state is CoreState.not_running assert entry.state is ConfigEntryState.LOADED - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state is None timestamp = future_timestamp(100) @@ -142,7 +142,7 @@ async def test_delay_load_during_startup(hass: HomeAssistant) -> None: assert hass.state is CoreState.running - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state.state == timestamp.isoformat() assert state.attributes.get("error") == "None" assert state.attributes.get("is_valid") diff --git a/tests/components/cert_expiry/test_sensors.py b/tests/components/cert_expiry/test_sensors.py index 0fbf276cdea..e6a526c7c9e 100644 --- a/tests/components/cert_expiry/test_sensors.py +++ b/tests/components/cert_expiry/test_sensors.py @@ -36,7 +36,7 @@ async def test_async_setup_entry(mock_now, hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state is not None assert state.state != STATE_UNAVAILABLE assert state.state == timestamp.isoformat() @@ -62,7 +62,7 @@ async def test_async_setup_entry_bad_cert(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state is not None assert state.state != STATE_UNAVAILABLE assert state.attributes.get("error") == "some error" @@ -90,7 +90,7 @@ async def test_update_sensor(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state is not None assert state.state != STATE_UNAVAILABLE assert state.state == timestamp.isoformat() @@ -105,7 +105,7 @@ async def test_update_sensor(hass: HomeAssistant) -> None: async_fire_time_changed(hass, utcnow() + timedelta(hours=24)) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state is not None assert state.state != STATE_UNAVAILABLE assert state.state == timestamp.isoformat() @@ -134,7 +134,7 @@ async def test_update_sensor_network_errors(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state is not None assert state.state != STATE_UNAVAILABLE assert state.state == timestamp.isoformat() @@ -152,7 +152,7 @@ async def test_update_sensor_network_errors(hass: HomeAssistant) -> None: next_update = starting_time + timedelta(hours=48) - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state.state == STATE_UNAVAILABLE with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch( @@ -162,7 +162,7 @@ async def test_update_sensor_network_errors(hass: HomeAssistant) -> None: async_fire_time_changed(hass, utcnow() + timedelta(hours=48)) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state is not None assert state.state != STATE_UNAVAILABLE assert state.state == timestamp.isoformat() @@ -178,7 +178,7 @@ async def test_update_sensor_network_errors(hass: HomeAssistant) -> None: async_fire_time_changed(hass, utcnow() + timedelta(hours=72)) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state is not None assert state.state == STATE_UNKNOWN assert state.attributes.get("error") == "something bad" @@ -192,5 +192,5 @@ async def test_update_sensor_network_errors(hass: HomeAssistant) -> None: async_fire_time_changed(hass, utcnow() + timedelta(hours=96)) await hass.async_block_till_done() - state = hass.states.get("sensor.cert_expiry_timestamp_example_com") + state = hass.states.get("sensor.example_com_cert_expiry") assert state.state == STATE_UNAVAILABLE From d761b5ddbf1d956c2ef81fef5d4c8f97f8684296 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Thu, 17 Aug 2023 19:37:34 +0200 Subject: [PATCH 0602/1151] Add tests and typing to Tado config flow (#98281) * Upgrading tests * Code improvements and removing unused function * Update homeassistant/components/tado/config_flow.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/tado/config_flow.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/tado/config_flow.py Co-authored-by: Joost Lekkerkerker * Update tests/components/tado/test_config_flow.py Co-authored-by: Joost Lekkerkerker * Update tests/components/tado/test_config_flow.py Co-authored-by: Joost Lekkerkerker * Update tests/components/tado/test_config_flow.py Co-authored-by: Joost Lekkerkerker * Update tests/components/tado/test_config_flow.py Co-authored-by: Joost Lekkerkerker * Update tests/components/tado/test_config_flow.py Co-authored-by: Joost Lekkerkerker * Importing Any * Rerunning Blackformatter * Adding fallback scenario to options flow * Adding constants * Adding a retry on the exceptions * Refactoring to standard * Update homeassistant/components/tado/config_flow.py Co-authored-by: G Johansson * Adding type to validate_input * Updating test --------- Co-authored-by: Joost Lekkerkerker Co-authored-by: G Johansson --- homeassistant/components/tado/config_flow.py | 24 ++-- tests/components/tado/test_config_flow.py | 127 ++++++++++++++++--- 2 files changed, 122 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index ec195573203..a755622ea76 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from PyTado.interface import Tado import requests.exceptions @@ -31,7 +32,9 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -66,7 +69,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 the initial step.""" errors = {} if user_input is not None: @@ -105,13 +110,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return await self.async_step_user() - def _username_already_configured(self, user_input): - """See if we already have a username matching user input configured.""" - existing_username = { - entry.data[CONF_USERNAME] for entry in self._async_current_entries() - } - return user_input[CONF_USERNAME] in existing_username - @staticmethod @callback def async_get_options_flow( @@ -122,16 +120,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for tado.""" + """Handle an option flow for Tado.""" def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle options flow.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) data_schema = vol.Schema( { diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index f0fef1dff5a..dcbb33b587e 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -2,18 +2,24 @@ from http import HTTPStatus from unittest.mock import MagicMock, patch +import pytest import requests from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.components.tado.const import DOMAIN +from homeassistant.components.tado.const import ( + CONF_FALLBACK, + CONST_OVERLAY_TADO_DEFAULT, + DOMAIN, +) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -def _get_mock_tado_api(getMe=None): +def _get_mock_tado_api(getMe=None) -> MagicMock: mock_tado = MagicMock() if isinstance(getMe, Exception): type(mock_tado).getMe = MagicMock(side_effect=getMe) @@ -22,13 +28,100 @@ def _get_mock_tado_api(getMe=None): return mock_tado -async def test_form(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("exception", "error"), + [ + (KeyError, "invalid_auth"), + (RuntimeError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_form_exceptions( + hass: HomeAssistant, exception: Exception, error: str +) -> None: + """Test we handle Form Exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.tado.config_flow.Tado", + side_effect=exception, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": error} + + # Test a retry to recover, upon failure + mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) + + with patch( + "homeassistant.components.tado.config_flow.Tado", + return_value=mock_tado_api, + ), patch( + "homeassistant.components.tado.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "myhome" + assert result["data"] == { + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test config flow options.""" + entry = MockConfigEntry(domain=DOMAIN, data={"username": "test-username"}) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.tado.async_setup_entry", + return_value=True, + ): + 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, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {CONF_FALLBACK: CONST_OVERLAY_TADO_DEFAULT}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == {CONF_FALLBACK: CONST_OVERLAY_TADO_DEFAULT} + + +async def test_create_entry(hass: HomeAssistant) -> None: """Test we can setup though the user path.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) @@ -40,15 +133,15 @@ async def test_form(hass: HomeAssistant) -> None: "homeassistant.components.tado.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"], {"username": "test-username", "password": "test-password"}, ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" - assert result2["title"] == "myhome" - assert result2["data"] == { + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "myhome" + assert result["data"] == { "username": "test-username", "password": "test-password", } @@ -69,13 +162,13 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: "homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": "test-username", "password": "test-password"}, ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} + assert result["type"] == "form" + assert result["errors"] == {"base": "invalid_auth"} async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -92,13 +185,13 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: "homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": "test-username", "password": "test-password"}, ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result["type"] == "form" + assert result["errors"] == {"base": "cannot_connect"} async def test_no_homes(hass: HomeAssistant) -> None: @@ -113,13 +206,13 @@ async def test_no_homes(hass: HomeAssistant) -> None: "homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": "test-username", "password": "test-password"}, ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "no_homes"} + assert result["type"] == "form" + assert result["errors"] == {"base": "no_homes"} async def test_form_homekit(hass: HomeAssistant) -> None: From c17f08a3f543e63da66d5a10156776ee150c5f11 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 17 Aug 2023 19:41:11 +0200 Subject: [PATCH 0603/1151] Create a single entity for new met.no config entries (#98098) * Create a single entity for new met.no config entries * Fix lying docstring * Fix test --- homeassistant/components/met/weather.py | 55 +++++++++++++++---------- tests/components/met/test_weather.py | 50 ++++++++++++---------- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 2fcde1e05f0..e7aea21875a 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -15,6 +15,7 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_GUST_SPEED, ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, Forecast, WeatherEntity, WeatherEntityFeature, @@ -30,6 +31,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -48,19 +50,38 @@ async def async_setup_entry( ) -> None: """Add a weather entity from a config_entry.""" coordinator: MetDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - [ - MetWeather( - coordinator, - config_entry.data, - hass.config.units is METRIC_SYSTEM, - False, - ), + entity_registry = er.async_get(hass) + + entities = [ + MetWeather( + coordinator, config_entry.data, hass.config.units is METRIC_SYSTEM, False + ) + ] + + # Add hourly entity to legacy config entries + if entity_registry.async_get_entity_id( + WEATHER_DOMAIN, + DOMAIN, + _calculate_unique_id(config_entry.data, True), + ): + entities.append( MetWeather( coordinator, config_entry.data, hass.config.units is METRIC_SYSTEM, True - ), - ] - ) + ) + ) + + async_add_entities(entities) + + +def _calculate_unique_id(config: MappingProxyType[str, Any], hourly: bool) -> str: + """Calculate unique ID.""" + name_appendix = "" + if hourly: + name_appendix = "-hourly" + if config.get(CONF_TRACK_HOME): + return f"home{name_appendix}" + + return f"{config[CONF_LATITUDE]}-{config[CONF_LONGITUDE]}{name_appendix}" def format_condition(condition: str) -> str: @@ -96,6 +117,7 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): ) -> None: """Initialise the platform with a data instance and site.""" super().__init__(coordinator) + self._attr_unique_id = _calculate_unique_id(config, hourly) self._config = config self._is_metric = is_metric self._hourly = hourly @@ -105,17 +127,6 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Return if we are tracking home.""" return self._config.get(CONF_TRACK_HOME, False) - @property - def unique_id(self) -> str: - """Return unique ID.""" - name_appendix = "" - if self._hourly: - name_appendix = "-hourly" - if self.track_home: - return f"home{name_appendix}" - - return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{name_appendix}" - @property def name(self) -> str: """Return the name of the sensor.""" diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 2941935fcfc..5a28b8eceb0 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -6,6 +6,33 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +async def test_new_config_entry(hass: HomeAssistant, mock_weather) -> None: + """Test the expected entities are created.""" + registry = er.async_get(hass) + await hass.config_entries.flow.async_init("met", context={"source": "onboarding"}) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids("weather")) == 1 + + entry = hass.config_entries.async_entries()[0] + assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 1 + + +async def test_legacy_config_entry(hass: HomeAssistant, mock_weather) -> None: + """Test the expected entities are created.""" + registry = er.async_get(hass) + registry.async_get_or_create( + WEATHER_DOMAIN, + DOMAIN, + "home-hourly", + ) + await hass.config_entries.flow.async_init("met", context={"source": "onboarding"}) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids("weather")) == 2 + + entry = hass.config_entries.async_entries()[0] + assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 2 + + async def test_tracking_home(hass: HomeAssistant, mock_weather) -> None: """Test we track home.""" await hass.config_entries.flow.async_init("met", context={"source": "onboarding"}) @@ -13,17 +40,6 @@ async def test_tracking_home(hass: HomeAssistant, mock_weather) -> None: assert len(hass.states.async_entity_ids("weather")) == 1 assert len(mock_weather.mock_calls) == 4 - # Test the hourly sensor is disabled by default - registry = er.async_get(hass) - - state = hass.states.get("weather.forecast_test_home_hourly") - assert state is None - - entry = registry.async_get("weather.forecast_test_home_hourly") - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - # Test we track config await hass.config.async_update(latitude=10, longitude=20) await hass.async_block_till_done() @@ -44,23 +60,13 @@ async def test_tracking_home(hass: HomeAssistant, mock_weather) -> None: async def test_not_tracking_home(hass: HomeAssistant, mock_weather) -> None: """Test when we not track home.""" - # Pre-create registry entry for disabled by default hourly weather - registry = er.async_get(hass) - registry.async_get_or_create( - WEATHER_DOMAIN, - DOMAIN, - "10-20-hourly", - suggested_object_id="forecast_somewhere_hourly", - disabled_by=None, - ) - await hass.config_entries.flow.async_init( "met", context={"source": config_entries.SOURCE_USER}, data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0}, ) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("weather")) == 2 + assert len(hass.states.async_entity_ids("weather")) == 1 assert len(mock_weather.mock_calls) == 4 # Test we do not track config From 49d2c60992bb3741921c968d7e3d9dc810cf7cb7 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 17 Aug 2023 18:58:58 -0500 Subject: [PATCH 0604/1151] Add pipeline VAD events (#98603) * Add stt-vad-start and stt-vad-end pipeline events * Update tests --- .../components/assist_pipeline/pipeline.py | 22 +++++++++++++++++++ .../assist_pipeline/snapshots/test_init.ambr | 6 +++++ tests/components/assist_pipeline/test_init.py | 12 +++++----- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 3303895eec2..320812b2039 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -254,6 +254,8 @@ class PipelineEventType(StrEnum): WAKE_WORD_START = "wake_word-start" WAKE_WORD_END = "wake_word-end" STT_START = "stt-start" + STT_VAD_START = "stt-vad-start" + STT_VAD_END = "stt-vad-end" STT_END = "stt-end" INTENT_START = "intent-start" INTENT_END = "intent-end" @@ -612,11 +614,31 @@ class PipelineRun: stream: AsyncIterable[bytes], ) -> AsyncGenerator[bytes, None]: """Stop stream when voice command is finished.""" + sent_vad_start = False + timestamp_ms = 0 async for chunk in stream: if not segmenter.process(chunk): + # Silence detected at the end of voice command + self.process_event( + PipelineEvent( + PipelineEventType.STT_VAD_END, + {"timestamp": timestamp_ms}, + ) + ) break + if segmenter.in_command and (not sent_vad_start): + # Speech detected at start of voice command + self.process_event( + PipelineEvent( + PipelineEventType.STT_VAD_START, + {"timestamp": timestamp_ms}, + ) + ) + sent_vad_start = True + yield chunk + timestamp_ms += (len(chunk) // 2) // 16 # milliseconds @ 16Khz # Transcribe audio stream result = await self.stt_provider.async_process_audio_stream( diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index d0330952f04..58835e37973 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -311,6 +311,12 @@ }), 'type': , }), + dict({ + 'data': dict({ + 'timestamp': 0, + }), + 'type': , + }), dict({ 'data': dict({ 'stt_output': dict({ diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 44e448aa785..184f479f830 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -40,7 +40,7 @@ async def test_pipeline_from_audio_stream_auto( In this test, no pipeline is specified. """ - events = [] + events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): yield b"part1" @@ -79,7 +79,7 @@ async def test_pipeline_from_audio_stream_legacy( """ client = await hass_ws_client(hass) - events = [] + events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): yield b"part1" @@ -139,7 +139,7 @@ async def test_pipeline_from_audio_stream_entity( """ client = await hass_ws_client(hass) - events = [] + events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): yield b"part1" @@ -199,7 +199,7 @@ async def test_pipeline_from_audio_stream_no_stt( """ client = await hass_ws_client(hass) - events = [] + events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): yield b"part1" @@ -257,7 +257,7 @@ async def test_pipeline_from_audio_stream_unknown_pipeline( In this test, the pipeline does not exist. """ - events = [] + events: list[assist_pipeline.PipelineEvent] = [] async def audio_data(): yield b"part1" @@ -294,7 +294,7 @@ async def test_pipeline_from_audio_stream_wake_word( ) -> None: """Test creating a pipeline from an audio stream with wake word.""" - events = [] + events: list[assist_pipeline.PipelineEvent] = [] # [0, 1, ...] wake_chunk_1 = bytes(it.islice(it.cycle(range(256)), BYTES_ONE_SECOND)) From f6a9be937b8144973ea9402a9f0a7852048cc6f1 Mon Sep 17 00:00:00 2001 From: lymanepp <4195527+lymanepp@users.noreply.github.com> Date: Fri, 18 Aug 2023 01:41:25 -0400 Subject: [PATCH 0605/1151] Add humidity and dew point to tomorrow.io integration (#98496) * Add humidity and dew point to tomorrow.io integration * Fix ruff complaints * Make mypy happy * Merge emontnemery's changes * Fix formatting error * Add fake humidity and dew point to test data (first interval only) * Fix inconsistency * Fix inconsistency --- homeassistant/components/tomorrowio/weather.py | 11 +++++++++++ tests/components/tomorrowio/fixtures/v4.json | 4 ++++ .../tomorrowio/snapshots/test_weather.ambr | 12 ++++++++++++ tests/components/tomorrowio/test_weather.py | 6 ++++++ 4 files changed, 33 insertions(+) diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index 333aa0cd472..ec77a2c8040 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -7,6 +7,8 @@ from pytomorrowio.const import DAILY, FORECASTS, HOURLY, NOWCAST, WeatherCode from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_HUMIDITY, + ATTR_FORECAST_NATIVE_DEW_POINT, ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP_LOW, @@ -44,6 +46,7 @@ from .const import ( DOMAIN, MAX_FORECASTS, TMRW_ATTR_CONDITION, + TMRW_ATTR_DEW_POINT, TMRW_ATTR_HUMIDITY, TMRW_ATTR_OZONE, TMRW_ATTR_PRECIPITATION, @@ -138,6 +141,8 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): precipitation_probability: int | None, temp: float | None, temp_low: float | None, + humidity: float | None, + dew_point: float | None, wind_direction: float | None, wind_speed: float | None, ) -> Forecast: @@ -156,6 +161,8 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, ATTR_FORECAST_NATIVE_TEMP: temp, ATTR_FORECAST_NATIVE_TEMP_LOW: temp_low, + ATTR_FORECAST_HUMIDITY: humidity, + ATTR_FORECAST_NATIVE_DEW_POINT: dew_point, ATTR_FORECAST_WIND_BEARING: wind_direction, ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed, } @@ -259,6 +266,8 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): temp = values.get(TMRW_ATTR_TEMPERATURE_HIGH) temp_low = None + dew_point = values.get(TMRW_ATTR_DEW_POINT) + humidity = values.get(TMRW_ATTR_HUMIDITY) wind_direction = values.get(TMRW_ATTR_WIND_DIRECTION) wind_speed = values.get(TMRW_ATTR_WIND_SPEED) @@ -285,6 +294,8 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): precipitation_probability, temp, temp_low, + humidity, + dew_point, wind_direction, wind_speed, ) diff --git a/tests/components/tomorrowio/fixtures/v4.json b/tests/components/tomorrowio/fixtures/v4.json index 0ca4f348956..c511263fb5f 100644 --- a/tests/components/tomorrowio/fixtures/v4.json +++ b/tests/components/tomorrowio/fixtures/v4.json @@ -908,6 +908,8 @@ "values": { "temperatureMin": 44.13, "temperatureMax": 44.13, + "dewPoint": 12.76, + "humidity": 58.46, "windSpeed": 9.33, "windDirection": 315.14, "weatherCode": 1000, @@ -2206,6 +2208,8 @@ "values": { "temperatureMin": 26.11, "temperatureMax": 45.93, + "dewPoint": 12.76, + "humidity": 58.46, "windSpeed": 9.49, "windDirection": 239.6, "weatherCode": 1000, diff --git a/tests/components/tomorrowio/snapshots/test_weather.ambr b/tests/components/tomorrowio/snapshots/test_weather.ambr index 40ff18658c6..a938cb10e44 100644 --- a/tests/components/tomorrowio/snapshots/test_weather.ambr +++ b/tests/components/tomorrowio/snapshots/test_weather.ambr @@ -4,6 +4,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T11:00:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 45.9, @@ -148,6 +150,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T11:00:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 45.9, @@ -292,6 +296,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T17:48:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 44.1, @@ -512,6 +518,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T17:48:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 44.1, @@ -733,6 +741,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T11:00:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 45.9, @@ -879,6 +889,8 @@ dict({ 'condition': 'sunny', 'datetime': '2021-03-07T17:48:00+00:00', + 'dew_point': 12.8, + 'humidity': 58, 'precipitation': 0.0, 'precipitation_probability': 0, 'temperature': 44.1, diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index 8490b94a7f9..a6a5e935614 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -24,6 +24,8 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_FORECAST, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_DEW_POINT, + ATTR_FORECAST_HUMIDITY, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -164,6 +166,8 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, ATTR_FORECAST_TEMP: 45.9, ATTR_FORECAST_TEMP_LOW: 26.1, + ATTR_FORECAST_DEW_POINT: 12.8, + ATTR_FORECAST_HUMIDITY: 58, ATTR_FORECAST_WIND_BEARING: 239.6, ATTR_FORECAST_WIND_SPEED: 34.16, # 9.49 m/s -> km/h } @@ -191,6 +195,8 @@ async def test_v4_weather_legacy_entities(hass: HomeAssistant) -> None: assert weather_state.attributes[ATTR_FORECAST][0] == { ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", + ATTR_FORECAST_DEW_POINT: 12.8, + ATTR_FORECAST_HUMIDITY: 58, ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, ATTR_FORECAST_TEMP: 45.9, From 9fdad592c213aa29cca1c9f7bfaefaec05769289 Mon Sep 17 00:00:00 2001 From: Faidon Liambotis Date: Fri, 18 Aug 2023 09:23:48 +0300 Subject: [PATCH 0606/1151] Add option to disable MQTT Alarm Control Panel supported features (#98363) * Make MQTT Alarm Control Panel features conditional The MQTT Alarm Control Panel currently enables all features (arm home, arm away, arm night, arm vacation, arm custom bypass) unconditionally. This clutters the interface and can even be potentially dangerous, by enabling modes that the remote alarm may not support. Make all the features conditional, by adding a new "supported_features" configuration option, comprising a list of the supported features as options. Feature enablement seems inconsistent across the MQTT component; this implementation is most alike to the Humidifier modes option, but using a generic "supported_features" name that other implementations may reuse in the future. The default value of this new setting remains to be all features, which while it may be overly expansive, is necessary to maintain backwards compatibility. * Apply suggestions from code review * Use vol.Optional() instead of vol.Required() for "supported_features". * Move the initialization of _attr_supported_features to _setup_from_config. Co-authored-by: Jan Bouwhuis * Apply suggestions from emontnemery's code review * Use vol.In() instead of cv.multi_seelct() * Remove superfluous _attr_supported_features initializers, already present in the base class. Co-authored-by: Erik Montnemery * Add invalid config tests for the MQTT Alarm Control Panel * Set expected_features to None in the invalid MQTT Alarm Control Panel tests * Add another expected_features=None in the invalid tests Co-authored-by: Jan Bouwhuis --------- Co-authored-by: Jan Bouwhuis Co-authored-by: Erik Montnemery --- .../components/mqtt/alarm_control_panel.py | 28 +++--- homeassistant/components/mqtt/const.py | 1 + .../mqtt/test_alarm_control_panel.py | 93 +++++++++++++++++++ 3 files changed, 110 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 06f91403057..a0939fdc615 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -39,6 +39,7 @@ from .const import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + CONF_SUPPORTED_FEATURES, ) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -47,6 +48,15 @@ from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) +_SUPPORTED_FEATURES = { + "arm_home": AlarmControlPanelEntityFeature.ARM_HOME, + "arm_away": AlarmControlPanelEntityFeature.ARM_AWAY, + "arm_night": AlarmControlPanelEntityFeature.ARM_NIGHT, + "arm_vacation": AlarmControlPanelEntityFeature.ARM_VACATION, + "arm_custom_bypass": AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, + "trigger": AlarmControlPanelEntityFeature.TRIGGER, +} + CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CODE_DISARM_REQUIRED = "code_disarm_required" CONF_CODE_TRIGGER_REQUIRED = "code_trigger_required" @@ -81,6 +91,9 @@ REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT" PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { + vol.Optional(CONF_SUPPORTED_FEATURES, default=list(_SUPPORTED_FEATURES)): [ + vol.In(_SUPPORTED_FEATURES) + ], vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_DISARM_REQUIRED, default=True): cv.boolean, @@ -167,6 +180,9 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): config[CONF_COMMAND_TEMPLATE], entity=self ).async_render + for feature in self._config[CONF_SUPPORTED_FEATURES]: + self._attr_supported_features |= _SUPPORTED_FEATURES[feature] + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @@ -214,18 +230,6 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): """Return the state of the device.""" return self._state - @property - def supported_features(self) -> AlarmControlPanelEntityFeature: - """Return the list of supported features.""" - return ( - AlarmControlPanelEntityFeature.ARM_HOME - | AlarmControlPanelEntityFeature.ARM_AWAY - | AlarmControlPanelEntityFeature.ARM_NIGHT - | AlarmControlPanelEntityFeature.ARM_VACATION - | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS - | AlarmControlPanelEntityFeature.TRIGGER - ) - @property def code_format(self) -> alarm.CodeFormat | None: """Return one or more digits/characters.""" diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index fcdfeb4bd7d..97d2e1473f5 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -28,6 +28,7 @@ CONF_WS_PATH = "ws_path" CONF_WS_HEADERS = "ws_headers" CONF_WILL_MESSAGE = "will_message" CONF_PAYLOAD_RESET = "payload_reset" +CONF_SUPPORTED_FEATURES = "supported_features" CONF_ACTION_TEMPLATE = "action_template" CONF_ACTION_TOPIC = "action_topic" diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index e69839e6b16..35fba9e2a0c 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -7,6 +7,7 @@ from unittest.mock import patch import pytest from homeassistant.components import alarm_control_panel, mqtt +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.components.mqtt.alarm_control_panel import ( MQTT_ALARM_ATTRIBUTES_BLOCKED, ) @@ -74,6 +75,15 @@ from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient CODE_NUMBER = "1234" CODE_TEXT = "HELLO_CODE" +DEFAULT_FEATURES = ( + AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_NIGHT + | AlarmControlPanelEntityFeature.ARM_VACATION + | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS + | AlarmControlPanelEntityFeature.TRIGGER +) + DEFAULT_CONFIG = { mqtt.DOMAIN: { alarm_control_panel.DOMAIN: { @@ -223,6 +233,89 @@ async def test_ignore_update_state_if_unknown_via_state_topic( assert hass.states.get(entity_id).state == STATE_UNKNOWN +@pytest.mark.parametrize( + ("hass_config", "expected_features", "valid"), + [ + ( + DEFAULT_CONFIG, + DEFAULT_FEATURES, + True, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": []},), + ), + AlarmControlPanelEntityFeature(0), + True, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": ["arm_home"]},), + ), + AlarmControlPanelEntityFeature.ARM_HOME, + True, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": ["arm_home", "arm_away"]},), + ), + AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY, + True, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": "invalid"},), + ), + None, + False, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": ["invalid"]},), + ), + None, + False, + ), + ( + help_custom_config( + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, + ({"supported_features": ["arm_home", "invalid"]},), + ), + None, + False, + ), + ], +) +async def test_supported_features( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_features: AlarmControlPanelEntityFeature | None, + valid: bool, +) -> None: + """Test conditional enablement of supported features.""" + if valid: + await mqtt_mock_entry() + assert ( + hass.states.get("alarm_control_panel.test").attributes["supported_features"] + == expected_features + ) + else: + with pytest.raises(AssertionError): + await mqtt_mock_entry() + + @pytest.mark.parametrize( ("hass_config", "service", "payload"), [ From ab9d6ce61ac27338372a1563aca800966b170433 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 18 Aug 2023 08:40:23 +0200 Subject: [PATCH 0607/1151] New integration for Comelit SimpleHome (#96552) * New integration for Comelit SimpleHome * Address first review comments * cleanup * aiocomelit bump and coordinator cleanup * address review comments * Fix some review comments * Use config_entry.unique_id as last resort * review comments * Add config_flow tests * fix pre-commit missing checks * test_conflig_flow coverage to 100% * fix tests * address latest review comments * new ruff rule * address review comments * simplify unique_id --- .coveragerc | 4 + CODEOWNERS | 2 + homeassistant/components/comelit/__init__.py | 34 ++++ .../components/comelit/config_flow.py | 145 +++++++++++++++++ homeassistant/components/comelit/const.py | 6 + .../components/comelit/coordinator.py | 50 ++++++ homeassistant/components/comelit/light.py | 78 +++++++++ .../components/comelit/manifest.json | 10 ++ homeassistant/components/comelit/strings.json | 31 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/comelit/__init__.py | 1 + tests/components/comelit/const.py | 16 ++ tests/components/comelit/test_config_flow.py | 154 ++++++++++++++++++ 16 files changed, 544 insertions(+) create mode 100644 homeassistant/components/comelit/__init__.py create mode 100644 homeassistant/components/comelit/config_flow.py create mode 100644 homeassistant/components/comelit/const.py create mode 100644 homeassistant/components/comelit/coordinator.py create mode 100644 homeassistant/components/comelit/light.py create mode 100644 homeassistant/components/comelit/manifest.json create mode 100644 homeassistant/components/comelit/strings.json create mode 100644 tests/components/comelit/__init__.py create mode 100644 tests/components/comelit/const.py create mode 100644 tests/components/comelit/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 71542ebad3a..93958a67973 100644 --- a/.coveragerc +++ b/.coveragerc @@ -168,6 +168,10 @@ omit = homeassistant/components/cmus/media_player.py homeassistant/components/coinbase/sensor.py homeassistant/components/comed_hourly_pricing/sensor.py + homeassistant/components/comelit/__init__.py + homeassistant/components/comelit/const.py + homeassistant/components/comelit/coordinator.py + homeassistant/components/comelit/light.py homeassistant/components/comfoconnect/fan.py homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index bd1b8ed49f0..812caea4da5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -209,6 +209,8 @@ build.json @home-assistant/supervisor /tests/components/coinbase/ @tombrien /homeassistant/components/color_extractor/ @GenericStudent /tests/components/color_extractor/ @GenericStudent +/homeassistant/components/comelit/ @chemelli74 +/tests/components/comelit/ @chemelli74 /homeassistant/components/comfoconnect/ @michaelarnauts /tests/components/comfoconnect/ @michaelarnauts /homeassistant/components/command_line/ @gjohansson-ST diff --git a/homeassistant/components/comelit/__init__.py b/homeassistant/components/comelit/__init__.py new file mode 100644 index 00000000000..2c73922582c --- /dev/null +++ b/homeassistant/components/comelit/__init__.py @@ -0,0 +1,34 @@ +"""Comelit integration.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PIN, Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import ComelitSerialBridge + +PLATFORMS = [Platform.LIGHT] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Comelit platform.""" + coordinator = ComelitSerialBridge(hass, entry.data[CONF_HOST], entry.data[CONF_PIN]) + + 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) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + coordinator: ComelitSerialBridge = hass.data[DOMAIN][entry.entry_id] + await coordinator.api.logout() + await coordinator.api.close() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py new file mode 100644 index 00000000000..dd6227a6583 --- /dev/null +++ b/homeassistant/components/comelit/config_flow.py @@ -0,0 +1,145 @@ +"""Config flow for Comelit integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from aiocomelit import ComeliteSerialBridgeAPi, exceptions as aiocomelit_exceptions +import voluptuous as vol + +from homeassistant import core, exceptions +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PIN +from homeassistant.data_entry_flow import FlowResult + +from .const import _LOGGER, DOMAIN + +DEFAULT_HOST = "192.168.1.252" +DEFAULT_PIN = "111111" + + +def user_form_schema(user_input: dict[str, Any] | None) -> vol.Schema: + """Return user form schema.""" + user_input = user_input or {} + return vol.Schema( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): str, + vol.Optional(CONF_PIN, default=DEFAULT_PIN): str, + } + ) + + +STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): str}) + + +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: + """Validate the user input allows us to connect.""" + + api = ComeliteSerialBridgeAPi(data[CONF_HOST], data[CONF_PIN]) + + try: + await api.login() + except aiocomelit_exceptions.CannotConnect as err: + raise CannotConnect from err + except aiocomelit_exceptions.CannotAuthenticate as err: + raise InvalidAuth from err + finally: + await api.logout() + await api.close() + + return {"title": data[CONF_HOST]} + + +class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Comelit.""" + + VERSION = 1 + _reauth_entry: ConfigEntry | None + _reauth_host: str + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=user_form_schema(user_input) + ) + + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=user_form_schema(user_input), errors=errors + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle reauth flow.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + self._reauth_host = entry_data[CONF_HOST] + self.context["title_placeholders"] = {"host": self._reauth_host} + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle reauth confirm.""" + assert self._reauth_entry + errors = {} + + if user_input is not None: + try: + await validate_input( + self.hass, {CONF_HOST: self._reauth_host} | user_input + ) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self.hass.config_entries.async_update_entry( + self._reauth_entry, + data={ + CONF_HOST: self._reauth_host, + CONF_PIN: user_input[CONF_PIN], + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders={CONF_HOST: self._reauth_entry.data[CONF_HOST]}, + data_schema=STEP_REAUTH_DATA_SCHEMA, + errors=errors, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/comelit/const.py b/homeassistant/components/comelit/const.py new file mode 100644 index 00000000000..e08caa55f76 --- /dev/null +++ b/homeassistant/components/comelit/const.py @@ -0,0 +1,6 @@ +"""Comelit constants.""" +import logging + +_LOGGER = logging.getLogger(__package__) + +DOMAIN = "comelit" diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py new file mode 100644 index 00000000000..beb7266c403 --- /dev/null +++ b/homeassistant/components/comelit/coordinator.py @@ -0,0 +1,50 @@ +"""Support for Comelit.""" +import asyncio +from datetime import timedelta +from typing import Any + +from aiocomelit import ComeliteSerialBridgeAPi +import aiohttp + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import _LOGGER, DOMAIN + + +class ComelitSerialBridge(DataUpdateCoordinator): + """Queries Comelit Serial Bridge.""" + + def __init__(self, hass: HomeAssistant, host: str, pin: int) -> None: + """Initialize the scanner.""" + + self._host = host + self._pin = pin + + self.api = ComeliteSerialBridgeAPi(host, pin) + + super().__init__( + hass=hass, + logger=_LOGGER, + name=f"{DOMAIN}-{host}-coordinator", + update_interval=timedelta(seconds=5), + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update router data.""" + _LOGGER.debug("Polling Comelit Serial Bridge host: %s", self._host) + try: + logged = await self.api.login() + except (asyncio.exceptions.TimeoutError, aiohttp.ClientConnectorError) as err: + _LOGGER.warning("Connection error for %s", self._host) + raise UpdateFailed(f"Error fetching data: {repr(err)}") from err + + if not logged: + raise ConfigEntryAuthFailed + + devices_data = await self.api.get_all_devices() + alarm_data = await self.api.get_alarm_config() + await self.api.logout() + + return devices_data | alarm_data diff --git a/homeassistant/components/comelit/light.py b/homeassistant/components/comelit/light.py new file mode 100644 index 00000000000..9a893bd929c --- /dev/null +++ b/homeassistant/components/comelit/light.py @@ -0,0 +1,78 @@ +"""Support for lights.""" +from __future__ import annotations + +from typing import Any + +from aiocomelit import ComelitSerialBridgeObject +from aiocomelit.const import LIGHT, LIGHT_OFF, LIGHT_ON + +from homeassistant.components.light import LightEntity +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 homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import ComelitSerialBridge + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Comelit lights.""" + + coordinator: ComelitSerialBridge = hass.data[DOMAIN][config_entry.entry_id] + + # Use config_entry.entry_id as base for unique_id because no serial number or mac is available + async_add_entities( + ComelitLightEntity(coordinator, device, config_entry.entry_id) + for device in coordinator.data[LIGHT].values() + ) + + +class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity): + """Light device.""" + + _attr_has_entity_name = True + _attr_name = None + + def __init__( + self, + coordinator: ComelitSerialBridge, + device: ComelitSerialBridgeObject, + config_entry_unique_id: str | None, + ) -> None: + """Init light entity.""" + self._api = coordinator.api + self._device = device + super().__init__(coordinator) + self._attr_unique_id = f"{config_entry_unique_id}-{device.index}" + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, self._attr_unique_id), + }, + manufacturer="Comelit", + model="Serial Bridge", + name=device.name, + ) + + async def _light_set_state(self, state: int) -> None: + """Set desired light state.""" + await self.coordinator.api.light_switch(self._device.index, state) + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + await self._light_set_state(LIGHT_ON) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self._light_set_state(LIGHT_OFF) + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self.coordinator.data[LIGHT][self._device.index].status == LIGHT_ON diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json new file mode 100644 index 00000000000..fc7f2a3fc12 --- /dev/null +++ b/homeassistant/components/comelit/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "comelit", + "name": "Comelit SimpleHome", + "codeowners": ["@chemelli74"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/comelit", + "iot_class": "local_polling", + "loggers": ["aiocomelit"], + "requirements": ["aiocomelit==0.0.5"] +} diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json new file mode 100644 index 00000000000..6508f58412e --- /dev/null +++ b/homeassistant/components/comelit/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "description": "Please enter the correct PIN for VEDO system: {host}", + "data": { + "pin": "[%key:common::config_flow::data::pin%]" + } + }, + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "pin": "[%key:common::config_flow::data::pin%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7de32dc5071..0bfbf362eb3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -81,6 +81,7 @@ FLOWS = { "cloudflare", "co2signal", "coinbase", + "comelit", "control4", "coolmaster", "cpuspeed", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index ed51bcc7dbf..40883ef3d7c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -883,6 +883,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "comelit": { + "name": "Comelit SimpleHome", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "comfoconnect": { "name": "Zehnder ComfoAir Q", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 65a6ab08bb2..66e73e07026 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -208,6 +208,9 @@ aiobafi6==0.8.2 # homeassistant.components.aws aiobotocore==2.1.0 +# homeassistant.components.comelit +aiocomelit==0.0.5 + # homeassistant.components.dhcp aiodiscover==1.4.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7df3a7172b4..36450cb31da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -189,6 +189,9 @@ aiobafi6==0.8.2 # homeassistant.components.aws aiobotocore==2.1.0 +# homeassistant.components.comelit +aiocomelit==0.0.5 + # homeassistant.components.dhcp aiodiscover==1.4.16 diff --git a/tests/components/comelit/__init__.py b/tests/components/comelit/__init__.py new file mode 100644 index 00000000000..916a684de4b --- /dev/null +++ b/tests/components/comelit/__init__.py @@ -0,0 +1 @@ +"""Tests for the Comelit SimpleHome integration.""" diff --git a/tests/components/comelit/const.py b/tests/components/comelit/const.py new file mode 100644 index 00000000000..36955b0b0a9 --- /dev/null +++ b/tests/components/comelit/const.py @@ -0,0 +1,16 @@ +"""Common stuff for Comelit SimpleHome tests.""" +from homeassistant.components.comelit.const import DOMAIN +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PIN + +MOCK_CONFIG = { + DOMAIN: { + CONF_DEVICES: [ + { + CONF_HOST: "fake_host", + CONF_PIN: "1234", + } + ] + } +} + +MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] diff --git a/tests/components/comelit/test_config_flow.py b/tests/components/comelit/test_config_flow.py new file mode 100644 index 00000000000..2fb9e836efb --- /dev/null +++ b/tests/components/comelit/test_config_flow.py @@ -0,0 +1,154 @@ +"""Tests for Comelit SimpleHome config flow.""" +from unittest.mock import patch + +from aiocomelit import CannotAuthenticate, CannotConnect +import pytest + +from homeassistant.components.comelit.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import MOCK_USER_DATA + +from tests.common import MockConfigEntry + + +async def test_user(hass: HomeAssistant) -> None: + """Test starting a flow by user.""" + with patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.login", + ), patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + ), patch( + "homeassistant.components.comelit.async_setup_entry" + ) as mock_setup_entry, patch( + "requests.get" + ) as mock_request_get: + mock_request_get.return_value.status_code = 200 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_PIN] == "1234" + assert not result["result"].unique_id + await hass.async_block_till_done() + + assert mock_setup_entry.called + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (CannotConnect, "cannot_connect"), + (CannotAuthenticate, "invalid_auth"), + (ConnectionResetError, "unknown"), + ], +) +async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> None: + """Test starting a flow by user with a connection error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + with patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.login", + side_effect=side_effect, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == error + + +async def test_reauth_successful(hass: HomeAssistant) -> None: + """Test starting a reauthentication flow.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.login", + ), patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + ), patch("homeassistant.components.comelit.async_setup_entry"), patch( + "requests.get" + ) as mock_request_get: + mock_request_get.return_value.status_code = 200 + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PIN: "other_fake_pin", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (CannotConnect, "cannot_connect"), + (CannotAuthenticate, "invalid_auth"), + (ConnectionResetError, "unknown"), + ], +) +async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> None: + """Test starting a reauthentication flow but no connection found.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.login", side_effect=side_effect + ), patch( + "aiocomelit.api.ComeliteSerialBridgeAPi.logout", + ), patch( + "homeassistant.components.comelit.async_setup_entry" + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PIN: "other_fake_pin", + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"]["base"] == error From 6b82bf2bc7bc56eb1b0b1ca59c70b3d243816721 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 18 Aug 2023 01:07:44 -0700 Subject: [PATCH 0608/1151] Fix Flume leak detected sensor (#98560) --- homeassistant/components/flume/coordinator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index 70a99f56968..1f590b0cd16 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -93,8 +93,11 @@ class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]): def _update_lists(self): """Query flume for notification list.""" + # Get notifications (read or unread). + # The related binary sensors (leak detected, high flow, low battery) + # will be active until the notification is deleted in the Flume app. self.notifications: list[dict[str, Any]] = pyflume.FlumeNotificationList( - self.auth, read="true" + self.auth, read=None ).notification_list _LOGGER.debug("Notifications %s", self.notifications) From d3ee2366b0e7660087b1c70075960c20f99de9e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Aug 2023 03:09:15 -0500 Subject: [PATCH 0609/1151] Bump dbus-fast to 1.91.4 (#98600) --- 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 b1281af2bc2..99cbfe918b7 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.8.0", - "dbus-fast==1.91.2" + "dbus-fast==1.91.4" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bac607545e6..72b9872d3bf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.8.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 -dbus-fast==1.91.2 +dbus-fast==1.91.4 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 66e73e07026..b32eb8a2d05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.91.2 +dbus-fast==1.91.4 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36450cb31da..36356e0588c 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.91.2 +dbus-fast==1.91.4 # homeassistant.components.debugpy debugpy==1.6.7 From 89705a22cf9bd0362bd67c2ef7b4ba221b4e0c16 Mon Sep 17 00:00:00 2001 From: Niels Perfors Date: Fri, 18 Aug 2023 10:26:01 +0200 Subject: [PATCH 0610/1151] Verisure unpack (#98605) --- .../components/verisure/coordinator.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index bc3b68922b0..bbfaed0a0a4 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -83,13 +83,16 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed("Could not read overview") from err def unpack(overview: list, value: str) -> dict | list: - return next( - ( - item["data"]["installation"][value] - for item in overview - if value in item.get("data", {}).get("installation", {}) - ), - [], + return ( + next( + ( + item["data"]["installation"][value] + for item in overview + if value in item.get("data", {}).get("installation", {}) + ), + [], + ) + or [] ) # Store data in a way Home Assistant can easily consume it From 2f204d5747a97de4659b792a3b84c78f3089f1a7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Aug 2023 10:38:21 +0200 Subject: [PATCH 0611/1151] Remove unneeded startswith in content check of image upload (#98599) --- homeassistant/components/image_upload/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/image_upload/__init__.py b/homeassistant/components/image_upload/__init__.py index 6486d584b0e..6faa690b4cb 100644 --- a/homeassistant/components/image_upload/__init__.py +++ b/homeassistant/components/image_upload/__init__.py @@ -78,8 +78,10 @@ class ImageStorageCollection(collection.DictStorageCollection): data = self.CREATE_SCHEMA(dict(data)) uploaded_file: FileField = data["file"] - if not uploaded_file.content_type.startswith( - ("image/gif", "image/jpeg", "image/png") + if uploaded_file.content_type not in ( + "image/gif", + "image/jpeg", + "image/png", ): raise vol.Invalid("Only jpeg, png, and gif images are allowed") From 5a7084e78c4f66660ff59a239f0d2bdf83c5dc80 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 18 Aug 2023 10:48:57 +0200 Subject: [PATCH 0612/1151] Correct number of registers to read for sensors for modbus (#98534) --- homeassistant/components/modbus/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index bed5932a303..e4f4f9b8d66 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -68,7 +68,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): """Initialize the modbus register sensor.""" super().__init__(hub, entry) if slave_count: - self._count = self._count * slave_count + self._count = self._count * (slave_count + 1) 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 d5338e88f214ad89761a0fb5432b267a3d2f018b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 18 Aug 2023 08:49:43 +0000 Subject: [PATCH 0613/1151] Fix the availability condition for Shelly N current sensor (#98518) --- homeassistant/components/shelly/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 896ffd72327..cd9980921c8 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -541,7 +541,8 @@ RPC_SENSORS: Final = { native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, - available=lambda status: status["n_current"] is not None, + available=lambda status: (status and status["n_current"]) is not None, + removal_condition=lambda _config, status, _key: "n_current" not in status, entity_registry_enabled_default=False, ), "total_current": RpcSensorDescription( From 9be532cea9370ac5a0e6415825a945f2c448de9b Mon Sep 17 00:00:00 2001 From: Luca Leonardo Scorcia Date: Fri, 18 Aug 2023 04:52:22 -0400 Subject: [PATCH 0614/1151] Fix inconsistent lyric temperature unit (#98457) --- homeassistant/components/lyric/climate.py | 22 +++++++++++++--------- homeassistant/components/lyric/sensor.py | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index 099a0a028d0..df90ebcd6cf 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -21,7 +21,12 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_HALVES, + PRECISION_WHOLE, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform @@ -113,7 +118,6 @@ async def async_setup_entry( ), location, device, - hass.config.units.temperature_unit, ) ) @@ -140,10 +144,15 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): description: ClimateEntityDescription, location: LyricLocation, device: LyricDevice, - temperature_unit: str, ) -> None: """Initialize Honeywell Lyric climate entity.""" - self._temperature_unit = temperature_unit + # Use the native temperature unit from the device settings + if device.units == "Fahrenheit": + self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT + self._attr_precision = PRECISION_WHOLE + else: + self._attr_temperature_unit = UnitOfTemperature.CELSIUS + self._attr_precision = PRECISION_HALVES # Setup supported hvac modes self._attr_hvac_modes = [HVACMode.OFF] @@ -176,11 +185,6 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): return SUPPORT_FLAGS_LCC return SUPPORT_FLAGS_TCC - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return self._temperature_unit - @property def current_temperature(self) -> float | None: """Return the current temperature.""" diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 1201a675a5d..1e15ff58b18 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -17,7 +17,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE +from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -76,6 +76,11 @@ async def async_setup_entry( for location in coordinator.data.locations: for device in location.devices: if device.indoorTemperature: + if device.units == "Fahrenheit": + native_temperature_unit = UnitOfTemperature.FAHRENHEIT + else: + native_temperature_unit = UnitOfTemperature.CELSIUS + entities.append( LyricSensor( coordinator, @@ -84,7 +89,7 @@ async def async_setup_entry( name="Indoor Temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=hass.config.units.temperature_unit, + native_unit_of_measurement=native_temperature_unit, value=lambda device: device.indoorTemperature, ), location, @@ -108,6 +113,11 @@ async def async_setup_entry( ) ) if device.outdoorTemperature: + if device.units == "Fahrenheit": + native_temperature_unit = UnitOfTemperature.FAHRENHEIT + else: + native_temperature_unit = UnitOfTemperature.CELSIUS + entities.append( LyricSensor( coordinator, @@ -116,7 +126,7 @@ async def async_setup_entry( name="Outdoor Temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=hass.config.units.temperature_unit, + native_unit_of_measurement=native_temperature_unit, value=lambda device: device.outdoorTemperature, ), location, From e42b9e6c4c7a85847d3de25e4b40093b2917c3c0 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 18 Aug 2023 10:52:57 +0200 Subject: [PATCH 0615/1151] Modbus: set state_class etc in slaves. (#98332) --- homeassistant/components/modbus/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index e4f4f9b8d66..97794729ab2 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_NAME, CONF_SENSORS, CONF_UNIQUE_ID, @@ -72,6 +73,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): 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) + self._attr_device_class = entry.get(CONF_DEVICE_CLASS) async def async_setup_slaves( self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any] @@ -160,6 +162,8 @@ class SlaveSensor( self._attr_unique_id = entry.get(CONF_UNIQUE_ID) if self._attr_unique_id: self._attr_unique_id = f"{self._attr_unique_id}_{idx}" + self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) + self._attr_state_class = entry.get(CONF_STATE_CLASS) self._attr_available = False super().__init__(coordinator) From 59d37f65d5d59e2077ccc35e5440cbf56fdfeffe Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 18 Aug 2023 10:55:39 +0200 Subject: [PATCH 0616/1151] Correct modbus config validator: slave/swap (#97798) --- homeassistant/components/modbus/validators.py | 71 +++++++++++-------- tests/components/modbus/test_init.py | 11 ++- tests/components/modbus/test_sensor.py | 2 +- 3 files changed, 53 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index ee9d40dd874..40461e3effd 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -65,25 +65,14 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: name = config[CONF_NAME] structure = config.get(CONF_STRUCTURE) slave_count = config.get(CONF_SLAVE_COUNT, 0) + 1 - swap_type = config.get(CONF_SWAP) - if config[CONF_DATA_TYPE] != DataType.CUSTOM: - if structure: - error = f"{name} structure: cannot be mixed with {data_type}" + slave = config.get(CONF_SLAVE, 0) + swap_type = config.get(CONF_SWAP, CONF_SWAP_NONE) + if config[CONF_DATA_TYPE] == DataType.CUSTOM: + if slave or slave_count > 1: + error = f"{name}: `{CONF_STRUCTURE}` illegal with `{CONF_SLAVE_COUNT}` / `{CONF_SLAVE}`" raise vol.Invalid(error) - if data_type not in DEFAULT_STRUCT_FORMAT: - error = f"Error in sensor {name}. data_type `{data_type}` not supported" - raise vol.Invalid(error) - - structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" - if CONF_COUNT not in config: - config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count - if slave_count > 1: - structure = f">{slave_count}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" - else: - structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" - else: - if slave_count > 1: - error = f"{name} structure: cannot be mixed with {CONF_SLAVE_COUNT}" + if swap_type != CONF_SWAP_NONE: + error = f"{name}: `{CONF_STRUCTURE}` illegal with `{CONF_SWAP}`" raise vol.Invalid(error) if not structure: error = ( @@ -102,19 +91,43 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: f"Structure request {size} bytes, " f"but {count} registers have a size of {bytecount} bytes" ) + return { + **config, + CONF_STRUCTURE: structure, + CONF_SWAP: swap_type, + } - if swap_type != CONF_SWAP_NONE: - if swap_type == CONF_SWAP_BYTE: - regs_needed = 1 - else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD - regs_needed = 2 - if count < regs_needed or (count % regs_needed) != 0: - raise vol.Invalid( - f"Error in sensor {name} swap({swap_type}) " - "not possible due to the registers " - f"count: {count}, needed: {regs_needed}" - ) + if structure: + error = f"{name} structure: cannot be mixed with {data_type}" + raise vol.Invalid(error) + if data_type not in DEFAULT_STRUCT_FORMAT: + error = f"Error in sensor {name}. data_type `{data_type}` not supported" + raise vol.Invalid(error) + if (slave or slave_count > 1) and data_type == DataType.STRING: + error = ( + f"{name}: `{data_type}` illegal with `{CONF_SLAVE_COUNT}` / `{CONF_SLAVE}`" + ) + raise vol.Invalid(error) + if CONF_COUNT not in config: + config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count + if swap_type != CONF_SWAP_NONE: + if swap_type == CONF_SWAP_BYTE: + regs_needed = 1 + else: # CONF_SWAP_WORD_BYTE, CONF_SWAP_WORD + regs_needed = 2 + count = config[CONF_COUNT] + if count < regs_needed or (count % regs_needed) != 0: + raise vol.Invalid( + f"Error in sensor {name} swap({swap_type}) " + "not possible due to the registers " + f"count: {count}, needed: {regs_needed}" + ) + structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" + if slave_count > 1: + structure = f">{slave_count}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" + else: + structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" return { **config, CONF_STRUCTURE: structure, diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 35c01ec478b..6ad1e33821c 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -181,7 +181,6 @@ async def test_nan_validator() -> None: CONF_COUNT: 2, CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: ">i", - CONF_SWAP: CONF_SWAP_BYTE, }, ], ) @@ -239,6 +238,16 @@ async def test_ok_struct_validator(do_config) -> None: CONF_STRUCTURE: ">f", CONF_SLAVE_COUNT: 5, }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_DATA_TYPE: DataType.STRING, + CONF_SLAVE_COUNT: 2, + }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_DATA_TYPE: DataType.INT16, + CONF_SWAP: CONF_SWAP_WORD, + }, ], ) async def test_exception_struct_validator(do_config) -> None: diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 06b0b68a746..e7d15c971c9 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -246,7 +246,7 @@ async def test_config_sensor(hass: HomeAssistant, mock_modbus) -> None: }, ] }, - f"Error in sensor {TEST_ENTITY_NAME} swap(word) not possible due to the registers count: 1, needed: 2", + f"{TEST_ENTITY_NAME}: `structure` illegal with `swap`", ), ], ) From 7ac2c61f2431c54541d2d84772755290f5e65486 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 18 Aug 2023 11:02:30 +0200 Subject: [PATCH 0617/1151] Fix copy-paste error in comments of number tests (#98615) --- tests/components/number/test_init.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 37c0b175faa..d77a67e4ada 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -818,22 +818,22 @@ async def test_name(hass: HomeAssistant) -> None: ), ) - # Unnamed sensor without device class -> no name + # Unnamed number without device class -> no name entity1 = NumberEntity() entity1.entity_id = "number.test1" - # Unnamed sensor with device class but has_entity_name False -> no name + # Unnamed number with device class but has_entity_name False -> no name entity2 = NumberEntity() entity2.entity_id = "number.test2" entity2._attr_device_class = NumberDeviceClass.TEMPERATURE - # Unnamed sensor with device class and has_entity_name True -> named + # Unnamed number with device class and has_entity_name True -> named entity3 = NumberEntity() entity3.entity_id = "number.test3" entity3._attr_device_class = NumberDeviceClass.TEMPERATURE entity3._attr_has_entity_name = True - # Unnamed sensor with device class and has_entity_name True -> named + # Unnamed number with device class and has_entity_name True -> named entity4 = NumberEntity() entity4.entity_id = "number.test4" entity4.entity_description = NumberEntityDescription( From 80a5e341b52d863396c63a69a68d30134f36e8ab Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 18 Aug 2023 11:48:00 +0200 Subject: [PATCH 0618/1151] Add device to Garage Amsterdam entity (#98573) --- homeassistant/components/garages_amsterdam/entity.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/garages_amsterdam/entity.py b/homeassistant/components/garages_amsterdam/entity.py index 894506f7da9..df06f47dff5 100644 --- a/homeassistant/components/garages_amsterdam/entity.py +++ b/homeassistant/components/garages_amsterdam/entity.py @@ -1,12 +1,13 @@ """Generic entity for Garages Amsterdam.""" from __future__ import annotations +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .const import ATTRIBUTION +from .const import ATTRIBUTION, DOMAIN class GaragesAmsterdamEntity(CoordinatorEntity): @@ -22,3 +23,8 @@ class GaragesAmsterdamEntity(CoordinatorEntity): self._attr_unique_id = f"{garage_name}-{info_type}" self._garage_name = garage_name self._info_type = info_type + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, garage_name)}, + name=garage_name, + entry_type=DeviceEntryType.SERVICE, + ) From 5ef6c036102290042bfe9a3b87609c9f031a557f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 18 Aug 2023 13:05:53 +0200 Subject: [PATCH 0619/1151] Log entity_id payload and template on MQTT value template error (#98353) * Log entity_id payload and template on error * Also handle cases with default values. * Do not log payload twice Co-authored-by: Erik Montnemery * Tweak test to assert without payload * black --------- Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/models.py | 39 ++++++++++++++++++++----- tests/components/mqtt/test_init.py | 32 ++++++++++++++++++++ 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 9afa3de3f48..a936c9e420d 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -231,11 +231,21 @@ class MqttValueTemplate: values, self._value_template, ) - rendered_payload = ( - self._value_template.async_render_with_possible_json_value( - payload, variables=values + try: + rendered_payload = ( + self._value_template.async_render_with_possible_json_value( + payload, variables=values + ) ) - ) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error( + "%s: %s rendering template for entity '%s', template: '%s'", + type(ex).__name__, + ex, + self._entity.entity_id if self._entity else "n/a", + self._value_template.template, + ) + raise ex return rendered_payload _LOGGER.debug( @@ -248,9 +258,24 @@ class MqttValueTemplate: default, self._value_template, ) - rendered_payload = self._value_template.async_render_with_possible_json_value( - payload, default, variables=values - ) + try: + rendered_payload = ( + self._value_template.async_render_with_possible_json_value( + payload, default, variables=values + ) + ) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error( + "%s: %s rendering template for entity '%s', template: " + "'%s', default value: %s and payload: %s", + type(ex).__name__, + ex, + self._entity.entity_id if self._entity else "n/a", + self._value_template.template, + default, + payload, + ) + raise ex return rendered_payload diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index c0d7a94de5b..e3a12a2c24e 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -41,6 +41,7 @@ from .test_common import help_all_subscribe_calls from tests.common import ( MockConfigEntry, + MockEntity, async_fire_mqtt_message, async_fire_time_changed, mock_restore_cache, @@ -417,6 +418,37 @@ async def test_value_template_value(hass: HomeAssistant) -> None: assert template_state_calls.call_count == 1 +async def test_value_template_fails( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the rendering of MQTT value template fails.""" + + # test rendering a value fails + entity = MockEntity(entity_id="sensor.test") + entity.hass = hass + tpl = template.Template("{{ value_json.some_var * 2 }}") + val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass, entity=entity) + with pytest.raises(TypeError): + val_tpl.async_render_with_possible_json_value('{"some_var": null }') + await hass.async_block_till_done() + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' " + "rendering template for entity 'sensor.test', " + "template: '{{ value_json.some_var * 2 }}'" + ) in caplog.text + caplog.clear() + with pytest.raises(TypeError): + val_tpl.async_render_with_possible_json_value( + '{"some_var": null }', default=100 + ) + assert ( + "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' " + "rendering template for entity 'sensor.test', " + "template: '{{ value_json.some_var * 2 }}', default value: 100 and payload: " + '{"some_var": null }' + ) in caplog.text + + async def test_service_call_without_topic_does_not_publish( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: From 66685b796d9a2e932688f0699f5c4d81443eaa97 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 18 Aug 2023 13:09:35 +0200 Subject: [PATCH 0620/1151] Update frontend to 20230802.1 (#98616) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 84d1d4f5e27..986dfd6ba52 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230802.0"] + "requirements": ["home-assistant-frontend==20230802.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 72b9872d3bf..7a93328f3ff 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.69.0 hassil==1.2.5 home-assistant-bluetooth==1.10.2 -home-assistant-frontend==20230802.0 +home-assistant-frontend==20230802.1 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b32eb8a2d05..02e462f94e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -988,7 +988,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230802.0 +home-assistant-frontend==20230802.1 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36356e0588c..8bb737295fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -771,7 +771,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230802.0 +home-assistant-frontend==20230802.1 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From 1c56c398971611e511a0728cb75860ef5a639523 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 18 Aug 2023 13:10:13 +0200 Subject: [PATCH 0621/1151] modbus config: count and slave_count can normally not be mixed. (#97902) --- homeassistant/components/modbus/validators.py | 21 ++++++++++++------- tests/components/modbus/test_init.py | 6 ++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 40461e3effd..f3336e5cb0c 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -67,6 +67,17 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: slave_count = config.get(CONF_SLAVE_COUNT, 0) + 1 slave = config.get(CONF_SLAVE, 0) swap_type = config.get(CONF_SWAP, CONF_SWAP_NONE) + if ( + slave_count > 1 + and count > 1 + and data_type not in (DataType.CUSTOM, DataType.STRING) + ): + error = f"{name} {CONF_COUNT} cannot be mixed with {data_type}" + raise vol.Invalid(error) + if config[CONF_DATA_TYPE] != DataType.CUSTOM: + if structure: + error = f"{name} structure: cannot be mixed with {data_type}" + if config[CONF_DATA_TYPE] == DataType.CUSTOM: if slave or slave_count > 1: error = f"{name}: `{CONF_STRUCTURE}` illegal with `{CONF_SLAVE_COUNT}` / `{CONF_SLAVE}`" @@ -96,17 +107,11 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: CONF_STRUCTURE: structure, CONF_SWAP: swap_type, } - - if structure: - error = f"{name} structure: cannot be mixed with {data_type}" - raise vol.Invalid(error) if data_type not in DEFAULT_STRUCT_FORMAT: error = f"Error in sensor {name}. data_type `{data_type}` not supported" raise vol.Invalid(error) - if (slave or slave_count > 1) and data_type == DataType.STRING: - error = ( - f"{name}: `{data_type}` illegal with `{CONF_SLAVE_COUNT}` / `{CONF_SLAVE}`" - ) + if slave_count > 1 and data_type == DataType.STRING: + error = f"{name}: `{data_type}` illegal with `{CONF_SLAVE_COUNT}`" raise vol.Invalid(error) if CONF_COUNT not in config: diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 6ad1e33821c..e305a0294c8 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -248,6 +248,12 @@ async def test_ok_struct_validator(do_config) -> None: CONF_DATA_TYPE: DataType.INT16, CONF_SWAP: CONF_SWAP_WORD, }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_COUNT: 2, + CONF_SLAVE_COUNT: 2, + CONF_DATA_TYPE: DataType.INT32, + }, ], ) async def test_exception_struct_validator(do_config) -> None: From fc444e4cd63476477d0e6cf4db9de5e151aae590 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 18 Aug 2023 13:15:59 +0200 Subject: [PATCH 0622/1151] Allow control of pump mode for nibe (#98499) * Allow control of pump mode --------- Co-authored-by: G Johansson --- .../components/nibe_heatpump/climate.py | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/climate.py b/homeassistant/components/nibe_heatpump/climate.py index 0df787de986..4ab709ae947 100644 --- a/homeassistant/components/nibe_heatpump/climate.py +++ b/homeassistant/components/nibe_heatpump/climate.py @@ -24,6 +24,7 @@ from homeassistant.components.climate import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -70,7 +71,7 @@ class NibeClimateEntity(CoordinatorEntity[Coordinator], ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE_RANGE | ClimateEntityFeature.TARGET_TEMPERATURE ) - _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF, HVACMode.HEAT] + _attr_hvac_modes = [HVACMode.AUTO, HVACMode.HEAT, HVACMode.HEAT_COOL] _attr_target_temperature_step = 0.5 _attr_max_temp = 35.0 _attr_min_temp = 5.0 @@ -101,7 +102,7 @@ class NibeClimateEntity(CoordinatorEntity[Coordinator], ClimateEntity): self._attr_unique_id = f"{coordinator.unique_id}-{key}" self._attr_device_info = coordinator.device_info self._attr_hvac_action = HVACAction.IDLE - self._attr_hvac_mode = HVACMode.OFF + self._attr_hvac_mode = HVACMode.AUTO self._attr_target_temperature_high = None self._attr_target_temperature_low = None self._attr_target_temperature = None @@ -138,7 +139,7 @@ class NibeClimateEntity(CoordinatorEntity[Coordinator], ClimateEntity): self._attr_current_temperature = _get_float(self._coil_current) - mode = HVACMode.OFF + mode = HVACMode.AUTO if _get_value(self._coil_use_room_sensor) == "ON": if ( _get_value(self._coil_cooling_with_room_sensor) @@ -225,3 +226,25 @@ class NibeClimateEntity(CoordinatorEntity[Coordinator], ClimateEntity): if (temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH)) is not None: await coordinator.async_write_coil(self._coil_setpoint_cool, temperature) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + coordinator = self.coordinator + + if hvac_mode == HVACMode.HEAT_COOL: + await coordinator.async_write_coil( + self._coil_cooling_with_room_sensor, "ON" + ) + await coordinator.async_write_coil(self._coil_use_room_sensor, "ON") + elif hvac_mode == HVACMode.HEAT: + await coordinator.async_write_coil( + self._coil_cooling_with_room_sensor, "OFF" + ) + await coordinator.async_write_coil(self._coil_use_room_sensor, "ON") + elif hvac_mode == HVACMode.AUTO: + await coordinator.async_write_coil( + self._coil_cooling_with_room_sensor, "OFF" + ) + await coordinator.async_write_coil(self._coil_use_room_sensor, "OFF") + else: + raise HomeAssistantError(f"{hvac_mode} mode not supported for {self.name}") From c268adb07e9c9dfc72260cac9a25639fad60df09 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 18 Aug 2023 13:23:04 +0200 Subject: [PATCH 0623/1151] modbus: Repair swap for slaves (#97960) --- .../components/modbus/base_platform.py | 19 +- homeassistant/components/modbus/climate.py | 2 +- homeassistant/components/modbus/sensor.py | 5 +- tests/components/modbus/test_sensor.py | 183 +++++++++++++++++- 4 files changed, 192 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index e4c657a6c54..9cf582a5dda 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -50,10 +50,12 @@ from .const import ( CONF_NAN_VALUE, CONF_PRECISION, CONF_SCALE, + CONF_SLAVE_COUNT, CONF_STATE_OFF, CONF_STATE_ON, CONF_SWAP, CONF_SWAP_BYTE, + CONF_SWAP_NONE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, CONF_VERIFY, @@ -154,15 +156,25 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): """Initialize the switch.""" super().__init__(hub, config) self._swap = config[CONF_SWAP] + if self._swap == CONF_SWAP_NONE: + self._swap = None self._data_type = config[CONF_DATA_TYPE] self._structure: str = config[CONF_STRUCTURE] self._precision = config[CONF_PRECISION] self._scale = config[CONF_SCALE] self._offset = config[CONF_OFFSET] - self._count = config[CONF_COUNT] + self._slave_count = config.get(CONF_SLAVE_COUNT, 0) + self._slave_size = self._count = config[CONF_COUNT] - def _swap_registers(self, registers: list[int]) -> list[int]: + def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: """Do swap as needed.""" + if slave_count: + swapped = [] + for i in range(0, self._slave_count + 1): + inx = i * self._slave_size + inx2 = inx + self._slave_size + swapped.extend(self._swap_registers(registers[inx:inx2], 0)) + return swapped if self._swap in (CONF_SWAP_BYTE, CONF_SWAP_WORD_BYTE): # convert [12][34] --> [21][43] for i, register in enumerate(registers): @@ -192,7 +204,8 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): def unpack_structure_result(self, registers: list[int]) -> str | None: """Convert registers to proper result.""" - registers = self._swap_registers(registers) + if self._swap: + registers = self._swap_registers(registers, self._slave_count) byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) if self._data_type == DataType.STRING: return byte_string.decode() diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 95f8bee0bc9..7170716d43e 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -210,7 +210,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): int.from_bytes(as_bytes[i : i + 2], "big") for i in range(0, len(as_bytes), 2) ] - registers = self._swap_registers(raw_regs) + registers = self._swap_registers(raw_regs, 0) if self._data_type in ( DataType.INT16, diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 97794729ab2..fe2d4bc415d 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -134,10 +134,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): self._coordinator.async_set_updated_data(None) else: self._attr_native_value = result - if self._attr_native_value is None: - self._attr_available = False - else: - self._attr_available = True + self._attr_available = self._attr_native_value is not None self._lazy_errors = self._lazy_error_count self.async_write_ha_state() diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index e7d15c971c9..298daa1397f 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -615,9 +615,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: CONF_ADDRESS: 51, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_DATA_TYPE: DataType.UINT32, - CONF_SCALE: 1, - CONF_OFFSET: 0, - CONF_PRECISION: 0, + CONF_SCAN_INTERVAL: 1, }, ], }, @@ -689,17 +687,184 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: ) async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: """Run test for sensor.""" - assert hass.states.get(ENTITY_ID).state == expected[0] entity_registry = er.async_get(hass) - - for i in range(1, len(expected)): - entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i}".replace(" ", "_") - assert hass.states.get(entity_id).state == expected[i] - unique_id = f"{SLAVE_UNIQUE_ID}_{i}" + for i in range(0, len(expected)): + entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") + unique_id = f"{SLAVE_UNIQUE_ID}" + if i: + entity_id = f"{entity_id}_{i}" + unique_id = f"{unique_id}_{i}" entry = entity_registry.async_get(entity_id) + state = hass.states.get(entity_id).state + assert state == expected[i] assert entry.unique_id == unique_id +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 1, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + ("config_addon", "register_words", "do_exception", "expected"), + [ + ( + { + CONF_SLAVE_COUNT: 0, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_SWAP: CONF_SWAP_BYTE, + CONF_DATA_TYPE: DataType.UINT16, + }, + [0x0102], + False, + [str(int(0x0201))], + ), + ( + { + CONF_SLAVE_COUNT: 0, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_SWAP: CONF_SWAP_WORD, + CONF_DATA_TYPE: DataType.UINT32, + }, + [0x0102, 0x0304], + False, + [str(int(0x03040102))], + ), + ( + { + CONF_SLAVE_COUNT: 0, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_SWAP: CONF_SWAP_WORD, + CONF_DATA_TYPE: DataType.UINT64, + }, + [0x0102, 0x0304, 0x0506, 0x0708], + False, + [str(int(0x0708050603040102))], + ), + ( + { + CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT16, + CONF_SWAP: CONF_SWAP_BYTE, + }, + [0x0102, 0x0304], + False, + [str(int(0x0201)), str(int(0x0403))], + ), + ( + { + CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT32, + CONF_SWAP: CONF_SWAP_WORD, + }, + [0x0102, 0x0304, 0x0506, 0x0708], + False, + [str(int(0x03040102)), str(int(0x07080506))], + ), + ( + { + CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT64, + CONF_SWAP: CONF_SWAP_WORD, + }, + [0x0102, 0x0304, 0x0506, 0x0708, 0x0901, 0x0902, 0x0903, 0x0904], + False, + [str(int(0x0708050603040102)), str(int(0x0904090309020901))], + ), + ( + { + CONF_SLAVE_COUNT: 3, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT16, + CONF_SWAP: CONF_SWAP_BYTE, + }, + [0x0102, 0x0304, 0x0506, 0x0708], + False, + [str(int(0x0201)), str(int(0x0403)), str(int(0x0605)), str(int(0x0807))], + ), + ( + { + CONF_SLAVE_COUNT: 3, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT32, + CONF_SWAP: CONF_SWAP_WORD, + }, + [ + 0x0102, + 0x0304, + 0x0506, + 0x0708, + 0x090A, + 0x0B0C, + 0x0D0E, + 0x0F00, + ], + False, + [ + str(int(0x03040102)), + str(int(0x07080506)), + str(int(0x0B0C090A)), + str(int(0x0F000D0E)), + ], + ), + ( + { + CONF_SLAVE_COUNT: 3, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.UINT64, + CONF_SWAP: CONF_SWAP_WORD, + }, + [ + 0x0601, + 0x0602, + 0x0603, + 0x0604, + 0x0701, + 0x0702, + 0x0703, + 0x0704, + 0x0801, + 0x0802, + 0x0803, + 0x0804, + 0x0901, + 0x0902, + 0x0903, + 0x0904, + ], + False, + [ + str(int(0x0604060306020601)), + str(int(0x0704070307020701)), + str(int(0x0804080308020801)), + str(int(0x0904090309020901)), + ], + ), + ], +) +async def test_slave_swap_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: + """Run test for sensor.""" + for i in range(0, len(expected)): + entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") + if i: + entity_id = f"{entity_id}_{i}" + state = hass.states.get(entity_id).state + assert state == expected[i] + + @pytest.mark.parametrize( "do_config", [ From 790523126effe84de725b469f3dd3946709719ca Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 18 Aug 2023 13:40:35 +0200 Subject: [PATCH 0624/1151] Name unnamed update entities by their device class (#98579) --- homeassistant/components/ezviz/strings.json | 5 - homeassistant/components/ezviz/update.py | 1 - .../components/litterrobot/strings.json | 5 - .../components/litterrobot/update.py | 1 - .../components/rainmachine/strings.json | 5 - .../components/rainmachine/update.py | 1 - homeassistant/components/update/__init__.py | 7 ++ tests/components/update/test_init.py | 114 +++++++++++++++++- 8 files changed, 120 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 373f9af22fc..11144f8ae71 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -223,11 +223,6 @@ "name": "Follow movement" } }, - "update": { - "firmware": { - "name": "[%key:component::update::entity_component::firmware::name%]" - } - }, "siren": { "siren": { "name": "[%key:component::siren::title%]" diff --git a/homeassistant/components/ezviz/update.py b/homeassistant/components/ezviz/update.py index 6a80a579080..003397d8dda 100644 --- a/homeassistant/components/ezviz/update.py +++ b/homeassistant/components/ezviz/update.py @@ -24,7 +24,6 @@ PARALLEL_UPDATES = 1 UPDATE_ENTITY_TYPES = UpdateEntityDescription( key="version", - translation_key="firmware", device_class=UpdateDeviceClass.FIRMWARE, ) diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index 8436d24902c..7acfad69735 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -131,11 +131,6 @@ "litter_box": { "name": "Litter box" } - }, - "update": { - "firmware": { - "name": "Firmware" - } } }, "services": { diff --git a/homeassistant/components/litterrobot/update.py b/homeassistant/components/litterrobot/update.py index 9b8391c5bae..584a6af77c2 100644 --- a/homeassistant/components/litterrobot/update.py +++ b/homeassistant/components/litterrobot/update.py @@ -24,7 +24,6 @@ SCAN_INTERVAL = timedelta(days=1) FIRMWARE_UPDATE_ENTITY = UpdateEntityDescription( key="firmware", - translation_key="firmware", device_class=UpdateDeviceClass.FIRMWARE, ) diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index fc48ebce4eb..ac2b86754e5 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -91,11 +91,6 @@ "hot_days_extra_watering": { "name": "Extra water on hot days" } - }, - "update": { - "firmware": { - "name": "Firmware" - } } }, "services": { diff --git a/homeassistant/components/rainmachine/update.py b/homeassistant/components/rainmachine/update.py index 372319ba9a0..8d5690b5320 100644 --- a/homeassistant/components/rainmachine/update.py +++ b/homeassistant/components/rainmachine/update.py @@ -44,7 +44,6 @@ UPDATE_STATE_MAP = { UPDATE_DESCRIPTION = RainMachineEntityDescription( key="update", - translation_key="firmware", api_category=DATA_MACHINE_FIRMWARE_UPDATE_STATUS, ) diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index b9d01629536..e23032e24fe 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -216,6 +216,13 @@ class UpdateEntity(RestoreEntity): """Version installed and in use.""" return self._attr_installed_version + def _default_to_device_class_name(self) -> bool: + """Return True if an unnamed entity should be named by its device class. + + For updates this is True if the entity has a device class. + """ + return self.device_class is not None + @property def device_class(self) -> UpdateDeviceClass | None: """Return the class of this entity.""" diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index a7780f54f70..73f98c9e2db 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -1,4 +1,5 @@ """The tests for the Update component.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest @@ -24,6 +25,7 @@ from homeassistant.components.update.const import ( ATTR_TITLE, UpdateEntityFeature, ) +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, @@ -34,12 +36,24 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.setup import async_setup_component -from tests.common import MockEntityPlatform, mock_restore_cache +from tests.common import ( + MockConfigEntry, + MockEntityPlatform, + MockModule, + MockPlatform, + mock_config_flow, + mock_integration, + mock_platform, + mock_restore_cache, +) from tests.typing import WebSocketGenerator +TEST_DOMAIN = "test" + class MockUpdateEntity(UpdateEntity): """Mock UpdateEntity to use in tests.""" @@ -752,3 +766,101 @@ async def test_release_notes_entity_does_not_support_release_notes( result = await client.receive_json() assert result["error"]["code"] == "not_supported" assert result["error"]["message"] == "Entity does not support release notes" + + +class MockFlow(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, MockFlow): + yield + + +async def test_name(hass: HomeAssistant) -> None: + """Test update name.""" + + 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, DOMAIN) + return True + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + ), + ) + + # Unnamed update entity without device class -> no name + entity1 = UpdateEntity() + entity1.entity_id = "update.test1" + + # Unnamed update entity with device class but has_entity_name False -> no name + entity2 = UpdateEntity() + entity2.entity_id = "update.test2" + entity2._attr_device_class = UpdateDeviceClass.FIRMWARE + + # Unnamed update entity with device class and has_entity_name True -> named + entity3 = UpdateEntity() + entity3.entity_id = "update.test3" + entity3._attr_device_class = UpdateDeviceClass.FIRMWARE + entity3._attr_has_entity_name = True + + # Unnamed update entity with device class and has_entity_name True -> named + entity4 = UpdateEntity() + entity4.entity_id = "update.test4" + entity4.entity_description = UpdateEntityDescription( + "test", + UpdateDeviceClass.FIRMWARE, + has_entity_name=True, + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test update platform via config entry.""" + async_add_entities([entity1, entity2, entity3, entity4]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{DOMAIN}", + MockPlatform(async_setup_entry=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() + + state = hass.states.get(entity1.entity_id) + assert state + assert "device_class" not in state.attributes + assert "friendly_name" not in state.attributes + + state = hass.states.get(entity2.entity_id) + assert state + assert state.attributes.get("device_class") == "firmware" + assert "friendly_name" not in state.attributes + + expected = { + "device_class": "firmware", + "friendly_name": "Firmware", + } + state = hass.states.get(entity3.entity_id) + assert state + assert expected.items() <= state.attributes.items() + + state = hass.states.get(entity4.entity_id) + assert state + assert expected.items() <= state.attributes.items() From 4096de2dad415c52fe42b4c60ae7885c1d8268ab Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 18 Aug 2023 16:31:07 +0200 Subject: [PATCH 0625/1151] Add Yale Smart Living diagnostics test (#98590) * Yale test diagnostics * clean * From review --- tests/components/yale_smart_alarm/conftest.py | 64 ++++ .../yale_smart_alarm/fixtures/get_all.json | 331 +++++++++++++++++ .../snapshots/test_diagnostics.ambr | 344 ++++++++++++++++++ .../yale_smart_alarm/test_diagnostics.py | 24 ++ 4 files changed, 763 insertions(+) create mode 100644 tests/components/yale_smart_alarm/conftest.py create mode 100644 tests/components/yale_smart_alarm/fixtures/get_all.json create mode 100644 tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr create mode 100644 tests/components/yale_smart_alarm/test_diagnostics.py diff --git a/tests/components/yale_smart_alarm/conftest.py b/tests/components/yale_smart_alarm/conftest.py new file mode 100644 index 00000000000..144a24a4897 --- /dev/null +++ b/tests/components/yale_smart_alarm/conftest.py @@ -0,0 +1,64 @@ +"""Fixtures for the Yale Smart Living integration.""" +from __future__ import annotations + +import json +from typing import Any +from unittest.mock import patch + +import pytest +from yalesmartalarmclient.const import YALE_STATE_ARM_FULL + +from homeassistant.components.yale_smart_alarm.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + +ENTRY_CONFIG = { + "username": "test-username", + "password": "new-test-password", + "name": "Yale Smart Alarm", + "area_id": "1", +} +OPTIONS_CONFIG = {"lock_code_digits": 6} + + +@pytest.fixture +async def load_config_entry( + hass: HomeAssistant, load_json: dict[str, Any] +) -> MockConfigEntry: + """Set up the Yale Smart Living integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + options=OPTIONS_CONFIG, + entry_id="1", + unique_id="username", + version=1, + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.yale_smart_alarm.coordinator.YaleSmartAlarmClient", + autospec=True, + ) as mock_client_class: + client = mock_client_class.return_value + client.auth = None + client.lock_api = None + client.get_all.return_value = load_json + client.get_armed_status.return_value = YALE_STATE_ARM_FULL + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +@pytest.fixture(name="load_json", scope="session") +def load_json_from_fixture() -> dict[str, Any]: + """Load fixture with json data and return.""" + + data_fixture = load_fixture("get_all.json", "yale_smart_alarm") + json_data: dict[str, Any] = json.loads(data_fixture) + return json_data diff --git a/tests/components/yale_smart_alarm/fixtures/get_all.json b/tests/components/yale_smart_alarm/fixtures/get_all.json new file mode 100644 index 00000000000..08f60fafd3f --- /dev/null +++ b/tests/components/yale_smart_alarm/fixtures/get_all.json @@ -0,0 +1,331 @@ +{ + "DEVICES": [ + { + "area": "1", + "no": "1", + "rf": null, + "address": "123", + "type": "device_type.door_lock", + "name": "Device1", + "status1": "device_status.lock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:01", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": "35", + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "123", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.lock"], + "trigger_by_zone": [] + } + ], + "MODE": [ + { + "area": "1", + "mode": "disarm" + } + ], + "STATUS": { + "acfail": "main.normal", + "battery": "main.normal", + "tamper": "main.normal", + "jam": "main.normal", + "rssi": "1", + "gsm_rssi": "0", + "imei": "", + "imsi": "" + }, + "CYCLE": { + "model": [ + { + "area": "1", + "mode": "disarm" + } + ], + "panel_status": { + "warning_snd_mute": "0" + }, + "device_status": [ + { + "area": "1", + "no": "1", + "rf": null, + "address": "124", + "type": "device_type.door_lock", + "name": "Device2", + "status1": "device_status.lock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:02", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": "35", + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "124", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.lock"], + "trigger_by_zone": [] + } + ], + "capture_latest": null, + "report_event_latest": { + "utc_event_time": null, + "time": "1692271914", + "report_id": "1027299996", + "id": "9999", + "event_time": null, + "cid_code": "1807" + }, + "alarm_event_latest": null + }, + "ONLINE": "online", + "HISTORY": [ + { + "report_id": "1027299996", + "cid": "18180701000", + "event_type": "1807", + "user": 0, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 11:31:54", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027299889", + "cid": "18180201101", + "event_type": "1802", + "user": 101, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 11:31:43", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027299587", + "cid": "18180701000", + "event_type": "1807", + "user": 0, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 11:31:11", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027296099", + "cid": "18180101001", + "event_type": "1801", + "user": 1, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 11:24:52", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027273782", + "cid": "18180701000", + "event_type": "1807", + "user": 0, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 10:43:21", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027273230", + "cid": "18180201101", + "event_type": "1802", + "user": 101, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 10:42:09", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027100172", + "cid": "18180701000", + "event_type": "1807", + "user": 0, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 05:28:57", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027099978", + "cid": "18180101001", + "event_type": "1801", + "user": 1, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/17 05:28:39", + "status_temp_format": "C", + "cid_source": "DEVICE" + }, + { + "report_id": "1027093266", + "cid": "18160200000", + "event_type": "1602", + "user": "", + "area": 0, + "zone": 0, + "name": "", + "type": "", + "event_time": null, + "time": "2023/08/17 05:17:12", + "status_temp_format": "C", + "cid_source": "SYSTEM" + }, + { + "report_id": "1026912623", + "cid": "18180701000", + "event_type": "1807", + "user": 0, + "area": 1, + "zone": 1, + "name": "Device1", + "type": "device_type.door_lock", + "event_time": null, + "time": "2023/08/16 20:29:36", + "status_temp_format": "C", + "cid_source": "DEVICE" + } + ], + "PANEL INFO": { + "mac": "00:00:00:00:10", + "report_account": "username", + "xml_version": "2", + "version": "MINIGW-MZ-1_G 1.0.1.29A,,4.1.2.6.2,00:1D:94:0B:5E:A7,10111112,ML_yamga", + "net_version": "MINIGW-MZ-1_G 1.0.1.29A", + "rf51_version": "", + "zb_version": "4.1.2.6.2", + "zw_version": "", + "SMS_Balance": "50", + "voice_balance": "0", + "name": "", + "contact": "", + "mail_address": "username@fake.com", + "phone": "UK-01902364606 / Sweden-0770373710 / Demark-89887818 / Norway-81569036", + "service_time": "UK - Mon to Fri 8:30 til 17:30 / Scandinavia - Mon to Fri 8:00 til 20:00, Sat to Sun 10:00 til 15:00", + "dealer_name": "Poland" + }, + "AUTH CHECK": { + "user_id": "username", + "id": "username", + "mail_address": "username@fake.com", + "mac": "00:00:00:00:20", + "is_auth": "1", + "master": "1", + "first_login": "1", + "name": "Device1", + "token_time": "2023-08-17 16:19:20", + "agent": false, + "xml_version": "2", + "dealer_id": "605", + "dealer_group": "yale" + } +} diff --git a/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr b/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..faff1c5103a --- /dev/null +++ b/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr @@ -0,0 +1,344 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'AUTH CHECK': dict({ + 'agent': False, + 'dealer_group': 'yale', + 'dealer_id': '605', + 'first_login': '1', + 'id': '**REDACTED**', + 'is_auth': '1', + 'mac': '**REDACTED**', + 'mail_address': '**REDACTED**', + 'master': '1', + 'name': '**REDACTED**', + 'token_time': '2023-08-17 16:19:20', + 'user_id': '**REDACTED**', + 'xml_version': '2', + }), + 'CYCLE': dict({ + 'alarm_event_latest': None, + 'capture_latest': None, + 'device_status': list([ + dict({ + '_state': 'locked', + '_state2': 'closed', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '35', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '1', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.lock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.lock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + ]), + 'model': list([ + dict({ + 'area': '1', + 'mode': 'disarm', + }), + ]), + 'panel_status': dict({ + 'warning_snd_mute': '0', + }), + 'report_event_latest': dict({ + 'cid_code': '1807', + 'event_time': None, + 'id': '**REDACTED**', + 'report_id': '1027299996', + 'time': '1692271914', + 'utc_event_time': None, + }), + }), + 'DEVICES': list([ + dict({ + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '35', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '1', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.lock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.lock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + ]), + 'HISTORY': list([ + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027299996', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:31:54', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180201101', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1802', + 'name': '**REDACTED**', + 'report_id': '1027299889', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:31:43', + 'type': 'device_type.door_lock', + 'user': 101, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027299587', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:31:11', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180101001', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1801', + 'name': '**REDACTED**', + 'report_id': '1027296099', + 'status_temp_format': 'C', + 'time': '2023/08/17 11:24:52', + 'type': 'device_type.door_lock', + 'user': 1, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027273782', + 'status_temp_format': 'C', + 'time': '2023/08/17 10:43:21', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180201101', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1802', + 'name': '**REDACTED**', + 'report_id': '1027273230', + 'status_temp_format': 'C', + 'time': '2023/08/17 10:42:09', + 'type': 'device_type.door_lock', + 'user': 101, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1027100172', + 'status_temp_format': 'C', + 'time': '2023/08/17 05:28:57', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + dict({ + 'area': 1, + 'cid': '18180101001', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1801', + 'name': '**REDACTED**', + 'report_id': '1027099978', + 'status_temp_format': 'C', + 'time': '2023/08/17 05:28:39', + 'type': 'device_type.door_lock', + 'user': 1, + 'zone': 1, + }), + dict({ + 'area': 0, + 'cid': '18160200000', + 'cid_source': 'SYSTEM', + 'event_time': None, + 'event_type': '1602', + 'name': '', + 'report_id': '1027093266', + 'status_temp_format': 'C', + 'time': '2023/08/17 05:17:12', + 'type': '', + 'user': '', + 'zone': 0, + }), + dict({ + 'area': 1, + 'cid': '18180701000', + 'cid_source': 'DEVICE', + 'event_time': None, + 'event_type': '1807', + 'name': '**REDACTED**', + 'report_id': '1026912623', + 'status_temp_format': 'C', + 'time': '2023/08/16 20:29:36', + 'type': 'device_type.door_lock', + 'user': 0, + 'zone': 1, + }), + ]), + 'MODE': list([ + dict({ + 'area': '1', + 'mode': 'disarm', + }), + ]), + 'ONLINE': 'online', + 'PANEL INFO': dict({ + 'SMS_Balance': '50', + 'contact': '', + 'dealer_name': 'Poland', + 'mac': '**REDACTED**', + 'mail_address': '**REDACTED**', + 'name': '', + 'net_version': 'MINIGW-MZ-1_G 1.0.1.29A', + 'phone': 'UK-01902364606 / Sweden-0770373710 / Demark-89887818 / Norway-81569036', + 'report_account': '**REDACTED**', + 'rf51_version': '', + 'service_time': 'UK - Mon to Fri 8:30 til 17:30 / Scandinavia - Mon to Fri 8:00 til 20:00, Sat to Sun 10:00 til 15:00', + 'version': 'MINIGW-MZ-1_G 1.0.1.29A,,4.1.2.6.2,00:1D:94:0B:5E:A7,10111112,ML_yamga', + 'voice_balance': '0', + 'xml_version': '2', + 'zb_version': '4.1.2.6.2', + 'zw_version': '', + }), + 'STATUS': dict({ + 'acfail': 'main.normal', + 'battery': 'main.normal', + 'gsm_rssi': '0', + 'imei': '', + 'imsi': '', + 'jam': 'main.normal', + 'rssi': '1', + 'tamper': 'main.normal', + }), + }) +# --- diff --git a/tests/components/yale_smart_alarm/test_diagnostics.py b/tests/components/yale_smart_alarm/test_diagnostics.py new file mode 100644 index 00000000000..8796eeb465b --- /dev/null +++ b/tests/components/yale_smart_alarm/test_diagnostics.py @@ -0,0 +1,24 @@ +"""Test Yale Smart Living diagnostics.""" +from __future__ import annotations + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + load_config_entry: ConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test generating diagnostics for a config entry.""" + entry = load_config_entry + + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert diag == snapshot From 4073f56c5eb4205457b219d3f04a2edd868fa279 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 18 Aug 2023 16:40:24 +0200 Subject: [PATCH 0626/1151] Remove default code in Yale Smart Living (#94675) * Remove default code in Yale Smart Living * Test and remove check * Finalize * migration * add back * add back 2 * Fix tests * Fix migration if code not exist --- .../components/yale_smart_alarm/__init__.py | 32 ++++++++++- .../yale_smart_alarm/config_flow.py | 28 +++------ .../components/yale_smart_alarm/lock.py | 6 +- .../components/yale_smart_alarm/strings.json | 4 -- .../yale_smart_alarm/test_config_flow.py | 57 +++++-------------- 5 files changed, 56 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/__init__.py b/homeassistant/components/yale_smart_alarm/__init__.py index 763742cce70..830d8d9f69e 100644 --- a/homeassistant/components/yale_smart_alarm/__init__.py +++ b/homeassistant/components/yale_smart_alarm/__init__.py @@ -1,11 +1,14 @@ """The yale_smart_alarm component.""" from __future__ import annotations +from homeassistant.components.lock import CONF_DEFAULT_CODE, DOMAIN as LOCK_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CODE from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import entity_registry as er -from .const import COORDINATOR, DOMAIN, PLATFORMS +from .const import COORDINATOR, DOMAIN, LOGGER, PLATFORMS from .coordinator import YaleDataUpdateCoordinator @@ -39,3 +42,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return True return False + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + LOGGER.debug("Migrating from version %s", entry.version) + + if entry.version == 1: + if config_entry_default_code := entry.options.get(CONF_CODE): + entity_reg = er.async_get(hass) + entries = er.async_entries_for_config_entry(entity_reg, entry.entry_id) + for entity in entries: + if entity.entity_id.startswith("lock"): + entity_reg.async_update_entity_options( + entity.entity_id, + LOCK_DOMAIN, + {CONF_DEFAULT_CODE: config_entry_default_code}, + ) + new_options = entry.options.copy() + del new_options[CONF_CODE] + + hass.config_entries.async_update_entry(entry, options=new_options) + + entry.version = 2 + + LOGGER.info("Migration to version %s successful", entry.version) + + return True diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py index a2462df41cb..ff813d43d78 100644 --- a/homeassistant/components/yale_smart_alarm/config_flow.py +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -9,7 +9,7 @@ from yalesmartalarmclient.client import YaleSmartAlarmClient from yalesmartalarmclient.exceptions import AuthenticationError from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv @@ -44,7 +44,7 @@ DATA_SCHEMA_AUTH = vol.Schema( class YaleConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Yale integration.""" - VERSION = 1 + VERSION = 2 entry: ConfigEntry | None @@ -155,32 +155,22 @@ class YaleOptionsFlowHandler(OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage Yale options.""" - errors = {} + errors: dict[str, Any] = {} if user_input: - if len(user_input.get(CONF_CODE, "")) not in [ - 0, - user_input[CONF_LOCK_CODE_DIGITS], - ]: - errors["base"] = "code_format_mismatch" - else: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) return self.async_show_form( step_id="init", data_schema=vol.Schema( { - vol.Optional( - CONF_CODE, - description={ - "suggested_value": self.entry.options.get(CONF_CODE) - }, - ): str, vol.Optional( CONF_LOCK_CODE_DIGITS, - default=self.entry.options.get( - CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS - ), + description={ + "suggested_value": self.entry.options.get( + CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS + ) + }, ): int, } ), diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index 397a9cc8db1..50d7b28c52b 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_CODE, CONF_CODE +from homeassistant.const import ATTR_CODE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -52,9 +52,7 @@ class YaleDoorlock(YaleEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" - code: str | None = kwargs.get( - ATTR_CODE, self.coordinator.entry.options.get(CONF_CODE) - ) + code: str | None = kwargs.get(ATTR_CODE) return await self.async_set_lock("unlocked", code) async def async_lock(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/yale_smart_alarm/strings.json b/homeassistant/components/yale_smart_alarm/strings.json index ec0c5d0702a..a51d151d7d9 100644 --- a/homeassistant/components/yale_smart_alarm/strings.json +++ b/homeassistant/components/yale_smart_alarm/strings.json @@ -31,13 +31,9 @@ "step": { "init": { "data": { - "code": "Default code for locks, used if none is given", "lock_code_digits": "Number of digits in PIN code for locks" } } - }, - "error": { - "code_format_mismatch": "The code does not match the required number of digits" } }, "entity": { diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py index 4553a120060..90c0b78baf5 100644 --- a/tests/components/yale_smart_alarm/test_config_flow.py +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -121,6 +121,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: "name": "Yale Smart Alarm", "area_id": "1", }, + version=2, ) entry.add_to_hass(hass) @@ -187,6 +188,7 @@ async def test_reauth_flow_error( "name": "Yale Smart Alarm", "area_id": "1", }, + version=2, ) entry.add_to_hass(hass) @@ -248,11 +250,20 @@ async def test_options_flow(hass: HomeAssistant) -> None: entry = MockConfigEntry( domain=DOMAIN, unique_id="test-username", - data={}, + data={ + "username": "test-username", + "password": "test-password", + "name": "Yale Smart Alarm", + "area_id": "1", + }, + version=2, ) entry.add_to_hass(hass) with patch( + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", + return_value=True, + ), patch( "homeassistant.components.yale_smart_alarm.async_setup_entry", return_value=True, ): @@ -266,48 +277,8 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"code": "123456", "lock_code_digits": 6}, + user_input={"lock_code_digits": 6}, ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"] == {"code": "123456", "lock_code_digits": 6} - - -async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: - """Test options config flow with a code format mismatch error.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="test-username", - data={}, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.yale_smart_alarm.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" - assert result["errors"] == {} - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"code": "123", "lock_code_digits": 6}, - ) - - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] == {"base": "code_format_mismatch"} - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"code": "123456", "lock_code_digits": 6}, - ) - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"] == {"code": "123456", "lock_code_digits": 6} + assert result["data"] == {"lock_code_digits": 6} From 93683cef2742b84c4c4c1cf20254e2508f2dec73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 18 Aug 2023 20:10:29 +0300 Subject: [PATCH 0627/1151] Use zoneinfo instead of pytz, mark pytz as banned in ruff (#98613) Refs #43439, #49643. --- pyproject.toml | 4 ++++ tests/components/unifiprotect/test_media_source.py | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4e477440cde..1587adbea74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -488,6 +488,7 @@ select = [ "SIM401", # Use get from dict with default instead of an if block "T100", # Trace found: {name} used "T20", # flake8-print + "TID251", # Banned imports "TRY004", # Prefer TypeError exception for invalid type "TRY200", # Use raise from to specify exception cause "TRY302", # Remove exception handler; error is immediately re-raised @@ -531,6 +532,9 @@ voluptuous = "vol" [tool.ruff.flake8-pytest-style] fixture-parentheses = false +[tool.ruff.flake8-tidy-imports.banned-api] +"pytz".msg = "use zoneinfo instead" + [tool.ruff.isort] force-sort-within-sections = true known-first-party = [ diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index e19985aea3f..c5690ef5e92 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -5,7 +5,6 @@ from ipaddress import IPv4Address from unittest.mock import AsyncMock, Mock, patch import pytest -import pytz from pyunifiprotect.data import ( Bootstrap, Camera, @@ -441,7 +440,7 @@ ONE_MONTH_SIMPLE = ( minute=0, second=0, microsecond=0, - tzinfo=pytz.timezone("US/Pacific"), + tzinfo=dt_util.get_time_zone("US/Pacific"), ), 1, ) @@ -454,7 +453,7 @@ TWO_MONTH_SIMPLE = ( minute=0, second=0, microsecond=0, - tzinfo=pytz.timezone("US/Pacific"), + tzinfo=dt_util.get_time_zone("US/Pacific"), ), 2, ) @@ -513,7 +512,7 @@ ONE_MONTH_TIMEZONE = ( minute=0, second=0, microsecond=0, - tzinfo=pytz.timezone("US/Pacific"), + tzinfo=dt_util.get_time_zone("US/Pacific"), ), 1, ) @@ -526,7 +525,7 @@ TWO_MONTH_TIMEZONE = ( minute=0, second=0, microsecond=0, - tzinfo=pytz.timezone("US/Pacific"), + tzinfo=dt_util.get_time_zone("US/Pacific"), ), 2, ) From 90b976457841bea4204b249f46a7f8e54cf77c19 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 18 Aug 2023 19:24:33 +0200 Subject: [PATCH 0628/1151] Bump hatasmota to 0.7.0 (#98636) * Bump hatasmota to 0.7.0 * Update tests according to new entity naming --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/mixins.py | 2 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/tasmota/test_binary_sensor.py | 20 +- tests/components/tasmota/test_common.py | 70 ++--- tests/components/tasmota/test_cover.py | 14 +- tests/components/tasmota/test_discovery.py | 10 +- tests/components/tasmota/test_fan.py | 41 +-- tests/components/tasmota/test_light.py | 262 +++++++++--------- tests/components/tasmota/test_switch.py | 26 +- 11 files changed, 238 insertions(+), 213 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index f235256f772..220bc4e31fb 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["hatasmota"], "mqtt": ["tasmota/discovery/#"], - "requirements": ["HATasmota==0.6.5"] + "requirements": ["HATasmota==0.7.0"] } diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index 859b11ebd4c..e99106d09e8 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -32,6 +32,8 @@ _LOGGER = logging.getLogger(__name__) class TasmotaEntity(Entity): """Base class for Tasmota entities.""" + _attr_has_entity_name = True + def __init__(self, tasmota_entity: HATasmotaEntity) -> None: """Initialize.""" self._tasmota_entity = tasmota_entity diff --git a/requirements_all.txt b/requirements_all.txt index 02e462f94e4..1afbd67743c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -29,7 +29,7 @@ DoorBirdPy==2.1.0 HAP-python==4.7.1 # homeassistant.components.tasmota -HATasmota==0.6.5 +HATasmota==0.7.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8bb737295fb..3d0f59d7bca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -28,7 +28,7 @@ DoorBirdPy==2.1.0 HAP-python==4.7.1 # homeassistant.components.tasmota -HATasmota==0.6.5 +HATasmota==0.7.0 # homeassistant.components.doods # homeassistant.components.generic diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 6a82a0f0e73..2bfb4a9d5e2 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -125,13 +125,13 @@ async def test_controlling_state_via_mqtt_switchname( ) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -139,35 +139,35 @@ async def test_controlling_state_via_mqtt_switchname( async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Custom Name":{"Action":"ON"}}' ) - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_ON async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Custom Name":{"Action":"OFF"}}' ) - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_OFF # Test periodic state update async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", '{"Custom Name":"ON"}') - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", '{"Custom Name":"OFF"}') - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_OFF # Test polled state update async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", '{"StatusSNS":{"Custom Name":"ON"}}' ) - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_ON async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", '{"StatusSNS":{"Custom Name":"OFF"}}' ) - state = hass.states.get("binary_sensor.custom_name") + state = hass.states.get("binary_sensor.tasmota_custom_name") assert state.state == STATE_OFF @@ -243,9 +243,9 @@ async def test_friendly_names( assert state.state == "unavailable" assert state.attributes.get("friendly_name") == "Tasmota binary_sensor 1" - state = hass.states.get("binary_sensor.beer") + state = hass.states.get("binary_sensor.tasmota_beer") assert state.state == "unavailable" - assert state.attributes.get("friendly_name") == "Beer" + assert state.attributes.get("friendly_name") == "Tasmota Beer" async def test_off_delay( diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 703dd2a1893..a184f650fae 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -129,7 +129,7 @@ async def help_test_availability_when_connection_lost( domain, config, sensor_config=None, - entity_id="test", + object_id="tasmota_test", ): """Test availability after MQTT disconnection. @@ -156,7 +156,7 @@ async def help_test_availability_when_connection_lost( config_get_state_online(config), ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE # Disconnected from MQTT server -> state changed to unavailable @@ -165,7 +165,7 @@ async def help_test_availability_when_connection_lost( await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE # Reconnected to MQTT server -> state still unavailable @@ -174,7 +174,7 @@ async def help_test_availability_when_connection_lost( await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE # Receive LWT again @@ -184,7 +184,7 @@ async def help_test_availability_when_connection_lost( config_get_state_online(config), ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE @@ -194,7 +194,7 @@ async def help_test_availability( domain, config, sensor_config=None, - entity_id="test", + object_id="tasmota_test", ): """Test availability. @@ -214,7 +214,7 @@ async def help_test_availability( ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message( @@ -223,7 +223,7 @@ async def help_test_availability( config_get_state_online(config), ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message( @@ -232,7 +232,7 @@ async def help_test_availability( config_get_state_offline(config), ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE @@ -242,7 +242,7 @@ async def help_test_availability_discovery_update( domain, config, sensor_config=None, - entity_id="test", + object_id="tasmota_test", ): """Test update of discovered TasmotaAvailability. @@ -280,17 +280,17 @@ async def help_test_availability_discovery_update( ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, availability_topic1, online1) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, availability_topic1, offline1) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE # Change availability settings @@ -302,13 +302,13 @@ async def help_test_availability_discovery_update( async_fire_mqtt_message(hass, availability_topic1, online2) async_fire_mqtt_message(hass, availability_topic2, online1) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, availability_topic2, online2) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE @@ -390,8 +390,8 @@ async def help_test_discovery_removal( config2, sensor_config1=None, sensor_config2=None, - entity_id="test", - name="Test", + object_id="tasmota_test", + name="Tasmota Test", ): """Test removal of discovered entity.""" device_reg = dr.async_get(hass) @@ -416,11 +416,11 @@ async def help_test_discovery_removal( connections={(dr.CONNECTION_NETWORK_MAC, config1[CONF_MAC])} ) assert device_entry is not None - entity_entry = entity_reg.async_get(f"{domain}.{entity_id}") + entity_entry = entity_reg.async_get(f"{domain}.{object_id}") assert entity_entry is not None # Verify state is added - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state is not None assert state.name == name @@ -439,11 +439,11 @@ async def help_test_discovery_removal( connections={(dr.CONNECTION_NETWORK_MAC, config2[CONF_MAC])} ) assert device_entry is not None - entity_entry = entity_reg.async_get(f"{domain}.{entity_id}") + entity_entry = entity_reg.async_get(f"{domain}.{object_id}") assert entity_entry is None # Verify state is removed - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state is None @@ -455,8 +455,8 @@ async def help_test_discovery_update_unchanged( config, discovery_update, sensor_config=None, - entity_id="test", - name="Test", + object_id="tasmota_test", + name="Tasmota Test", ): """Test update of discovered component with and without changes. @@ -479,7 +479,7 @@ async def help_test_discovery_update_unchanged( ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state is not None assert state.name == name @@ -538,7 +538,13 @@ async def help_test_discovery_device_remove( async def help_test_entity_id_update_subscriptions( - hass, mqtt_mock, domain, config, topics=None, sensor_config=None, entity_id="test" + hass, + mqtt_mock, + domain, + config, + topics=None, + sensor_config=None, + object_id="tasmota_test", ): """Test MQTT subscriptions are managed when entity_id is updated.""" entity_reg = er.async_get(hass) @@ -562,7 +568,7 @@ async def help_test_entity_id_update_subscriptions( topics = [get_topic_tele_state(config), get_topic_tele_will(config)] assert len(topics) > 0 - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state is not None assert mqtt_mock.async_subscribe.call_count == len(topics) for topic in topics: @@ -570,11 +576,11 @@ async def help_test_entity_id_update_subscriptions( mqtt_mock.async_subscribe.reset_mock() entity_reg.async_update_entity( - f"{domain}.{entity_id}", new_entity_id=f"{domain}.milk" + f"{domain}.{object_id}", new_entity_id=f"{domain}.milk" ) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state is None state = hass.states.get(f"{domain}.milk") @@ -584,7 +590,7 @@ async def help_test_entity_id_update_subscriptions( async def help_test_entity_id_update_discovery_update( - hass, mqtt_mock, domain, config, sensor_config=None, entity_id="test" + hass, mqtt_mock, domain, config, sensor_config=None, object_id="tasmota_test" ): """Test MQTT discovery update after entity_id is updated.""" entity_reg = er.async_get(hass) @@ -606,16 +612,16 @@ async def help_test_entity_id_update_discovery_update( async_fire_mqtt_message(hass, topic, config_get_state_online(config)) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, topic, config_get_state_offline(config)) await hass.async_block_till_done() - state = hass.states.get(f"{domain}.{entity_id}") + state = hass.states.get(f"{domain}.{object_id}") assert state.state == STATE_UNAVAILABLE entity_reg.async_update_entity( - f"{domain}.{entity_id}", new_entity_id=f"{domain}.milk" + f"{domain}.{object_id}", new_entity_id=f"{domain}.milk" ) await hass.async_block_till_done() assert hass.states.get(f"{domain}.milk") diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index 156ea365b48..5c1364f1f77 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -658,7 +658,7 @@ async def test_availability_when_connection_lost( mqtt_mock, Platform.COVER, config, - entity_id="test_cover_1", + object_id="test_cover_1", ) @@ -671,7 +671,7 @@ async def test_availability( config["rl"][0] = 3 config["rl"][1] = 3 await help_test_availability( - hass, mqtt_mock, Platform.COVER, config, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, object_id="test_cover_1" ) @@ -684,7 +684,7 @@ async def test_availability_discovery_update( config["rl"][0] = 3 config["rl"][1] = 3 await help_test_availability_discovery_update( - hass, mqtt_mock, Platform.COVER, config, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, object_id="test_cover_1" ) @@ -727,7 +727,7 @@ async def test_discovery_removal_cover( Platform.COVER, config1, config2, - entity_id="test_cover_1", + object_id="test_cover_1", name="Test cover 1", ) @@ -753,7 +753,7 @@ async def test_discovery_update_unchanged_cover( Platform.COVER, config, discovery_update, - entity_id="test_cover_1", + object_id="test_cover_1", name="Test cover 1", ) @@ -787,7 +787,7 @@ async def test_entity_id_update_subscriptions( get_topic_tele_will(config), ] await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, Platform.COVER, config, topics, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, topics, object_id="test_cover_1" ) @@ -800,5 +800,5 @@ async def test_entity_id_update_discovery_update( config["rl"][0] = 3 config["rl"][1] = 3 await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, Platform.COVER, config, entity_id="test_cover_1" + hass, mqtt_mock, Platform.COVER, config, object_id="test_cover_1" ) diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 9a3f4f91ec7..4fd9f293498 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -143,12 +143,12 @@ async def test_correct_config_discovery( connections={(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None - entity_entry = entity_reg.async_get("switch.test") + entity_entry = entity_reg.async_get("switch.tasmota_test") assert entity_entry is not None - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state is not None - assert state.name == "Test" + assert state.name == "Tasmota Test" assert (mac, "switch", "relay", 0) in hass.data[ALREADY_DISCOVERED] @@ -530,11 +530,11 @@ async def test_entity_duplicate_discovery( ) await hass.async_block_till_done() - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") state_duplicate = hass.states.get("binary_sensor.beer1") assert state is not None - assert state.name == "Test" + assert state.name == "Tasmota Test" assert state_duplicate is None assert ( f"Entity already added, sending update: switch ('{mac}', 'switch', 'relay', 0)" diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 0b99036518e..2a50e2d43b5 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -226,10 +226,9 @@ async def test_availability_when_connection_lost( ) -> None: """Test availability after MQTT disconnection.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 await help_test_availability_when_connection_lost( - hass, mqtt_client_mock, mqtt_mock, Platform.FAN, config + hass, mqtt_client_mock, mqtt_mock, Platform.FAN, config, object_id="tasmota" ) @@ -238,9 +237,10 @@ async def test_availability( ) -> None: """Test availability.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 - await help_test_availability(hass, mqtt_mock, Platform.FAN, config) + await help_test_availability( + hass, mqtt_mock, Platform.FAN, config, object_id="tasmota" + ) async def test_availability_discovery_update( @@ -248,9 +248,10 @@ async def test_availability_discovery_update( ) -> None: """Test availability discovery update.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 - await help_test_availability_discovery_update(hass, mqtt_mock, Platform.FAN, config) + await help_test_availability_discovery_update( + hass, mqtt_mock, Platform.FAN, config, object_id="tasmota" + ) async def test_availability_poll_state( @@ -276,14 +277,19 @@ async def test_discovery_removal_fan( ) -> None: """Test removal of discovered fan.""" config1 = copy.deepcopy(DEFAULT_CONFIG) - config1["dn"] = "Test" config1["if"] = 1 config2 = copy.deepcopy(DEFAULT_CONFIG) - config2["dn"] = "Test" config2["if"] = 0 await help_test_discovery_removal( - hass, mqtt_mock, caplog, Platform.FAN, config1, config2 + hass, + mqtt_mock, + caplog, + Platform.FAN, + config1, + config2, + object_id="tasmota", + name="Tasmota", ) @@ -295,13 +301,19 @@ async def test_discovery_update_unchanged_fan( ) -> None: """Test update of discovered fan.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 with patch( "homeassistant.components.tasmota.fan.TasmotaFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, Platform.FAN, config, discovery_update + hass, + mqtt_mock, + caplog, + Platform.FAN, + config, + discovery_update, + object_id="tasmota", + name="Tasmota", ) @@ -310,7 +322,6 @@ async def test_discovery_device_remove( ) -> None: """Test device registry remove.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 unique_id = f"{DEFAULT_CONFIG['mac']}_fan_fan_ifan" await help_test_discovery_device_remove( @@ -323,7 +334,6 @@ async def test_entity_id_update_subscriptions( ) -> None: """Test MQTT subscriptions are managed when entity_id is updated.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 topics = [ get_topic_stat_result(config), @@ -331,7 +341,7 @@ async def test_entity_id_update_subscriptions( get_topic_tele_will(config), ] await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, Platform.FAN, config, topics + hass, mqtt_mock, Platform.FAN, config, topics, object_id="tasmota" ) @@ -340,8 +350,7 @@ async def test_entity_id_update_discovery_update( ) -> None: """Test MQTT discovery update when entity_id is updated.""" config = copy.deepcopy(DEFAULT_CONFIG) - config["dn"] = "Test" config["if"] = 1 await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, Platform.FAN, config + hass, mqtt_mock, Platform.FAN, config, object_id="tasmota" ) diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 612bda8bb08..5c8339a6f89 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -53,7 +53,7 @@ async def test_attributes_on_off( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None @@ -82,7 +82,7 @@ async def test_attributes_dimmer_tuya( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None @@ -110,7 +110,7 @@ async def test_attributes_dimmer( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None @@ -138,7 +138,7 @@ async def test_attributes_ct( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") == 153 assert state.attributes.get("max_mireds") == 500 @@ -167,7 +167,7 @@ async def test_attributes_ct_reduced( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") == 200 assert state.attributes.get("max_mireds") == 380 @@ -195,7 +195,7 @@ async def test_attributes_rgb( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") == [ "Solid", "Wake up", @@ -232,7 +232,7 @@ async def test_attributes_rgbw( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") == [ "Solid", "Wake up", @@ -269,7 +269,7 @@ async def test_attributes_rgbww( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") == [ "Solid", "Wake up", @@ -307,7 +307,7 @@ async def test_attributes_rgbww_reduced( await hass.async_block_till_done() async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("effect_list") == [ "Solid", "Wake up", @@ -341,37 +341,37 @@ async def test_controlling_state_via_mqtt_on_off( ) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "onoff" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "onoff" async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes @@ -392,32 +392,32 @@ async def test_controlling_state_via_mqtt_ct( ) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "color_temp" @@ -425,7 +425,7 @@ async def test_controlling_state_via_mqtt_ct( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 assert state.attributes.get("color_mode") == "color_temp" @@ -434,7 +434,7 @@ async def test_controlling_state_via_mqtt_ct( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"255,128"}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 assert state.attributes.get("brightness") == 128 @@ -457,32 +457,32 @@ async def test_controlling_state_via_mqtt_rgbw( ) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "hs" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":0}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "hs" @@ -490,7 +490,7 @@ async def test_controlling_state_via_mqtt_rgbw( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":75,"White":75}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 191 assert state.attributes.get("color_mode") == "white" @@ -500,7 +500,7 @@ async def test_controlling_state_via_mqtt_rgbw( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"HSBColor":"30,100,50","White":0}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("hs_color") == (30, 100) @@ -509,7 +509,7 @@ async def test_controlling_state_via_mqtt_rgbw( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("rgb_color") is None @@ -518,7 +518,7 @@ async def test_controlling_state_via_mqtt_rgbw( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":0}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 0 assert state.attributes.get("rgb_color") is None @@ -527,18 +527,18 @@ async def test_controlling_state_via_mqtt_rgbw( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("effect") == "Cycle down" async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF @@ -558,32 +558,32 @@ async def test_controlling_state_via_mqtt_rgbww( ) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "color_temp" @@ -593,7 +593,7 @@ async def test_controlling_state_via_mqtt_rgbww( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"HSBColor":"30,100,50","White":0}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("hs_color") == (30, 100) assert state.attributes.get("color_mode") == "hs" @@ -601,7 +601,7 @@ async def test_controlling_state_via_mqtt_rgbww( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON # Setting white > 0 should clear the color assert "rgb_color" not in state.attributes @@ -610,7 +610,7 @@ async def test_controlling_state_via_mqtt_rgbww( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 assert state.attributes.get("color_mode") == "color_temp" @@ -618,7 +618,7 @@ async def test_controlling_state_via_mqtt_rgbww( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON # Setting white to 0 should clear the color_temp assert "color_temp" not in state.attributes @@ -628,18 +628,18 @@ async def test_controlling_state_via_mqtt_rgbww( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("effect") == "Cycle down" async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF @@ -660,32 +660,32 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( ) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_mode") == "color_temp" @@ -695,7 +695,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","HSBColor":"30,100,0","White":0}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("hs_color") == (30, 100) assert state.attributes.get("color_mode") == "hs" @@ -705,7 +705,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":0}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("hs_color") == (30, 100) assert state.attributes.get("color_mode") == "hs" @@ -713,7 +713,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON # Setting white > 0 should clear the color assert "rgb_color" not in state.attributes @@ -722,7 +722,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 assert state.attributes.get("color_mode") == "color_temp" @@ -730,7 +730,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON # Setting white to 0 should clear the color_temp assert not state.attributes.get("color_temp") @@ -739,18 +739,18 @@ async def test_controlling_state_via_mqtt_rgbww_tuya( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("effect") == "Cycle down" async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF @@ -772,25 +772,25 @@ async def test_sending_mqtt_commands_on_off( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Power1", "ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Power1", "OFF", 0, False ) @@ -816,32 +816,32 @@ async def test_sending_mqtt_commands_rgbww_tuya( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent - await common.async_turn_on(hass, "light.test", brightness=192) + await common.async_turn_on(hass, "light.tasmota_test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer3 75", 0, False ) @@ -866,39 +866,39 @@ async def test_sending_mqtt_commands_rgbw_legacy( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent - await common.async_turn_on(hass, "light.test", brightness=192) + await common.async_turn_on(hass, "light.tasmota_test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False ) mqtt_mock.async_publish.reset_mock() # Set color when setting color - await common.async_turn_on(hass, "light.test", hs_color=[0, 100]) + await common.async_turn_on(hass, "light.tasmota_test", hs_color=[0, 100]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 0;NoDelay;HsbColor2 100", @@ -908,7 +908,7 @@ async def test_sending_mqtt_commands_rgbw_legacy( mqtt_mock.async_publish.reset_mock() # Set white when setting white - await common.async_turn_on(hass, "light.test", white=128) + await common.async_turn_on(hass, "light.tasmota_test", white=128) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;White 50", @@ -918,7 +918,7 @@ async def test_sending_mqtt_commands_rgbw_legacy( mqtt_mock.async_publish.reset_mock() # rgbw_color should be converted - await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) + await common.async_turn_on(hass, "light.tasmota_test", rgbw_color=[128, 64, 32, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 20;NoDelay;HsbColor2 75", @@ -928,7 +928,7 @@ async def test_sending_mqtt_commands_rgbw_legacy( mqtt_mock.async_publish.reset_mock() # rgbw_color should be converted - await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) + await common.async_turn_on(hass, "light.tasmota_test", rgbw_color=[16, 64, 32, 128]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 141;NoDelay;HsbColor2 25", @@ -937,7 +937,7 @@ async def test_sending_mqtt_commands_rgbw_legacy( ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") + await common.async_turn_on(hass, "light.tasmota_test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;Scheme 4", @@ -965,39 +965,39 @@ async def test_sending_mqtt_commands_rgbw( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent - await common.async_turn_on(hass, "light.test", brightness=192) + await common.async_turn_on(hass, "light.tasmota_test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False ) mqtt_mock.async_publish.reset_mock() # Set color when setting color - await common.async_turn_on(hass, "light.test", hs_color=[180, 50]) + await common.async_turn_on(hass, "light.tasmota_test", hs_color=[180, 50]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 180;NoDelay;HsbColor2 50", @@ -1007,7 +1007,7 @@ async def test_sending_mqtt_commands_rgbw( mqtt_mock.async_publish.reset_mock() # Set white when setting white - await common.async_turn_on(hass, "light.test", white=128) + await common.async_turn_on(hass, "light.tasmota_test", white=128) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;White 50", @@ -1017,7 +1017,7 @@ async def test_sending_mqtt_commands_rgbw( mqtt_mock.async_publish.reset_mock() # rgbw_color should be converted - await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) + await common.async_turn_on(hass, "light.tasmota_test", rgbw_color=[128, 64, 32, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 20;NoDelay;HsbColor2 75", @@ -1027,7 +1027,7 @@ async def test_sending_mqtt_commands_rgbw( mqtt_mock.async_publish.reset_mock() # rgbw_color should be converted - await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) + await common.async_turn_on(hass, "light.tasmota_test", rgbw_color=[16, 64, 32, 128]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 141;NoDelay;HsbColor2 25", @@ -1036,7 +1036,7 @@ async def test_sending_mqtt_commands_rgbw( ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") + await common.async_turn_on(hass, "light.tasmota_test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;Scheme 4", @@ -1064,38 +1064,38 @@ async def test_sending_mqtt_commands_rgbww( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent - await common.async_turn_on(hass, "light.test", brightness=192) + await common.async_turn_on(hass, "light.tasmota_test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", hs_color=[240, 75]) + await common.async_turn_on(hass, "light.tasmota_test", hs_color=[240, 75]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;HsbColor1 240;NoDelay;HsbColor2 75", @@ -1104,7 +1104,7 @@ async def test_sending_mqtt_commands_rgbww( ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", color_temp=200) + await common.async_turn_on(hass, "light.tasmota_test", color_temp=200) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;CT 200", @@ -1113,7 +1113,7 @@ async def test_sending_mqtt_commands_rgbww( ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") + await common.async_turn_on(hass, "light.tasmota_test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON;NoDelay;Scheme 4", @@ -1142,32 +1142,32 @@ async def test_sending_mqtt_commands_power_unlinked( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT message is sent - await common.async_turn_on(hass, "light.test") + await common.async_turn_on(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF # Turn the light off and verify MQTT message is sent - await common.async_turn_off(hass, "light.test") + await common.async_turn_off(hass, "light.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent; POWER should be sent - await common.async_turn_on(hass, "light.test", brightness=192) + await common.async_turn_on(hass, "light.tasmota_test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75;NoDelay;Power1 ON", @@ -1195,14 +1195,14 @@ async def test_transition( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Dim the light from 0->100: Speed should be 4*2=8 - await common.async_turn_on(hass, "light.test", brightness=255, transition=4) + await common.async_turn_on(hass, "light.tasmota_test", brightness=255, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 100", @@ -1212,7 +1212,9 @@ async def test_transition( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->100: Speed should be capped at 40 - await common.async_turn_on(hass, "light.test", brightness=255, transition=100) + await common.async_turn_on( + hass, "light.tasmota_test", brightness=255, transition=100 + ) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Dimmer 100", @@ -1222,7 +1224,7 @@ async def test_transition( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->0: Speed should be 1 - await common.async_turn_on(hass, "light.test", brightness=0, transition=100) + await common.async_turn_on(hass, "light.tasmota_test", brightness=0, transition=100) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 1;NoDelay;Power1 OFF", @@ -1232,7 +1234,7 @@ async def test_transition( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->50: Speed should be 4*2*2=16 - await common.async_turn_on(hass, "light.test", brightness=128, transition=4) + await common.async_turn_on(hass, "light.tasmota_test", brightness=128, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 16;NoDelay;Dimmer 50", @@ -1245,12 +1247,12 @@ async def test_transition( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 # Dim the light from 50->0: Speed should be 6*2*2=24 - await common.async_turn_off(hass, "light.test", transition=6) + await common.async_turn_off(hass, "light.tasmota_test", transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 OFF", @@ -1263,12 +1265,12 @@ async def test_transition( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":100}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 255 # Dim the light from 100->0: Speed should be 0 - await common.async_turn_off(hass, "light.test", transition=0) + await common.async_turn_off(hass, "light.tasmota_test", transition=0) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 0;NoDelay;Power1 OFF", @@ -1286,13 +1288,15 @@ async def test_transition( ' "Color":"0,255,0","HSBColor":"120,100,50","White":0}' ), ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("rgb_color") == (0, 255, 0) # Set color of the light from 0,255,0 to 255,0,0 @ 50%: Speed should be 6*2*2=24 - await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6) + await common.async_turn_on( + hass, "light.tasmota_test", rgb_color=[255, 0, 0], transition=6 + ) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", ( @@ -1310,13 +1314,15 @@ async def test_transition( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":100, "Color":"0,255,0","HSBColor":"120,100,50"}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 255 assert state.attributes.get("rgb_color") == (0, 255, 0) # Set color of the light from 0,255,0 to 255,0,0 @ 100%: Speed should be 6*2=12 - await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6) + await common.async_turn_on( + hass, "light.tasmota_test", rgb_color=[255, 0, 0], transition=6 + ) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", ( @@ -1334,13 +1340,13 @@ async def test_transition( "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50, "CT":153, "White":50}', ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_temp") == 153 # Set color_temp of the light from 153 to 500 @ 50%: Speed should be 6*2*2=24 - await common.async_turn_on(hass, "light.test", color_temp=500, transition=6) + await common.async_turn_on(hass, "light.tasmota_test", color_temp=500, transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 24;NoDelay;Power1 ON;NoDelay;CT 500", @@ -1353,13 +1359,13 @@ async def test_transition( async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50, "CT":500}' ) - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 128 assert state.attributes.get("color_temp") == 500 # Set color_temp of the light from 500 to 326 @ 50%: Speed should be 6*2*2*2=48->40 - await common.async_turn_on(hass, "light.test", color_temp=326, transition=6) + await common.async_turn_on(hass, "light.tasmota_test", color_temp=326, transition=6) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Power1 ON;NoDelay;CT 326", @@ -1388,14 +1394,14 @@ async def test_transition_fixed( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Dim the light from 0->100: Speed should be 4*2=8 - await common.async_turn_on(hass, "light.test", brightness=255, transition=4) + await common.async_turn_on(hass, "light.tasmota_test", brightness=255, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 100", @@ -1405,7 +1411,9 @@ async def test_transition_fixed( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->100: Speed should be capped at 40 - await common.async_turn_on(hass, "light.test", brightness=255, transition=100) + await common.async_turn_on( + hass, "light.tasmota_test", brightness=255, transition=100 + ) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 40;NoDelay;Dimmer 100", @@ -1415,7 +1423,7 @@ async def test_transition_fixed( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->0: Speed should be 4*2=8 - await common.async_turn_on(hass, "light.test", brightness=0, transition=4) + await common.async_turn_on(hass, "light.tasmota_test", brightness=0, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Power1 OFF", @@ -1425,7 +1433,7 @@ async def test_transition_fixed( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->50: Speed should be 4*2=8 - await common.async_turn_on(hass, "light.test", brightness=128, transition=4) + await common.async_turn_on(hass, "light.tasmota_test", brightness=128, transition=4) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 1;NoDelay;Speed2 8;NoDelay;Dimmer 50", @@ -1435,7 +1443,7 @@ async def test_transition_fixed( mqtt_mock.async_publish.reset_mock() # Dim the light from 0->50: Speed should be 0 - await common.async_turn_on(hass, "light.test", brightness=128, transition=0) + await common.async_turn_on(hass, "light.tasmota_test", brightness=128, transition=0) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade2 0;NoDelay;Dimmer 50", @@ -1463,7 +1471,7 @@ async def test_relay_as_light( state = hass.states.get("switch.test") assert state is None - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state is not None @@ -1631,14 +1639,14 @@ async def test_discovery_update_reconfigure_light( # Simple dimmer async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state.attributes.get("supported_features") == LightEntityFeature.TRANSITION assert state.attributes.get("supported_color_modes") == ["brightness"] # Reconfigure as RGB light async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data2) await hass.async_block_till_done() - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert ( state.attributes.get("supported_features") == LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index b79560214a8..b8d0ed2d060 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -47,34 +47,34 @@ async def test_controlling_state_via_mqtt( ) await hass.async_block_till_done() - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_ON async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF @@ -95,30 +95,30 @@ async def test_sending_mqtt_commands( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF await hass.async_block_till_done() await hass.async_block_till_done() mqtt_mock.async_publish.reset_mock() # Turn the switch on and verify MQTT message is sent - await common.async_turn_on(hass, "switch.test") + await common.async_turn_on(hass, "switch.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Power1", "ON", 0, False ) mqtt_mock.async_publish.reset_mock() # Tasmota is not optimistic, the state should still be off - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF # Turn the switch off and verify MQTT message is sent - await common.async_turn_off(hass, "switch.test") + await common.async_turn_off(hass, "switch.tasmota_test") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Power1", "OFF", 0, False ) - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state.state == STATE_OFF @@ -138,9 +138,9 @@ async def test_relay_as_light( ) await hass.async_block_till_done() - state = hass.states.get("switch.test") + state = hass.states.get("switch.tasmota_test") assert state is None - state = hass.states.get("light.test") + state = hass.states.get("light.tasmota_test") assert state is not None From 7fcc2dd44e84711f07f2ff9e55b5979851be3fbd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 18 Aug 2023 20:15:00 +0200 Subject: [PATCH 0629/1151] Make the check_config script open issue_registry read only (#98545) * Don't blow up if validators can't access the issue registry * Make the check_config script open issue_registry read only * Update tests/helpers/test_issue_registry.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/helpers/config_validation.py | 1 + homeassistant/helpers/issue_registry.py | 8 ++- homeassistant/helpers/storage.py | 5 ++ homeassistant/scripts/check_config.py | 2 + tests/helpers/test_issue_registry.py | 73 +++++++++++++++++++++- tests/helpers/test_storage.py | 30 +++++++++ 6 files changed, 115 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 122fd752a84..10f4918a20b 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1122,6 +1122,7 @@ def _no_yaml_config_schema( # pylint: disable-next=import-outside-toplevel from .issue_registry import IssueSeverity, async_create_issue + # HomeAssistantError is raised if called from the wrong thread with contextlib.suppress(HomeAssistantError): hass = async_get_hass() async_create_issue( diff --git a/homeassistant/helpers/issue_registry.py b/homeassistant/helpers/issue_registry.py index 30866ccf7cd..27d568a13de 100644 --- a/homeassistant/helpers/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -95,16 +95,18 @@ class IssueRegistryStore(Store[dict[str, list[dict[str, Any]]]]): class IssueRegistry: """Class to hold a registry of issues.""" - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, hass: HomeAssistant, *, read_only: bool = False) -> None: """Initialize the issue registry.""" self.hass = hass self.issues: dict[tuple[str, str], IssueEntry] = {} + self._read_only = read_only self._store = IssueRegistryStore( hass, STORAGE_VERSION_MAJOR, STORAGE_KEY, atomic_writes=True, minor_version=STORAGE_VERSION_MINOR, + read_only=read_only, ) @callback @@ -278,10 +280,10 @@ def async_get(hass: HomeAssistant) -> IssueRegistry: return cast(IssueRegistry, hass.data[DATA_REGISTRY]) -async def async_load(hass: HomeAssistant) -> None: +async def async_load(hass: HomeAssistant, *, read_only: bool = False) -> None: """Load issue registry.""" assert DATA_REGISTRY not in hass.data - hass.data[DATA_REGISTRY] = IssueRegistry(hass) + hass.data[DATA_REGISTRY] = IssueRegistry(hass, read_only=read_only) await hass.data[DATA_REGISTRY].async_load() diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index dd394c84f91..c83481365ab 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -93,6 +93,7 @@ class Store(Generic[_T]): atomic_writes: bool = False, encoder: type[JSONEncoder] | None = None, minor_version: int = 1, + read_only: bool = False, ) -> None: """Initialize storage class.""" self.version = version @@ -107,6 +108,7 @@ class Store(Generic[_T]): self._load_task: asyncio.Future[_T | None] | None = None self._encoder = encoder self._atomic_writes = atomic_writes + self._read_only = read_only @property def path(self): @@ -344,6 +346,9 @@ class Store(Generic[_T]): self._data = None + if self._read_only: + return + try: await self._async_write_data(self.path, data) except (json_util.SerializationError, WriteError) as err: diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 5c81c4664da..38fa9cc2463 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -19,6 +19,7 @@ from homeassistant.helpers import ( area_registry as ar, device_registry as dr, entity_registry as er, + issue_registry as ir, ) from homeassistant.helpers.check_config import async_check_ha_config_file from homeassistant.util.yaml import Secrets @@ -237,6 +238,7 @@ async def async_check_config(config_dir): await ar.async_load(hass) await dr.async_load(hass) await er.async_load(hass) + await ir.async_load(hass, read_only=True) components = await async_check_ha_config_file(hass) await hass.async_stop(force=True) return components diff --git a/tests/helpers/test_issue_registry.py b/tests/helpers/test_issue_registry.py index d184ccf0a2b..88f97a65421 100644 --- a/tests/helpers/test_issue_registry.py +++ b/tests/helpers/test_issue_registry.py @@ -9,7 +9,7 @@ from homeassistant.helpers import issue_registry as ir from tests.common import async_capture_events, flush_store -async def test_load_issues(hass: HomeAssistant) -> None: +async def test_load_save_issues(hass: HomeAssistant) -> None: """Make sure that we can load/save data correctly.""" issues = [ { @@ -209,6 +209,77 @@ async def test_load_issues(hass: HomeAssistant) -> None: assert issue4_registry2 == issue4 +@pytest.mark.parametrize("load_registries", [False]) +async def test_load_save_issues_read_only( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> None: + """Make sure that we don't save data when opened in read-only mode.""" + hass_storage[ir.STORAGE_KEY] = { + "version": ir.STORAGE_VERSION_MAJOR, + "minor_version": ir.STORAGE_VERSION_MINOR, + "data": { + "issues": [ + { + "created": "2022-07-19T09:41:13.746514+00:00", + "dismissed_version": "2022.7.0.dev0", + "domain": "test", + "is_persistent": False, + "issue_id": "issue_1", + }, + ] + }, + } + + issues = [ + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "is_fixable": True, + "is_persistent": False, + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED) + await ir.async_load(hass, read_only=True) + + for issue in issues: + ir.async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + is_persistent=issue["is_persistent"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_2", + } + + registry = ir.async_get(hass) + assert len(registry.issues) == 2 + + registry2 = ir.IssueRegistry(hass) + await flush_store(registry._store) + await registry2.async_load() + + assert len(registry2.issues) == 1 + + @pytest.mark.parametrize("load_registries", [False]) async def test_loading_issues_from_storage( hass: HomeAssistant, hass_storage: dict[str, Any] diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 81953c7d785..85aa4d2de0e 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -60,6 +60,12 @@ def store_v_2_1(hass): ) +@pytest.fixture +def read_only_store(hass): + """Fixture of a read only store.""" + return storage.Store(hass, MOCK_VERSION, MOCK_KEY, read_only=True) + + async def test_loading(hass: HomeAssistant, store) -> None: """Test we can save and load data.""" await store.async_save(MOCK_DATA) @@ -703,3 +709,27 @@ async def test_os_error_is_fatal(tmpdir: py.path.local) -> None: await store.async_load() await hass.async_stop(force=True) + + +async def test_read_only_store( + hass: HomeAssistant, read_only_store, hass_storage: dict[str, Any] +) -> None: + """Test store opened in read only mode does not save.""" + read_only_store.async_delay_save(lambda: MOCK_DATA, 1) + assert read_only_store.key not in hass_storage + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert read_only_store.key not in hass_storage + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + hass.state = CoreState.stopping + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert read_only_store.key not in hass_storage + + hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) + await hass.async_block_till_done() + assert read_only_store.key not in hass_storage From 268e5244f0746d1779ed00403392df604e2df998 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 18 Aug 2023 20:19:17 +0200 Subject: [PATCH 0630/1151] Cleanup ManualTriggerSensorEntity (#98629) * Cleanup ManualTriggerSensorEntity * ConfigType --- .../components/command_line/sensor.py | 40 ++++++----- homeassistant/components/rest/sensor.py | 3 +- homeassistant/components/scrape/sensor.py | 57 +++++++--------- homeassistant/components/sql/sensor.py | 68 +++++++------------ homeassistant/helpers/template_entity.py | 8 +-- 5 files changed, 74 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 2ccbdbc4785..f04320b159e 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -16,13 +16,12 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASSES_SCHEMA, SensorDeviceClass, - SensorEntity, - SensorStateClass, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( CONF_COMMAND, CONF_DEVICE_CLASS, + CONF_ICON, CONF_NAME, CONF_SCAN_INTERVAL, CONF_UNIQUE_ID, @@ -36,7 +35,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template -from homeassistant.helpers.template_entity import ManualTriggerEntity +from homeassistant.helpers.template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, + ManualTriggerSensorEntity, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util @@ -47,6 +50,16 @@ CONF_JSON_ATTRIBUTES = "json_attributes" DEFAULT_NAME = "Command Sensor" +TRIGGER_ENTITY_OPTIONS = ( + CONF_AVAILABILITY, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_PICTURE, + CONF_UNIQUE_ID, + CONF_STATE_CLASS, + CONF_UNIT_OF_MEASUREMENT, +) + SCAN_INTERVAL = timedelta(seconds=60) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -87,30 +100,25 @@ async def async_setup_platform( name: str = sensor_config[CONF_NAME] command: str = sensor_config[CONF_COMMAND] - unit: str | None = sensor_config.get(CONF_UNIT_OF_MEASUREMENT) value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) command_timeout: int = sensor_config[CONF_COMMAND_TIMEOUT] - unique_id: str | None = sensor_config.get(CONF_UNIQUE_ID) if value_template is not None: value_template.hass = hass json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES) scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - state_class: SensorStateClass | None = sensor_config.get(CONF_STATE_CLASS) data = CommandSensorData(hass, command, command_timeout) - trigger_entity_config = { - CONF_UNIQUE_ID: unique_id, - CONF_NAME: Template(name, hass), - CONF_DEVICE_CLASS: sensor_config.get(CONF_DEVICE_CLASS), - } + trigger_entity_config = {CONF_NAME: Template(name, hass)} + for key in TRIGGER_ENTITY_OPTIONS: + if key not in sensor_config: + continue + trigger_entity_config[key] = sensor_config[key] async_add_entities( [ CommandSensor( data, trigger_entity_config, - unit, - state_class, value_template, json_attributes, scan_interval, @@ -119,7 +127,7 @@ async def async_setup_platform( ) -class CommandSensor(ManualTriggerEntity, SensorEntity): +class CommandSensor(ManualTriggerSensorEntity): """Representation of a sensor that is using shell commands.""" _attr_should_poll = False @@ -128,8 +136,6 @@ class CommandSensor(ManualTriggerEntity, SensorEntity): self, data: CommandSensorData, config: ConfigType, - unit_of_measurement: str | None, - state_class: SensorStateClass | None, value_template: Template | None, json_attributes: list[str] | None, scan_interval: timedelta, @@ -141,8 +147,6 @@ class CommandSensor(ManualTriggerEntity, SensorEntity): self._json_attributes = json_attributes self._attr_native_value = None self._value_template = value_template - self._attr_native_unit_of_measurement = unit_of_measurement - self._attr_state_class = state_class self._scan_interval = scan_interval self._process_updates: asyncio.Lock | None = None diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index f7743a853ad..63a9d6f210c 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, SensorDeviceClass, - SensorEntity, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( @@ -118,7 +117,7 @@ async def async_setup_platform( ) -class RestSensor(ManualTriggerSensorEntity, RestEntity, SensorEntity): +class RestSensor(ManualTriggerSensorEntity, RestEntity): """Implementation of a REST sensor.""" def __init__( diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 7cd7e2197ab..2763d034804 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -6,11 +6,7 @@ from typing import Any, cast import voluptuous as vol -from homeassistant.components.sensor import ( - CONF_STATE_CLASS, - SensorDeviceClass, - SensorEntity, -) +from homeassistant.components.sensor import CONF_STATE_CLASS, SensorDeviceClass from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -32,6 +28,7 @@ from homeassistant.helpers.template_entity import ( CONF_PICTURE, TEMPLATE_SENSOR_BASE_SCHEMA, ManualTriggerEntity, + ManualTriggerSensorEntity, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -41,6 +38,16 @@ from .coordinator import ScrapeCoordinator _LOGGER = logging.getLogger(__name__) +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, @@ -63,25 +70,17 @@ async def async_setup_platform( if value_template is not None: value_template.hass = hass - trigger_entity_config = { - CONF_NAME: sensor_config[CONF_NAME], - CONF_DEVICE_CLASS: sensor_config.get(CONF_DEVICE_CLASS), - CONF_UNIQUE_ID: sensor_config.get(CONF_UNIQUE_ID), - } - if available := sensor_config.get(CONF_AVAILABILITY): - trigger_entity_config[CONF_AVAILABILITY] = available - if icon := sensor_config.get(CONF_ICON): - trigger_entity_config[CONF_ICON] = icon - if picture := sensor_config.get(CONF_PICTURE): - trigger_entity_config[CONF_PICTURE] = picture + trigger_entity_config = {CONF_NAME: sensor_config[CONF_NAME]} + for key in TRIGGER_ENTITY_OPTIONS: + if key not in sensor_config: + continue + trigger_entity_config[key] = sensor_config[key] entities.append( ScrapeSensor( hass, coordinator, trigger_entity_config, - sensor_config.get(CONF_UNIT_OF_MEASUREMENT), - sensor_config.get(CONF_STATE_CLASS), sensor_config[CONF_SELECT], sensor_config.get(CONF_ATTRIBUTE), sensor_config[CONF_INDEX], @@ -113,19 +112,17 @@ async def async_setup_entry( Template(value_string, hass) if value_string is not None else None ) - trigger_entity_config = { - CONF_NAME: name, - CONF_DEVICE_CLASS: sensor_config.get(CONF_DEVICE_CLASS), - CONF_UNIQUE_ID: sensor_config[CONF_UNIQUE_ID], - } + trigger_entity_config = {CONF_NAME: name} + for key in TRIGGER_ENTITY_OPTIONS: + if key not in sensor_config: + continue + trigger_entity_config[key] = sensor_config[key] entities.append( ScrapeSensor( hass, coordinator, trigger_entity_config, - sensor_config.get(CONF_UNIT_OF_MEASUREMENT), - sensor_config.get(CONF_STATE_CLASS), sensor_config[CONF_SELECT], sensor_config.get(CONF_ATTRIBUTE), sensor_config[CONF_INDEX], @@ -137,9 +134,7 @@ async def async_setup_entry( async_add_entities(entities) -class ScrapeSensor( - CoordinatorEntity[ScrapeCoordinator], ManualTriggerEntity, SensorEntity -): +class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEntity): """Representation of a web scrape sensor.""" def __init__( @@ -147,8 +142,6 @@ class ScrapeSensor( hass: HomeAssistant, coordinator: ScrapeCoordinator, trigger_entity_config: ConfigType, - unit_of_measurement: str | None, - state_class: str | None, select: str, attr: str | None, index: int, @@ -157,9 +150,7 @@ class ScrapeSensor( ) -> None: """Initialize a web scrape sensor.""" CoordinatorEntity.__init__(self, coordinator) - ManualTriggerEntity.__init__(self, hass, trigger_entity_config) - self._attr_native_unit_of_measurement = unit_of_measurement - self._attr_state_class = state_class + ManualTriggerSensorEntity.__init__(self, hass, trigger_entity_config) self._select = select self._attr = attr self._index = index diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index f750b364106..0b32b10f972 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -19,12 +19,7 @@ from homeassistant.components.recorder import ( SupportedDialect, get_instance, ) -from homeassistant.components.sensor import ( - CONF_STATE_CLASS, - SensorDeviceClass, - SensorEntity, - SensorStateClass, -) +from homeassistant.components.sensor import CONF_STATE_CLASS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -44,7 +39,7 @@ from homeassistant.helpers.template import Template from homeassistant.helpers.template_entity import ( CONF_AVAILABILITY, CONF_PICTURE, - ManualTriggerEntity, + ManualTriggerSensorEntity, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -56,6 +51,16 @@ _LOGGER = logging.getLogger(__name__) _SQL_LAMBDA_CACHE: LRUCache = LRUCache(1000) +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, @@ -69,43 +74,29 @@ async def async_setup_platform( name: Template = conf[CONF_NAME] query_str: str = conf[CONF_QUERY] - unit: str | None = conf.get(CONF_UNIT_OF_MEASUREMENT) value_template: Template | None = conf.get(CONF_VALUE_TEMPLATE) column_name: str = conf[CONF_COLUMN_NAME] unique_id: str | None = conf.get(CONF_UNIQUE_ID) db_url: str = resolve_db_url(hass, conf.get(CONF_DB_URL)) - device_class: SensorDeviceClass | None = conf.get(CONF_DEVICE_CLASS) - state_class: SensorStateClass | None = conf.get(CONF_STATE_CLASS) - availability: Template | None = conf.get(CONF_AVAILABILITY) - icon: Template | None = conf.get(CONF_ICON) - picture: Template | None = conf.get(CONF_PICTURE) if value_template is not None: value_template.hass = hass - trigger_entity_config = { - CONF_NAME: name, - CONF_DEVICE_CLASS: device_class, - CONF_UNIQUE_ID: unique_id, - } - if availability: - trigger_entity_config[CONF_AVAILABILITY] = availability - if icon: - trigger_entity_config[CONF_ICON] = icon - if picture: - trigger_entity_config[CONF_PICTURE] = picture + trigger_entity_config = {CONF_NAME: name} + for key in TRIGGER_ENTITY_OPTIONS: + if key not in conf: + continue + trigger_entity_config[key] = conf[key] await async_setup_sensor( hass, trigger_entity_config, query_str, column_name, - unit, value_template, unique_id, db_url, True, - state_class, async_add_entities, ) @@ -118,11 +109,8 @@ async def async_setup_entry( db_url: str = resolve_db_url(hass, entry.options.get(CONF_DB_URL)) name: str = entry.options[CONF_NAME] query_str: str = entry.options[CONF_QUERY] - unit: str | None = entry.options.get(CONF_UNIT_OF_MEASUREMENT) template: str | None = entry.options.get(CONF_VALUE_TEMPLATE) column_name: str = entry.options[CONF_COLUMN_NAME] - device_class: SensorDeviceClass | None = entry.options.get(CONF_DEVICE_CLASS, None) - state_class: SensorStateClass | None = entry.options.get(CONF_STATE_CLASS, None) value_template: Template | None = None if template is not None: @@ -135,23 +123,21 @@ async def async_setup_entry( value_template.hass = hass name_template = Template(name, hass) - trigger_entity_config = { - CONF_NAME: name_template, - CONF_DEVICE_CLASS: device_class, - CONF_UNIQUE_ID: entry.entry_id, - } + trigger_entity_config = {CONF_NAME: name_template} + for key in TRIGGER_ENTITY_OPTIONS: + if key not in entry.options: + continue + trigger_entity_config[key] = entry.options[key] await async_setup_sensor( hass, trigger_entity_config, query_str, column_name, - unit, value_template, entry.entry_id, db_url, False, - state_class, async_add_entities, ) @@ -191,12 +177,10 @@ async def async_setup_sensor( trigger_entity_config: ConfigType, query_str: str, column_name: str, - unit: str | None, value_template: Template | None, unique_id: str | None, db_url: str, yaml: bool, - state_class: SensorStateClass | None, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the SQL sensor.""" @@ -274,10 +258,8 @@ async def async_setup_sensor( sessmaker, query_str, column_name, - unit, value_template, yaml, - state_class, use_database_executor, ) ], @@ -317,7 +299,7 @@ def _generate_lambda_stmt(query: str) -> StatementLambdaElement: return lambda_stmt(lambda: text, lambda_cache=_SQL_LAMBDA_CACHE) -class SQLSensor(ManualTriggerEntity, SensorEntity): +class SQLSensor(ManualTriggerSensorEntity): """Representation of an SQL sensor.""" def __init__( @@ -326,17 +308,13 @@ class SQLSensor(ManualTriggerEntity, SensorEntity): sessmaker: scoped_session, query: str, column: str, - unit: str | None, value_template: Template | None, yaml: bool, - state_class: SensorStateClass | None, use_database_executor: bool, ) -> None: """Initialize the SQL sensor.""" super().__init__(self.hass, trigger_entity_config) self._query = query - self._attr_native_unit_of_measurement = unit - self._attr_state_class = state_class self._template = value_template self._column_name = column self.sessionmaker = sessmaker diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index 07e68152d64..70a0ee1d16c 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -486,7 +486,7 @@ class TriggerBaseEntity(Entity): def __init__( self, hass: HomeAssistant, - config: dict, + config: ConfigType, ) -> None: """Initialize the entity.""" self.hass = hass @@ -623,7 +623,7 @@ class ManualTriggerEntity(TriggerBaseEntity): def __init__( self, hass: HomeAssistant, - config: dict, + config: ConfigType, ) -> None: """Initialize the entity.""" TriggerBaseEntity.__init__(self, hass, config) @@ -655,13 +655,13 @@ class ManualTriggerEntity(TriggerBaseEntity): self._render_templates(variables) -class ManualTriggerSensorEntity(ManualTriggerEntity): +class ManualTriggerSensorEntity(ManualTriggerEntity, SensorEntity): """Template entity based on manual trigger data for sensor.""" def __init__( self, hass: HomeAssistant, - config: dict, + config: ConfigType, ) -> None: """Initialize the sensor entity.""" ManualTriggerEntity.__init__(self, hass, config) From f96446cb241aa103accb6eeb196f268da63d4c1a Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 18 Aug 2023 19:45:12 +0100 Subject: [PATCH 0631/1151] Clean up integration sensor (#98552) always update --- .../components/integration/sensor.py | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 6daecc6a305..ba17a448477 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -298,18 +298,14 @@ class IntegrationSensor(RestoreSensor): old_state = event.data["old_state"] new_state = event.data["new_state"] - # We may want to update our state before an early return, - # based on the source sensor's unit_of_measurement - # or device_class. - update_state = False - if ( source_state := self.hass.states.get(self._sensor_source_id) ) is None or source_state.state == STATE_UNAVAILABLE: self._attr_available = False - update_state = True - else: - self._attr_available = True + self.async_write_ha_state() + return + + self._attr_available = True if old_state is None or new_state is None: # we can't calculate the elapsed time, so we can't calculate the integral @@ -317,10 +313,7 @@ class IntegrationSensor(RestoreSensor): unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit is not None: - new_unit_of_measurement = self._unit(unit) - if self._unit_of_measurement != new_unit_of_measurement: - self._unit_of_measurement = new_unit_of_measurement - update_state = True + self._unit_of_measurement = self._unit(unit) if ( self.device_class is None @@ -329,10 +322,8 @@ class IntegrationSensor(RestoreSensor): ): self._attr_device_class = SensorDeviceClass.ENERGY self._attr_icon = None - update_state = True - if update_state: - self.async_write_ha_state() + self.async_write_ha_state() try: # integration as the Riemann integral of previous measures. From 7827f9ccaea272c37186fdd6630d6ab848bf07a1 Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Fri, 18 Aug 2023 12:20:04 -0700 Subject: [PATCH 0632/1151] Update country `province` validation (#84463) * Update country `province` validation. * Run pre-commit. * Add tests * Mod config flow --------- Co-authored-by: G Johansson --- .../components/workday/binary_sensor.py | 26 +++++++++++-------- .../components/workday/config_flow.py | 15 ++++++----- tests/components/workday/__init__.py | 9 +++++++ .../components/workday/test_binary_sensor.py | 16 ++++++++++++ 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index d1666fa9097..4c383543125 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -4,8 +4,13 @@ from __future__ import annotations from datetime import date, timedelta from typing import Any -import holidays -from holidays import DateLike, HolidayBase +from holidays import ( + DateLike, + HolidayBase, + __version__ as python_holidays_version, + country_holidays, + list_supported_countries, +) import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -43,7 +48,6 @@ from .const import ( def valid_country(value: Any) -> str: """Validate that the given country is supported.""" value = cv.string(value) - all_supported_countries = holidays.list_supported_countries() try: raw_value = value.encode("utf-8") @@ -53,7 +57,7 @@ def valid_country(value: Any) -> str: ) from err if not raw_value: raise vol.Invalid("Country name or the abbreviation must not be empty.") - if value not in all_supported_countries: + if value not in list_supported_countries(): raise vol.Invalid("Country is not supported.") return value @@ -123,17 +127,17 @@ async def async_setup_entry( province: str | None = entry.options.get(CONF_PROVINCE) sensor_name: str = entry.options[CONF_NAME] workdays: list[str] = entry.options[CONF_WORKDAYS] - - cls: HolidayBase = getattr(holidays, country) year: int = (dt_util.now() + timedelta(days=days_offset)).year - if province and province not in cls.subdivisions: + if country and country not in list_supported_countries(): + LOGGER.error("There is no country %s", country) + return + + if province and province not in list_supported_countries()[country]: LOGGER.error("There is no subdivision %s in country %s", province, country) return - obj_holidays = cls( - subdiv=province, years=year, language=cls.default_language - ) # type: ignore[operator] + obj_holidays: HolidayBase = country_holidays(country, subdiv=province, years=year) # Add custom holidays try: @@ -209,7 +213,7 @@ class IsWorkdaySensor(BinarySensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, manufacturer="python-holidays", - model=holidays.__version__, + model=python_holidays_version, name=name, ) diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index 15e04ffca93..54c6196b75b 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import Any -import holidays -from holidays import HolidayBase, list_supported_countries +from holidays import HolidayBase, country_holidays, list_supported_countries import voluptuous as vol from homeassistant.config_entries import ( @@ -77,12 +76,14 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None: if dt_util.parse_date(add_date) is None: raise AddDatesError("Incorrect date") - cls: HolidayBase = getattr(holidays, user_input[CONF_COUNTRY]) + cls: HolidayBase = country_holidays(user_input[CONF_COUNTRY]) year: int = dt_util.now().year - - obj_holidays = cls( - subdiv=user_input.get(CONF_PROVINCE), years=year, language=cls.default_language - ) # type: ignore[operator] + obj_holidays: HolidayBase = country_holidays( + user_input[CONF_COUNTRY], + subdiv=user_input.get(CONF_PROVINCE), + years=year, + language=cls.default_language, + ) for remove_date in user_input[CONF_REMOVE_HOLIDAYS]: if dt_util.parse_date(remove_date) is None: diff --git a/tests/components/workday/__init__.py b/tests/components/workday/__init__.py index 005a63397d9..f87328998e1 100644 --- a/tests/components/workday/__init__.py +++ b/tests/components/workday/__init__.py @@ -50,6 +50,15 @@ TEST_CONFIG_WITH_PROVINCE = { "add_holidays": [], "remove_holidays": [], } +TEST_CONFIG_INCORRECT_COUNTRY = { + "name": DEFAULT_NAME, + "country": "ZZ", + "excludes": DEFAULT_EXCLUDES, + "days_offset": DEFAULT_OFFSET, + "workdays": DEFAULT_WORKDAYS, + "add_holidays": [], + "remove_holidays": [], +} TEST_CONFIG_INCORRECT_PROVINCE = { "name": DEFAULT_NAME, "country": "DE", diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 71dd23c19a3..a8cea01a864 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -17,6 +17,7 @@ from . import ( TEST_CONFIG_EXAMPLE_2, TEST_CONFIG_INCLUDE_HOLIDAY, TEST_CONFIG_INCORRECT_ADD_REMOVE, + TEST_CONFIG_INCORRECT_COUNTRY, TEST_CONFIG_INCORRECT_PROVINCE, TEST_CONFIG_NO_PROVINCE, TEST_CONFIG_NO_STATE, @@ -187,6 +188,21 @@ async def test_setup_day_after_tomorrow( assert state.state == "off" +async def test_setup_faulty_country( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test setup with faulty province.""" + freezer.move_to(datetime(2017, 1, 6, 12, tzinfo=UTC)) # Friday + await init_integration(hass, TEST_CONFIG_INCORRECT_COUNTRY) + + state = hass.states.get("binary_sensor.workday_sensor") + assert state is None + + assert "There is no country" in caplog.text + + async def test_setup_faulty_province( hass: HomeAssistant, freezer: FrozenDateTimeFactory, From 9e42451934d0513eb53fbe84dfe5576f7727d695 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 18 Aug 2023 22:44:59 +0200 Subject: [PATCH 0633/1151] UniFi refactor using site data (#98549) * Clean up * Simplify admin verification * Streamline using sites in config_flow * Bump aiounifi --- homeassistant/components/unifi/button.py | 2 +- homeassistant/components/unifi/config_flow.py | 29 +++++++--------- homeassistant/components/unifi/controller.py | 32 +++-------------- homeassistant/components/unifi/diagnostics.py | 2 +- homeassistant/components/unifi/image.py | 2 +- homeassistant/components/unifi/manifest.json | 2 +- homeassistant/components/unifi/switch.py | 2 +- homeassistant/components/unifi/update.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_controller.py | 30 +++++++++------- tests/components/unifi/test_diagnostics.py | 2 +- tests/components/unifi/test_init.py | 2 +- tests/components/unifi/test_switch.py | 34 +++++++++---------- tests/components/unifi/test_update.py | 11 +++--- 15 files changed, 65 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/unifi/button.py b/homeassistant/components/unifi/button.py index 6b0660325f0..0235f6156cc 100644 --- a/homeassistant/components/unifi/button.py +++ b/homeassistant/components/unifi/button.py @@ -89,7 +89,7 @@ async def async_setup_entry( """Set up button platform for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - if controller.site_role != "admin": + if not controller.is_admin: return controller.register_platform_add_entities( diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 12f2d49e416..8c0696463c5 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -13,6 +13,7 @@ from types import MappingProxyType from typing import Any from urllib.parse import urlparse +from aiounifi.interfaces.sites import Sites import voluptuous as vol from homeassistant import config_entries @@ -63,6 +64,8 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): VERSION = 1 + sites: Sites + @staticmethod @callback def async_get_options_flow( @@ -74,8 +77,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): def __init__(self) -> None: """Initialize the UniFi Network flow.""" self.config: dict[str, Any] = {} - self.site_ids: dict[str, str] = {} - self.site_names: dict[str, str] = {} self.reauth_config_entry: config_entries.ConfigEntry | None = None self.reauth_schema: dict[vol.Marker, Any] = {} @@ -99,7 +100,8 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): controller = await get_unifi_controller( self.hass, MappingProxyType(self.config) ) - sites = await controller.sites() + await controller.sites.update() + self.sites = controller.sites except AuthenticationRequired: errors["base"] = "faulty_credentials" @@ -108,12 +110,10 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): errors["base"] = "service_unavailable" else: - self.site_ids = {site["_id"]: site["name"] for site in sites.values()} - self.site_names = {site["_id"]: site["desc"] for site in sites.values()} - if ( self.reauth_config_entry - and self.reauth_config_entry.unique_id in self.site_names + and self.reauth_config_entry.unique_id is not None + and self.reauth_config_entry.unique_id in self.sites ): return await self.async_step_site( {CONF_SITE_ID: self.reauth_config_entry.unique_id} @@ -148,7 +148,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): """Select site to control.""" if user_input is not None: unique_id = user_input[CONF_SITE_ID] - self.config[CONF_SITE_ID] = self.site_ids[unique_id] + self.config[CONF_SITE_ID] = self.sites[unique_id].name config_entry = await self.async_set_unique_id(unique_id) abort_reason = "configuration_updated" @@ -171,19 +171,16 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): await self.hass.config_entries.async_reload(config_entry.entry_id) return self.async_abort(reason=abort_reason) - site_nice_name = self.site_names[unique_id] + site_nice_name = self.sites[unique_id].description return self.async_create_entry(title=site_nice_name, data=self.config) - if len(self.site_names) == 1: - return await self.async_step_site( - {CONF_SITE_ID: next(iter(self.site_names))} - ) + if len(self.sites.values()) == 1: + return await self.async_step_site({CONF_SITE_ID: next(iter(self.sites))}) + site_names = {site.site_id: site.description for site in self.sites.values()} return self.async_show_form( step_id="site", - data_schema=vol.Schema( - {vol.Required(CONF_SITE_ID): vol.In(self.site_names)} - ), + data_schema=vol.Schema({vol.Required(CONF_SITE_ID): vol.In(site_names)}), ) async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 649d7c30fdb..c1ffa0aa57d 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -87,9 +87,8 @@ class UniFiController: self.available = True self.wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS] - self.site_id: str = "" - self._site_name: str | None = None - self._site_role: str | None = None + self.site = config_entry.data[CONF_SITE_ID] + self.is_admin = False self._cancel_heartbeat_check: CALLBACK_TYPE | None = None self._heartbeat_time: dict[str, datetime] = {} @@ -154,22 +153,6 @@ class UniFiController: host: str = self.config_entry.data[CONF_HOST] return host - @property - def site(self) -> str: - """Return the site of this config entry.""" - site_id: str = self.config_entry.data[CONF_SITE_ID] - return site_id - - @property - def site_name(self) -> str | None: - """Return the nice name of site.""" - return self._site_name - - @property - def site_role(self) -> str | None: - """Return the site user role of this controller.""" - return self._site_role - @property def mac(self) -> str | None: """Return the mac address of this controller.""" @@ -264,15 +247,8 @@ class UniFiController: """Set up a UniFi Network instance.""" await self.api.initialize() - sites = await self.api.sites() - for site in sites.values(): - if self.site == site["name"]: - self.site_id = site["_id"] - self._site_name = site["desc"] - break - - description = await self.api.site_description() - self._site_role = description[0]["site_role"] + assert self.config_entry.unique_id is not None + self.is_admin = self.api.sites[self.config_entry.unique_id].role == "admin" # Restore clients that are not a part of active clients list. entity_registry = er.async_get(self.hass) diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index 3c72c06d6f2..c01dc193078 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -94,7 +94,7 @@ async def async_get_config_entry_diagnostics( diag["config"] = async_redact_data( async_replace_dict_data(config_entry.as_dict(), macs_to_redact), REDACT_CONFIG ) - diag["site_role"] = controller.site_role + diag["role_is_admin"] = controller.is_admin diag["clients"] = { macs_to_redact[k]: async_redact_data( async_replace_dict_data(v.raw, macs_to_redact), REDACT_CLIENTS diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index 3ff893838c9..8231b87ee85 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -85,7 +85,7 @@ async def async_setup_entry( """Set up image platform for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - if controller.site_role != "admin": + if not controller.is_admin: return controller.register_platform_add_entities( diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 8f27263b288..579e64c5862 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==53"], + "requirements": ["aiounifi==55"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index a82b9e35d45..e2b4dda3912 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -274,7 +274,7 @@ async def async_setup_entry( """Set up switches for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - if controller.site_role != "admin": + if not controller.is_admin: return for mac in controller.option_block_clients: diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index 661a9016bdc..6526a02da83 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -103,7 +103,7 @@ class UnifiDeviceUpdateEntity(UnifiEntity[_HandlerT, _DataT], UpdateEntity): def async_initiate_state(self) -> None: """Initiate entity state.""" self._attr_supported_features = UpdateEntityFeature.PROGRESS - if self.controller.site_role == "admin": + if self.controller.is_admin: self._attr_supported_features |= UpdateEntityFeature.INSTALL self.async_update_state(ItemEvent.ADDED, self._obj_id) diff --git a/requirements_all.txt b/requirements_all.txt index 1afbd67743c..aa636c42372 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==53 +aiounifi==55 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d0f59d7bca..aa34d430dba 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==53 +aiounifi==55 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 2d28240a90d..a2be388af4c 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -80,7 +80,6 @@ ENTRY_OPTIONS = {} CONFIGURATION = [] SITE = [{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}] -DESCRIPTION = [{"name": "username", "site_name": "site_id", "site_role": "admin"}] def mock_default_unifi_requests( @@ -88,12 +87,13 @@ def mock_default_unifi_requests( host, site_id, sites=None, - description=None, clients_response=None, clients_all_response=None, devices_response=None, dpiapp_response=None, dpigroup_response=None, + port_forward_response=None, + system_information_response=None, wlans_response=None, ): """Mock default UniFi requests responses.""" @@ -111,12 +111,6 @@ def mock_default_unifi_requests( headers={"content-type": CONTENT_TYPE_JSON}, ) - aioclient_mock.get( - f"https://{host}:1234/api/s/{site_id}/self", - json={"data": description or [], "meta": {"rc": "ok"}}, - headers={"content-type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( f"https://{host}:1234/api/s/{site_id}/stat/sta", json={"data": clients_response or [], "meta": {"rc": "ok"}}, @@ -142,6 +136,16 @@ def mock_default_unifi_requests( json={"data": dpigroup_response or [], "meta": {"rc": "ok"}}, headers={"content-type": CONTENT_TYPE_JSON}, ) + aioclient_mock.get( + f"https://{host}:1234/api/s/{site_id}/rest/portforward", + json={"data": port_forward_response or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.get( + f"https://{host}:1234/api/s/{site_id}/stat/sysinfo", + json={"data": system_information_response or [], "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) aioclient_mock.get( f"https://{host}:1234/api/s/{site_id}/rest/wlanconf", json={"data": wlans_response or [], "meta": {"rc": "ok"}}, @@ -156,12 +160,13 @@ async def setup_unifi_integration( config=ENTRY_CONFIG, options=ENTRY_OPTIONS, sites=SITE, - site_description=DESCRIPTION, clients_response=None, clients_all_response=None, devices_response=None, dpiapp_response=None, dpigroup_response=None, + port_forward_response=None, + system_information_response=None, wlans_response=None, known_wireless_clients=None, controllers=None, @@ -192,12 +197,13 @@ async def setup_unifi_integration( host=config_entry.data[CONF_HOST], site_id=config_entry.data[CONF_SITE_ID], sites=sites, - description=site_description, clients_response=clients_response, clients_all_response=clients_all_response, devices_response=devices_response, dpiapp_response=dpiapp_response, dpigroup_response=dpigroup_response, + port_forward_response=port_forward_response, + system_information_response=system_information_response, wlans_response=wlans_response, ) @@ -230,9 +236,7 @@ async def test_controller_setup( assert forward_entry_setup.mock_calls[4][1] == (entry, SWITCH_DOMAIN) assert controller.host == ENTRY_CONFIG[CONF_HOST] - assert controller.site == ENTRY_CONFIG[CONF_SITE_ID] - assert controller.site_name == SITE[0]["desc"] - assert controller.site_role == SITE[0]["role"] + assert controller.is_admin == (SITE[0]["role"] == "admin") assert controller.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS assert controller.option_allow_uptime_sensors == DEFAULT_ALLOW_UPTIME_SENSORS diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index 5248836c08a..638e79ae649 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -141,7 +141,7 @@ async def test_entry_diagnostics( "unique_id": "1", "version": 1, }, - "site_role": "admin", + "role_is_admin": True, "clients": { "00:00:00:00:00:00": { "blocked": False, diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index cce26ac84cc..a1b817d67e2 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -24,7 +24,7 @@ async def test_successful_config_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that configured options for a host are loaded via config entry.""" - await setup_unifi_integration(hass, aioclient_mock, unique_id=None) + await setup_unifi_integration(hass, aioclient_mock) assert hass.data[UNIFI_DOMAIN] diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 5344ac901b7..c091fc5cc59 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -36,8 +36,8 @@ from homeassistant.util import dt as dt_util from .test_controller import ( CONTROLLER_HOST, - DESCRIPTION, ENTRY_CONFIG, + SITE, setup_unifi_integration, ) @@ -778,7 +778,7 @@ async def test_no_clients( }, ) - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 11 assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 @@ -803,13 +803,13 @@ async def test_not_admin( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that switch platform only work on an admin account.""" - description = deepcopy(DESCRIPTION) - description[0]["site_role"] = "not admin" + site = deepcopy(SITE) + site[0]["role"] = "not admin" await setup_unifi_integration( hass, aioclient_mock, options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, - site_description=description, + sites=site, clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) @@ -867,8 +867,8 @@ async def test_switches( await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert aioclient_mock.call_count == 11 - assert aioclient_mock.mock_calls[10][2] == { + assert aioclient_mock.call_count == 12 + assert aioclient_mock.mock_calls[11][2] == { "mac": "00:00:00:00:01:01", "cmd": "block-sta", } @@ -876,8 +876,8 @@ async def test_switches( await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert aioclient_mock.call_count == 12 - assert aioclient_mock.mock_calls[11][2] == { + assert aioclient_mock.call_count == 13 + assert aioclient_mock.mock_calls[12][2] == { "mac": "00:00:00:00:01:01", "cmd": "unblock-sta", } @@ -894,8 +894,8 @@ async def test_switches( {"entity_id": "switch.block_media_streaming"}, blocking=True, ) - assert aioclient_mock.call_count == 13 - assert aioclient_mock.mock_calls[12][2] == {"enabled": False} + assert aioclient_mock.call_count == 14 + assert aioclient_mock.mock_calls[13][2] == {"enabled": False} await hass.services.async_call( SWITCH_DOMAIN, @@ -903,8 +903,8 @@ async def test_switches( {"entity_id": "switch.block_media_streaming"}, blocking=True, ) - assert aioclient_mock.call_count == 14 - assert aioclient_mock.mock_calls[13][2] == {"enabled": True} + assert aioclient_mock.call_count == 15 + assert aioclient_mock.mock_calls[14][2] == {"enabled": True} async def test_remove_switches( @@ -990,8 +990,8 @@ async def test_block_switches( await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert aioclient_mock.call_count == 11 - assert aioclient_mock.mock_calls[10][2] == { + assert aioclient_mock.call_count == 12 + assert aioclient_mock.mock_calls[11][2] == { "mac": "00:00:00:00:01:01", "cmd": "block-sta", } @@ -999,8 +999,8 @@ async def test_block_switches( await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True ) - assert aioclient_mock.call_count == 12 - assert aioclient_mock.mock_calls[11][2] == { + assert aioclient_mock.call_count == 13 + assert aioclient_mock.mock_calls[12][2] == { "mac": "00:00:00:00:01:01", "cmd": "unblock-sta", } diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index 7cf8495b9db..e59eca371d6 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -26,7 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from .test_controller import DESCRIPTION, setup_unifi_integration +from .test_controller import SITE, setup_unifi_integration from tests.test_util.aiohttp import AiohttpClientMocker @@ -136,14 +136,11 @@ async def test_not_admin( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test that the INSTALL feature is not available on a non-admin account.""" - description = deepcopy(DESCRIPTION) - description[0]["site_role"] = "not admin" + site = deepcopy(SITE) + site[0]["role"] = "not admin" await setup_unifi_integration( - hass, - aioclient_mock, - site_description=description, - devices_response=[DEVICE_1], + hass, aioclient_mock, sites=site, devices_response=[DEVICE_1] ) assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 From a39af8aff9e123ffdac0f69906aa6070596c1b32 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 18 Aug 2023 23:03:56 +0200 Subject: [PATCH 0634/1151] Fix rest debug logging (#98649) Correct rest debug logging --- homeassistant/components/rest/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 1f331651165..61c88a14400 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -81,7 +81,7 @@ class RestData: "REST xml result could not be parsed and converted to JSON" ) else: - _LOGGER.debug("JSON converted from XML: %s", self.data) + _LOGGER.debug("JSON converted from XML: %s", value) return value async def async_update(self, log_errors: bool = True) -> None: From 1a032cebddca0a9a2bc41acafbc77ed5b6f3f7a1 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 18 Aug 2023 23:18:55 +0200 Subject: [PATCH 0635/1151] modbus: slave is allowed with custom (#98644) --- homeassistant/components/modbus/validators.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index f3336e5cb0c..b2e33a0f1f1 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -65,7 +65,6 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: name = config[CONF_NAME] structure = config.get(CONF_STRUCTURE) slave_count = config.get(CONF_SLAVE_COUNT, 0) + 1 - slave = config.get(CONF_SLAVE, 0) swap_type = config.get(CONF_SWAP, CONF_SWAP_NONE) if ( slave_count > 1 @@ -79,7 +78,7 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: error = f"{name} structure: cannot be mixed with {data_type}" if config[CONF_DATA_TYPE] == DataType.CUSTOM: - if slave or slave_count > 1: + if slave_count > 1: error = f"{name}: `{CONF_STRUCTURE}` illegal with `{CONF_SLAVE_COUNT}` / `{CONF_SLAVE}`" raise vol.Invalid(error) if swap_type != CONF_SWAP_NONE: From c1fb97f26b7ce512df589823dd653cfd98e777bc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 19 Aug 2023 02:28:27 +0200 Subject: [PATCH 0636/1151] Fix aiohttp DeprecationWarning (#98626) --- tests/components/cloud/test_subscription.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/cloud/test_subscription.py b/tests/components/cloud/test_subscription.py index bc5d149e914..9207c1fef2c 100644 --- a/tests/components/cloud/test_subscription.py +++ b/tests/components/cloud/test_subscription.py @@ -16,7 +16,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture(name="mocked_cloud") -def mocked_cloud_object(hass: HomeAssistant) -> Cloud: +async def mocked_cloud_object(hass: HomeAssistant) -> Cloud: """Mock cloud object.""" return Mock( accounts_server="accounts.nabucasa.com", From 7059252164d3eb42ab706fbccc4841440c62a461 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 18 Aug 2023 23:57:25 -0700 Subject: [PATCH 0637/1151] Bump opowerto 0.0.30 (#98660) --- 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 14720106f74..58d642ef9a1 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.0.29"] + "requirements": ["opower==0.0.30"] } diff --git a/requirements_all.txt b/requirements_all.txt index aa636c42372..e44a9420031 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1368,7 +1368,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.29 +opower==0.0.30 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa34d430dba..5438221320a 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.29 +opower==0.0.30 # homeassistant.components.oralb oralb-ble==0.17.6 From c526d236865abc0ea9309b563cbf8aff8f373c9e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 19 Aug 2023 09:13:22 +0000 Subject: [PATCH 0638/1151] Change naming of Shelly entities to correspond with HA guidelines (#97533) * Do not use the device name to create the entity name * Remove unnecessary return * Fix mypy complains * Gen1 * Uncapitalize description.name if channel name is used * Fix for climate and button * switch_3 -> switch 3 * Add _attr_has_entity_name to ShellyRestAttributeEntity * Capitalize channel name --- homeassistant/components/shelly/button.py | 2 +- homeassistant/components/shelly/climate.py | 8 ++--- homeassistant/components/shelly/entity.py | 5 +++ homeassistant/components/shelly/logbook.py | 3 +- homeassistant/components/shelly/utils.py | 33 +++++++++++-------- tests/components/shelly/test_binary_sensor.py | 2 +- tests/components/shelly/test_coordinator.py | 8 ++--- tests/components/shelly/test_cover.py | 24 +++++++------- tests/components/shelly/test_init.py | 8 ++--- tests/components/shelly/test_light.py | 10 +++--- tests/components/shelly/test_sensor.py | 2 +- tests/components/shelly/test_switch.py | 14 ++++---- tests/components/shelly/test_utils.py | 6 ++-- 13 files changed, 66 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index edc33c9a8a0..a505867b3e8 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -154,6 +154,7 @@ class ShellyButton( entity_description: ShellyButtonDescription[ ShellyRpcCoordinator | ShellyBlockCoordinator ] + _attr_has_entity_name = True def __init__( self, @@ -166,7 +167,6 @@ class ShellyButton( super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{coordinator.device.name} {description.name}" self._attr_unique_id = f"{coordinator.mac}_{description.key}" self._attr_device_info = DeviceInfo( connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index a9712e62d25..d77a491661c 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -130,6 +130,7 @@ class BlockSleepingClimate( ) _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_has_entity_name = True def __init__( self, @@ -173,11 +174,6 @@ class BlockSleepingClimate( """Set unique id of entity.""" return self._unique_id - @property - def name(self) -> str: - """Name of entity.""" - return self.coordinator.name - @property def target_temperature(self) -> float | None: """Set target temperature.""" @@ -354,7 +350,7 @@ class BlockSleepingClimate( severity=ir.IssueSeverity.ERROR, translation_key="device_not_calibrated", translation_placeholders={ - "device_name": self.name, + "device_name": self.coordinator.name, "ip_address": self.coordinator.device.ip_address, }, ) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 1dc7573b738..ac06624c750 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -321,6 +321,8 @@ class RestEntityDescription(EntityDescription): class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Helper class to represent a block entity.""" + _attr_has_entity_name = True + def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: """Initialize Shelly entity.""" super().__init__(coordinator) @@ -359,6 +361,8 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): """Helper class to represent a rpc entity.""" + _attr_has_entity_name = True + def __init__(self, coordinator: ShellyRpcCoordinator, key: str) -> None: """Initialize Shelly entity.""" super().__init__(coordinator) @@ -462,6 +466,7 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Class to load info from REST.""" entity_description: RestEntityDescription + _attr_has_entity_name = True def __init__( self, diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index d55ffe0fd28..b8f0c8e1744 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -42,7 +42,8 @@ def async_describe_events( rpc_coordinator = get_rpc_coordinator_by_device_id(hass, device_id) if rpc_coordinator and rpc_coordinator.device.initialized: key = f"input:{channel-1}" - input_name = get_rpc_entity_name(rpc_coordinator.device, key) + if iname := get_rpc_entity_name(rpc_coordinator.device, key): + input_name = iname elif click_type in BLOCK_INPUTS_EVENTS_TYPES: block_coordinator = get_block_coordinator_by_device_id(hass, device_id) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index a66b77ed94b..1faa36ce118 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -72,26 +72,26 @@ def get_block_entity_name( device: BlockDevice, block: Block | None, description: str | None = None, -) -> str: +) -> str | None: """Naming for block based switch and sensors.""" channel_name = get_block_channel_name(device, block) + if description and channel_name: + return f"{channel_name} {uncapitalize(description)}" if description: - return f"{channel_name} {description.lower()}" + return description return channel_name -def get_block_channel_name(device: BlockDevice, block: Block | None) -> str: +def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | None: """Get name based on device and channel name.""" - entity_name = device.name - if ( not block or block.type == "device" or get_number_of_channels(device, block) == 1 ): - return entity_name + return None assert block.channel @@ -108,7 +108,7 @@ def get_block_channel_name(device: BlockDevice, block: Block | None) -> str: else: base = ord("1") - return f"{entity_name} channel {chr(int(block.channel)+base)}" + return f"Channel {chr(int(block.channel)+base)}" def is_block_momentary_input( @@ -285,32 +285,32 @@ def get_model_name(info: dict[str, Any]) -> str: return cast(str, MODEL_NAMES.get(info["type"], info["type"])) -def get_rpc_channel_name(device: RpcDevice, key: str) -> str: +def get_rpc_channel_name(device: RpcDevice, key: str) -> str | None: """Get name based on device and channel name.""" key = key.replace("emdata", "em") if device.config.get("switch:0"): key = key.replace("input", "switch") - device_name = device.name entity_name: str | None = None if key in device.config: - entity_name = device.config[key].get("name", device_name) + entity_name = device.config[key].get("name") if entity_name is None: if key.startswith(("input:", "light:", "switch:")): - return f"{device_name} {key.replace(':', '_')}" - return device_name + return key.replace(":", " ").capitalize() return entity_name def get_rpc_entity_name( device: RpcDevice, key: str, description: str | None = None -) -> str: +) -> str | None: """Naming for RPC based switch and sensors.""" channel_name = get_rpc_channel_name(device, key) + if description and channel_name: + return f"{channel_name} {uncapitalize(description)}" if description: - return f"{channel_name} {description.lower()}" + return description return channel_name @@ -405,3 +405,8 @@ def mac_address_from_name(name: str) -> str | None: """Convert a name to a mac address.""" mac = name.partition(".")[0].partition("-")[-1] return mac.upper() if len(mac) == 12 else None + + +def uncapitalize(description: str) -> str: + """Uncapitalize the first letter of a description.""" + return description[:1].lower() + description[1:] diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index c067f5dffc9..ebc5089f884 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -165,7 +165,7 @@ async def test_rpc_binary_sensor( hass: HomeAssistant, mock_rpc_device, monkeypatch ) -> None: """Test RPC binary sensor.""" - entity_id = f"{BINARY_SENSOR_DOMAIN}.test_cover_0_overpowering" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_test_cover_0_overpowering" await init_integration(hass, 2) assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 8536c3d72e6..eb546ce5835 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -353,7 +353,7 @@ async def test_rpc_reload_on_cfg_change( ) await hass.async_block_till_done() - assert hass.states.get("switch.test_switch_0") is not None + assert hass.states.get("switch.test_name_test_switch_0") is not None # Wait for debouncer async_fire_time_changed( @@ -361,7 +361,7 @@ async def test_rpc_reload_on_cfg_change( ) await hass.async_block_till_done() - assert hass.states.get("switch.test_switch_0") is None + assert hass.states.get("switch.test_name_test_switch_0") is None async def test_rpc_reload_with_invalid_auth( @@ -588,7 +588,7 @@ async def test_rpc_reconnect_error( """Test RPC reconnect error.""" await init_integration(hass, 2) - assert hass.states.get("switch.test_switch_0").state == STATE_ON + assert hass.states.get("switch.test_name_test_switch_0").state == STATE_ON monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setattr( @@ -605,7 +605,7 @@ async def test_rpc_reconnect_error( ) await hass.async_block_till_done() - assert hass.states.get("switch.test_switch_0").state == STATE_UNAVAILABLE + assert hass.states.get("switch.test_name_test_switch_0").state == STATE_UNAVAILABLE async def test_rpc_polling_connection_error( diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index 08c0c76d35e..56740981fc5 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -97,10 +97,10 @@ async def test_rpc_device_services( await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.test_cover_0", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.test_name_test_cover_0", ATTR_POSITION: 50}, blocking=True, ) - state = hass.states.get("cover.test_cover_0") + state = hass.states.get("cover.test_name_test_cover_0") assert state.attributes[ATTR_CURRENT_POSITION] == 50 mutate_rpc_device_status( @@ -109,11 +109,11 @@ async def test_rpc_device_services( await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_cover_0"}, + {ATTR_ENTITY_ID: "cover.test_name_test_cover_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_cover_0").state == STATE_OPENING + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPENING mutate_rpc_device_status( monkeypatch, mock_rpc_device, "cover:0", "state", "closing" @@ -121,21 +121,21 @@ async def test_rpc_device_services( await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_cover_0"}, + {ATTR_ENTITY_ID: "cover.test_name_test_cover_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_cover_0").state == STATE_CLOSING + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSING mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") await hass.services.async_call( COVER_DOMAIN, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.test_cover_0"}, + {ATTR_ENTITY_ID: "cover.test_name_test_cover_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSED async def test_rpc_device_no_cover_keys( @@ -144,7 +144,7 @@ async def test_rpc_device_no_cover_keys( """Test RPC device without cover keys.""" monkeypatch.delitem(mock_rpc_device.status, "cover:0") await init_integration(hass, 2) - assert hass.states.get("cover.test_cover_0") is None + assert hass.states.get("cover.test_name_test_cover_0") is None async def test_rpc_device_update( @@ -153,11 +153,11 @@ async def test_rpc_device_update( """Test RPC device update.""" mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") await init_integration(hass, 2) - assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSED mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "open") mock_rpc_device.mock_update() - assert hass.states.get("cover.test_cover_0").state == STATE_OPEN + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPEN async def test_rpc_device_no_position_control( @@ -168,4 +168,4 @@ async def test_rpc_device_no_position_control( monkeypatch, mock_rpc_device, "cover:0", "pos_control", False ) await init_integration(hass, 2) - assert hass.states.get("cover.test_cover_0").state == STATE_OPEN + assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPEN diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index be6e319c8ac..a62dfda82f9 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -175,7 +175,7 @@ async def test_sleeping_rpc_device_online_new_firmware( ("gen", "entity_id"), [ (1, "switch.test_name_channel_1"), - (2, "switch.test_switch_0"), + (2, "switch.test_name_test_switch_0"), ], ) async def test_entry_unload( @@ -198,7 +198,7 @@ async def test_entry_unload( ("gen", "entity_id"), [ (1, "switch.test_name_channel_1"), - (2, "switch.test_switch_0"), + (2, "switch.test_name_test_switch_0"), ], ) async def test_entry_unload_device_not_ready( @@ -226,7 +226,7 @@ async def test_entry_unload_not_connected( entry = await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} ) - entity_id = "switch.test_switch_0" + entity_id = "switch.test_name_test_switch_0" assert entry.state is ConfigEntryState.LOADED assert hass.states.get(entity_id).state is STATE_ON @@ -252,7 +252,7 @@ async def test_entry_unload_not_connected_but_we_think_we_are( entry = await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} ) - entity_id = "switch.test_switch_0" + entity_id = "switch.test_name_test_switch_0" assert entry.state is ConfigEntryState.LOADED assert hass.states.get(entity_id).state is STATE_ON diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index 69d0fccf421..ab631516ec2 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -385,25 +385,25 @@ async def test_rpc_device_switch_type_lights_mode( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_switch_0"}, + {ATTR_ENTITY_ID: "light.test_name_test_switch_0"}, blocking=True, ) - assert hass.states.get("light.test_switch_0").state == STATE_ON + assert hass.states.get("light.test_name_test_switch_0").state == STATE_ON mutate_rpc_device_status(monkeypatch, mock_rpc_device, "switch:0", "output", False) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_switch_0"}, + {ATTR_ENTITY_ID: "light.test_name_test_switch_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("light.test_switch_0").state == STATE_OFF + assert hass.states.get("light.test_name_test_switch_0").state == STATE_OFF async def test_rpc_light(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None: """Test RPC light.""" - entity_id = f"{LIGHT_DOMAIN}.test_light_0" + entity_id = f"{LIGHT_DOMAIN}.test_name_test_light_0" monkeypatch.delitem(mock_rpc_device.status, "switch:0") await init_integration(hass, 2) diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index d87460fb17d..fe79b1d010a 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -262,7 +262,7 @@ async def test_block_sensor_unknown_value( async def test_rpc_sensor(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None: """Test RPC sensor.""" - entity_id = f"{SENSOR_DOMAIN}.test_cover_0_power" + entity_id = f"{SENSOR_DOMAIN}.test_name_test_cover_0_power" await init_integration(hass, 2) assert hass.states.get(entity_id).state == "85.3" diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 7a709e0cc2e..a93d752f9e2 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -149,20 +149,20 @@ async def test_rpc_device_services( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) - assert hass.states.get("switch.test_switch_0").state == STATE_ON + assert hass.states.get("switch.test_name_test_switch_0").state == STATE_ON monkeypatch.setitem(mock_rpc_device.status["switch:0"], "output", False) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("switch.test_switch_0").state == STATE_OFF + assert hass.states.get("switch.test_name_test_switch_0").state == STATE_OFF async def test_rpc_device_switch_type_lights_mode( @@ -173,7 +173,7 @@ async def test_rpc_device_switch_type_lights_mode( mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"] ) await init_integration(hass, 2) - assert hass.states.get("switch.test_switch_0") is None + assert hass.states.get("switch.test_name_test_switch_0") is None @pytest.mark.parametrize("exc", [DeviceConnectionError, RpcCallError(-1, "error")]) @@ -188,7 +188,7 @@ async def test_rpc_set_state_errors( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) @@ -209,7 +209,7 @@ async def test_rpc_auth_error( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/shelly/test_utils.py b/tests/components/shelly/test_utils.py index 1bf660deb2a..53fc77ed6ef 100644 --- a/tests/components/shelly/test_utils.py +++ b/tests/components/shelly/test_utils.py @@ -58,7 +58,7 @@ async def test_block_get_block_channel_name(mock_block_device, monkeypatch) -> N mock_block_device, mock_block_device.blocks[DEVICE_BLOCK_ID], ) - == "Test name channel 1" + == "Channel 1" ) monkeypatch.setitem(mock_block_device.settings["device"], "type", "SHEM-3") @@ -68,7 +68,7 @@ async def test_block_get_block_channel_name(mock_block_device, monkeypatch) -> N mock_block_device, mock_block_device.blocks[DEVICE_BLOCK_ID], ) - == "Test name channel A" + == "Channel A" ) monkeypatch.setitem( @@ -207,7 +207,7 @@ async def test_get_block_input_triggers(mock_block_device, monkeypatch) -> None: async def test_get_rpc_channel_name(mock_rpc_device) -> None: """Test get RPC channel name.""" assert get_rpc_channel_name(mock_rpc_device, "input:0") == "test switch_0" - assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Test name switch_3" + assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Switch 3" async def test_get_rpc_input_triggers(mock_rpc_device, monkeypatch) -> None: From 8a6bde1191ea59ab13bce34e19de360fbd6e2de3 Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Sat, 19 Aug 2023 21:36:23 +1200 Subject: [PATCH 0639/1151] Add Starlink device tracker (#91445) * Fetch location data and redact in diagnostics * Implement device tracker * Fix failing tests * Update starlink-grpc-core * Update coveragerc * Hardcode GPS source type * Use translations * Move DEVICE_TRACKERS a little higher in the file * Separate status and location check try/catches * Revert "Separate status and location check try/catches" This reverts commit 7628ec62f639845e9a7f5b460b8c66aad1d1dca3. --- .coveragerc | 1 + homeassistant/components/starlink/__init__.py | 1 + .../components/starlink/coordinator.py | 8 +- .../components/starlink/device_tracker.py | 73 +++++++++++++++++++ .../components/starlink/diagnostics.py | 2 +- .../components/starlink/strings.json | 5 ++ .../fixtures/location_data_success.json | 5 ++ tests/components/starlink/patchers.py | 7 +- .../starlink/snapshots/test_diagnostics.ambr | 5 ++ tests/components/starlink/test_diagnostics.py | 4 +- tests/components/starlink/test_init.py | 6 +- 11 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/starlink/device_tracker.py create mode 100644 tests/components/starlink/fixtures/location_data_success.json diff --git a/.coveragerc b/.coveragerc index 93958a67973..02b0cf7a143 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1187,6 +1187,7 @@ omit = homeassistant/components/starlink/binary_sensor.py homeassistant/components/starlink/button.py homeassistant/components/starlink/coordinator.py + homeassistant/components/starlink/device_tracker.py homeassistant/components/starlink/sensor.py homeassistant/components/starlink/switch.py homeassistant/components/starline/__init__.py diff --git a/homeassistant/components/starlink/__init__.py b/homeassistant/components/starlink/__init__.py index c59269d2e07..3413c4ff595 100644 --- a/homeassistant/components/starlink/__init__.py +++ b/homeassistant/components/starlink/__init__.py @@ -11,6 +11,7 @@ from .coordinator import StarlinkUpdateCoordinator PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index 3359706372e..95a5515ab21 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -10,8 +10,10 @@ from starlink_grpc import ( AlertDict, ChannelContext, GrpcError, + LocationDict, ObstructionDict, StatusDict, + location_data, reboot, set_stow_state, status_data, @@ -28,6 +30,7 @@ _LOGGER = logging.getLogger(__name__) class StarlinkData: """Contains data pulled from the Starlink system.""" + location: LocationDict status: StatusDict obstruction: ObstructionDict alert: AlertDict @@ -53,7 +56,10 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): status = await self.hass.async_add_executor_job( status_data, self.channel_context ) - return StarlinkData(*status) + location = await self.hass.async_add_executor_job( + location_data, self.channel_context + ) + return StarlinkData(location, *status) except GrpcError as exc: raise UpdateFailed from exc diff --git a/homeassistant/components/starlink/device_tracker.py b/homeassistant/components/starlink/device_tracker.py new file mode 100644 index 00000000000..eb832741f40 --- /dev/null +++ b/homeassistant/components/starlink/device_tracker.py @@ -0,0 +1,73 @@ +"""Contains device trackers exposed by the Starlink integration.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.device_tracker import SourceType, TrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import StarlinkData +from .entity import StarlinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all binary sensors for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkDeviceTrackerEntity(coordinator, description) + for description in DEVICE_TRACKERS + ) + + +@dataclass +class StarlinkDeviceTrackerEntityDescriptionMixin: + """Describes a Starlink device tracker.""" + + latitude_fn: Callable[[StarlinkData], float] + longitude_fn: Callable[[StarlinkData], float] + + +@dataclass +class StarlinkDeviceTrackerEntityDescription( + EntityDescription, StarlinkDeviceTrackerEntityDescriptionMixin +): + """Describes a Starlink button entity.""" + + +DEVICE_TRACKERS = [ + StarlinkDeviceTrackerEntityDescription( + key="device_location", + translation_key="device_location", + entity_registry_enabled_default=False, + latitude_fn=lambda data: data.location["latitude"], + longitude_fn=lambda data: data.location["longitude"], + ), +] + + +class StarlinkDeviceTrackerEntity(StarlinkEntity, TrackerEntity): + """A TrackerEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkDeviceTrackerEntityDescription + + @property + def source_type(self) -> SourceType | str: + """Return the source type, eg gps or router, of the device.""" + return SourceType.GPS + + @property + def latitude(self) -> float | None: + """Return latitude value of the device.""" + return self.entity_description.latitude_fn(self.coordinator.data) + + @property + def longitude(self) -> float | None: + """Return longitude value of the device.""" + return self.entity_description.longitude_fn(self.coordinator.data) diff --git a/homeassistant/components/starlink/diagnostics.py b/homeassistant/components/starlink/diagnostics.py index 10711e7155e..88e6485cf77 100644 --- a/homeassistant/components/starlink/diagnostics.py +++ b/homeassistant/components/starlink/diagnostics.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import StarlinkUpdateCoordinator -TO_REDACT = {"id"} +TO_REDACT = {"id", "latitude", "longitude", "altitude"} async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/starlink/strings.json b/homeassistant/components/starlink/strings.json index a9e50f5d39f..0ec85c68956 100644 --- a/homeassistant/components/starlink/strings.json +++ b/homeassistant/components/starlink/strings.json @@ -44,6 +44,11 @@ "name": "Unexpected location" } }, + "device_tracker": { + "device_location": { + "name": "Device location" + } + }, "sensor": { "ping": { "name": "Ping" diff --git a/tests/components/starlink/fixtures/location_data_success.json b/tests/components/starlink/fixtures/location_data_success.json new file mode 100644 index 00000000000..4d18d22d12e --- /dev/null +++ b/tests/components/starlink/fixtures/location_data_success.json @@ -0,0 +1,5 @@ +{ + "latitude": 37.422, + "longitude": -122.084, + "altitude": 100 +} diff --git a/tests/components/starlink/patchers.py b/tests/components/starlink/patchers.py index dfc0d2415df..d83451ecc17 100644 --- a/tests/components/starlink/patchers.py +++ b/tests/components/starlink/patchers.py @@ -8,11 +8,16 @@ SETUP_ENTRY_PATCHER = patch( "homeassistant.components.starlink.async_setup_entry", return_value=True ) -COORDINATOR_SUCCESS_PATCHER = patch( +STATUS_DATA_SUCCESS_PATCHER = patch( "homeassistant.components.starlink.coordinator.status_data", return_value=json.loads(load_fixture("status_data_success.json", "starlink")), ) +LOCATION_DATA_SUCCESS_PATCHER = patch( + "homeassistant.components.starlink.coordinator.location_data", + return_value=json.loads(load_fixture("location_data_success.json", "starlink")), +) + DEVICE_FOUND_PATCHER = patch( "homeassistant.components.starlink.config_flow.get_id", return_value="some-valid-id" ) diff --git a/tests/components/starlink/snapshots/test_diagnostics.ambr b/tests/components/starlink/snapshots/test_diagnostics.ambr index 6f859aaf50d..3bb7f235017 100644 --- a/tests/components/starlink/snapshots/test_diagnostics.ambr +++ b/tests/components/starlink/snapshots/test_diagnostics.ambr @@ -16,6 +16,11 @@ 'alert_thermal_throttle': False, 'alert_unexpected_location': False, }), + 'location': dict({ + 'altitude': '**REDACTED**', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), 'obstruction': dict({ 'raw_wedges_fraction_obstructed[]': list([ None, diff --git a/tests/components/starlink/test_diagnostics.py b/tests/components/starlink/test_diagnostics.py index 4bf8a619c88..231b58a2d5e 100644 --- a/tests/components/starlink/test_diagnostics.py +++ b/tests/components/starlink/test_diagnostics.py @@ -5,7 +5,7 @@ from homeassistant.components.starlink.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant -from .patchers import COORDINATOR_SUCCESS_PATCHER +from .patchers import LOCATION_DATA_SUCCESS_PATCHER, STATUS_DATA_SUCCESS_PATCHER from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -23,7 +23,7 @@ async def test_diagnostics( data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with COORDINATOR_SUCCESS_PATCHER: + with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/starlink/test_init.py b/tests/components/starlink/test_init.py index 72d3be52b4a..94a8a2a341b 100644 --- a/tests/components/starlink/test_init.py +++ b/tests/components/starlink/test_init.py @@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant -from .patchers import COORDINATOR_SUCCESS_PATCHER +from .patchers import LOCATION_DATA_SUCCESS_PATCHER, STATUS_DATA_SUCCESS_PATCHER from tests.common import MockConfigEntry @@ -16,7 +16,7 @@ async def test_successful_entry(hass: HomeAssistant) -> None: data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with COORDINATOR_SUCCESS_PATCHER: + with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -33,7 +33,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with COORDINATOR_SUCCESS_PATCHER: + with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) From 66c10facfa0432e37c425f62ba57c4ce28c377b1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 19 Aug 2023 09:48:23 +0000 Subject: [PATCH 0640/1151] Add Tractive `sleep` and `activity` sensors (#98575) * Add sleep and activity sensors * Use device class ENUM * Default value for value_fn --- homeassistant/components/tractive/__init__.py | 4 +++ homeassistant/components/tractive/const.py | 2 ++ homeassistant/components/tractive/sensor.py | 35 ++++++++++++++++++- .../components/tractive/strings.json | 20 +++++++++-- 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 043e074270e..c04676768c5 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -23,6 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( + ATTR_ACTIVITY_LABEL, ATTR_BUZZER, ATTR_CALORIES, ATTR_DAILY_GOAL, @@ -32,6 +33,7 @@ from .const import ( ATTR_MINUTES_DAY_SLEEP, ATTR_MINUTES_NIGHT_SLEEP, ATTR_MINUTES_REST, + ATTR_SLEEP_LABEL, ATTR_TRACKER_STATE, CLIENT, CLIENT_ID, @@ -281,10 +283,12 @@ class TractiveClient: def _send_wellness_update(self, event: dict[str, Any]) -> None: payload = { + ATTR_ACTIVITY_LABEL: event["wellness"]["activity_label"], ATTR_CALORIES: event["activity"]["calories"], ATTR_MINUTES_DAY_SLEEP: event["sleep"]["minutes_day_sleep"], ATTR_MINUTES_NIGHT_SLEEP: event["sleep"]["minutes_night_sleep"], ATTR_MINUTES_REST: event["activity"]["minutes_rest"], + ATTR_SLEEP_LABEL: event["wellness"]["sleep_label"], } self._dispatch_tracker_event( TRACKER_WELLNESS_STATUS_UPDATED, event["pet_id"], payload diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index 81936ae5d80..254a8c274f3 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -6,6 +6,7 @@ DOMAIN = "tractive" RECONNECT_INTERVAL = timedelta(seconds=10) +ATTR_ACTIVITY_LABEL = "activity_label" ATTR_BUZZER = "buzzer" ATTR_CALORIES = "calories" ATTR_DAILY_GOAL = "daily_goal" @@ -15,6 +16,7 @@ ATTR_MINUTES_ACTIVE = "minutes_active" ATTR_MINUTES_DAY_SLEEP = "minutes_day_sleep" ATTR_MINUTES_NIGHT_SLEEP = "minutes_night_sleep" ATTR_MINUTES_REST = "minutes_rest" +ATTR_SLEEP_LABEL = "sleep_label" ATTR_TRACKER_STATE = "tracker_state" # This client ID was issued by Tractive specifically for Home Assistant. diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 6891b74d31b..0d486606802 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -1,6 +1,7 @@ """Support for Tractive sensors.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from typing import Any @@ -19,15 +20,18 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from . import Trackables, TractiveClient from .const import ( + ATTR_ACTIVITY_LABEL, ATTR_CALORIES, ATTR_DAILY_GOAL, ATTR_MINUTES_ACTIVE, ATTR_MINUTES_DAY_SLEEP, ATTR_MINUTES_NIGHT_SLEEP, ATTR_MINUTES_REST, + ATTR_SLEEP_LABEL, ATTR_TRACKER_STATE, CLIENT, DOMAIN, @@ -53,11 +57,14 @@ class TractiveSensorEntityDescription( """Class describing Tractive sensor entities.""" hardware_sensor: bool = False + value_fn: Callable[[StateType], StateType] = lambda state: state class TractiveSensor(TractiveEntity, SensorEntity): """Tractive sensor.""" + entity_description: TractiveSensorEntityDescription + def __init__( self, client: TractiveClient, @@ -82,7 +89,9 @@ class TractiveSensor(TractiveEntity, SensorEntity): @callback def handle_status_update(self, event: dict[str, Any]) -> None: """Handle status update.""" - self._attr_native_value = event[self.entity_description.key] + self._attr_native_value = self.entity_description.value_fn( + event[self.entity_description.key] + ) super().handle_status_update(event) @@ -159,6 +168,30 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), + TractiveSensorEntityDescription( + key=ATTR_SLEEP_LABEL, + translation_key="sleep", + icon="mdi:sleep", + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, + value_fn=lambda state: state.lower() if isinstance(state, str) else state, + device_class=SensorDeviceClass.ENUM, + options=[ + "ok", + "good", + ], + ), + TractiveSensorEntityDescription( + key=ATTR_ACTIVITY_LABEL, + translation_key="activity", + icon="mdi:run", + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, + value_fn=lambda state: state.lower() if isinstance(state, str) else state, + device_class=SensorDeviceClass.ENUM, + options=[ + "ok", + "good", + ], + ), ) diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index 4053d2658f5..e315a8e6013 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -30,15 +30,22 @@ } }, "sensor": { + "activity": { + "name": "Activity", + "state": { + "ok": "OK", + "good": "Good" + } + }, + "activity_time": { + "name": "Activity time" + }, "calories": { "name": "Calories burned" }, "daily_goal": { "name": "Daily goal" }, - "activity_time": { - "name": "Activity time" - }, "minutes_day_sleep": { "name": "Day sleep" }, @@ -48,6 +55,13 @@ "rest_time": { "name": "Rest time" }, + "sleep": { + "name": "Sleep", + "state": { + "ok": "OK", + "good": "Good" + } + }, "tracker_battery_level": { "name": "Tracker battery" }, From 39fc4b3d662ea5682a26801457fa369d454bbdba Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 19 Aug 2023 12:31:40 +0200 Subject: [PATCH 0641/1151] Reolink add pan position sensor (#98592) * Add PTZ pan position sensor * fix typing * fix typing --- homeassistant/components/reolink/sensor.py | 78 +++++++++++++++++-- homeassistant/components/reolink/strings.json | 3 + 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/reolink/sensor.py b/homeassistant/components/reolink/sensor.py index af8d049dbc6..6282f29e442 100644 --- a/homeassistant/components/reolink/sensor.py +++ b/homeassistant/components/reolink/sensor.py @@ -21,14 +21,30 @@ from homeassistant.helpers.typing import StateType from . import ReolinkData from .const import DOMAIN -from .entity import ReolinkHostCoordinatorEntity +from .entity import ReolinkChannelCoordinatorEntity, ReolinkHostCoordinatorEntity + + +@dataclass +class ReolinkSensorEntityDescriptionMixin: + """Mixin values for Reolink sensor entities for a camera channel.""" + + value: Callable[[Host, int], int] + + +@dataclass +class ReolinkSensorEntityDescription( + SensorEntityDescription, ReolinkSensorEntityDescriptionMixin +): + """A class that describes sensor entities for a camera channel.""" + + supported: Callable[[Host, int], bool] = lambda api, ch: True @dataclass class ReolinkHostSensorEntityDescriptionMixin: """Mixin values for Reolink host sensor entities.""" - value: Callable[[Host], bool] + value: Callable[[Host], int] @dataclass @@ -37,9 +53,21 @@ class ReolinkHostSensorEntityDescription( ): """A class that describes host sensor entities.""" - supported: Callable[[Host], bool] = lambda host: True + supported: Callable[[Host], bool] = lambda api: True +SENSORS = ( + ReolinkSensorEntityDescription( + key="ptz_pan_position", + translation_key="ptz_pan_position", + icon="mdi:pan", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda api, ch: api.ptz_pan_position(ch), + supported=lambda api, ch: api.supported(ch, "ptz_position"), + ), +) + HOST_SENSORS = ( ReolinkHostSensorEntityDescription( key="wifi_signal", @@ -62,11 +90,45 @@ async def async_setup_entry( """Set up a Reolink IP Camera.""" reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - ReolinkHostSensorEntity(reolink_data, entity_description) - for entity_description in HOST_SENSORS - if entity_description.supported(reolink_data.host.api) + entities: list[ReolinkSensorEntity | ReolinkHostSensorEntity] = [ + ReolinkSensorEntity(reolink_data, channel, entity_description) + for entity_description in SENSORS + for channel in reolink_data.host.api.channels + if entity_description.supported(reolink_data.host.api, channel) + ] + entities.extend( + [ + ReolinkHostSensorEntity(reolink_data, entity_description) + for entity_description in HOST_SENSORS + if entity_description.supported(reolink_data.host.api) + ] ) + async_add_entities(entities) + + +class ReolinkSensorEntity(ReolinkChannelCoordinatorEntity, SensorEntity): + """Base sensor class for Reolink IP camera sensors.""" + + entity_description: ReolinkSensorEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + channel: int, + entity_description: ReolinkSensorEntityDescription, + ) -> None: + """Initialize Reolink sensor.""" + super().__init__(reolink_data, channel) + self.entity_description = entity_description + + self._attr_unique_id = ( + f"{self._host.unique_id}_{channel}_{entity_description.key}" + ) + + @property + def native_value(self) -> StateType | date | datetime | Decimal: + """Return the value reported by the sensor.""" + return self.entity_description.value(self._host.api, self._channel) class ReolinkHostSensorEntity(ReolinkHostCoordinatorEntity, SensorEntity): @@ -79,7 +141,7 @@ class ReolinkHostSensorEntity(ReolinkHostCoordinatorEntity, SensorEntity): reolink_data: ReolinkData, entity_description: ReolinkHostSensorEntityDescription, ) -> None: - """Initialize Reolink binary sensor.""" + """Initialize Reolink host sensor.""" super().__init__(reolink_data) self.entity_description = entity_description diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 08ee78fd930..cdaeb7d0656 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -102,6 +102,9 @@ "sensor": { "wifi_signal": { "name": "Wi-Fi signal" + }, + "ptz_pan_position": { + "name": "PTZ pan position" } } } From f318063a77c77eb9d3f8926a1439ace0ed5eddb9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Aug 2023 06:13:22 -0500 Subject: [PATCH 0642/1151] Bump dbus-fast to 1.92.0 (#98656) --- 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 99cbfe918b7..1ae23633bdf 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.8.0", - "dbus-fast==1.91.4" + "dbus-fast==1.92.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7a93328f3ff..78b14aaa590 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.8.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 -dbus-fast==1.91.4 +dbus-fast==1.92.0 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 e44a9420031..ccd6fff254a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.91.4 +dbus-fast==1.92.0 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5438221320a..ee6a47b2405 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.91.4 +dbus-fast==1.92.0 # homeassistant.components.debugpy debugpy==1.6.7 From 309499123644dcaea8757ccea932713c1d82c213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 19 Aug 2023 15:17:17 +0300 Subject: [PATCH 0643/1151] Upgrade ruff to 0.0.285 (#98647) --- .pre-commit-config.yaml | 2 +- .../components/ambiclimate/config_flow.py | 2 +- homeassistant/components/bluetooth/scanner.py | 3 +- homeassistant/components/camera/__init__.py | 4 +- .../components/denonavr/media_player.py | 3 +- .../components/esphome/config_flow.py | 4 +- .../components/esphome/entry_data.py | 2 +- homeassistant/components/freebox/home_base.py | 2 +- homeassistant/components/fritz/common.py | 21 +++---- homeassistant/components/logbook/models.py | 2 +- homeassistant/components/mqtt/client.py | 2 +- homeassistant/components/mqtt/models.py | 3 +- .../components/recorder/models/context.py | 6 +- homeassistant/components/recorder/util.py | 2 +- homeassistant/components/template/light.py | 32 ++++------- homeassistant/components/upcloud/__init__.py | 4 +- .../components/websocket_api/connection.py | 6 +- .../components/worldtidesinfo/sensor.py | 4 +- .../components/zerproc/config_flow.py | 2 +- homeassistant/helpers/config_validation.py | 2 +- homeassistant/helpers/service.py | 2 +- homeassistant/util/json.py | 8 +-- pyproject.toml | 1 + requirements_test_pre_commit.txt | 2 +- script/hassfest/manifest.py | 1 + script/scaffold/__main__.py | 11 +++- script/translations/download.py | 3 +- script/translations/upload.py | 1 + script/translations/util.py | 4 +- script/version_bump.py | 7 ++- tests/components/calendar/test_trigger.py | 2 +- tests/components/emulated_hue/test_hue_api.py | 2 +- tests/components/modbus/test_sensor.py | 56 +++++++++---------- tests/components/number/test_init.py | 2 +- .../openalpr_cloud/test_image_processing.py | 2 +- tests/components/ps4/test_config_flow.py | 4 +- tests/components/yeelight/test_light.py | 2 +- tests/helpers/test_condition.py | 8 +-- .../custom_components/test/update.py | 2 +- 39 files changed, 109 insertions(+), 119 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18cbb082145..77740d6279e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.280 + rev: v0.0.285 hooks: - id: ruff args: diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 16d790cc09c..0d259cf337a 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -100,7 +100,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: token_info = await oauth.get_access_token(code) except ambiclimate.AmbiclimateOauthError: - _LOGGER.error("Failed to get access token", exc_info=True) + _LOGGER.exception("Failed to get access token") return None store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index f0b7df528e1..eb3ce11b644 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -349,11 +349,10 @@ class HaScanner(BaseHaScanner): try: await self._async_start() except ScannerStartError as ex: - _LOGGER.error( + _LOGGER.exception( "%s: Failed to restart Bluetooth scanner: %s", self.name, ex, - exc_info=True, ) async def _async_reset_adapter(self) -> None: diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 486c964bb45..af64b2f1953 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -246,8 +246,8 @@ async def async_get_still_stream( await response.write( bytes( "--frameboundary\r\n" - "Content-Type: {}\r\n" - "Content-Length: {}\r\n\r\n".format(content_type, len(img_bytes)), + f"Content-Type: {content_type}\r\n" + f"Content-Length: {len(img_bytes)}\r\n\r\n", "utf-8", ) + img_bytes diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index cad6656d01d..c3dfbeb1011 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -196,11 +196,10 @@ def async_log_errors( ) except DenonAvrError as err: available = False - _LOGGER.error( + _LOGGER.exception( "Error %s occurred in method %s for Denon AVR receiver", err, func.__name__, - exc_info=True, ) finally: if available and not self.available: diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 5011439c778..898fb55a3ac 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -427,9 +427,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.error("Error talking to the dashboard: %s", err) return False except json.JSONDecodeError as err: - _LOGGER.error( - "Error parsing response from dashboard: %s", err, exc_info=True - ) + _LOGGER.exception("Error parsing response from dashboard: %s", err) return False self._noise_psk = noise_psk diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index b7870e9cca0..ad9403e3601 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -312,7 +312,7 @@ class RuntimeEntryData: and subscription_key not in stale_state and state_type is not CameraState and not ( - state_type is SensorState # pylint: disable=unidiomatic-typecheck + state_type is SensorState # noqa: E721 and (platform_info := self.info.get(SensorInfo)) and (entity_info := platform_info.get(state.key)) and (cast(SensorInfo, entity_info)).force_update diff --git a/homeassistant/components/freebox/home_base.py b/homeassistant/components/freebox/home_base.py index 37709cbf494..dc887229086 100644 --- a/homeassistant/components/freebox/home_base.py +++ b/homeassistant/components/freebox/home_base.py @@ -126,7 +126,7 @@ class FreeboxHomeEntity(Entity): ) if not node: _LOGGER.warning( - "The Freebox Home device has no node for: " + ep_type + "/" + name + "The Freebox Home device has no node for: %s/%s", ep_type, name ) return None return node.get("value") diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index d61ce334804..69773778121 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -785,27 +785,20 @@ class AvmWrapper(FritzBoxTools): ) return result except FritzSecurityError: - _LOGGER.error( - ( - "Authorization Error: Please check the provided credentials and" - " verify that you can log into the web interface" - ), - exc_info=True, + _LOGGER.exception( + "Authorization Error: Please check the provided credentials and" + " verify that you can log into the web interface" ) except FRITZ_EXCEPTIONS: - _LOGGER.error( + _LOGGER.exception( "Service/Action Error: cannot execute service %s with action %s", service_name, action_name, - exc_info=True, ) except FritzConnectionException: - _LOGGER.error( - ( - "Connection Error: Please check the device is properly configured" - " for remote login" - ), - exc_info=True, + _LOGGER.exception( + "Connection Error: Please check the device is properly configured" + " for remote login" ) return {} diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index e351ee6bb61..82c05e612e3 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -65,7 +65,7 @@ class LazyEventPartialState: self.context_parent_id_bin: bytes | None = self.row.context_parent_id_bin # We need to explicitly check for the row is EventAsRow as the unhappy path # to fetch row.data for Row is very expensive - if type(row) is EventAsRow: # pylint: disable=unidiomatic-typecheck + if type(row) is EventAsRow: # noqa: E721 # If its an EventAsRow we can avoid the whole # json decode process as we already have the data self.data = row.data diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 0c351e69bcf..62f1f55401d 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -361,7 +361,7 @@ class EnsureJobAfterCooldown: except asyncio.CancelledError: pass except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error cleaning up task", exc_info=True) + _LOGGER.exception("Error cleaning up task") class MQTT: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index a936c9e420d..99267d9572a 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -294,13 +294,12 @@ class EntityTopicState: try: entity.async_write_ha_state() except Exception: # pylint: disable=broad-except - _LOGGER.error( + _LOGGER.exception( "Exception raised when updating state of %s, topic: " "'%s' with payload: %s", entity.entity_id, msg.topic, msg.payload, - exc_info=True, ) @callback diff --git a/homeassistant/components/recorder/models/context.py b/homeassistant/components/recorder/models/context.py index f722c519833..f25e4d4412f 100644 --- a/homeassistant/components/recorder/models/context.py +++ b/homeassistant/components/recorder/models/context.py @@ -18,7 +18,7 @@ def ulid_to_bytes_or_none(ulid: str | None) -> bytes | None: try: return ulid_to_bytes(ulid) except ValueError as ex: - _LOGGER.error("Error converting ulid %s to bytes: %s", ulid, ex, exc_info=True) + _LOGGER.exception("Error converting ulid %s to bytes: %s", ulid, ex) return None @@ -29,9 +29,7 @@ def bytes_to_ulid_or_none(_bytes: bytes | None) -> str | None: try: return bytes_to_ulid(_bytes) except ValueError as ex: - _LOGGER.error( - "Error converting bytes %s to ulid: %s", _bytes, ex, exc_info=True - ) + _LOGGER.exception("Error converting bytes %s to ulid: %s", _bytes, ex) return None diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index f3de9824a16..1c3e07f40fd 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -132,7 +132,7 @@ def session_scope( need_rollback = True session.commit() except Exception as err: # pylint: disable=broad-except - _LOGGER.error("Error executing query: %s", err, exc_info=True) + _LOGGER.exception("Error executing query: %s", err) if need_rollback: session.rollback() if not exception_filter or not exception_filter(err): diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index b403034208a..a3dd1fd1ef3 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -459,9 +459,8 @@ class LightTemplate(TemplateEntity, LightEntity): ) self._brightness = None except ValueError: - _LOGGER.error( - "Template must supply an integer brightness from 0-255, or 'None'", - exc_info=True, + _LOGGER.exception( + "Template must supply an integer brightness from 0-255, or 'None'" ) self._brightness = None @@ -559,12 +558,9 @@ class LightTemplate(TemplateEntity, LightEntity): ) self._temperature = None except ValueError: - _LOGGER.error( - ( - "Template must supply an integer temperature within the range for" - " this light, or 'None'" - ), - exc_info=True, + _LOGGER.exception( + "Template must supply an integer temperature within the range for" + " this light, or 'None'" ) self._temperature = None @@ -620,12 +616,9 @@ class LightTemplate(TemplateEntity, LightEntity): return self._max_mireds = int(render) except ValueError: - _LOGGER.error( - ( - "Template must supply an integer temperature within the range for" - " this light, or 'None'" - ), - exc_info=True, + _LOGGER.exception( + "Template must supply an integer temperature within the range for" + " this light, or 'None'" ) self._max_mireds = None @@ -638,12 +631,9 @@ class LightTemplate(TemplateEntity, LightEntity): return self._min_mireds = int(render) except ValueError: - _LOGGER.error( - ( - "Template must supply an integer temperature within the range for" - " this light, or 'None'" - ), - exc_info=True, + _LOGGER.exception( + "Template must supply an integer temperature within the range for" + " this light, or 'None'" ) self._min_mireds = None diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 283842adaaa..7f832eb733f 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -122,10 +122,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await hass.async_add_executor_job(manager.authenticate) except upcloud_api.UpCloudAPIError: - _LOGGER.error("Authentication failed", exc_info=True) + _LOGGER.exception("Authentication failed") return False except requests.exceptions.RequestException as err: - _LOGGER.error("Failed to connect", exc_info=True) + _LOGGER.exception("Failed to connect") raise ConfigEntryNotReady from err if entry.options.get(CONF_SCAN_INTERVAL): diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index f598906661c..1dbda62ab95 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -164,12 +164,12 @@ class ActiveConnection: if ( # Not using isinstance as we don't care about children # as these are always coming from JSON - type(msg) is not dict # pylint: disable=unidiomatic-typecheck + type(msg) is not dict # noqa: E721 or ( not (cur_id := msg.get("id")) - or type(cur_id) is not int # pylint: disable=unidiomatic-typecheck + or type(cur_id) is not int # noqa: E721 or not (type_ := msg.get("type")) - or type(type_) is not str # pylint: disable=unidiomatic-typecheck + or type(type_) is not str # noqa: E721 ) ): self.logger.error("Received invalid command: %s", msg) diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index 776f6c6e20f..1a5c7ae39a2 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -115,8 +115,8 @@ class WorldTidesInfoSensor(SensorEntity): start = int(time.time()) resource = ( "https://www.worldtides.info/api?extremes&length=86400" - "&key={}&lat={}&lon={}&start={}" - ).format(self._key, self._lat, self._lon, start) + f"&key={self._key}&lat={self._lat}&lon={self._lon}&start={start}" + ) try: self.data = requests.get(resource, timeout=10).json() diff --git a/homeassistant/components/zerproc/config_flow.py b/homeassistant/components/zerproc/config_flow.py index e68c51cd7eb..a9fd20ce241 100644 --- a/homeassistant/components/zerproc/config_flow.py +++ b/homeassistant/components/zerproc/config_flow.py @@ -17,7 +17,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: devices = await pyzerproc.discover() return len(devices) > 0 except pyzerproc.ZerprocException: - _LOGGER.error("Unable to discover nearby Zerproc devices", exc_info=True) + _LOGGER.exception("Unable to discover nearby Zerproc devices") return False diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 10f4918a20b..5e0d66e0a9a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -586,7 +586,7 @@ def string(value: Any) -> str: raise vol.Invalid("string value is None") # This is expected to be the most common case, so check it first. - if type(value) is str: # pylint: disable=unidiomatic-typecheck + if type(value) is str: # noqa: E721 return value if isinstance(value, template_helper.ResultWrapper): diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 74823dea953..3eb537f9649 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -597,7 +597,7 @@ async def async_get_all_descriptions( ints_or_excs = await async_get_integrations(hass, missing) integrations: list[Integration] = [] for domain, int_or_exc in ints_or_excs.items(): - if type(int_or_exc) is Integration: # pylint: disable=unidiomatic-typecheck + if type(int_or_exc) is Integration: # noqa: E721 integrations.append(int_or_exc) continue if TYPE_CHECKING: diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 724c3ebf8d3..60aa920ed6a 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -42,7 +42,7 @@ def json_loads_array(__obj: bytes | bytearray | memoryview | str) -> JsonArrayTy """Parse JSON data and ensure result is a list.""" value: JsonValueType = json_loads(__obj) # Avoid isinstance overhead as we are not interested in list subclasses - if type(value) is list: # pylint: disable=unidiomatic-typecheck + if type(value) is list: # noqa: E721 return value raise ValueError(f"Expected JSON to be parsed as a list got {type(value)}") @@ -51,7 +51,7 @@ def json_loads_object(__obj: bytes | bytearray | memoryview | str) -> JsonObject """Parse JSON data and ensure result is a dictionary.""" value: JsonValueType = json_loads(__obj) # Avoid isinstance overhead as we are not interested in dict subclasses - if type(value) is dict: # pylint: disable=unidiomatic-typecheck + if type(value) is dict: # noqa: E721 return value raise ValueError(f"Expected JSON to be parsed as a dict got {type(value)}") @@ -89,7 +89,7 @@ def load_json_array( default = [] value: JsonValueType = load_json(filename, default=default) # Avoid isinstance overhead as we are not interested in list subclasses - if type(value) is list: # pylint: disable=unidiomatic-typecheck + if type(value) is list: # noqa: E721 return value _LOGGER.exception( "Expected JSON to be parsed as a list got %s in: %s", {type(value)}, filename @@ -108,7 +108,7 @@ def load_json_object( default = {} value: JsonValueType = load_json(filename, default=default) # Avoid isinstance overhead as we are not interested in dict subclasses - if type(value) is dict: # pylint: disable=unidiomatic-typecheck + if type(value) is dict: # noqa: E721 return value _LOGGER.exception( "Expected JSON to be parsed as a dict got %s in: %s", {type(value)}, filename diff --git a/pyproject.toml b/pyproject.toml index 1587adbea74..f8753e34680 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -266,6 +266,7 @@ disable = [ "missing-module-docstring", # D100 "multiple-imports", #E401 "singleton-comparison", # E711, E712 + "subprocess-run-check", # PLW1510 "superfluous-parens", # UP034 "ungrouped-imports", # I001 "unidiomatic-typecheck", # E721 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index e91cbe1ff62..844d796e7af 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -2,5 +2,5 @@ black==23.7.0 codespell==2.2.2 -ruff==0.0.280 +ruff==0.0.285 yamllint==1.32.0 diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 4515f52d8a3..4a15acb2d1d 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -397,4 +397,5 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: ["pre-commit", "run", "--hook-stage", "manual", "prettier", "--files"] + manifests_resorted, stdout=subprocess.DEVNULL, + check=True, ) diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 2d4454c254b..42a8355db59 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -77,11 +77,13 @@ def main(): pipe_null = {} if args.develop else {"stdout": subprocess.DEVNULL} print("Running hassfest to pick up new information.") - subprocess.run(["python", "-m", "script.hassfest"], **pipe_null) + subprocess.run(["python", "-m", "script.hassfest"], **pipe_null, check=True) print() print("Running gen_requirements_all to pick up new information.") - subprocess.run(["python", "-m", "script.gen_requirements_all"], **pipe_null) + subprocess.run( + ["python", "-m", "script.gen_requirements_all"], **pipe_null, check=True + ) print() print("Running script/translations_develop to pick up new translation strings.") @@ -95,13 +97,16 @@ def main(): info.domain, ], **pipe_null, + check=True, ) print() if args.develop: print("Running tests") print(f"$ pytest -vvv tests/components/{info.domain}") - subprocess.run(["pytest", "-vvv", f"tests/components/{info.domain}"]) + subprocess.run( + ["pytest", "-vvv", f"tests/components/{info.domain}"], check=True + ) print() docs.print_relevant_docs(args.template, info) diff --git a/script/translations/download.py b/script/translations/download.py index 6d4ce91263a..bcab3b511c3 100755 --- a/script/translations/download.py +++ b/script/translations/download.py @@ -44,7 +44,8 @@ def run_download_docker(): "json", "--unzip-to", "/opt/dest", - ] + ], + check=False, ) print() diff --git a/script/translations/upload.py b/script/translations/upload.py index 02d964a94c9..1a1819af863 100755 --- a/script/translations/upload.py +++ b/script/translations/upload.py @@ -42,6 +42,7 @@ def run_upload_docker(): "--convert-placeholders=false", "--replace-modified", ], + check=False, ) print() diff --git a/script/translations/util.py b/script/translations/util.py index 0c8c8a2a30f..9f41253fa02 100644 --- a/script/translations/util.py +++ b/script/translations/util.py @@ -48,7 +48,9 @@ def get_current_branch(): """Get current branch.""" return ( subprocess.run( - ["git", "rev-parse", "--abbrev-ref", "HEAD"], stdout=subprocess.PIPE + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + stdout=subprocess.PIPE, + check=True, ) .stdout.decode() .strip() diff --git a/script/version_bump.py b/script/version_bump.py index 4a38adbd677..ae01b1e6bed 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -161,7 +161,10 @@ def main(): ) arguments = parser.parse_args() - if arguments.commit and subprocess.run(["git", "diff", "--quiet"]).returncode == 1: + if ( + arguments.commit + and subprocess.run(["git", "diff", "--quiet"], check=False).returncode == 1 + ): print("Cannot use --commit because git is dirty.") return @@ -177,7 +180,7 @@ def main(): if not arguments.commit: return - subprocess.run(["git", "commit", "-nam", f"Bumped version to {bumped}"]) + subprocess.run(["git", "commit", "-nam", f"Bumped version to {bumped}"], check=True) def test_bump_version(): diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 45dd9d6afe1..02aebf3ce92 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -100,7 +100,7 @@ class FakeSchedule: async def fire_time(self, trigger_time: datetime.datetime) -> None: """Fire an alarm and wait.""" - _LOGGER.debug(f"Firing alarm @ {dt_util.as_local(trigger_time)}") + _LOGGER.debug("Firing alarm @ %s", dt_util.as_local(trigger_time)) self.freezer.move_to(trigger_time) async_fire_time_changed(self.hass, trigger_time) await self.hass.async_block_till_done() diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index acfb11ced0a..b42b40b2739 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -314,7 +314,7 @@ async def test_discover_lights(hass: HomeAssistant, hue_client) -> None: await hass.async_block_till_done() result_json = await async_get_lights(hue_client) - assert "1" not in result_json.keys() + assert "1" not in result_json devices = {val["uniqueid"] for val in result_json.values()} assert "00:2f:d2:31:ce:c5:55:cc-ee" not in devices # light.ceiling_lights diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 298daa1397f..8481adc0d0f 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -505,7 +505,7 @@ async def test_config_wrong_struct_sensor( }, [0x0102], False, - str(int(0x0102)), + str(0x0102), ), ( { @@ -514,7 +514,7 @@ async def test_config_wrong_struct_sensor( }, [0x0201], False, - str(int(0x0102)), + str(0x0102), ), ( { @@ -523,7 +523,7 @@ async def test_config_wrong_struct_sensor( }, [0x0102, 0x0304], False, - str(int(0x02010403)), + str(0x02010403), ), ( { @@ -532,7 +532,7 @@ async def test_config_wrong_struct_sensor( }, [0x0102, 0x0304], False, - str(int(0x03040102)), + str(0x03040102), ), ( { @@ -541,25 +541,25 @@ async def test_config_wrong_struct_sensor( }, [0x0102, 0x0304], False, - str(int(0x04030201)), + str(0x04030201), ), ( { CONF_DATA_TYPE: DataType.INT32, - CONF_MAX_VALUE: int(0x02010400), + CONF_MAX_VALUE: 0x02010400, }, [0x0201, 0x0403], False, - str(int(0x02010400)), + str(0x02010400), ), ( { CONF_DATA_TYPE: DataType.INT32, - CONF_MIN_VALUE: int(0x02010404), + CONF_MIN_VALUE: 0x02010404, }, [0x0201, 0x0403], False, - str(int(0x02010404)), + str(0x02010404), ), ( { @@ -573,20 +573,20 @@ async def test_config_wrong_struct_sensor( ( { CONF_DATA_TYPE: DataType.INT32, - CONF_ZERO_SUPPRESS: int(0x00000001), + CONF_ZERO_SUPPRESS: 0x00000001, }, [0x0000, 0x0002], False, - str(int(0x00000002)), + str(0x00000002), ), ( { CONF_DATA_TYPE: DataType.INT32, - CONF_ZERO_SUPPRESS: int(0x00000002), + CONF_ZERO_SUPPRESS: 0x00000002, }, [0x0000, 0x0002], False, - str(int(0)), + str(0), ), ( { @@ -727,7 +727,7 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non }, [0x0102], False, - [str(int(0x0201))], + [str(0x0201)], ), ( { @@ -738,7 +738,7 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non }, [0x0102, 0x0304], False, - [str(int(0x03040102))], + [str(0x03040102)], ), ( { @@ -749,7 +749,7 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non }, [0x0102, 0x0304, 0x0506, 0x0708], False, - [str(int(0x0708050603040102))], + [str(0x0708050603040102)], ), ( { @@ -760,7 +760,7 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non }, [0x0102, 0x0304], False, - [str(int(0x0201)), str(int(0x0403))], + [str(0x0201), str(0x0403)], ), ( { @@ -771,7 +771,7 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non }, [0x0102, 0x0304, 0x0506, 0x0708], False, - [str(int(0x03040102)), str(int(0x07080506))], + [str(0x03040102), str(0x07080506)], ), ( { @@ -782,7 +782,7 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non }, [0x0102, 0x0304, 0x0506, 0x0708, 0x0901, 0x0902, 0x0903, 0x0904], False, - [str(int(0x0708050603040102)), str(int(0x0904090309020901))], + [str(0x0708050603040102), str(0x0904090309020901)], ), ( { @@ -793,7 +793,7 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non }, [0x0102, 0x0304, 0x0506, 0x0708], False, - [str(int(0x0201)), str(int(0x0403)), str(int(0x0605)), str(int(0x0807))], + [str(0x0201), str(0x0403), str(0x0605), str(0x0807)], ), ( { @@ -814,10 +814,10 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non ], False, [ - str(int(0x03040102)), - str(int(0x07080506)), - str(int(0x0B0C090A)), - str(int(0x0F000D0E)), + str(0x03040102), + str(0x07080506), + str(0x0B0C090A), + str(0x0F000D0E), ], ), ( @@ -847,10 +847,10 @@ async def test_slave_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> Non ], False, [ - str(int(0x0604060306020601)), - str(int(0x0704070307020701)), - str(int(0x0804080308020801)), - str(int(0x0904090309020901)), + str(0x0604060306020601), + str(0x0704070307020701), + str(0x0804080308020801), + str(0x0904090309020901), ], ), ], diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index d77a67e4ada..23758fe345d 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -506,7 +506,7 @@ async def test_restore_number_save_state( assert state["entity_id"] == entity0.entity_id extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"] assert extra_data == RESTORE_DATA - assert type(extra_data["native_value"]) == float + assert isinstance(extra_data["native_value"], float) @pytest.mark.parametrize( diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index dfda0b0d282..8dd8326ddd8 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -157,7 +157,7 @@ async def test_openalpr_process_image( ] assert len(event_data) == 1 assert event_data[0]["plate"] == "H786P0J" - assert event_data[0]["confidence"] == float(90.436699) + assert event_data[0]["confidence"] == 90.436699 assert event_data[0]["entity_id"] == "image_processing.test_local" diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 702fd313e6d..242470fa8e7 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -56,8 +56,8 @@ MOCK_CONFIG_ADDITIONAL = { CONF_CODE: MOCK_CODE, } MOCK_DATA = {CONF_TOKEN: MOCK_CREDS, "devices": [MOCK_DEVICE]} -MOCK_UDP_PORT = int(987) -MOCK_TCP_PORT = int(997) +MOCK_UDP_PORT = 987 +MOCK_TCP_PORT = 997 MOCK_AUTO = {"Config Mode": "Auto Discover"} MOCK_MANUAL = {"Config Mode": "Manual Entry", CONF_IP_ADDRESS: MOCK_HOST} diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 6161f66fdd1..47dbd54baa9 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -181,7 +181,7 @@ async def test_services(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - await hass.services.async_call(domain, service, data, blocking=True) if payload is None: mocked_method.assert_called_once() - elif type(payload) == list: + elif isinstance(payload, list): mocked_method.assert_called_once_with(*payload) else: mocked_method.assert_called_once_with(**payload) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index fdbe9eb316b..2512f426f13 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -303,7 +303,7 @@ async def test_and_condition_shorthand(hass: HomeAssistant) -> None: test = await condition.async_from_config(hass, config) assert config["alias"] == "And Condition Shorthand" - assert "and" not in config.keys() + assert "and" not in config hass.states.async_set("sensor.temperature", 120) assert not test(hass) @@ -345,7 +345,7 @@ async def test_and_condition_list_shorthand(hass: HomeAssistant) -> None: test = await condition.async_from_config(hass, config) assert config["alias"] == "And Condition List Shorthand" - assert "and" not in config.keys() + assert "and" not in config hass.states.async_set("sensor.temperature", 120) assert not test(hass) @@ -577,7 +577,7 @@ async def test_or_condition_shorthand(hass: HomeAssistant) -> None: test = await condition.async_from_config(hass, config) assert config["alias"] == "Or Condition Shorthand" - assert "or" not in config.keys() + assert "or" not in config hass.states.async_set("sensor.temperature", 120) assert not test(hass) @@ -809,7 +809,7 @@ async def test_not_condition_shorthand(hass: HomeAssistant) -> None: test = await condition.async_from_config(hass, config) assert config["alias"] == "Not Condition Shorthand" - assert "not" not in config.keys() + assert "not" not in config hass.states.async_set("sensor.temperature", 101) assert test(hass) diff --git a/tests/testing_config/custom_components/test/update.py b/tests/testing_config/custom_components/test/update.py index 5d2292e9249..36b4e7c692f 100644 --- a/tests/testing_config/custom_components/test/update.py +++ b/tests/testing_config/custom_components/test/update.py @@ -61,7 +61,7 @@ class MockUpdateEntity(MockEntity, UpdateEntity): if version is not None: self._values["installed_version"] = version - _LOGGER.info(f"Installed update with version: {version}") + _LOGGER.info("Installed update with version: %s", version) else: self._values["installed_version"] = self.latest_version _LOGGER.info("Installed latest update") From 5965918c86d9c9e5bb8b8ce1639a7e349284a2fc Mon Sep 17 00:00:00 2001 From: "Benjamin Paul [MSFT]" Date: Sat, 19 Aug 2023 10:42:13 -0400 Subject: [PATCH 0644/1151] Add Fan Speed number entity to Nexia (#98642) * Add Fan Speed support to Nexia integration Adds a new "set_fan_speed" service to the Nexia integration, to allow setting speed of the air-handler/blower fans. * Add Fan Speed to Nexia Tests * Remove mistakenly-added changes to Climate tests A previous version of this change made modifications to the base Climate entity, but that approach was later abandonded. These changes to Climate tests were left over from that, and should never have been included. * Add Fan Speed Number Entity * Remove Set Fan Speed Service * Remove fan_speed attribute The fan_speed attribute is not necessary with the new Number entity. * Address reviewer feedback Rename test function to reflect fact that fan speed entities are entities, and not sensors. Added missing typing to variables. Sorted list of platforms * Add test_set_fan_speed Also adds new test fixture for mock response to API call * Update homeassistant/components/nexia/number.py * Name change --------- Co-authored-by: G Johansson --- homeassistant/components/nexia/const.py | 3 +- homeassistant/components/nexia/number.py | 72 + .../nexia/fixtures/set_fan_speed_2293892.json | 3086 +++++++++++++++++ tests/components/nexia/test_number.py | 62 + tests/components/nexia/util.py | 5 + 5 files changed, 3227 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/nexia/number.py create mode 100644 tests/components/nexia/fixtures/set_fan_speed_2293892.json create mode 100644 tests/components/nexia/test_number.py diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index 493fdd8a403..fe2d6527ea0 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -2,10 +2,11 @@ from homeassistant.const import Platform PLATFORMS = [ - Platform.SENSOR, Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.NUMBER, Platform.SCENE, + Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/nexia/number.py b/homeassistant/components/nexia/number.py new file mode 100644 index 00000000000..acb99c2ed01 --- /dev/null +++ b/homeassistant/components/nexia/number.py @@ -0,0 +1,72 @@ +"""Support for Nexia / Trane XL Thermostats.""" +from __future__ import annotations + +from nexia.home import NexiaHome +from nexia.thermostat import NexiaThermostat + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import NexiaDataUpdateCoordinator +from .entity import NexiaThermostatEntity +from .util import percent_conv + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors for a Nexia device.""" + coordinator: NexiaDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + nexia_home: NexiaHome = coordinator.nexia_home + + entities: list[NexiaThermostatEntity] = [] + for thermostat_id in nexia_home.get_thermostat_ids(): + thermostat = nexia_home.get_thermostat_by_id(thermostat_id) + if thermostat.has_variable_fan_speed(): + entities.append( + NexiaFanSpeedEntity( + coordinator, thermostat, thermostat.get_variable_fan_speed_limits() + ) + ) + async_add_entities(entities) + + +class NexiaFanSpeedEntity(NexiaThermostatEntity, NumberEntity): + """Provides Nexia Fan Speed support.""" + + _attr_native_unit_of_measurement = PERCENTAGE + _attr_icon = "mdi:fan" + + def __init__( + self, + coordinator: NexiaDataUpdateCoordinator, + thermostat: NexiaThermostat, + valid_range: tuple[float, float], + ) -> None: + """Initialize the entity.""" + super().__init__( + coordinator, + thermostat, + name=f"{thermostat.get_name()} Fan speed", + unique_id=f"{thermostat.thermostat_id}_fan_speed_setpoint", + ) + min_value, max_value = valid_range + self._attr_native_min_value = percent_conv(min_value) + self._attr_native_max_value = percent_conv(max_value) + + @property + def native_value(self) -> float: + """Return the current value.""" + fan_speed = self._thermostat.get_fan_speed_setpoint() + return percent_conv(fan_speed) + + async def async_set_native_value(self, value: float) -> None: + """Set a new value.""" + await self._thermostat.set_fan_setpoint(value / 100) + self._signal_thermostat_update() diff --git a/tests/components/nexia/fixtures/set_fan_speed_2293892.json b/tests/components/nexia/fixtures/set_fan_speed_2293892.json new file mode 100644 index 00000000000..bad0fccb2ad --- /dev/null +++ b/tests/components/nexia/fixtures/set_fan_speed_2293892.json @@ -0,0 +1,3086 @@ +{ + "success": true, + "error": null, + "result": { + "id": 2293892, + "name": "Master Suite", + "name_editable": true, + "features": [ + { + "name": "advanced_info", + "items": [ + { + "type": "label_value", + "label": "Model", + "value": "XL1050" + }, + { + "type": "label_value", + "label": "AUID", + "value": "0281B02C" + }, + { + "type": "label_value", + "label": "Firmware Build Number", + "value": "1581321824" + }, + { + "type": "label_value", + "label": "Firmware Build Date", + "value": "2020-02-10 08:03:44 UTC" + }, + { + "type": "label_value", + "label": "Firmware Version", + "value": "5.9.1" + }, + { + "type": "label_value", + "label": "Zoning Enabled", + "value": "yes" + } + ] + }, + { + "name": "thermostat", + "temperature": 73, + "status": "Cooling", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99 + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "group", + "members": [ + { + "type": "xxl_zone", + "id": 83394133, + "name": "Bath Closet", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + } + }, + { + "type": "xxl_zone", + "id": 83394130, + "name": "Master", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 71 + }, + "operating_state": "Damper Open", + "heating_setpoint": 63, + "cooling_setpoint": 71, + "zone_status": "Damper Open", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Open", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 71, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + } + }, + { + "type": "xxl_zone", + "id": 83394136, + "name": "Nick Office", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + } + }, + { + "type": "xxl_zone", + "id": 83394127, + "name": "Snooze Room", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 72, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + } + }, + { + "type": "xxl_zone", + "id": 83394139, + "name": "Safe Room", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + } + } + ] + }, + { + "name": "thermostat_fan_mode", + "label": "Fan Mode", + "options": [ + { + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + "header": true + }, + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "value": "auto", + "display_value": "Auto", + "status_icon": { + "name": "thermostat_fan_on", + "modifiers": [] + }, + "actions": { + "update_thermostat_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" + } + } + }, + { + "name": "thermostat_compressor_speed", + "compressor_speed": 0.69 + }, + { + "name": "runtime_history", + "actions": { + "get_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=daily" + }, + "get_monthly_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=monthly" + } + } + } + ], + "icon": [ + { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + { + "name": "thermostat", + "modifiers": ["temperature-74"] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2293892" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e3fc90c7-2885-4f57-ae76-99e9ec81eef0" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/967361e8aed874aa5230930fd0e0bbd8b653261e982a6e0e" + } + }, + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "settings": [ + { + "type": "fan_mode", + "title": "Fan Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "labels": ["Auto", "On", "Circulate"], + "values": ["auto", "on", "circulate"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" + } + } + }, + { + "type": "fan_speed", + "title": "Fan Speed", + "current_value": 0.5, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + }, + { + "value": 0.7, + "label": "70%" + }, + { + "value": 0.75, + "label": "75%" + }, + { + "value": 0.8, + "label": "80%" + }, + { + "value": 0.85, + "label": "85%" + }, + { + "value": 0.9, + "label": "90%" + }, + { + "value": 0.95, + "label": "95%" + }, + { + "value": 1.0, + "label": "100%" + } + ], + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%" + ], + "values": [ + 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, + 0.95, 1.0 + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_speed" + } + } + }, + { + "type": "fan_circulation_time", + "title": "Fan Circulation Time", + "current_value": 30, + "options": [ + { + "value": 10, + "label": "10 minutes" + }, + { + "value": 15, + "label": "15 minutes" + }, + { + "value": 20, + "label": "20 minutes" + }, + { + "value": 25, + "label": "25 minutes" + }, + { + "value": 30, + "label": "30 minutes" + }, + { + "value": 35, + "label": "35 minutes" + }, + { + "value": 40, + "label": "40 minutes" + }, + { + "value": 45, + "label": "45 minutes" + }, + { + "value": 50, + "label": "50 minutes" + }, + { + "value": 55, + "label": "55 minutes" + } + ], + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes" + ], + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_circulation_time" + } + } + }, + { + "type": "air_cleaner_mode", + "title": "Air Cleaner Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "quick", + "label": "Quick" + }, + { + "value": "allergy", + "label": "Allergy" + } + ], + "labels": ["Auto", "Quick", "Allergy"], + "values": ["auto", "quick", "allergy"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/air_cleaner_mode" + } + } + }, + { + "type": "dehumidify", + "title": "Cooling Dehumidify Set Point", + "current_value": 0.45, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + } + ], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/dehumidify" + } + } + }, + { + "type": "scale", + "title": "Temperature Scale", + "current_value": "f", + "options": [ + { + "value": "f", + "label": "F" + }, + { + "value": "c", + "label": "C" + } + ], + "labels": ["F", "C"], + "values": ["f", "c"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/scale" + } + } + } + ], + "status_secondary": null, + "status_tertiary": null, + "type": "xxl_thermostat", + "has_outdoor_temperature": true, + "outdoor_temperature": "87", + "has_indoor_humidity": true, + "connected": true, + "indoor_humidity": "52", + "system_status": "Cooling", + "delta": 3, + "zones": [ + { + "type": "xxl_zone", + "id": 83394133, + "name": "Bath Closet", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + } + }, + { + "type": "xxl_zone", + "id": 83394130, + "name": "Master", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 71 + }, + "operating_state": "Damper Open", + "heating_setpoint": 63, + "cooling_setpoint": 71, + "zone_status": "Damper Open", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Open", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 71, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + } + }, + { + "type": "xxl_zone", + "id": 83394136, + "name": "Nick Office", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + } + }, + { + "type": "xxl_zone", + "id": 83394127, + "name": "Snooze Room", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 72, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + } + }, + { + "type": "xxl_zone", + "id": 83394139, + "name": "Safe Room", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + } + }, + { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + } + } + ], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [ + { + "name": "thermostat", + "temperature": 74, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [ + { + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + } + } + ] + } +} diff --git a/tests/components/nexia/test_number.py b/tests/components/nexia/test_number.py new file mode 100644 index 00000000000..7f4c5f92ab6 --- /dev/null +++ b/tests/components/nexia/test_number.py @@ -0,0 +1,62 @@ +"""The number entity tests for the nexia platform.""" + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.core import HomeAssistant + +from .util import async_init_integration + + +async def test_create_fan_speed_number_entities(hass: HomeAssistant) -> None: + """Test creation of fan speed number entities.""" + + await async_init_integration(hass) + + state = hass.states.get("number.master_suite_fan_speed") + assert state.state == "35.0" + expected_attributes = { + "attribution": "Data provided by Trane Technologies", + "friendly_name": "Master Suite Fan speed", + "min": 35, + "max": 100, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("number.downstairs_east_wing_fan_speed") + assert state.state == "35.0" + expected_attributes = { + "attribution": "Data provided by Trane Technologies", + "friendly_name": "Downstairs East Wing Fan speed", + "min": 35, + "max": 100, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + +async def test_set_fan_speed(hass: HomeAssistant) -> None: + """Test setting fan speed.""" + + await async_init_integration(hass) + + state_before = hass.states.get("number.master_suite_fan_speed") + assert state_before.state == "35.0" + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: 50}, + blocking=True, + target={"entity_id": "number.master_suite_fan_speed"}, + ) + state = hass.states.get("number.master_suite_fan_speed") + assert state.state == "50.0" diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index 318a317fae4..d47e3fd3d6a 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -22,6 +22,7 @@ async def async_init_integration( house_fixture = "nexia/mobile_houses_123456.json" session_fixture = "nexia/session_123456.json" sign_in_fixture = "nexia/sign_in.json" + set_fan_speed_fixture = "nexia/set_fan_speed_2293892.json" with mock_aiohttp_client() as mock_session, patch( "nexia.home.load_or_create_uuid", return_value=uuid.uuid4() ): @@ -46,6 +47,10 @@ async def async_init_integration( nexia.API_MOBILE_ACCOUNTS_SIGN_IN_URL, text=load_fixture(sign_in_fixture), ) + mock_session.post( + "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_speed", + text=load_fixture(set_fan_speed_fixture), + ) entry = MockConfigEntry( domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} ) From 91965a74d8bcafde976d4c18fdbb4b0323287ff4 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Sat, 19 Aug 2023 13:30:12 -0400 Subject: [PATCH 0645/1151] Enphase remove operating (#98682) --- .../components/enphase_envoy/binary_sensor.py | 12 ------------ homeassistant/components/enphase_envoy/strings.json | 3 --- 2 files changed, 15 deletions(-) diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index 77d41ccf375..009b5d18338 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -51,12 +51,6 @@ ENCHARGE_SENSORS = ( 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, - ), ) RELAY_STATUS_SENSOR = BinarySensorEntityDescription( @@ -88,12 +82,6 @@ ENPOWER_SENSORS = ( 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", diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index f023bc7d114..477da2b3211 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -29,9 +29,6 @@ "dc_switch": { "name": "DC switch" }, - "operating": { - "name": "Operating" - }, "grid_status": { "name": "Grid status" }, From f020d17dd8608c98f33698c3054e19f724b6fb8e Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Sat, 19 Aug 2023 21:32:20 +0200 Subject: [PATCH 0646/1151] Support Eco Mode Preset in Climates (#98359) * Fix #86145 * Add missing test coverage * Add tests for state --- .../components/homematicip_cloud/climate.py | 4 ++- .../homematicip_cloud/test_climate.py | 35 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 6e4959a4789..09d00e9bee1 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -180,7 +180,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): ) or self._has_switch: if not profile_names: presets.append(PRESET_NONE) - presets.append(PRESET_BOOST) + presets.extend([PRESET_BOOST, PRESET_ECO]) presets.extend(profile_names) @@ -223,6 +223,8 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): await self._device.set_boost(False) if preset_mode == PRESET_BOOST: await self._device.set_boost() + if preset_mode == PRESET_ECO: + await self._device.set_control_mode(HMIP_ECO_CM) if preset_mode in self._device_profile_names: profile_idx = self._get_profile_idx_by_name(preset_mode) if self._device.controlMode != HMIP_AUTOMATIC_CM: diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index a586d5fe27d..b042e3daa6c 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -59,7 +59,12 @@ async def test_hmip_heating_group_heat( assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes["current_humidity"] == 47 assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" - assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_BOOST, "STD", "Winter"] + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_BOOST, + PRESET_ECO, + "STD", + "Winter", + ] service_call_counter = len(hmip_device.mock_calls) @@ -219,6 +224,21 @@ async def test_hmip_heating_group_heat( # Only fire event from last async_manipulate_test_data available. assert hmip_device.mock_calls[-1][0] == "fire_update_event" + assert ha_state.state == HVACMode.AUTO + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": PRESET_ECO}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 25 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("ECO",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO + assert ha_state.state == HVACMode.AUTO + await async_manipulate_test_data(hass, hmip_device, "floorHeatingMode", "RADIATOR") await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.1) ha_state = hass.states.get(entity_id) @@ -376,7 +396,12 @@ async def test_hmip_heating_group_heat_with_switch( assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes["current_humidity"] == 43 assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" - assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_BOOST, "STD", "P2"] + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_BOOST, + PRESET_ECO, + "STD", + "P2", + ] async def test_hmip_heating_group_heat_with_radiator( @@ -401,7 +426,11 @@ async def test_hmip_heating_group_heat_with_radiator( assert ha_state.attributes["max_temp"] == 30.0 assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes[ATTR_PRESET_MODE] is None - assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_NONE, PRESET_BOOST] + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_NONE, + PRESET_BOOST, + PRESET_ECO, + ] async def test_hmip_climate_services( From 5f5c012b0a2addfb1b70c6f8edd42c7c0af6c8a2 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sat, 19 Aug 2023 21:34:07 +0200 Subject: [PATCH 0647/1151] Duotecno code-cleanup (#98675) * small code cleanup, use a wrapper for the commands * Better decorator naming * update the error information --- homeassistant/components/duotecno/cover.py | 27 ++++++--------------- homeassistant/components/duotecno/entity.py | 27 +++++++++++++++++++++ homeassistant/components/duotecno/light.py | 19 ++++----------- homeassistant/components/duotecno/switch.py | 15 ++++-------- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/duotecno/cover.py b/homeassistant/components/duotecno/cover.py index a6fb49c30e0..0be9daf572b 100644 --- a/homeassistant/components/duotecno/cover.py +++ b/homeassistant/components/duotecno/cover.py @@ -8,11 +8,10 @@ from duotecno.unit import DuoswitchUnit from homeassistant.components.cover import CoverEntity, CoverEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import DuotecnoEntity +from .entity import DuotecnoEntity, api_call async def async_setup_entry( @@ -54,29 +53,17 @@ class DuotecnoCover(DuotecnoEntity, CoverEntity): """Return if the cover is closing.""" return self._unit.is_closing() + @api_call async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - try: - await self._unit.open() - except OSError as err: - raise HomeAssistantError( - "Transmit for the open_cover packet failed" - ) from err + await self._unit.open() + @api_call async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - try: - await self._unit.close() - except OSError as err: - raise HomeAssistantError( - "Transmit for the close_cover packet failed" - ) from err + await self._unit.close() + @api_call async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - try: - await self._unit.stop() - except OSError as err: - raise HomeAssistantError( - "Transmit for the stop_cover packet failed" - ) from err + await self._unit.stop() diff --git a/homeassistant/components/duotecno/entity.py b/homeassistant/components/duotecno/entity.py index 5715593ad2d..d38d52a0d26 100644 --- a/homeassistant/components/duotecno/entity.py +++ b/homeassistant/components/duotecno/entity.py @@ -1,8 +1,13 @@ """Support for Velbus devices.""" from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps +from typing import Any, Concatenate, ParamSpec, TypeVar + from duotecno.unit import BaseUnit +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity @@ -35,3 +40,25 @@ class DuotecnoEntity(Entity): async def _on_update(self) -> None: """When a unit has an update.""" self.async_write_ha_state() + + +_T = TypeVar("_T", bound="DuotecnoEntity") +_P = ParamSpec("_P") + + +def api_call( + func: Callable[Concatenate[_T, _P], Awaitable[None]] +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: + """Catch command exceptions.""" + + @wraps(func) + async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Wrap all command methods.""" + try: + await func(self, *args, **kwargs) + except OSError as exc: + raise HomeAssistantError( + f"Error calling {func.__name__} on entity {self.entity_id}" + ) from exc + + return cmd_wrapper diff --git a/homeassistant/components/duotecno/light.py b/homeassistant/components/duotecno/light.py index da288b6cbe0..9aee4513fca 100644 --- a/homeassistant/components/duotecno/light.py +++ b/homeassistant/components/duotecno/light.py @@ -6,11 +6,10 @@ from duotecno.unit import DimUnit 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 from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import DuotecnoEntity +from .entity import DuotecnoEntity, api_call async def async_setup_entry( @@ -40,6 +39,7 @@ class DuotecnoLight(DuotecnoEntity, LightEntity): """Return the brightness of the light.""" return int((self._unit.get_dimmer_state() * 255) / 100) + @api_call async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" if (val := kwargs.get(ATTR_BRIGHTNESS)) is not None: @@ -48,18 +48,9 @@ class DuotecnoLight(DuotecnoEntity, LightEntity): else: # restore state val = None - try: - await self._unit.set_dimmer_state(val) - except OSError as err: - raise HomeAssistantError( - "Transmit for the set_dimmer_state packet failed" - ) from err + await self._unit.set_dimmer_state(val) + @api_call async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" - try: - await self._unit.set_dimmer_state(0) - except OSError as err: - raise HomeAssistantError( - "Transmit for the set_dimmer_state packet failed" - ) from err + await self._unit.set_dimmer_state(0) diff --git a/homeassistant/components/duotecno/switch.py b/homeassistant/components/duotecno/switch.py index a9921de85d3..63bab750543 100644 --- a/homeassistant/components/duotecno/switch.py +++ b/homeassistant/components/duotecno/switch.py @@ -6,11 +6,10 @@ from duotecno.unit import SwitchUnit from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import DuotecnoEntity +from .entity import DuotecnoEntity, api_call async def async_setup_entry( @@ -35,16 +34,12 @@ class DuotecnoSwitch(DuotecnoEntity, SwitchEntity): """Return true if the switch is on.""" return self._unit.is_on() + @api_call async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the switch to turn on.""" - try: - await self._unit.turn_on() - except OSError as err: - raise HomeAssistantError("Transmit for the turn_on packet failed") from err + await self._unit.turn_on() + @api_call async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the switch to turn off.""" - try: - await self._unit.turn_off() - except OSError as err: - raise HomeAssistantError("Transmit for the turn_off packet failed") from err + await self._unit.turn_off() From 384cee481a73c25ae33cda93aa7df2bcaaebc3b6 Mon Sep 17 00:00:00 2001 From: andresb5555 Date: Sat, 19 Aug 2023 22:38:03 +0300 Subject: [PATCH 0648/1151] Add vicare sensor gas_summary_consumption_heating_lastsevendays (#95280) * Add missing sensor entity gas_summary_consumption_heating_lastsevendays * Changed location of the sensor gas_summary_consumption_heating_lastsevendays code as per suggestion by joostlek --- homeassistant/components/vicare/sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index a4b9e9d7f92..24f23b0da0a 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -228,6 +228,14 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, ), + ViCareSensorEntityDescription( + key="gas_summary_consumption_heating_lastsevendays", + name="Heating gas consumption last seven days", + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + value_getter=lambda api: api.getGasSummaryConsumptionHeatingLastSevenDays(), + unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), ViCareSensorEntityDescription( key="hotwater_gas_summary_consumption_heating_currentday", name="Hot water gas consumption current day", From 3fc043d99e5840d3c3df408e74059c1100abe088 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 20 Aug 2023 09:04:48 +0200 Subject: [PATCH 0649/1151] Deduplicate Tasmota sensor tests (#98628) --- tests/components/tasmota/test_sensor.py | 431 +++++++++++------------- 1 file changed, 206 insertions(+), 225 deletions(-) diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 6a896615c73..22ee652aef3 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -38,7 +38,7 @@ from .test_common import ( from tests.common import async_fire_mqtt_message, async_fire_time_changed from tests.typing import MqttMockHAClient, MqttMockPahoClient -BAD_INDEXED_SENSOR_CONFIG_3 = { +BAD_LIST_SENSOR_CONFIG_3 = { "sn": { "Time": "2020-09-25T12:47:15", "ENERGY": { @@ -47,7 +47,9 @@ BAD_INDEXED_SENSOR_CONFIG_3 = { } } -INDEXED_SENSOR_CONFIG = { +# This configuration has some sensors where values are lists +# Home Assistant maps this to one sensor for each list item +LIST_SENSOR_CONFIG = { "sn": { "Time": "2020-09-25T12:47:15", "ENERGY": { @@ -74,7 +76,8 @@ INDEXED_SENSOR_CONFIG = { } } -INDEXED_SENSOR_CONFIG_2 = { +# Same as LIST_SENSOR_CONFIG, but Total is also a list +LIST_SENSOR_CONFIG_2 = { "sn": { "Time": "2020-09-25T12:47:15", "ENERGY": { @@ -101,8 +104,9 @@ INDEXED_SENSOR_CONFIG_2 = { } } - -NESTED_SENSOR_CONFIG_1 = { +# This configuration has some sensors where values are dicts +# Home Assistant maps this to one sensor for each dictionary item +DICT_SENSOR_CONFIG_1 = { "sn": { "Time": "2020-03-03T00:00:00+00:00", "TX23": { @@ -119,7 +123,22 @@ NESTED_SENSOR_CONFIG_1 = { } } -NESTED_SENSOR_CONFIG_2 = { +# Similar to LIST_SENSOR_CONFIG, but Total is a dict +DICT_SENSOR_CONFIG_2 = { + "sn": { + "Time": "2023-01-27T11:04:56", + "ENERGY": { + "Total": { + "Phase1": 0.017, + "Phase2": 0.017, + }, + "TotalStartTime": "2018-11-23T15:33:47", + }, + } +} + + +TEMPERATURE_SENSOR_CONFIG = { "sn": { "Time": "2023-01-27T11:04:56", "DS18B20": { @@ -131,65 +150,33 @@ NESTED_SENSOR_CONFIG_2 = { } -async def test_controlling_state_via_mqtt( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota -) -> None: - """Test state update via MQTT.""" - config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG) - mac = config["mac"] - - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/config", - json.dumps(config), - ) - await hass.async_block_till_done() - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/sensors", - json.dumps(sensor_config), - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.tasmota_dht11_temperature") - assert state.state == "unavailable" - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - entity_reg = er.async_get(hass) - entry = entity_reg.async_get("sensor.tasmota_dht11_temperature") - assert entry.disabled is False - assert entry.disabled_by is None - assert entry.entity_category is None - - async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") - await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_dht11_temperature") - assert state.state == STATE_UNKNOWN - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - # Test periodic state update - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/SENSOR", '{"DHT11":{"Temperature":20.5}}' - ) - state = hass.states.get("sensor.tasmota_dht11_temperature") - assert state.state == "20.5" - - # Test polled state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"DHT11":{"Temperature":20.0}}}', - ) - state = hass.states.get("sensor.tasmota_dht11_temperature") - assert state.state == "20.0" - - @pytest.mark.parametrize( ("sensor_config", "entity_ids", "messages", "states"), [ ( - NESTED_SENSOR_CONFIG_1, + DEFAULT_SENSOR_CONFIG, + ["sensor.tasmota_dht11_temperature"], + ( + '{"DHT11":{"Temperature":20.5}}', + '{"StatusSNS":{"DHT11":{"Temperature":20.0}}}', + ), + ( + { + "sensor.tasmota_dht11_temperature": { + "state": "20.5", + "attributes": { + "device_class": "temperature", + "unit_of_measurement": "°C", + }, + }, + }, + { + "sensor.tasmota_dht11_temperature": {"state": "20.0"}, + }, + ), + ), + ( + DICT_SENSOR_CONFIG_1, ["sensor.tasmota_tx23_speed_act", "sensor.tasmota_tx23_dir_card"], ( '{"TX23":{"Speed":{"Act":"12.3"},"Dir": {"Card": "WSW"}}}', @@ -197,17 +184,50 @@ async def test_controlling_state_via_mqtt( ), ( { - "sensor.tasmota_tx23_speed_act": "12.3", - "sensor.tasmota_tx23_dir_card": "WSW", + "sensor.tasmota_tx23_speed_act": { + "state": "12.3", + "attributes": { + "device_class": None, + "unit_of_measurement": "km/h", + }, + }, + "sensor.tasmota_tx23_dir_card": {"state": "WSW"}, }, { - "sensor.tasmota_tx23_speed_act": "23.4", - "sensor.tasmota_tx23_dir_card": "ESE", + "sensor.tasmota_tx23_speed_act": {"state": "23.4"}, + "sensor.tasmota_tx23_dir_card": {"state": "ESE"}, }, ), ), ( - NESTED_SENSOR_CONFIG_2, + LIST_SENSOR_CONFIG, + [ + "sensor.tasmota_energy_totaltariff_0", + "sensor.tasmota_energy_totaltariff_1", + ], + ( + '{"ENERGY":{"TotalTariff":[1.2,3.4]}}', + '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', + ), + ( + { + "sensor.tasmota_energy_totaltariff_0": { + "state": "1.2", + "attributes": { + "device_class": None, + "unit_of_measurement": None, + }, + }, + "sensor.tasmota_energy_totaltariff_1": {"state": "3.4"}, + }, + { + "sensor.tasmota_energy_totaltariff_0": {"state": "5.6"}, + "sensor.tasmota_energy_totaltariff_1": {"state": "7.8"}, + }, + ), + ), + ( + TEMPERATURE_SENSOR_CONFIG, ["sensor.tasmota_ds18b20_temperature", "sensor.tasmota_ds18b20_id"], ( '{"DS18B20":{"Id": "01191ED79190","Temperature": 12.3}}', @@ -215,18 +235,117 @@ async def test_controlling_state_via_mqtt( ), ( { - "sensor.tasmota_ds18b20_temperature": "12.3", - "sensor.tasmota_ds18b20_id": "01191ED79190", + "sensor.tasmota_ds18b20_temperature": { + "state": "12.3", + "attributes": { + "device_class": "temperature", + "unit_of_measurement": "°C", + }, + }, + "sensor.tasmota_ds18b20_id": {"state": "01191ED79190"}, }, { - "sensor.tasmota_ds18b20_temperature": "23.4", - "sensor.tasmota_ds18b20_id": "meep", + "sensor.tasmota_ds18b20_temperature": {"state": "23.4"}, + "sensor.tasmota_ds18b20_id": {"state": "meep"}, + }, + ), + ), + # Test simple Total sensor + ( + LIST_SENSOR_CONFIG, + ["sensor.tasmota_energy_total"], + ( + '{"ENERGY":{"Total":1.2,"TotalStartTime":"2018-11-23T15:33:47"}}', + '{"StatusSNS":{"ENERGY":{"Total":5.6,"TotalStartTime":"2018-11-23T16:33:47"}}}', + ), + ( + { + "sensor.tasmota_energy_total": { + "state": "1.2", + "attributes": { + "device_class": "energy", + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + "unit_of_measurement": "kWh", + }, + }, + }, + { + "sensor.tasmota_energy_total": {"state": "5.6"}, + }, + ), + ), + # Test list Total sensors + ( + LIST_SENSOR_CONFIG_2, + ["sensor.tasmota_energy_total_0", "sensor.tasmota_energy_total_1"], + ( + '{"ENERGY":{"Total":[1.2, 3.4],"TotalStartTime":"2018-11-23T15:33:47"}}', + '{"StatusSNS":{"ENERGY":{"Total":[5.6, 7.8],"TotalStartTime":"2018-11-23T16:33:47"}}}', + ), + ( + { + "sensor.tasmota_energy_total_0": { + "state": "1.2", + "attributes": { + "device_class": "energy", + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + "unit_of_measurement": "kWh", + }, + }, + "sensor.tasmota_energy_total_1": { + "state": "3.4", + "attributes": { + "device_class": "energy", + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + "unit_of_measurement": "kWh", + }, + }, + }, + { + "sensor.tasmota_energy_total_0": {"state": "5.6"}, + "sensor.tasmota_energy_total_1": {"state": "7.8"}, + }, + ), + ), + # Test dict Total sensors + ( + DICT_SENSOR_CONFIG_2, + [ + "sensor.tasmota_energy_total_phase1", + "sensor.tasmota_energy_total_phase2", + ], + ( + '{"ENERGY":{"Total":{"Phase1":1.2, "Phase2":3.4},"TotalStartTime":"2018-11-23T15:33:47"}}', + '{"StatusSNS":{"ENERGY":{"Total":{"Phase1":5.6, "Phase2":7.8},"TotalStartTime":"2018-11-23T15:33:47"}}}', + ), + ( + { + "sensor.tasmota_energy_total_phase1": { + "state": "1.2", + "attributes": { + "device_class": "energy", + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + "unit_of_measurement": "kWh", + }, + }, + "sensor.tasmota_energy_total_phase2": { + "state": "3.4", + "attributes": { + "device_class": "energy", + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + "unit_of_measurement": "kWh", + }, + }, + }, + { + "sensor.tasmota_energy_total_phase1": {"state": "5.6"}, + "sensor.tasmota_energy_total_phase2": {"state": "7.8"}, }, ), ), ], ) -async def test_nested_sensor_state_via_mqtt( +async def test_controlling_state_via_mqtt( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota, @@ -236,6 +355,7 @@ async def test_nested_sensor_state_via_mqtt( states, ) -> None: """Test state update via MQTT.""" + entity_reg = er.async_get(hass) config = copy.deepcopy(DEFAULT_CONFIG) sensor_config = copy.deepcopy(sensor_config) mac = config["mac"] @@ -258,6 +378,11 @@ async def test_nested_sensor_state_via_mqtt( assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + entry = entity_reg.async_get(entity_id) + assert entry.disabled is False + assert entry.disabled_by is None + assert entry.entity_category is None + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() for entity_id in entity_ids: @@ -269,163 +394,19 @@ async def test_nested_sensor_state_via_mqtt( async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/SENSOR", messages[0]) for entity_id in entity_ids: state = hass.states.get(entity_id) - assert state.state == states[0][entity_id] + expected_state = states[0][entity_id] + assert state.state == expected_state["state"] + for attribute, expected in expected_state.get("attributes", {}).items(): + assert state.attributes.get(attribute) == expected # Test polled state update async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/STATUS10", messages[1]) for entity_id in entity_ids: state = hass.states.get(entity_id) - assert state.state == states[1][entity_id] - - -async def test_indexed_sensor_state_via_mqtt( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota -) -> None: - """Test state update via MQTT.""" - config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(INDEXED_SENSOR_CONFIG) - mac = config["mac"] - - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/config", - json.dumps(config), - ) - await hass.async_block_till_done() - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/sensors", - json.dumps(sensor_config), - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.tasmota_energy_totaltariff_1") - assert state.state == "unavailable" - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") - await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_energy_totaltariff_1") - assert state.state == STATE_UNKNOWN - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - # Test periodic state update - async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/SENSOR", '{"ENERGY":{"TotalTariff":[1.2,3.4]}}' - ) - state = hass.states.get("sensor.tasmota_energy_totaltariff_1") - assert state.state == "3.4" - - # Test polled state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"ENERGY":{"TotalTariff":[5.6,7.8]}}}', - ) - state = hass.states.get("sensor.tasmota_energy_totaltariff_1") - assert state.state == "7.8" - - -async def test_indexed_sensor_state_via_mqtt2( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota -) -> None: - """Test state update via MQTT for sensor with last_reset property.""" - config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(INDEXED_SENSOR_CONFIG) - mac = config["mac"] - - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/config", - json.dumps(config), - ) - await hass.async_block_till_done() - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/sensors", - json.dumps(sensor_config), - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.tasmota_energy_total") - assert state.state == "unavailable" - assert not state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - - async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") - await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_energy_total") - assert state.state == STATE_UNKNOWN - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - # Test periodic state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/tele/SENSOR", - '{"ENERGY":{"Total":1.2,"TotalStartTime":"2018-11-23T15:33:47"}}', - ) - state = hass.states.get("sensor.tasmota_energy_total") - assert state.state == "1.2" - - # Test polled state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"ENERGY":{"Total":5.6,"TotalStartTime":"2018-11-23T16:33:47"}}}', - ) - state = hass.states.get("sensor.tasmota_energy_total") - assert state.state == "5.6" - - -async def test_indexed_sensor_state_via_mqtt3( - hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota -) -> None: - """Test state update via MQTT for indexed sensor with last_reset property.""" - config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(INDEXED_SENSOR_CONFIG_2) - mac = config["mac"] - - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/config", - json.dumps(config), - ) - await hass.async_block_till_done() - async_fire_mqtt_message( - hass, - f"{DEFAULT_PREFIX}/{mac}/sensors", - json.dumps(sensor_config), - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.tasmota_energy_total_1") - assert state.state == "unavailable" - assert not state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - - async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") - await hass.async_block_till_done() - state = hass.states.get("sensor.tasmota_energy_total_1") - assert state.state == STATE_UNKNOWN - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - # Test periodic state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/tele/SENSOR", - '{"ENERGY":{"Total":[1.2, 3.4],"TotalStartTime":"2018-11-23T15:33:47"}}', - ) - state = hass.states.get("sensor.tasmota_energy_total_1") - assert state.state == "3.4" - - # Test polled state update - async_fire_mqtt_message( - hass, - "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"ENERGY":{"Total":[5.6,7.8],"TotalStartTime":"2018-11-23T16:33:47"}}}', - ) - state = hass.states.get("sensor.tasmota_energy_total_1") - assert state.state == "7.8" + expected_state = states[1][entity_id] + assert state.state == expected_state["state"] + for attribute, expected in expected_state.get("attributes", {}).items(): + assert state.attributes.get(attribute) == expected async def test_bad_indexed_sensor_state_via_mqtt( @@ -433,7 +414,7 @@ async def test_bad_indexed_sensor_state_via_mqtt( ) -> None: """Test state update via MQTT where sensor is not matching configuration.""" config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(BAD_INDEXED_SENSOR_CONFIG_3) + sensor_config = copy.deepcopy(BAD_LIST_SENSOR_CONFIG_3) mac = config["mac"] async_fire_mqtt_message( @@ -784,7 +765,7 @@ async def test_nested_sensor_attributes( ) -> None: """Test correct attributes for sensors.""" config = copy.deepcopy(DEFAULT_CONFIG) - sensor_config = copy.deepcopy(NESTED_SENSOR_CONFIG_1) + sensor_config = copy.deepcopy(DICT_SENSOR_CONFIG_1) mac = config["mac"] async_fire_mqtt_message( From e484066f2bf394f1069a3bfcee2890ba02bc8b16 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 20 Aug 2023 10:17:28 +0200 Subject: [PATCH 0650/1151] Remove dead code from __main__.py (#98694) --- homeassistant/__main__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index f7ba18d3d75..9e4afa018a6 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -148,16 +148,6 @@ def get_arguments() -> argparse.Namespace: return arguments -def cmdline() -> list[str]: - """Collect path and arguments to re-execute the current hass instance.""" - if os.path.basename(sys.argv[0]) == "__main__.py": - modulepath = os.path.dirname(sys.argv[0]) - os.environ["PYTHONPATH"] = os.path.dirname(modulepath) - return [sys.executable, "-m", "homeassistant"] + list(sys.argv[1:]) - - return sys.argv - - def check_threads() -> None: """Check if there are any lingering threads.""" try: From 614904512cda52a674fcc6cf70a9de56c6e163c1 Mon Sep 17 00:00:00 2001 From: Niels Perfors Date: Sun, 20 Aug 2023 13:45:58 +0200 Subject: [PATCH 0651/1151] Verisure Improve Unpack (#98696) --- .../components/verisure/coordinator.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index bbfaed0a0a4..3779af4fd16 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -83,17 +83,15 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed("Could not read overview") from err def unpack(overview: list, value: str) -> dict | list: - return ( - next( - ( - item["data"]["installation"][value] - for item in overview - if value in item.get("data", {}).get("installation", {}) - ), - [], - ) - or [] + unpacked: dict | list | None = next( + ( + item["data"]["installation"][value] + for item in overview + if value in item.get("data", {}).get("installation", {}) + ), + None, ) + return unpacked or [] # Store data in a way Home Assistant can easily consume it self._overview = overview From f07724ff52a8b226619ded35441e64073bc15130 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 20 Aug 2023 14:02:53 +0200 Subject: [PATCH 0652/1151] Add additional tasmota tests (#98695) --- tests/components/tasmota/test_cover.py | 1 + tests/components/tasmota/test_sensor.py | 42 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index 5c1364f1f77..e2bdc8b2ca7 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -55,6 +55,7 @@ async def test_missing_relay( @pytest.mark.parametrize( ("relay_config", "num_covers"), [ + ([3, 3, 3, 3, 3, 3, 1, 1, 3, 3] + [3, 3] * 12, 16), ([3, 3, 3, 3, 3, 3, 1, 1, 3, 3], 4), ([3, 3, 3, 3, 0, 0, 0, 0], 2), ([3, 3, 1, 1, 0, 0, 0, 0], 1), diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 22ee652aef3..4e79b8ad0d5 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -585,6 +585,48 @@ async def test_status_sensor_state_via_mqtt( assert not entity.force_update +@pytest.mark.parametrize("status_sensor_disabled", [False]) +async def test_battery_sensor_state_via_mqtt( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota +) -> None: + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["bat"] = 1 # BatteryPercentage feature enabled + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get("sensor.tasmota_battery_level") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + await hass.async_block_till_done() + state = hass.states.get("sensor.tasmota_battery_level") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Test pushed state update + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"BatteryPercentage":55}' + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.tasmota_battery_level") + assert state.state == "55" + assert state.attributes == { + "device_class": "battery", + "friendly_name": "Tasmota Battery Level", + "state_class": "measurement", + "unit_of_measurement": "%", + } + + @pytest.mark.parametrize("status_sensor_disabled", [False]) async def test_single_shot_status_sensor_state_via_mqtt( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota From 38af44225ebed6cf2a87028e6e8e4f0cd4ae01b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Aug 2023 07:49:33 -0500 Subject: [PATCH 0653/1151] Refactor doorbird to avoid using events internally (#98585) --- .coveragerc | 1 + homeassistant/components/doorbird/__init__.py | 64 ++----------------- homeassistant/components/doorbird/camera.py | 3 +- homeassistant/components/doorbird/device.py | 17 +++++ homeassistant/components/doorbird/util.py | 4 +- homeassistant/components/doorbird/view.py | 58 +++++++++++++++++ 6 files changed, 85 insertions(+), 62 deletions(-) create mode 100644 homeassistant/components/doorbird/view.py diff --git a/.coveragerc b/.coveragerc index 02b0cf7a143..58aa8eb52c3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -221,6 +221,7 @@ omit = homeassistant/components/doorbird/device.py homeassistant/components/doorbird/entity.py homeassistant/components/doorbird/util.py + homeassistant/components/doorbird/view.py homeassistant/components/dormakaba_dkey/__init__.py homeassistant/components/dormakaba_dkey/binary_sensor.py homeassistant/components/dormakaba_dkey/entity.py diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index d1ad91bbb2c..bf5fdeb1f60 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -5,13 +5,11 @@ from http import HTTPStatus import logging from typing import Any -from aiohttp import web from doorbirdpy import DoorBird import requests import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -20,21 +18,20 @@ from homeassistant.const import ( CONF_TOKEN, CONF_USERNAME, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import API_URL, CONF_EVENTS, DOMAIN, PLATFORMS +from .const import CONF_EVENTS, DOMAIN, PLATFORMS from .device import ConfiguredDoorBird from .models import DoorBirdData -from .util import get_door_station_by_token +from .view import DoorBirdRequestView _LOGGER = logging.getLogger(__name__) CONF_CUSTOM_URL = "hass_url_override" -RESET_DEVICE_FAVORITES = "doorbird_reset_favorites" DEVICE_SCHEMA = vol.Schema( { @@ -54,29 +51,8 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the DoorBird component.""" hass.data.setdefault(DOMAIN, {}) - # Provide an endpoint for the door stations to call to trigger events hass.http.register_view(DoorBirdRequestView) - - def _reset_device_favorites_handler(event: Event) -> None: - """Handle clearing favorites on device.""" - if (token := event.data.get("token")) is None: - return - - door_station = get_door_station_by_token(hass, token) - - if door_station is None: - _LOGGER.error("Device not found for provided token") - return - - # Clear webhooks - favorites: dict[str, list[str]] = door_station.device.favorites() - for favorite_type, favorite_ids in favorites.items(): - for favorite_id in favorite_ids: - door_station.device.delete_favorite(favorite_type, favorite_id) - - hass.bus.async_listen(RESET_DEVICE_FAVORITES, _reset_device_favorites_handler) - return True @@ -150,6 +126,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_register_events( hass: HomeAssistant, door_station: ConfiguredDoorBird ) -> bool: + """Register events on device.""" try: await hass.async_add_executor_job(door_station.register_events, hass) except requests.exceptions.HTTPError: @@ -190,36 +167,3 @@ def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: Confi if modified: hass.config_entries.async_update_entry(entry, options=options) - - -class DoorBirdRequestView(HomeAssistantView): - """Provide a page for the device to call.""" - - requires_auth = False - url = API_URL - name = API_URL[1:].replace("/", ":") - extra_urls = [API_URL + "/{event}"] - - async def get(self, request: web.Request, event: str) -> web.Response: - """Respond to requests from the device.""" - hass: HomeAssistant = request.app["hass"] - token: str | None = request.query.get("token") - if token is None or (device := get_door_station_by_token(hass, token)) is None: - return web.Response( - status=HTTPStatus.UNAUTHORIZED, text="Invalid token provided." - ) - - if device: - event_data = device.get_event_data(event) - else: - event_data = {} - - if event == "clear": - hass.bus.async_fire(RESET_DEVICE_FAVORITES, {"token": token}) - - message = f"HTTP Favorites cleared for {device.slug}" - return web.Response(text=message) - - hass.bus.async_fire(f"{DOMAIN}_{event}", event_data) - - return web.Response(text="OK") diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 06bdb494463..a29272168d4 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -128,5 +128,6 @@ class DoorBirdCamera(DoorBirdEntity, Camera): """Unsubscribe from events.""" event_to_entity_id = self._door_bird_data.event_entity_ids for event in self._door_station.events: - del event_to_entity_id[event] + # If the clear api was called, the events may not be in the dict + event_to_entity_id.pop(event, None) await super().async_will_remove_from_hass() diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index aced0d8723f..3a50700fa37 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -145,3 +145,20 @@ class ConfiguredDoorBird: "html5_viewer_url": self._device.html5_viewer_url, ATTR_ENTITY_ID: self._event_entity_ids.get(event), } + + +async def async_reset_device_favorites( + hass: HomeAssistant, door_station: ConfiguredDoorBird +) -> None: + """Handle clearing favorites on device.""" + await hass.async_add_executor_job(_reset_device_favorites, door_station) + + +def _reset_device_favorites(door_station: ConfiguredDoorBird) -> None: + """Handle clearing favorites on device.""" + # Clear webhooks + door_bird = door_station.device + favorites: dict[str, list[str]] = door_bird.favorites() + for favorite_type, favorite_ids in favorites.items(): + for favorite_id in favorite_ids: + door_bird.delete_favorite(favorite_type, favorite_id) diff --git a/homeassistant/components/doorbird/util.py b/homeassistant/components/doorbird/util.py index 52c1417a67c..b3b62a4985a 100644 --- a/homeassistant/components/doorbird/util.py +++ b/homeassistant/components/doorbird/util.py @@ -1,5 +1,7 @@ """DoorBird integration utils.""" +from typing import Any + from homeassistant.core import HomeAssistant from .const import DOMAIN @@ -7,7 +9,7 @@ from .device import ConfiguredDoorBird from .models import DoorBirdData -def get_mac_address_from_door_station_info(door_station_info): +def get_mac_address_from_door_station_info(door_station_info: dict[str, Any]) -> str: """Get the mac address depending on the device type.""" return door_station_info.get("PRIMARY_MAC_ADDR", door_station_info["WIFI_MAC_ADDR"]) diff --git a/homeassistant/components/doorbird/view.py b/homeassistant/components/doorbird/view.py new file mode 100644 index 00000000000..fca72d36fc1 --- /dev/null +++ b/homeassistant/components/doorbird/view.py @@ -0,0 +1,58 @@ +"""Support for DoorBird devices.""" +from __future__ import annotations + +from http import HTTPStatus +import logging + +from aiohttp import web + +from homeassistant.components.http import HomeAssistantView +from homeassistant.core import HomeAssistant + +from .const import API_URL, DOMAIN +from .device import async_reset_device_favorites +from .util import get_door_station_by_token + +_LOGGER = logging.getLogger(__name__) + + +class DoorBirdRequestView(HomeAssistantView): + """Provide a page for the device to call.""" + + requires_auth = False + url = API_URL + name = API_URL[1:].replace("/", ":") + extra_urls = [API_URL + "/{event}"] + + async def get(self, request: web.Request, event: str) -> web.Response: + """Respond to requests from the device.""" + hass: HomeAssistant = request.app["hass"] + token: str | None = request.query.get("token") + if ( + token is None + or (door_station := get_door_station_by_token(hass, token)) is None + ): + return web.Response( + status=HTTPStatus.UNAUTHORIZED, text="Invalid token provided." + ) + + if door_station: + event_data = door_station.get_event_data(event) + else: + event_data = {} + + if event == "clear": + await async_reset_device_favorites(hass, door_station) + message = f"HTTP Favorites cleared for {door_station.slug}" + return web.Response(text=message) + + # + # This integration uses a multiple different events. + # It would be a major breaking change to change this to + # a single event at this point. + # + # Do not copy this pattern in the future + # for any new integrations. + # + hass.bus.async_fire(f"{DOMAIN}_{event}", event_data) + return web.Response(text="OK") From 4c3640878fdb75d884b953b587a9ca5140309718 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 20 Aug 2023 18:30:28 +0200 Subject: [PATCH 0654/1151] Filter some pytest warnings (#98689) --- pyproject.toml | 91 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f8753e34680..fcc47ed2c31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -429,7 +429,96 @@ norecursedirs = [ log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" log_date_format = "%Y-%m-%d %H:%M:%S" asyncio_mode = "auto" -filterwarnings = ["error::sqlalchemy.exc.SAWarning"] +filterwarnings = [ + "error::sqlalchemy.exc.SAWarning", + + # -- HomeAssistant - aiohttp + # Overwrite web.Application to pass a custom default argument to _make_request + "ignore:Inheritance class HomeAssistantApplication from web.Application is discouraged:DeprecationWarning", + # Hass wraps `ClientSession.close` to emit a warning if the session is closed accidentally + "ignore:Setting custom ClientSession.close attribute is discouraged:DeprecationWarning:homeassistant.helpers.aiohttp_client", + # Modify app state for testing + "ignore:Changing state of started or joined application is deprecated:DeprecationWarning:tests.components.http.test_ban", + + # -- Tests + # Ignore custom pytest marks + "ignore:Unknown pytest.mark.disable_autouse_fixture:pytest.PytestUnknownMarkWarning:tests.components.met", + + # -- design choice 3rd party + # https://github.com/gwww/elkm1/blob/2.2.5/elkm1_lib/util.py#L8-L19 + "ignore:ssl.TLSVersion.TLSv1 is deprecated:DeprecationWarning:elkm1_lib.util", + # https://github.com/michaeldavie/env_canada/blob/v0.5.36/env_canada/ec_cache.py + "ignore:Inheritance class CacheClientSession from ClientSession is discouraged:DeprecationWarning:env_canada.ec_cache", + # https://github.com/bachya/regenmaschine/blob/2023.08.0/regenmaschine/client.py#L51 + "ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client", + + # -- Setuptools DeprecationWarnings + # https://github.com/googleapis/google-cloud-python/issues/11184 + # https://github.com/zopefoundation/meta/issues/194 + "ignore:Deprecated call to `pkg_resources.declare_namespace\\(('google.*'|'pywinusb'|'repoze'|'xbox'|'zope')\\)`:DeprecationWarning:pkg_resources", + "ignore:Deprecated call to `pkg_resources.declare_namespace\\('google.*'\\)`:DeprecationWarning:google.rpc", + + # -- tracked upstream / open PRs + # https://github.com/caronc/apprise/issues/659 - v1.4.5 + "ignore:Use setlocale\\(\\), getencoding\\(\\) and getlocale\\(\\) instead:DeprecationWarning:apprise.AppriseLocal", + # https://github.com/beetbox/mediafile/issues/67 - v0.12.0 + "ignore:'imghdr' is deprecated and slated for removal in Python 3.13:DeprecationWarning:mediafile", + # https://github.com/eclipse/paho.mqtt.python/issues/653 - v1.6.1 + # https://github.com/eclipse/paho.mqtt.python/pull/665 + "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:paho.mqtt.client", + # https://github.com/PythonCharmers/python-future/issues/488 - v0.18.3 + "ignore:the imp module is deprecated in favour of importlib and slated for removal in Python 3.12:DeprecationWarning:future.standard_library", + # https://github.com/foxel/python_ndms2_client/issues/6 - v0.1.2 + "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:ndms2_client.connection", + # https://github.com/grahamwetzler/smart-meter-texas/pull/143 - v0.5.3 + "ignore:ssl.OP_NO_SSL\\*/ssl.OP_NO_TLS\\* options are deprecated:DeprecationWarning:smart_meter_texas", + # https://github.com/pytest-dev/pytest-cov/issues/557 - v4.1.0 + # Should resolve itself once pytest-xdist 4.0 is released and the option is removed + "ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated:DeprecationWarning:xdist.plugin", + + # -- fixed, waiting for release / update + # https://github.com/gurumitts/pylutron-caseta/pull/143 - >0.18.1 + "ignore:ssl.PROTOCOL_TLSv1_2 is deprecated:DeprecationWarning:pylutron_caseta.smartbridge", + # https://github.com/Danielhiversen/pyMillLocal/pull/8 - >=0.3.0 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:mill_local", + # https://github.com/home-assistant/core/pull/98619 - update botocore to >=1.31.17 + "ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning:botocore.utils", + "ignore:'urllib3.contrib.pyopenssl' module is deprecated and will be removed in a future release of urllib3 2.x:DeprecationWarning:botocore.httpsession", + + # -- not helpful + # pyatmo.__init__ imports deprecated moduls from itself - v7.5.0 + "ignore:The module pyatmo.* is deprecated:DeprecationWarning:pyatmo", + + # -- unmaintained projects, last release about 2+ years + # https://pypi.org/project/agent-py/ - v0.0.23 - 2020-06-04 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:agent.a", + # https://pypi.org/project/aiomodernforms/ - v0.1.8 - 2021-06-27 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:aiomodernforms.modernforms", + # https://pypi.org/project/directv/ - v0.4.0 - 2020-09-12 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:directv.directv", + # https://pypi.org/project/emulated-roku/ - v0.2.1 - 2020-01-23 (archived) + "ignore:loop argument is deprecated:DeprecationWarning:emulated_roku", + # https://pypi.org/project/foobot_async/ - v1.0.0 - 2020-11-24 + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:foobot_async", + # https://pypi.org/project/lark-parser/ - v0.12.0 - 2021-08-30 -> moved to `lark` + # https://pypi.org/project/commentjson/ - v0.9.0 - 2020-10-05 + # https://github.com/vaidik/commentjson/issues/51 + # Fixed upstream, commentjson depends on old version and seems to be unmaintained + "ignore:module '(sre_parse|sre_constants)' is deprecate:DeprecationWarning:lark.utils", + # https://pypi.org/project/lomond/ - v0.3.3 - 2018-09-21 + "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:lomond.session", + # https://pypi.org/project/passlib/ - v1.7.4 - 2020-10-08 + "ignore:'crypt' is deprecated and slated for removal in Python 3.13:DeprecationWarning:passlib.utils", + # https://pypi.org/project/pyqwikswitch/ - v0.94 - 2019-08-19 + "ignore:client.loop property is deprecated:DeprecationWarning:pyqwikswitch.async_", + "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:pyqwikswitch.async_", + # https://pypi.org/project/rxv/ - v0.7.0 - 2021-10-10 + "ignore:defusedxml.cElementTree is deprecated, import from defusedxml.ElementTree instead:DeprecationWarning:rxv.ssdp", + # https://pypi.org/project/vilfo-api-client/ - v0.4.1 - 2021-11-06 + "ignore:Function 'semver.compare' is deprecated. Deprecated since version 3.0.0:PendingDeprecationWarning:.*vilfo.client", + # https://pypi.org/project/webrtcvad/ - v2.0.10 - 2017-01-08 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:webrtcvad", +] [tool.ruff] target-version = "py310" From c6c9d235303a21c1c533c52772f530fd9fb86bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 20 Aug 2023 23:09:36 +0300 Subject: [PATCH 0655/1151] Remove no longer used nest binary sensor (#98714) --- homeassistant/components/nest/binary_sensor.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 homeassistant/components/nest/binary_sensor.py diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py deleted file mode 100644 index 6d9331744ef..00000000000 --- a/homeassistant/components/nest/binary_sensor.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Support for Nest binary sensors that dispatches between API versions.""" - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_SDM -from .legacy.binary_sensor import async_setup_legacy_entry - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up the binary sensors.""" - assert DATA_SDM not in entry.data - await async_setup_legacy_entry(hass, entry, async_add_entities) From 53b596101bb41dadb518bad2083683aa8a018f70 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 20 Aug 2023 13:29:16 -0700 Subject: [PATCH 0656/1151] Bump opower to 0.0.31 (#98716) --- 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 58d642ef9a1..aff1ad2f599 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.0.30"] + "requirements": ["opower==0.0.31"] } diff --git a/requirements_all.txt b/requirements_all.txt index ccd6fff254a..f0a8d957c34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1368,7 +1368,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.30 +opower==0.0.31 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee6a47b2405..17f2e3f88e9 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.30 +opower==0.0.31 # homeassistant.components.oralb oralb-ble==0.17.6 From a29e4a5f02074813f5f3a1bf2ba60f6a7ff23665 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 20 Aug 2023 22:30:43 +0200 Subject: [PATCH 0657/1151] Move Workday failures to __init__ (#98651) Workday failures in init --- homeassistant/components/workday/__init__.py | 15 ++++++++++++++- homeassistant/components/workday/binary_sensor.py | 8 -------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index d1ad8456bab..d8d31451567 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -1,15 +1,28 @@ """Sensor to indicate whether the current day is a workday.""" from __future__ import annotations +from holidays import list_supported_countries + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError -from .const import PLATFORMS +from .const import CONF_COUNTRY, CONF_PROVINCE, LOGGER, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Workday from a config entry.""" + country: str = entry.options[CONF_COUNTRY] + province: str | None = entry.options.get(CONF_PROVINCE) + if country and country not in list_supported_countries(): + LOGGER.error("There is no country %s", country) + raise ConfigEntryError("Selected country is not valid") + + if province and province not in list_supported_countries()[country]: + LOGGER.error("There is no subdivision %s in country %s", province, country) + raise ConfigEntryError("Selected province is not valid") + entry.async_on_unload(entry.add_update_listener(async_update_listener)) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 4c383543125..6b6dfbffa5d 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -129,14 +129,6 @@ async def async_setup_entry( workdays: list[str] = entry.options[CONF_WORKDAYS] year: int = (dt_util.now() + timedelta(days=days_offset)).year - if country and country not in list_supported_countries(): - LOGGER.error("There is no country %s", country) - return - - if province and province not in list_supported_countries()[country]: - LOGGER.error("There is no subdivision %s in country %s", province, country) - return - obj_holidays: HolidayBase = country_holidays(country, subdiv=province, years=year) # Add custom holidays From 687bf5e8082b23debda360cebf7e84dfc858d6f5 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 21 Aug 2023 08:43:52 +0200 Subject: [PATCH 0658/1151] Ignore ble name for gardena (#98126) --- .../gardena_bluetooth/config_flow.py | 20 +++++++++++----- .../gardena_bluetooth/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../snapshots/test_config_flow.ambr | 24 +++++++++---------- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/config_flow.py b/homeassistant/components/gardena_bluetooth/config_flow.py index 3e981675057..7b34edd29af 100644 --- a/homeassistant/components/gardena_bluetooth/config_flow.py +++ b/homeassistant/components/gardena_bluetooth/config_flow.py @@ -5,9 +5,9 @@ import logging from typing import Any from gardena_bluetooth.client import Client -from gardena_bluetooth.const import DeviceInformation, ScanService +from gardena_bluetooth.const import PRODUCT_NAMES, DeviceInformation, ScanService from gardena_bluetooth.exceptions import CharacteristicNotFound, CommunicationFailure -from gardena_bluetooth.parse import ManufacturerData, ProductGroup +from gardena_bluetooth.parse import ManufacturerData, ProductType import voluptuous as vol from homeassistant import config_entries @@ -34,7 +34,13 @@ def _is_supported(discovery_info: BluetoothServiceInfo): return False manufacturer_data = ManufacturerData.decode(data) - if manufacturer_data.group != ProductGroup.WATER_CONTROL: + product_type = ProductType.from_manufacturer_data(manufacturer_data) + + if product_type not in ( + ProductType.PUMP, + ProductType.VALVE, + ProductType.WATER_COMPUTER, + ): _LOGGER.debug("Unsupported device: %s", manufacturer_data) return False @@ -42,9 +48,11 @@ def _is_supported(discovery_info: BluetoothServiceInfo): def _get_name(discovery_info: BluetoothServiceInfo): - if discovery_info.name and discovery_info.name != discovery_info.address: - return discovery_info.name - return "Gardena Device" + data = discovery_info.manufacturer_data[ManufacturerData.company] + manufacturer_data = ManufacturerData.decode(data) + product_type = ProductType.from_manufacturer_data(manufacturer_data) + + return PRODUCT_NAMES.get(product_type, "Gardena Device") class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json index 0226460d4d8..af72ff7f69d 100644 --- a/homeassistant/components/gardena_bluetooth/manifest.json +++ b/homeassistant/components/gardena_bluetooth/manifest.json @@ -13,5 +13,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth", "iot_class": "local_polling", - "requirements": ["gardena_bluetooth==1.0.2"] + "requirements": ["gardena_bluetooth==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index f0a8d957c34..522f424878d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -829,7 +829,7 @@ fritzconnection[qr]==1.12.2 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena_bluetooth==1.0.2 +gardena_bluetooth==1.2.0 # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 17f2e3f88e9..5de4c5da4c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ fritzconnection[qr]==1.12.2 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena_bluetooth==1.0.2 +gardena_bluetooth==1.2.0 # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 diff --git a/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr b/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr index fde70b60a01..24cef3c349e 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr @@ -3,7 +3,7 @@ FlowResultSnapshot({ 'data_schema': None, 'description_placeholders': dict({ - 'name': 'Timer', + 'name': 'Gardena Water Computer', }), 'errors': None, 'flow_id': , @@ -19,7 +19,7 @@ 'confirm_only': True, 'source': 'bluetooth', 'title_placeholders': dict({ - 'name': 'Timer', + 'name': 'Gardena Water Computer', }), 'unique_id': '00000000-0000-0000-0000-000000000001', }), @@ -44,11 +44,11 @@ 'pref_disable_new_entities': False, 'pref_disable_polling': False, 'source': 'bluetooth', - 'title': 'Timer', + 'title': 'Gardena Water Computer', 'unique_id': '00000000-0000-0000-0000-000000000001', 'version': 1, }), - 'title': 'Timer', + 'title': 'Gardena Water Computer', 'type': , 'version': 1, }) @@ -124,7 +124,7 @@ 'options': list([ tuple( '00000000-0000-0000-0000-000000000001', - 'Timer', + 'Gardena Water Computer', ), ]), 'required': True, @@ -144,7 +144,7 @@ FlowResultSnapshot({ 'data_schema': None, 'description_placeholders': dict({ - 'name': 'Timer', + 'name': 'Gardena Water Computer', }), 'errors': None, 'flow_id': , @@ -182,11 +182,11 @@ 'options': list([ tuple( '00000000-0000-0000-0000-000000000001', - 'Timer', + 'Gardena Water Computer', ), tuple( '00000000-0000-0000-0000-000000000002', - 'Gardena Device', + 'Gardena Water Computer', ), ]), 'required': True, @@ -206,7 +206,7 @@ FlowResultSnapshot({ 'data_schema': None, 'description_placeholders': dict({ - 'name': 'Timer', + 'name': 'Gardena Water Computer', }), 'errors': None, 'flow_id': , @@ -222,7 +222,7 @@ 'confirm_only': True, 'source': 'user', 'title_placeholders': dict({ - 'name': 'Timer', + 'name': 'Gardena Water Computer', }), 'unique_id': '00000000-0000-0000-0000-000000000001', }), @@ -247,11 +247,11 @@ 'pref_disable_new_entities': False, 'pref_disable_polling': False, 'source': 'user', - 'title': 'Timer', + 'title': 'Gardena Water Computer', 'unique_id': '00000000-0000-0000-0000-000000000001', 'version': 1, }), - 'title': 'Timer', + 'title': 'Gardena Water Computer', 'type': , 'version': 1, }) From af689d7c3ef4d00e3f73b058b9056cad22255f3c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 09:12:43 +0200 Subject: [PATCH 0659/1151] Use snapshot assertion for Accuweather diagnostics (#98725) --- .../snapshots/test_diagnostics.ambr | 304 ++++++++++++++++++ .../accuweather/test_diagnostics.py | 13 +- 2 files changed, 309 insertions(+), 8 deletions(-) create mode 100644 tests/components/accuweather/snapshots/test_diagnostics.ambr diff --git a/tests/components/accuweather/snapshots/test_diagnostics.ambr b/tests/components/accuweather/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..b3c0c1de752 --- /dev/null +++ b/tests/components/accuweather/snapshots/test_diagnostics.ambr @@ -0,0 +1,304 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config_entry_data': dict({ + 'api_key': '**REDACTED**', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'Home', + }), + 'coordinator_data': dict({ + 'ApparentTemperature': dict({ + 'Imperial': dict({ + 'Unit': 'F', + 'UnitType': 18, + 'Value': 73.0, + }), + 'Metric': dict({ + 'Unit': 'C', + 'UnitType': 17, + 'Value': 22.8, + }), + }), + 'Ceiling': dict({ + 'Imperial': dict({ + 'Unit': 'ft', + 'UnitType': 0, + 'Value': 10500.0, + }), + 'Metric': dict({ + 'Unit': 'm', + 'UnitType': 5, + 'Value': 3200.0, + }), + }), + 'CloudCover': 10, + 'DewPoint': dict({ + 'Imperial': dict({ + 'Unit': 'F', + 'UnitType': 18, + 'Value': 61.0, + }), + 'Metric': dict({ + 'Unit': 'C', + 'UnitType': 17, + 'Value': 16.2, + }), + }), + 'HasPrecipitation': False, + 'IndoorRelativeHumidity': 67, + 'ObstructionsToVisibility': '', + 'Past24HourTemperatureDeparture': dict({ + 'Imperial': dict({ + 'Unit': 'F', + 'UnitType': 18, + 'Value': 0.0, + }), + 'Metric': dict({ + 'Unit': 'C', + 'UnitType': 17, + 'Value': 0.3, + }), + }), + 'Precip1hr': dict({ + 'Imperial': dict({ + 'Unit': 'in', + 'UnitType': 1, + 'Value': 0.0, + }), + 'Metric': dict({ + 'Unit': 'mm', + 'UnitType': 3, + 'Value': 0.0, + }), + }), + 'PrecipitationSummary': dict({ + 'Past12Hours': dict({ + 'Imperial': dict({ + 'Unit': 'in', + 'UnitType': 1, + 'Value': 0.15, + }), + 'Metric': dict({ + 'Unit': 'mm', + 'UnitType': 3, + 'Value': 3.8, + }), + }), + 'Past18Hours': dict({ + 'Imperial': dict({ + 'Unit': 'in', + 'UnitType': 1, + 'Value': 0.2, + }), + 'Metric': dict({ + 'Unit': 'mm', + 'UnitType': 3, + 'Value': 5.1, + }), + }), + 'Past24Hours': dict({ + 'Imperial': dict({ + 'Unit': 'in', + 'UnitType': 1, + 'Value': 0.3, + }), + 'Metric': dict({ + 'Unit': 'mm', + 'UnitType': 3, + 'Value': 7.6, + }), + }), + 'Past3Hours': dict({ + 'Imperial': dict({ + 'Unit': 'in', + 'UnitType': 1, + 'Value': 0.05, + }), + 'Metric': dict({ + 'Unit': 'mm', + 'UnitType': 3, + 'Value': 1.3, + }), + }), + 'Past6Hours': dict({ + 'Imperial': dict({ + 'Unit': 'in', + 'UnitType': 1, + 'Value': 0.05, + }), + 'Metric': dict({ + 'Unit': 'mm', + 'UnitType': 3, + 'Value': 1.3, + }), + }), + 'Past9Hours': dict({ + 'Imperial': dict({ + 'Unit': 'in', + 'UnitType': 1, + 'Value': 0.1, + }), + 'Metric': dict({ + 'Unit': 'mm', + 'UnitType': 3, + 'Value': 2.5, + }), + }), + 'PastHour': dict({ + 'Imperial': dict({ + 'Unit': 'in', + 'UnitType': 1, + 'Value': 0.0, + }), + 'Metric': dict({ + 'Unit': 'mm', + 'UnitType': 3, + 'Value': 0.0, + }), + }), + 'Precipitation': dict({ + 'Imperial': dict({ + 'Unit': 'in', + 'UnitType': 1, + 'Value': 0.0, + }), + 'Metric': dict({ + 'Unit': 'mm', + 'UnitType': 3, + 'Value': 0.0, + }), + }), + }), + 'PrecipitationType': None, + 'Pressure': dict({ + 'Imperial': dict({ + 'Unit': 'inHg', + 'UnitType': 12, + 'Value': 29.88, + }), + 'Metric': dict({ + 'Unit': 'mb', + 'UnitType': 14, + 'Value': 1012.0, + }), + }), + 'PressureTendency': dict({ + 'Code': 'F', + 'LocalizedText': 'Falling', + }), + 'RealFeelTemperature': dict({ + 'Imperial': dict({ + 'Unit': 'F', + 'UnitType': 18, + 'Value': 77.0, + }), + 'Metric': dict({ + 'Unit': 'C', + 'UnitType': 17, + 'Value': 25.1, + }), + }), + 'RealFeelTemperatureShade': dict({ + 'Imperial': dict({ + 'Unit': 'F', + 'UnitType': 18, + 'Value': 70.0, + }), + 'Metric': dict({ + 'Unit': 'C', + 'UnitType': 17, + 'Value': 21.1, + }), + }), + 'RelativeHumidity': 67, + 'Temperature': dict({ + 'Imperial': dict({ + 'Unit': 'F', + 'UnitType': 18, + 'Value': 73.0, + }), + 'Metric': dict({ + 'Unit': 'C', + 'UnitType': 17, + 'Value': 22.6, + }), + }), + 'UVIndex': 6, + 'UVIndexText': 'High', + 'Visibility': dict({ + 'Imperial': dict({ + 'Unit': 'mi', + 'UnitType': 2, + 'Value': 10.0, + }), + 'Metric': dict({ + 'Unit': 'km', + 'UnitType': 6, + 'Value': 16.1, + }), + }), + 'WeatherIcon': 1, + 'WeatherText': 'Sunny', + 'WetBulbTemperature': dict({ + 'Imperial': dict({ + 'Unit': 'F', + 'UnitType': 18, + 'Value': 65.0, + }), + 'Metric': dict({ + 'Unit': 'C', + 'UnitType': 17, + 'Value': 18.6, + }), + }), + 'Wind': dict({ + 'Direction': dict({ + 'Degrees': 180, + 'English': 'S', + 'Localized': 'S', + }), + 'Speed': dict({ + 'Imperial': dict({ + 'Unit': 'mi/h', + 'UnitType': 9, + 'Value': 9.0, + }), + 'Metric': dict({ + 'Unit': 'km/h', + 'UnitType': 7, + 'Value': 14.5, + }), + }), + }), + 'WindChillTemperature': dict({ + 'Imperial': dict({ + 'Unit': 'F', + 'UnitType': 18, + 'Value': 73.0, + }), + 'Metric': dict({ + 'Unit': 'C', + 'UnitType': 17, + 'Value': 22.8, + }), + }), + 'WindGust': dict({ + 'Speed': dict({ + 'Imperial': dict({ + 'Unit': 'mi/h', + 'UnitType': 9, + 'Value': 12.6, + }), + 'Metric': dict({ + 'Unit': 'km/h', + 'UnitType': 7, + 'Value': 20.3, + }), + }), + }), + 'forecast': list([ + ]), + }), + }) +# --- diff --git a/tests/components/accuweather/test_diagnostics.py b/tests/components/accuweather/test_diagnostics.py index 98be70d9ec6..7c13f318cc3 100644 --- a/tests/components/accuweather/test_diagnostics.py +++ b/tests/components/accuweather/test_diagnostics.py @@ -1,4 +1,5 @@ """Test AccuWeather diagnostics.""" +from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant @@ -10,7 +11,9 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" entry = await init_integration(hass) @@ -23,10 +26,4 @@ async def test_entry_diagnostics( result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert result["config_entry_data"] == { - "api_key": "**REDACTED**", - "latitude": "**REDACTED**", - "longitude": "**REDACTED**", - "name": "Home", - } - assert result["coordinator_data"] == coordinator_data + assert result == snapshot From a713d7585f6d80798cf3ffc5377ca352b69e8d22 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 21 Aug 2023 10:49:11 +0300 Subject: [PATCH 0660/1151] Bump aioshelly to 6.0.0 (#98719) --- homeassistant/components/shelly/__init__.py | 8 +++--- .../components/shelly/coordinator.py | 25 +++++++++++++------ homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/shelly/conftest.py | 16 ++++++------ 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index e5e90bf19af..65b60546f61 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -4,10 +4,10 @@ from __future__ import annotations import contextlib from typing import Any, Final -from aioshelly.block_device import BlockDevice +from aioshelly.block_device import BlockDevice, BlockUpdateType from aioshelly.common import ConnectionOptions from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError -from aioshelly.rpc_device import RpcDevice, UpdateType +from aioshelly.rpc_device import RpcDevice, RpcUpdateType import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -168,7 +168,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b await hass.config_entries.async_forward_entry_setups(entry, platforms) @callback - def _async_device_online(_: Any) -> None: + def _async_device_online(_: Any, update_type: BlockUpdateType) -> None: LOGGER.debug("Device %s is online, resuming setup", entry.title) shelly_entry_data.device = None @@ -253,7 +253,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo await hass.config_entries.async_forward_entry_setups(entry, platforms) @callback - def _async_device_online(_: Any, update_type: UpdateType) -> None: + def _async_device_online(_: Any, update_type: RpcUpdateType) -> None: LOGGER.debug("Device %s is online, resuming setup", entry.title) shelly_entry_data.device = None diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 0d4a091b729..7829cd76567 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -9,9 +9,9 @@ from typing import Any, Generic, TypeVar, cast import aioshelly from aioshelly.ble import async_ensure_ble_enabled, async_stop_scanner -from aioshelly.block_device import BlockDevice +from aioshelly.block_device import BlockDevice, BlockUpdateType from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError -from aioshelly.rpc_device import RpcDevice, UpdateType +from aioshelly.rpc_device import RpcDevice, RpcUpdateType from awesomeversion import AwesomeVersion from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -295,10 +295,17 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): ) device_update_info(self.hass, self.device, self.entry) + @callback + def _async_handle_update( + self, device_: BlockDevice, update_type: BlockUpdateType + ) -> None: + """Handle device update.""" + self.async_set_updated_data(None) + def async_setup(self) -> None: """Set up the coordinator.""" super().async_setup() - self.device.subscribe_updates(self.async_set_updated_data) + self.device.subscribe_updates(self._async_handle_update) def shutdown(self) -> None: """Shutdown the coordinator.""" @@ -535,16 +542,18 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): ) @callback - def _async_handle_update(self, device_: RpcDevice, update_type: UpdateType) -> None: + def _async_handle_update( + self, device_: RpcDevice, update_type: RpcUpdateType + ) -> None: """Handle device update.""" - if update_type is UpdateType.INITIALIZED: + if update_type is RpcUpdateType.INITIALIZED: self.hass.async_create_task(self._async_connected()) self.async_set_updated_data(None) - elif update_type is UpdateType.DISCONNECTED: + elif update_type is RpcUpdateType.DISCONNECTED: self.hass.async_create_task(self._async_disconnected()) - elif update_type is UpdateType.STATUS: + elif update_type is RpcUpdateType.STATUS: self.async_set_updated_data(None) - elif update_type is UpdateType.EVENT and (event := self.device.event): + elif update_type is RpcUpdateType.EVENT and (event := self.device.event): self._async_device_event_handler(event) def async_setup(self) -> None: diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 6031b2dcc82..c76e2102fa1 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==5.4.0"], + "requirements": ["aioshelly==6.0.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 522f424878d..3374173e250 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.4.0 +aioshelly==6.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5de4c5da4c1..29f6338db1f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -314,7 +314,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.4.0 +aioshelly==6.0.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 96e888d7509..2f33e76d336 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -3,8 +3,8 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock, PropertyMock, patch -from aioshelly.block_device import BlockDevice -from aioshelly.rpc_device import RpcDevice, UpdateType +from aioshelly.block_device import BlockDevice, BlockUpdateType +from aioshelly.rpc_device import RpcDevice, RpcUpdateType import pytest from homeassistant.components.shelly.const import ( @@ -247,7 +247,9 @@ async def mock_block_device(): with patch("aioshelly.block_device.BlockDevice.create") as block_device_mock: def update(): - block_device_mock.return_value.subscribe_updates.call_args[0][0]({}) + block_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, BlockUpdateType.COAP_PERIODIC + ) device = Mock( spec=BlockDevice, @@ -291,7 +293,7 @@ async def mock_pre_ble_rpc_device(): def update(): rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( - {}, UpdateType.STATUS + {}, RpcUpdateType.STATUS ) device = _mock_rpc_device("0.11.0") @@ -310,17 +312,17 @@ async def mock_rpc_device(): def update(): rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( - {}, UpdateType.STATUS + {}, RpcUpdateType.STATUS ) def event(): rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( - {}, UpdateType.EVENT + {}, RpcUpdateType.EVENT ) def disconnected(): rpc_device_mock.return_value.subscribe_updates.call_args[0][0]( - {}, UpdateType.DISCONNECTED + {}, RpcUpdateType.DISCONNECTED ) device = _mock_rpc_device("0.12.0") From 976f6582e10a4b53c921967035df0b91f44768b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Aug 2023 04:48:54 -0400 Subject: [PATCH 0661/1151] Reduce overhead to update august activities (#98730) --- homeassistant/components/august/activity.py | 63 +++++++++++---------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index 1768d3291a7..fdb399f0646 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -1,6 +1,7 @@ """Consume the august activity stream.""" import asyncio from datetime import datetime +from functools import partial import logging from aiohttp import ClientError @@ -9,7 +10,7 @@ from yalexs.api_async import ApiAsync from yalexs.pubnub_async import AugustPubNub from yalexs.util import get_latest_activity -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow @@ -58,33 +59,38 @@ class ActivityStream(AugustSubscriberMixin): self._did_first_update = False self.pubnub = pubnub self._update_debounce: dict[str, Debouncer] = {} + self._update_debounce_jobs: dict[str, HassJob] = {} - async def async_setup(self): + async def _async_update_house_id_later( + self, debouncer: Debouncer, _: datetime + ) -> None: + """Call a debouncer from async_call_later.""" + await debouncer.async_call() + + async def async_setup(self) -> None: """Token refresh check and catch up the activity stream.""" - self._update_debounce = { - house_id: self._async_create_debouncer(house_id) - for house_id in self._house_ids - } + update_debounce = self._update_debounce + update_debounce_jobs = self._update_debounce_jobs + for house_id in self._house_ids: + debouncer = Debouncer( + self._hass, + _LOGGER, + cooldown=ACTIVITY_DEBOUNCE_COOLDOWN, + immediate=True, + function=partial(self._async_update_house_id, house_id), + ) + update_debounce[house_id] = debouncer + update_debounce_jobs[house_id] = HassJob( + partial(self._async_update_house_id_later, debouncer), + f"debounced august activity update for {house_id}", + cancel_on_shutdown=True, + ) + await self._async_refresh(utcnow()) self._did_first_update = True @callback - def _async_create_debouncer(self, house_id): - """Create a debouncer for the house id.""" - - async def _async_update_house_id(): - await self._async_update_house_id(house_id) - - return Debouncer( - self._hass, - _LOGGER, - cooldown=ACTIVITY_DEBOUNCE_COOLDOWN, - immediate=True, - function=_async_update_house_id, - ) - - @callback - def async_stop(self): + def async_stop(self) -> None: """Cleanup any debounces.""" for debouncer in self._update_debounce.values(): debouncer.async_cancel() @@ -127,28 +133,23 @@ class ActivityStream(AugustSubscriberMixin): @callback def async_schedule_house_id_refresh(self, house_id: str) -> None: """Update for a house activities now and once in the future.""" - if cancels := self._schedule_updates.get(house_id): - _async_cancel_future_scheduled_updates(cancels) + if future_updates := self._schedule_updates.setdefault(house_id, []): + _async_cancel_future_scheduled_updates(future_updates) debouncer = self._update_debounce[house_id] - 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 _update_house_activities(now: datetime) -> None: - await debouncer.async_call() - + job = self._update_debounce_jobs[house_id] for step in (1, 2): future_updates.append( async_call_later( self._hass, (step * ACTIVITY_DEBOUNCE_COOLDOWN) + 0.1, - _update_house_activities, + job, ) ) From 605c55109d7f4283b77b187649a3805c9ca0bc3c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Aug 2023 04:56:39 -0400 Subject: [PATCH 0662/1151] Bump yalexs to 1.7.0 (#98720) --- 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 98c9cbacbda..272a0ca4335 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.2", "yalexs-ble==2.2.3"] + "requirements": ["yalexs==1.7.0", "yalexs-ble==2.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3374173e250..7e5536a4158 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2728,7 +2728,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.2.3 # homeassistant.components.august -yalexs==1.5.2 +yalexs==1.7.0 # homeassistant.components.yeelight yeelight==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29f6338db1f..93e1fc31808 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2010,7 +2010,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.2.3 # homeassistant.components.august -yalexs==1.5.2 +yalexs==1.7.0 # homeassistant.components.yeelight yeelight==0.7.13 From 30a0cb1674c32729af0e8b609f748ccd605df28a Mon Sep 17 00:00:00 2001 From: Markus Ressel Date: Mon, 21 Aug 2023 11:09:39 +0200 Subject: [PATCH 0663/1151] Fix octoprint down every two minutes (#90001) --- .../components/octoprint/__init__.py | 30 +++++++++++++------ .../components/octoprint/config_flow.py | 29 +++++++++++++----- .../components/octoprint/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 1ca0dc1f5d5..07b2fa1a15d 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -5,6 +5,7 @@ from datetime import timedelta import logging from typing import cast +import aiohttp from pyoctoprintapi import ApiError, OctoprintClient, PrinterOffline from pyoctoprintapi.exceptions import UnauthorizedException import voluptuous as vol @@ -22,11 +23,11 @@ from homeassistant.const import ( CONF_SENSORS, CONF_SSL, CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.typing import ConfigType @@ -163,14 +164,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = {**entry.data, CONF_VERIFY_SSL: True} hass.config_entries.async_update_entry(entry, data=data) - verify_ssl = entry.data[CONF_VERIFY_SSL] - websession = async_get_clientsession(hass, verify_ssl=verify_ssl) + connector = aiohttp.TCPConnector( + force_close=True, + ssl=False if not entry.data[CONF_VERIFY_SSL] else None, + ) + session = aiohttp.ClientSession(connector=connector) + + @callback + def _async_close_websession(event: Event) -> None: + """Close websession.""" + session.detach() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close_websession) + client = OctoprintClient( - entry.data[CONF_HOST], - websession, - entry.data[CONF_PORT], - entry.data[CONF_SSL], - entry.data[CONF_PATH], + host=entry.data[CONF_HOST], + session=session, + port=entry.data[CONF_PORT], + ssl=entry.data[CONF_SSL], + path=entry.data[CONF_PATH], ) client.set_api_key(entry.data[CONF_API_KEY]) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 33aaff8976e..09ac53ecf5b 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -6,6 +6,7 @@ from collections.abc import Mapping import logging from typing import Any +import aiohttp from pyoctoprintapi import ApiError, OctoprintClient, OctoprintException import voluptuous as vol from yarl import URL @@ -22,7 +23,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import DOMAIN @@ -58,6 +58,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for OctoPrint.""" self.discovery_schema = None self._user_input = None + self._sessions: list[aiohttp.ClientSession] = [] async def async_step_user(self, user_input=None): """Handle the initial step.""" @@ -260,14 +261,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _get_octoprint_client(self, user_input: dict) -> OctoprintClient: """Build an octoprint client from the user_input.""" verify_ssl = user_input.get(CONF_VERIFY_SSL, True) - session = async_get_clientsession(self.hass, verify_ssl=verify_ssl) - return OctoprintClient( - user_input[CONF_HOST], - session, - user_input[CONF_PORT], - user_input[CONF_SSL], - user_input[CONF_PATH], + + connector = aiohttp.TCPConnector( + force_close=True, + ssl=False if not verify_ssl else None, ) + session = aiohttp.ClientSession(connector=connector) + self._sessions.append(session) + + return OctoprintClient( + host=user_input[CONF_HOST], + session=session, + port=user_input[CONF_PORT], + ssl=user_input[CONF_SSL], + path=user_input[CONF_PATH], + ) + + def async_remove(self): + """Detach the session.""" + for session in self._sessions: + session.detach() class CannotConnect(exceptions.HomeAssistantError): diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index e4bc70e5d86..005cf5305d9 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/octoprint", "iot_class": "local_polling", "loggers": ["pyoctoprintapi"], - "requirements": ["pyoctoprintapi==0.1.11"], + "requirements": ["pyoctoprintapi==0.1.12"], "ssdp": [ { "manufacturer": "The OctoPrint Project", diff --git a/requirements_all.txt b/requirements_all.txt index 7e5536a4158..71491c71b93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1887,7 +1887,7 @@ pynzbgetapi==0.2.0 pyobihai==1.4.2 # homeassistant.components.octoprint -pyoctoprintapi==0.1.11 +pyoctoprintapi==0.1.12 # homeassistant.components.ombi pyombi==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93e1fc31808..ca546120778 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1400,7 +1400,7 @@ pynzbgetapi==0.2.0 pyobihai==1.4.2 # homeassistant.components.octoprint -pyoctoprintapi==0.1.11 +pyoctoprintapi==0.1.12 # homeassistant.components.openuv pyopenuv==2023.02.0 From f373b27a3b62d667f78aab013ade8c72f649e44b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 21 Aug 2023 11:14:54 +0200 Subject: [PATCH 0664/1151] Update aioqsw to v0.3.3 (#98737) --- homeassistant/components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index 17825110490..1b9ba097b36 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", "iot_class": "local_polling", "loggers": ["aioqsw"], - "requirements": ["aioqsw==0.3.2"] + "requirements": ["aioqsw==0.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 71491c71b93..3f322928836 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -324,7 +324,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.3.2 +aioqsw==0.3.3 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca546120778..6fccaf61b1a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -299,7 +299,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.3.2 +aioqsw==0.3.3 # homeassistant.components.recollect_waste aiorecollect==1.0.8 From c0bb3dd6e0c58b75518c75b3f22dd7d52f5968fd Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 21 Aug 2023 04:15:58 -0500 Subject: [PATCH 0665/1151] Use snapshot assertion for Jellyfin diagnostics (#98732) --- .../jellyfin/snapshots/test_diagnostics.ambr | 1788 +++++++++++++++++ tests/components/jellyfin/test_diagnostics.py | 602 +----- 2 files changed, 1795 insertions(+), 595 deletions(-) create mode 100644 tests/components/jellyfin/snapshots/test_diagnostics.ambr diff --git a/tests/components/jellyfin/snapshots/test_diagnostics.ambr b/tests/components/jellyfin/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..c992628f034 --- /dev/null +++ b/tests/components/jellyfin/snapshots/test_diagnostics.ambr @@ -0,0 +1,1788 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'entry': dict({ + 'data': dict({ + 'client_device_id': 'entry-id', + 'password': '**REDACTED**', + 'url': 'https://example.com', + 'username': 'test-username', + }), + 'title': 'Jellyfin', + }), + 'server': dict({ + 'id': 'SERVER-UUID', + 'name': 'JELLYFIN-SERVER', + 'version': None, + }), + 'sessions': list([ + dict({ + 'capabilities': dict({ + 'AppStoreUrl': 'string', + 'DeviceProfile': dict({ + 'AlbumArtPn': 'string', + 'CodecProfiles': list([ + dict({ + 'ApplyConditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Codec': 'string', + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'Type': 'Video', + }), + ]), + 'ContainerProfiles': list([ + dict({ + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'Type': 'Audio', + }), + ]), + 'DirectPlayProfiles': list([ + dict({ + 'AudioCodec': 'string', + 'Container': 'string', + 'Type': 'Audio', + 'VideoCodec': 'string', + }), + ]), + 'EnableAlbumArtInDidl': False, + 'EnableMSMediaReceiverRegistrar': False, + 'EnableSingleAlbumArtLimit': False, + 'EnableSingleSubtitleLimit': False, + 'FriendlyName': 'string', + 'Id': 'string', + 'Identification': dict({ + 'FriendlyName': 'string', + 'Headers': list([ + dict({ + 'Match': 'Equals', + 'Name': 'string', + 'Value': 'string', + }), + ]), + 'Manufacturer': 'string', + 'ManufacturerUrl': 'string', + 'ModelDescription': 'string', + 'ModelName': 'string', + 'ModelNumber': 'string', + 'ModelUrl': 'string', + 'SerialNumber': 'string', + }), + 'IgnoreTranscodeByteRangeRequests': False, + 'Manufacturer': 'string', + 'ManufacturerUrl': 'string', + 'MaxAlbumArtHeight': 0, + 'MaxAlbumArtWidth': 0, + 'MaxIconHeight': 0, + 'MaxIconWidth': 0, + 'MaxStaticBitrate': 0, + 'MaxStaticMusicBitrate': 0, + 'MaxStreamingBitrate': 0, + 'ModelDescription': 'string', + 'ModelName': 'string', + 'ModelNumber': 'string', + 'ModelUrl': 'string', + 'MusicStreamingTranscodingBitrate': 0, + 'Name': 'string', + 'ProtocolInfo': 'string', + 'RequiresPlainFolders': False, + 'RequiresPlainVideoItems': False, + 'ResponseProfiles': list([ + dict({ + 'AudioCodec': 'string', + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'MimeType': 'string', + 'OrgPn': 'string', + 'Type': 'Audio', + 'VideoCodec': 'string', + }), + ]), + 'SerialNumber': 'string', + 'SonyAggregationFlags': 'string', + 'SubtitleProfiles': list([ + dict({ + 'Container': 'string', + 'DidlMode': 'string', + 'Format': 'string', + 'Language': 'string', + 'Method': 'Encode', + }), + ]), + 'SupportedMediaTypes': 'string', + 'TimelineOffsetSeconds': 0, + 'TranscodingProfiles': list([ + dict({ + 'AudioCodec': 'string', + 'BreakOnNonKeyFrames': False, + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'Context': 'Streaming', + 'CopyTimestamps': False, + 'EnableMpegtsM2TsMode': False, + 'EnableSubtitlesInManifest': False, + 'EstimateContentLength': False, + 'MaxAudioChannels': 'string', + 'MinSegments': 0, + 'Protocol': 'string', + 'SegmentLength': 0, + 'TranscodeSeekInfo': 'Auto', + 'Type': 'Audio', + 'VideoCodec': 'string', + }), + ]), + 'UserId': 'string', + 'XmlRootAttributes': list([ + dict({ + 'Name': 'string', + 'Value': 'string', + }), + ]), + }), + 'IconUrl': 'string', + 'MessageCallbackUrl': 'string', + 'PlayableMediaTypes': list([ + 'Video', + ]), + 'SupportedCommands': list([ + 'VolumeSet', + 'Mute', + ]), + 'SupportsContentUploading': True, + 'SupportsMediaControl': True, + 'SupportsPersistentIdentifier': True, + 'SupportsSync': True, + }), + 'client_name': 'Jellyfin for Developers', + 'client_version': '1.0.0', + 'device_id': 'DEVICE-UUID', + 'device_name': 'JELLYFIN-DEVICE', + 'id': 'SESSION-UUID', + 'now_playing': dict({ + 'AirDays': list([ + 'Sunday', + ]), + 'AirTime': 'string', + 'AirsAfterSeasonNumber': 0, + 'AirsBeforeEpisodeNumber': 0, + 'AirsBeforeSeasonNumber': 0, + 'Album': 'string', + 'AlbumArtist': 'string', + 'AlbumArtists': list([ + dict({ + 'Id': '38a5a5bb-dc30-49a2-b175-1de0d1488c43', + 'Name': 'string', + }), + ]), + 'AlbumCount': 0, + 'AlbumId': '21af9851-8e39-43a9-9c47-513d3b9e99fc', + 'AlbumPrimaryImageTag': 'string', + 'Altitude': 0, + 'Aperture': 0, + 'ArtistCount': 0, + 'ArtistItems': list([ + dict({ + 'Id': '38a5a5bb-dc30-49a2-b175-1de0d1488c43', + 'Name': 'string', + }), + ]), + 'Artists': list([ + 'string', + ]), + 'AspectRatio': 'string', + 'Audio': 'Mono', + 'BackdropImageTags': list([ + 'string', + ]), + 'CameraMake': 'string', + 'CameraModel': 'string', + 'CanDelete': True, + 'CanDownload': True, + 'ChannelId': '04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff', + 'ChannelName': 'string', + 'ChannelNumber': 'string', + 'ChannelPrimaryImageTag': 'string', + 'ChannelType': 'TV', + 'Chapters': list([ + dict({ + 'ImageDateModified': '2019-08-24T14:15:22Z', + 'ImagePath': 'string', + 'ImageTag': 'string', + 'Name': 'string', + 'StartPositionTicks': 0, + }), + ]), + 'ChildCount': 0, + 'CollectionType': 'string', + 'CommunityRating': 0, + 'CompletionPercentage': 0, + 'Container': 'string', + 'CriticRating': 0, + 'CumulativeRunTimeTicks': 0, + 'CurrentProgram': dict({ + }), + 'CustomRating': 'string', + 'DateCreated': '2019-08-24T14:15:22Z', + 'DateLastMediaAdded': '2019-08-24T14:15:22Z', + 'DisplayOrder': 'string', + 'DisplayPreferencesId': 'string', + 'EnableMediaSourceDisplay': True, + 'EndDate': '2019-08-24T14:15:22Z', + 'EpisodeCount': 0, + 'EpisodeTitle': 'string', + 'Etag': 'string', + 'ExposureTime': 0, + 'ExternalUrls': list([ + dict({ + 'Name': 'string', + 'Url': 'string', + }), + ]), + 'ExtraType': 'string', + 'FocalLength': 0, + 'ForcedSortName': 'string', + 'GenreItems': list([ + dict({ + 'Id': '38a5a5bb-dc30-49a2-b175-1de0d1488c43', + 'Name': 'string', + }), + ]), + 'Genres': list([ + 'string', + ]), + 'HasSubtitles': True, + 'Height': 0, + 'Id': 'EPISODE-UUID', + 'ImageBlurHashes': dict({ + 'Art': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Backdrop': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Banner': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Box': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'BoxRear': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Chapter': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Disc': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Logo': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Menu': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Primary': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Profile': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Screenshot': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Thumb': dict({ + 'property1': 'string', + 'property2': 'string', + }), + }), + 'ImageOrientation': 'TopLeft', + 'ImageTags': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'IndexNumber': 3, + 'IndexNumberEnd': 0, + 'IsFolder': False, + 'IsHD': True, + 'IsKids': True, + 'IsLive': True, + 'IsMovie': True, + 'IsNews': True, + 'IsPlaceHolder': True, + 'IsPremiere': True, + 'IsRepeat': True, + 'IsSeries': True, + 'IsSports': True, + 'IsoSpeedRating': 0, + 'IsoType': 'Dvd', + 'Latitude': 0, + 'LocalTrailerCount': 0, + 'LocationType': 'FileSystem', + 'LockData': True, + 'LockedFields': list([ + 'Cast', + ]), + 'Longitude': 0, + 'MediaSourceCount': 0, + 'MediaSources': list([ + dict({ + 'AnalyzeDurationMs': 0, + 'Bitrate': 0, + 'BufferMs': 0, + 'Container': 'string', + 'DefaultAudioStreamIndex': 0, + 'DefaultSubtitleStreamIndex': 0, + 'ETag': 'string', + 'EncoderPath': 'string', + 'EncoderProtocol': 'File', + 'Formats': list([ + 'string', + ]), + 'GenPtsInput': True, + 'Id': 'string', + 'IgnoreDts': True, + 'IgnoreIndex': True, + 'IsInfiniteStream': True, + 'IsRemote': True, + 'IsoType': 'Dvd', + 'LiveStreamId': 'string', + 'MediaAttachments': list([ + dict({ + 'Codec': 'string', + 'CodecTag': 'string', + 'Comment': 'string', + 'DeliveryUrl': 'string', + 'FileName': 'string', + 'Index': 0, + 'MimeType': 'string', + }), + ]), + 'MediaStreams': list([ + dict({ + 'AspectRatio': 'string', + 'AverageFrameRate': 0, + 'BitDepth': 0, + 'BitRate': 0, + 'BlPresentFlag': 0, + 'ChannelLayout': 'string', + 'Channels': 0, + 'Codec': 'string', + 'CodecTag': 'string', + 'CodecTimeBase': 'string', + 'ColorPrimaries': 'string', + 'ColorRange': 'string', + 'ColorSpace': 'string', + 'ColorTransfer': 'string', + 'Comment': 'string', + 'DeliveryMethod': 'Encode', + 'DeliveryUrl': 'string', + 'DisplayTitle': 'string', + 'DvBlSignalCompatibilityId': 0, + 'DvLevel': 0, + 'DvProfile': 0, + 'DvVersionMajor': 0, + 'DvVersionMinor': 0, + 'ElPresentFlag': 0, + 'Height': 0, + 'Index': 0, + 'IsAVC': True, + 'IsAnamorphic': True, + 'IsDefault': True, + 'IsExternal': True, + 'IsExternalUrl': True, + 'IsForced': True, + 'IsInterlaced': True, + 'IsTextSubtitleStream': True, + 'Language': 'string', + 'Level': 0, + 'LocalizedDefault': 'string', + 'LocalizedExternal': 'string', + 'LocalizedForced': 'string', + 'LocalizedUndefined': 'string', + 'NalLengthSize': 'string', + 'PacketLength': 0, + 'Path': 'string', + 'PixelFormat': 'string', + 'Profile': 'string', + 'RealFrameRate': 0, + 'RefFrames': 0, + 'RpuPresentFlag': 0, + 'SampleRate': 0, + 'Score': 0, + 'SupportsExternalStream': True, + 'TimeBase': 'string', + 'Title': 'string', + 'Type': 'Audio', + 'VideoDoViTitle': 'string', + 'VideoRange': 'string', + 'VideoRangeType': 'string', + 'Width': 0, + }), + ]), + 'Name': 'string', + 'OpenToken': 'string', + 'Path': 'string', + 'Protocol': 'File', + 'ReadAtNativeFramerate': True, + 'RequiredHttpHeaders': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'RequiresClosing': True, + 'RequiresLooping': True, + 'RequiresOpening': True, + 'RunTimeTicks': 0, + 'Size': 0, + 'SupportsDirectPlay': True, + 'SupportsDirectStream': True, + 'SupportsProbing': True, + 'SupportsTranscoding': True, + 'Timestamp': 'None', + 'TranscodingContainer': 'string', + 'TranscodingSubProtocol': 'string', + 'TranscodingUrl': 'string', + 'Type': 'Default', + 'Video3DFormat': 'HalfSideBySide', + 'VideoType': 'VideoFile', + }), + ]), + 'MediaStreams': list([ + dict({ + 'AspectRatio': 'string', + 'AverageFrameRate': 0, + 'BitDepth': 0, + 'BitRate': 0, + 'BlPresentFlag': 0, + 'ChannelLayout': 'string', + 'Channels': 0, + 'Codec': 'string', + 'CodecTag': 'string', + 'CodecTimeBase': 'string', + 'ColorPrimaries': 'string', + 'ColorRange': 'string', + 'ColorSpace': 'string', + 'ColorTransfer': 'string', + 'Comment': 'string', + 'DeliveryMethod': 'Encode', + 'DeliveryUrl': 'string', + 'DisplayTitle': 'string', + 'DvBlSignalCompatibilityId': 0, + 'DvLevel': 0, + 'DvProfile': 0, + 'DvVersionMajor': 0, + 'DvVersionMinor': 0, + 'ElPresentFlag': 0, + 'Height': 0, + 'Index': 0, + 'IsAVC': True, + 'IsAnamorphic': True, + 'IsDefault': True, + 'IsExternal': True, + 'IsExternalUrl': True, + 'IsForced': True, + 'IsInterlaced': True, + 'IsTextSubtitleStream': True, + 'Language': 'string', + 'Level': 0, + 'LocalizedDefault': 'string', + 'LocalizedExternal': 'string', + 'LocalizedForced': 'string', + 'LocalizedUndefined': 'string', + 'NalLengthSize': 'string', + 'PacketLength': 0, + 'Path': 'string', + 'PixelFormat': 'string', + 'Profile': 'string', + 'RealFrameRate': 0, + 'RefFrames': 0, + 'RpuPresentFlag': 0, + 'SampleRate': 0, + 'Score': 0, + 'SupportsExternalStream': True, + 'TimeBase': 'string', + 'Title': 'string', + 'Type': 'Audio', + 'VideoDoViTitle': 'string', + 'VideoRange': 'string', + 'VideoRangeType': 'string', + 'Width': 0, + }), + ]), + 'MediaType': 'string', + 'MovieCount': 0, + 'MusicVideoCount': 0, + 'Name': 'EPISODE', + 'Number': 'string', + 'OfficialRating': 'string', + 'OriginalTitle': 'string', + 'Overview': 'string', + 'ParentArtImageTag': 'string', + 'ParentArtItemId': '10c1875b-b82c-48e8-bae9-939a5e68dc2f', + 'ParentBackdropImageTags': list([ + 'string', + ]), + 'ParentBackdropItemId': 'c22fd826-17fc-44f4-9b04-1eb3e8fb9173', + 'ParentId': 'PARENT-UUID', + 'ParentIndexNumber': 1, + 'ParentLogoImageTag': 'string', + 'ParentLogoItemId': 'c78d400f-de5c-421e-8714-4fb05d387233', + 'ParentPrimaryImageItemId': 'string', + 'ParentPrimaryImageTag': 'string', + 'ParentThumbImageTag': 'string', + 'ParentThumbItemId': 'ae6ff707-333d-4994-be6d-b83ca1b35f46', + 'PartCount': 0, + 'Path': 'string', + 'People': list([ + dict({ + 'Id': '38a5a5bb-dc30-49a2-b175-1de0d1488c43', + 'ImageBlurHashes': dict({ + 'Art': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Backdrop': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Banner': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Box': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'BoxRear': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Chapter': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Disc': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Logo': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Menu': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Primary': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Profile': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Screenshot': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Thumb': dict({ + 'property1': 'string', + 'property2': 'string', + }), + }), + 'Name': 'string', + 'PrimaryImageTag': 'string', + 'Role': 'string', + 'Type': 'string', + }), + ]), + 'PlayAccess': 'Full', + 'PlaylistItemId': 'string', + 'PreferredMetadataCountryCode': 'string', + 'PreferredMetadataLanguage': 'string', + 'PremiereDate': '2019-08-24T14:15:22Z', + 'PrimaryImageAspectRatio': 0, + 'ProductionLocations': list([ + 'string', + ]), + 'ProductionYear': 0, + 'ProgramCount': 0, + 'ProgramId': 'string', + 'ProviderIds': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'RecursiveItemCount': 0, + 'RemoteTrailers': list([ + dict({ + 'Name': 'string', + 'Url': 'string', + }), + ]), + 'RunTimeTicks': 600000000, + 'ScreenshotImageTags': list([ + 'string', + ]), + 'SeasonId': 'SEASON-UUID', + 'SeasonName': 'SEASON', + 'SeriesCount': 0, + 'SeriesId': 'SERIES-UUID', + 'SeriesName': 'SERIES', + 'SeriesPrimaryImageTag': 'string', + 'SeriesStudio': 'HASS', + 'SeriesThumbImageTag': 'string', + 'SeriesTimerId': 'string', + 'ServerId': 'SERVER-UUID', + 'ShutterSpeed': 0, + 'Software': 'string', + 'SongCount': 0, + 'SortName': 'string', + 'SourceType': 'string', + 'SpecialFeatureCount': 0, + 'StartDate': '2019-08-24T14:15:22Z', + 'Status': 'string', + 'Studios': list([ + dict({ + 'Id': '38a5a5bb-dc30-49a2-b175-1de0d1488c43', + 'Name': 'string', + }), + ]), + 'SupportsSync': True, + 'Taglines': list([ + 'string', + ]), + 'Tags': list([ + 'string', + ]), + 'TimerId': 'string', + 'TrailerCount': 0, + 'Type': 'Episode', + 'UserData': dict({ + 'IsFavorite': True, + 'ItemId': 'string', + 'Key': 'string', + 'LastPlayedDate': '2019-08-24T14:15:22Z', + 'Likes': True, + 'PlayCount': 0, + 'PlaybackPositionTicks': 0, + 'Played': True, + 'PlayedPercentage': 0, + 'Rating': 0, + 'UnplayedItemCount': 0, + }), + 'Video3DFormat': 'HalfSideBySide', + 'VideoType': 'VideoFile', + 'Width': 0, + }), + 'play_state': dict({ + 'AudioStreamIndex': 0, + 'CanSeek': True, + 'IsMuted': True, + 'IsPaused': True, + 'LiveStreamId': 'string', + 'MediaSourceId': 'string', + 'PlayMethod': 'Transcode', + 'PositionTicks': 100000000, + 'RepeatMode': 'RepeatNone', + 'SubtitleStreamIndex': 0, + 'VolumeLevel': 0, + }), + 'user_id': '08ba1929-681e-4b24-929b-9245852f65c0', + }), + dict({ + 'capabilities': dict({ + 'AppStoreUrl': 'string', + 'DeviceProfile': dict({ + 'AlbumArtPn': 'string', + 'CodecProfiles': list([ + dict({ + 'ApplyConditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Codec': 'string', + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'Type': 'Video', + }), + ]), + 'ContainerProfiles': list([ + dict({ + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'Type': 'Audio', + }), + ]), + 'DirectPlayProfiles': list([ + dict({ + 'AudioCodec': 'string', + 'Container': 'string', + 'Type': 'Audio', + 'VideoCodec': 'string', + }), + ]), + 'EnableAlbumArtInDidl': False, + 'EnableMSMediaReceiverRegistrar': False, + 'EnableSingleAlbumArtLimit': False, + 'EnableSingleSubtitleLimit': False, + 'FriendlyName': 'string', + 'Id': 'string', + 'Identification': dict({ + 'FriendlyName': 'string', + 'Headers': list([ + dict({ + 'Match': 'Equals', + 'Name': 'string', + 'Value': 'string', + }), + ]), + 'Manufacturer': 'string', + 'ManufacturerUrl': 'string', + 'ModelDescription': 'string', + 'ModelName': 'string', + 'ModelNumber': 'string', + 'ModelUrl': 'string', + 'SerialNumber': 'string', + }), + 'IgnoreTranscodeByteRangeRequests': False, + 'Manufacturer': 'string', + 'ManufacturerUrl': 'string', + 'MaxAlbumArtHeight': 0, + 'MaxAlbumArtWidth': 0, + 'MaxIconHeight': 0, + 'MaxIconWidth': 0, + 'MaxStaticBitrate': 0, + 'MaxStaticMusicBitrate': 0, + 'MaxStreamingBitrate': 0, + 'ModelDescription': 'string', + 'ModelName': 'string', + 'ModelNumber': 'string', + 'ModelUrl': 'string', + 'MusicStreamingTranscodingBitrate': 0, + 'Name': 'string', + 'ProtocolInfo': 'string', + 'RequiresPlainFolders': False, + 'RequiresPlainVideoItems': False, + 'ResponseProfiles': list([ + dict({ + 'AudioCodec': 'string', + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'MimeType': 'string', + 'OrgPn': 'string', + 'Type': 'Audio', + 'VideoCodec': 'string', + }), + ]), + 'SerialNumber': 'string', + 'SonyAggregationFlags': 'string', + 'SubtitleProfiles': list([ + dict({ + 'Container': 'string', + 'DidlMode': 'string', + 'Format': 'string', + 'Language': 'string', + 'Method': 'Encode', + }), + ]), + 'SupportedMediaTypes': 'string', + 'TimelineOffsetSeconds': 0, + 'TranscodingProfiles': list([ + dict({ + 'AudioCodec': 'string', + 'BreakOnNonKeyFrames': False, + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'Context': 'Streaming', + 'CopyTimestamps': False, + 'EnableMpegtsM2TsMode': False, + 'EnableSubtitlesInManifest': False, + 'EstimateContentLength': False, + 'MaxAudioChannels': 'string', + 'MinSegments': 0, + 'Protocol': 'string', + 'SegmentLength': 0, + 'TranscodeSeekInfo': 'Auto', + 'Type': 'Audio', + 'VideoCodec': 'string', + }), + ]), + 'UserId': 'string', + 'XmlRootAttributes': list([ + dict({ + 'Name': 'string', + 'Value': 'string', + }), + ]), + }), + 'IconUrl': 'string', + 'MessageCallbackUrl': 'string', + 'PlayableMediaTypes': list([ + 'Video', + ]), + 'SupportedCommands': list([ + 'VolumeSet', + 'Mute', + ]), + 'SupportsContentUploading': True, + 'SupportsMediaControl': True, + 'SupportsPersistentIdentifier': True, + 'SupportsSync': True, + }), + 'client_name': 'Jellyfin for Developers', + 'client_version': '1.0.0', + 'device_id': 'DEVICE-UUID-TWO', + 'device_name': 'JELLYFIN-DEVICE-TWO', + 'id': 'SESSION-UUID-TWO', + 'now_playing': dict({ + 'AirDays': list([ + 'Sunday', + ]), + 'AirTime': 'string', + 'AirsAfterSeasonNumber': 0, + 'AirsBeforeEpisodeNumber': 0, + 'AirsBeforeSeasonNumber': 0, + 'Album': 'string', + 'AlbumArtist': 'string', + 'AlbumArtists': list([ + dict({ + 'Id': '38a5a5bb-dc30-49a2-b175-1de0d1488c43', + 'Name': 'string', + }), + ]), + 'AlbumCount': 0, + 'AlbumId': '21af9851-8e39-43a9-9c47-513d3b9e99fc', + 'AlbumPrimaryImageTag': 'string', + 'Altitude': 0, + 'Aperture': 0, + 'ArtistCount': 0, + 'ArtistItems': list([ + dict({ + 'Id': '38a5a5bb-dc30-49a2-b175-1de0d1488c43', + 'Name': 'string', + }), + ]), + 'Artists': list([ + 'string', + ]), + 'AspectRatio': 'string', + 'Audio': 'Mono', + 'BackdropImageTags': list([ + 'string', + ]), + 'CameraMake': 'string', + 'CameraModel': 'string', + 'CanDelete': True, + 'CanDownload': True, + 'ChannelId': '04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff', + 'ChannelName': 'string', + 'ChannelNumber': 'string', + 'ChannelPrimaryImageTag': 'string', + 'ChannelType': 'TV', + 'Chapters': list([ + dict({ + 'ImageDateModified': '2019-08-24T14:15:22Z', + 'ImagePath': 'string', + 'ImageTag': 'string', + 'Name': 'string', + 'StartPositionTicks': 0, + }), + ]), + 'ChildCount': 0, + 'CollectionType': 'string', + 'CommunityRating': 0, + 'CompletionPercentage': 0, + 'Container': 'string', + 'CriticRating': 0, + 'CumulativeRunTimeTicks': 0, + 'CurrentProgram': dict({ + }), + 'CustomRating': 'string', + 'DateCreated': '2019-08-24T14:15:22Z', + 'DateLastMediaAdded': '2019-08-24T14:15:22Z', + 'DisplayOrder': 'string', + 'DisplayPreferencesId': 'string', + 'EnableMediaSourceDisplay': True, + 'EndDate': '2019-08-24T14:15:22Z', + 'EpisodeCount': 0, + 'EpisodeTitle': 'string', + 'Etag': 'string', + 'ExposureTime': 0, + 'ExternalUrls': list([ + dict({ + 'Name': 'string', + 'Url': 'string', + }), + ]), + 'ExtraType': 'string', + 'FocalLength': 0, + 'ForcedSortName': 'string', + 'GenreItems': list([ + dict({ + 'Id': '38a5a5bb-dc30-49a2-b175-1de0d1488c43', + 'Name': 'string', + }), + ]), + 'Genres': list([ + 'string', + ]), + 'HasSubtitles': True, + 'Height': 0, + 'Id': 'EPISODE-UUID', + 'ImageBlurHashes': dict({ + 'Art': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Backdrop': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Banner': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Box': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'BoxRear': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Chapter': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Disc': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Logo': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Menu': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Primary': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Profile': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Screenshot': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Thumb': dict({ + 'property1': 'string', + 'property2': 'string', + }), + }), + 'ImageOrientation': 'TopLeft', + 'ImageTags': dict({ + 'Backdrop': 'string', + 'property2': 'string', + }), + 'IndexNumber': 0, + 'IndexNumberEnd': 0, + 'IsFolder': False, + 'IsHD': True, + 'IsKids': True, + 'IsLive': True, + 'IsMovie': True, + 'IsNews': True, + 'IsPlaceHolder': True, + 'IsPremiere': True, + 'IsRepeat': True, + 'IsSeries': True, + 'IsSports': True, + 'IsoSpeedRating': 0, + 'IsoType': 'Dvd', + 'Latitude': 0, + 'LocalTrailerCount': 0, + 'LocationType': 'FileSystem', + 'LockData': True, + 'LockedFields': list([ + 'Cast', + ]), + 'Longitude': 0, + 'MediaSourceCount': 0, + 'MediaSources': list([ + dict({ + 'AnalyzeDurationMs': 0, + 'Bitrate': 0, + 'BufferMs': 0, + 'Container': 'string', + 'DefaultAudioStreamIndex': 0, + 'DefaultSubtitleStreamIndex': 0, + 'ETag': 'string', + 'EncoderPath': 'string', + 'EncoderProtocol': 'File', + 'Formats': list([ + 'string', + ]), + 'GenPtsInput': True, + 'Id': 'string', + 'IgnoreDts': True, + 'IgnoreIndex': True, + 'IsInfiniteStream': True, + 'IsRemote': True, + 'IsoType': 'Dvd', + 'LiveStreamId': 'string', + 'MediaAttachments': list([ + dict({ + 'Codec': 'string', + 'CodecTag': 'string', + 'Comment': 'string', + 'DeliveryUrl': 'string', + 'FileName': 'string', + 'Index': 0, + 'MimeType': 'string', + }), + ]), + 'MediaStreams': list([ + dict({ + 'AspectRatio': 'string', + 'AverageFrameRate': 0, + 'BitDepth': 0, + 'BitRate': 0, + 'BlPresentFlag': 0, + 'ChannelLayout': 'string', + 'Channels': 0, + 'Codec': 'string', + 'CodecTag': 'string', + 'CodecTimeBase': 'string', + 'ColorPrimaries': 'string', + 'ColorRange': 'string', + 'ColorSpace': 'string', + 'ColorTransfer': 'string', + 'Comment': 'string', + 'DeliveryMethod': 'Encode', + 'DeliveryUrl': 'string', + 'DisplayTitle': 'string', + 'DvBlSignalCompatibilityId': 0, + 'DvLevel': 0, + 'DvProfile': 0, + 'DvVersionMajor': 0, + 'DvVersionMinor': 0, + 'ElPresentFlag': 0, + 'Height': 0, + 'Index': 0, + 'IsAVC': True, + 'IsAnamorphic': True, + 'IsDefault': True, + 'IsExternal': True, + 'IsExternalUrl': True, + 'IsForced': True, + 'IsInterlaced': True, + 'IsTextSubtitleStream': True, + 'Language': 'string', + 'Level': 0, + 'LocalizedDefault': 'string', + 'LocalizedExternal': 'string', + 'LocalizedForced': 'string', + 'LocalizedUndefined': 'string', + 'NalLengthSize': 'string', + 'PacketLength': 0, + 'Path': 'string', + 'PixelFormat': 'string', + 'Profile': 'string', + 'RealFrameRate': 0, + 'RefFrames': 0, + 'RpuPresentFlag': 0, + 'SampleRate': 0, + 'Score': 0, + 'SupportsExternalStream': True, + 'TimeBase': 'string', + 'Title': 'string', + 'Type': 'Audio', + 'VideoDoViTitle': 'string', + 'VideoRange': 'string', + 'VideoRangeType': 'string', + 'Width': 0, + }), + ]), + 'Name': 'string', + 'OpenToken': 'string', + 'Path': 'string', + 'Protocol': 'File', + 'ReadAtNativeFramerate': True, + 'RequiredHttpHeaders': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'RequiresClosing': True, + 'RequiresLooping': True, + 'RequiresOpening': True, + 'RunTimeTicks': 0, + 'Size': 0, + 'SupportsDirectPlay': True, + 'SupportsDirectStream': True, + 'SupportsProbing': True, + 'SupportsTranscoding': True, + 'Timestamp': 'None', + 'TranscodingContainer': 'string', + 'TranscodingSubProtocol': 'string', + 'TranscodingUrl': 'string', + 'Type': 'Default', + 'Video3DFormat': 'HalfSideBySide', + 'VideoType': 'VideoFile', + }), + ]), + 'MediaStreams': list([ + dict({ + 'AspectRatio': 'string', + 'AverageFrameRate': 0, + 'BitDepth': 0, + 'BitRate': 0, + 'BlPresentFlag': 0, + 'ChannelLayout': 'string', + 'Channels': 0, + 'Codec': 'string', + 'CodecTag': 'string', + 'CodecTimeBase': 'string', + 'ColorPrimaries': 'string', + 'ColorRange': 'string', + 'ColorSpace': 'string', + 'ColorTransfer': 'string', + 'Comment': 'string', + 'DeliveryMethod': 'Encode', + 'DeliveryUrl': 'string', + 'DisplayTitle': 'string', + 'DvBlSignalCompatibilityId': 0, + 'DvLevel': 0, + 'DvProfile': 0, + 'DvVersionMajor': 0, + 'DvVersionMinor': 0, + 'ElPresentFlag': 0, + 'Height': 0, + 'Index': 0, + 'IsAVC': True, + 'IsAnamorphic': True, + 'IsDefault': True, + 'IsExternal': True, + 'IsExternalUrl': True, + 'IsForced': True, + 'IsInterlaced': True, + 'IsTextSubtitleStream': True, + 'Language': 'string', + 'Level': 0, + 'LocalizedDefault': 'string', + 'LocalizedExternal': 'string', + 'LocalizedForced': 'string', + 'LocalizedUndefined': 'string', + 'NalLengthSize': 'string', + 'PacketLength': 0, + 'Path': 'string', + 'PixelFormat': 'string', + 'Profile': 'string', + 'RealFrameRate': 0, + 'RefFrames': 0, + 'RpuPresentFlag': 0, + 'SampleRate': 0, + 'Score': 0, + 'SupportsExternalStream': True, + 'TimeBase': 'string', + 'Title': 'string', + 'Type': 'Audio', + 'VideoDoViTitle': 'string', + 'VideoRange': 'string', + 'VideoRangeType': 'string', + 'Width': 0, + }), + ]), + 'MediaType': 'string', + 'MovieCount': 0, + 'MusicVideoCount': 0, + 'Name': 'MOVIE', + 'Number': 'string', + 'OfficialRating': 'string', + 'OriginalTitle': 'string', + 'Overview': 'string', + 'ParentArtImageTag': 'string', + 'ParentArtItemId': '10c1875b-b82c-48e8-bae9-939a5e68dc2f', + 'ParentBackdropImageTags': list([ + 'string', + ]), + 'ParentBackdropItemId': '', + 'ParentId': '', + 'ParentIndexNumber': 0, + 'ParentLogoImageTag': 'string', + 'ParentLogoItemId': 'c78d400f-de5c-421e-8714-4fb05d387233', + 'ParentPrimaryImageItemId': 'string', + 'ParentPrimaryImageTag': 'string', + 'ParentThumbImageTag': 'string', + 'ParentThumbItemId': 'ae6ff707-333d-4994-be6d-b83ca1b35f46', + 'PartCount': 0, + 'Path': 'string', + 'People': list([ + dict({ + 'Id': '38a5a5bb-dc30-49a2-b175-1de0d1488c43', + 'ImageBlurHashes': dict({ + 'Art': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Backdrop': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Banner': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Box': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'BoxRear': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Chapter': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Disc': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Logo': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Menu': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Primary': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Profile': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Screenshot': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'Thumb': dict({ + 'property1': 'string', + 'property2': 'string', + }), + }), + 'Name': 'string', + 'PrimaryImageTag': 'string', + 'Role': 'string', + 'Type': 'string', + }), + ]), + 'PlayAccess': 'Full', + 'PlaylistItemId': 'string', + 'PreferredMetadataCountryCode': 'string', + 'PreferredMetadataLanguage': 'string', + 'PremiereDate': '2019-08-24T14:15:22Z', + 'PrimaryImageAspectRatio': 0, + 'ProductionLocations': list([ + 'string', + ]), + 'ProductionYear': 0, + 'ProgramCount': 0, + 'ProgramId': 'string', + 'ProviderIds': dict({ + 'property1': 'string', + 'property2': 'string', + }), + 'RecursiveItemCount': 0, + 'RemoteTrailers': list([ + dict({ + 'Name': 'string', + 'Url': 'string', + }), + ]), + 'RunTimeTicks': 2000000000, + 'ScreenshotImageTags': list([ + 'string', + ]), + 'SeasonId': 'SEASON-UUID', + 'SeasonName': 'SEASON', + 'SeriesCount': 0, + 'SeriesId': 'SERIES-UUID', + 'SeriesName': 'SERIES', + 'SeriesPrimaryImageTag': 'string', + 'SeriesStudio': 'HASS', + 'SeriesThumbImageTag': 'string', + 'SeriesTimerId': 'string', + 'ServerId': 'SERVER-UUID', + 'ShutterSpeed': 0, + 'Software': 'string', + 'SongCount': 0, + 'SortName': 'string', + 'SourceType': 'string', + 'SpecialFeatureCount': 0, + 'StartDate': '2019-08-24T14:15:22Z', + 'Status': 'string', + 'Studios': list([ + dict({ + 'Id': '38a5a5bb-dc30-49a2-b175-1de0d1488c43', + 'Name': 'string', + }), + ]), + 'SupportsSync': True, + 'Taglines': list([ + 'string', + ]), + 'Tags': list([ + 'string', + ]), + 'TimerId': 'string', + 'TrailerCount': 0, + 'Type': 'Movie', + 'UserData': dict({ + 'IsFavorite': True, + 'ItemId': 'string', + 'Key': 'string', + 'LastPlayedDate': '2019-08-24T14:15:22Z', + 'Likes': True, + 'PlayCount': 0, + 'PlaybackPositionTicks': 0, + 'Played': True, + 'PlayedPercentage': 0, + 'Rating': 0, + 'UnplayedItemCount': 0, + }), + 'Video3DFormat': 'HalfSideBySide', + 'VideoType': 'VideoFile', + 'Width': 0, + }), + 'play_state': dict({ + 'AudioStreamIndex': 0, + 'CanSeek': True, + 'IsMuted': False, + 'IsPaused': False, + 'LiveStreamId': 'string', + 'MediaSourceId': 'string', + 'PlayMethod': 'Transcode', + 'PositionTicks': 230000000, + 'RepeatMode': 'RepeatNone', + 'SubtitleStreamIndex': 0, + 'VolumeLevel': 55, + }), + 'user_id': 'USER-UUID-TWO', + }), + dict({ + 'capabilities': dict({ + 'AppStoreUrl': 'string', + 'DeviceProfile': dict({ + 'AlbumArtPn': 'string', + 'CodecProfiles': list([ + dict({ + 'ApplyConditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Codec': 'string', + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'Type': 'Video', + }), + ]), + 'ContainerProfiles': list([ + dict({ + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'Type': 'Audio', + }), + ]), + 'DirectPlayProfiles': list([ + dict({ + 'AudioCodec': 'string', + 'Container': 'string', + 'Type': 'Audio', + 'VideoCodec': 'string', + }), + ]), + 'EnableAlbumArtInDidl': False, + 'EnableMSMediaReceiverRegistrar': False, + 'EnableSingleAlbumArtLimit': False, + 'EnableSingleSubtitleLimit': False, + 'FriendlyName': 'string', + 'Id': 'string', + 'Identification': dict({ + 'FriendlyName': 'string', + 'Headers': list([ + dict({ + 'Match': 'Equals', + 'Name': 'string', + 'Value': 'string', + }), + ]), + 'Manufacturer': 'string', + 'ManufacturerUrl': 'string', + 'ModelDescription': 'string', + 'ModelName': 'string', + 'ModelNumber': 'string', + 'ModelUrl': 'string', + 'SerialNumber': 'string', + }), + 'IgnoreTranscodeByteRangeRequests': False, + 'Manufacturer': 'string', + 'ManufacturerUrl': 'string', + 'MaxAlbumArtHeight': 0, + 'MaxAlbumArtWidth': 0, + 'MaxIconHeight': 0, + 'MaxIconWidth': 0, + 'MaxStaticBitrate': 0, + 'MaxStaticMusicBitrate': 0, + 'MaxStreamingBitrate': 0, + 'ModelDescription': 'string', + 'ModelName': 'string', + 'ModelNumber': 'string', + 'ModelUrl': 'string', + 'MusicStreamingTranscodingBitrate': 0, + 'Name': 'string', + 'ProtocolInfo': 'string', + 'RequiresPlainFolders': False, + 'RequiresPlainVideoItems': False, + 'ResponseProfiles': list([ + dict({ + 'AudioCodec': 'string', + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'MimeType': 'string', + 'OrgPn': 'string', + 'Type': 'Audio', + 'VideoCodec': 'string', + }), + ]), + 'SerialNumber': 'string', + 'SonyAggregationFlags': 'string', + 'SubtitleProfiles': list([ + dict({ + 'Container': 'string', + 'DidlMode': 'string', + 'Format': 'string', + 'Language': 'string', + 'Method': 'Encode', + }), + ]), + 'SupportedMediaTypes': 'string', + 'TimelineOffsetSeconds': 0, + 'TranscodingProfiles': list([ + dict({ + 'AudioCodec': 'string', + 'BreakOnNonKeyFrames': False, + 'Conditions': list([ + dict({ + 'Condition': 'Equals', + 'IsRequired': True, + 'Property': 'AudioChannels', + 'Value': 'string', + }), + ]), + 'Container': 'string', + 'Context': 'Streaming', + 'CopyTimestamps': False, + 'EnableMpegtsM2TsMode': False, + 'EnableSubtitlesInManifest': False, + 'EstimateContentLength': False, + 'MaxAudioChannels': 'string', + 'MinSegments': 0, + 'Protocol': 'string', + 'SegmentLength': 0, + 'TranscodeSeekInfo': 'Auto', + 'Type': 'Audio', + 'VideoCodec': 'string', + }), + ]), + 'UserId': 'string', + 'XmlRootAttributes': list([ + dict({ + 'Name': 'string', + 'Value': 'string', + }), + ]), + }), + 'IconUrl': 'string', + 'MessageCallbackUrl': 'string', + 'PlayableMediaTypes': list([ + 'Video', + ]), + 'SupportedCommands': list([ + 'MoveUp', + ]), + 'SupportsContentUploading': False, + 'SupportsMediaControl': False, + 'SupportsPersistentIdentifier': False, + 'SupportsSync': True, + }), + 'client_name': 'Jellyfin for Developers', + 'client_version': '2.0.0', + 'device_id': 'DEVICE-UUID-THREE', + 'device_name': 'JELLYFIN-DEVICE-THREE', + 'id': 'SESSION-UUID-THREE', + 'now_playing': None, + 'play_state': dict({ + 'AudioStreamIndex': 0, + 'CanSeek': True, + 'IsMuted': True, + 'IsPaused': False, + 'LiveStreamId': 'string', + 'MediaSourceId': 'string', + 'PlayMethod': 'Transcode', + 'PositionTicks': 0, + 'RepeatMode': 'RepeatNone', + 'SubtitleStreamIndex': 0, + 'VolumeLevel': 0, + }), + 'user_id': 'USER-UUID', + }), + dict({ + 'capabilities': dict({ + 'PlayableMediaTypes': list([ + 'Audio', + 'Video', + ]), + 'SupportedCommands': list([ + 'MoveUp', + 'MoveDown', + 'MoveLeft', + 'MoveRight', + 'PageUp', + 'PageDown', + 'PreviousLetter', + 'NextLetter', + 'ToggleOsd', + 'ToggleContextMenu', + 'Select', + 'Back', + 'SendKey', + 'SendString', + 'GoHome', + 'GoToSettings', + 'VolumeUp', + 'VolumeDown', + 'Mute', + 'Unmute', + 'ToggleMute', + 'SetVolume', + 'SetAudioStreamIndex', + 'SetSubtitleStreamIndex', + 'DisplayContent', + 'GoToSearch', + 'DisplayMessage', + 'SetRepeatMode', + 'SetShuffleQueue', + 'ChannelUp', + 'ChannelDown', + 'PlayMediaSource', + 'PlayTrailers', + ]), + 'SupportsContentUploading': False, + 'SupportsMediaControl': True, + 'SupportsPersistentIdentifier': False, + 'SupportsSync': False, + }), + 'client_name': 'Jellyfin Android', + 'client_version': '2.4.4', + 'device_id': 'DEVICE-UUID-FOUR', + 'device_name': 'JELLYFIN DEVICE FOUR', + 'id': 'SESSION-UUID-FOUR', + 'now_playing': dict({ + 'Album': 'ALBUM', + 'AlbumArtist': 'Album Artist', + 'AlbumArtists': list([ + dict({ + 'Id': '9a65b2c222ddb34e51f5cae360fad3a1', + 'Name': 'Album Artist', + }), + ]), + 'AlbumId': 'ALBUM-UUID', + 'ArtistItems': list([ + dict({ + 'Id': '1d864900526d9a9513b489f1cc28f8ca', + 'Name': 'Contributing Artist', + }), + ]), + 'Artists': list([ + 'Contributing Artist', + ]), + 'BackdropImageTags': list([ + ]), + 'ChannelId': None, + 'DateCreated': '2022-10-19T03:09:11.392057Z', + 'EnableMediaSourceDisplay': True, + 'ExternalUrls': list([ + ]), + 'GenreItems': list([ + ]), + 'Genres': list([ + ]), + 'Id': 'MUSIC-UUID', + 'ImageBlurHashes': dict({ + }), + 'ImageTags': dict({ + }), + 'IndexNumber': 1, + 'IsFolder': False, + 'LocalTrailerCount': 0, + 'LocationType': 'FileSystem', + 'MediaStreams': list([ + dict({ + 'BitRate': 256000, + 'ChannelLayout': 'stereo', + 'Channels': 2, + 'Codec': 'mp3', + 'DisplayTitle': 'MP3 - Stereo', + 'Index': 0, + 'IsDefault': False, + 'IsExternal': False, + 'IsForced': False, + 'IsInterlaced': False, + 'IsTextSubtitleStream': False, + 'Level': 0, + 'SampleRate': 44100, + 'SupportsExternalStream': False, + 'TimeBase': '1/14112000', + 'Type': 'Audio', + }), + ]), + 'MediaType': 'Audio', + 'Name': 'MUSIC FILE', + 'ParentId': '4c0343ed1bbcda094178076230051b7e', + 'Path': 'string', + 'ProviderIds': dict({ + }), + 'RunTimeTicks': 736391552, + 'ServerId': 'SERVER-UUID', + 'SpecialFeatureCount': 0, + 'Studios': list([ + ]), + 'Taglines': list([ + ]), + 'Type': 'Audio', + }), + 'play_state': dict({ + 'CanSeek': True, + 'IsMuted': False, + 'IsPaused': False, + 'MediaSourceId': 'a744119f757f88858f95aab1628708c4', + 'PlayMethod': 'DirectPlay', + 'PositionTicks': 220246970, + 'RepeatMode': 'RepeatNone', + 'VolumeLevel': 100, + }), + 'user_id': 'USER-UUID-TWO', + }), + ]), + }) +# --- diff --git a/tests/components/jellyfin/test_diagnostics.py b/tests/components/jellyfin/test_diagnostics.py index 15561f5294c..b56d864eaac 100644 --- a/tests/components/jellyfin/test_diagnostics.py +++ b/tests/components/jellyfin/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Jellyfin diagnostics.""" +from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant @@ -11,601 +12,12 @@ async def test_diagnostics( hass: HomeAssistant, init_integration: MockConfigEntry, hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test generating diagnostics for a config entry.""" - entry = init_integration + data = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) - diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert diag - assert diag["entry"] == { - "title": "Jellyfin", - "data": { - "url": "https://example.com", - "username": "test-username", - "password": "**REDACTED**", - "client_device_id": entry.entry_id, - }, - } - assert diag["server"] == { - "id": "SERVER-UUID", - "name": "JELLYFIN-SERVER", - "version": None, - } - assert diag["sessions"] - assert len(diag["sessions"]) == 4 - assert diag["sessions"][0] == { - "id": "SESSION-UUID", - "user_id": "08ba1929-681e-4b24-929b-9245852f65c0", - "device_id": "DEVICE-UUID", - "device_name": "JELLYFIN-DEVICE", - "client_name": "Jellyfin for Developers", - "client_version": "1.0.0", - "capabilities": { - "PlayableMediaTypes": ["Video"], - "SupportedCommands": ["VolumeSet", "Mute"], - "SupportsMediaControl": True, - "SupportsContentUploading": True, - "MessageCallbackUrl": "string", - "SupportsPersistentIdentifier": True, - "SupportsSync": True, - "DeviceProfile": { - "Name": "string", - "Id": "string", - "Identification": { - "FriendlyName": "string", - "ModelNumber": "string", - "SerialNumber": "string", - "ModelName": "string", - "ModelDescription": "string", - "ModelUrl": "string", - "Manufacturer": "string", - "ManufacturerUrl": "string", - "Headers": [ - {"Name": "string", "Value": "string", "Match": "Equals"} - ], - }, - "FriendlyName": "string", - "Manufacturer": "string", - "ManufacturerUrl": "string", - "ModelName": "string", - "ModelDescription": "string", - "ModelNumber": "string", - "ModelUrl": "string", - "SerialNumber": "string", - "EnableAlbumArtInDidl": False, - "EnableSingleAlbumArtLimit": False, - "EnableSingleSubtitleLimit": False, - "SupportedMediaTypes": "string", - "UserId": "string", - "AlbumArtPn": "string", - "MaxAlbumArtWidth": 0, - "MaxAlbumArtHeight": 0, - "MaxIconWidth": 0, - "MaxIconHeight": 0, - "MaxStreamingBitrate": 0, - "MaxStaticBitrate": 0, - "MusicStreamingTranscodingBitrate": 0, - "MaxStaticMusicBitrate": 0, - "SonyAggregationFlags": "string", - "ProtocolInfo": "string", - "TimelineOffsetSeconds": 0, - "RequiresPlainVideoItems": False, - "RequiresPlainFolders": False, - "EnableMSMediaReceiverRegistrar": False, - "IgnoreTranscodeByteRangeRequests": False, - "XmlRootAttributes": [{"Name": "string", "Value": "string"}], - "DirectPlayProfiles": [ - { - "Container": "string", - "AudioCodec": "string", - "VideoCodec": "string", - "Type": "Audio", - } - ], - "TranscodingProfiles": [ - { - "Container": "string", - "Type": "Audio", - "VideoCodec": "string", - "AudioCodec": "string", - "Protocol": "string", - "EstimateContentLength": False, - "EnableMpegtsM2TsMode": False, - "TranscodeSeekInfo": "Auto", - "CopyTimestamps": False, - "Context": "Streaming", - "EnableSubtitlesInManifest": False, - "MaxAudioChannels": "string", - "MinSegments": 0, - "SegmentLength": 0, - "BreakOnNonKeyFrames": False, - "Conditions": [ - { - "Condition": "Equals", - "Property": "AudioChannels", - "Value": "string", - "IsRequired": True, - } - ], - } - ], - "ContainerProfiles": [ - { - "Type": "Audio", - "Conditions": [ - { - "Condition": "Equals", - "Property": "AudioChannels", - "Value": "string", - "IsRequired": True, - } - ], - "Container": "string", - } - ], - "CodecProfiles": [ - { - "Type": "Video", - "Conditions": [ - { - "Condition": "Equals", - "Property": "AudioChannels", - "Value": "string", - "IsRequired": True, - } - ], - "ApplyConditions": [ - { - "Condition": "Equals", - "Property": "AudioChannels", - "Value": "string", - "IsRequired": True, - } - ], - "Codec": "string", - "Container": "string", - } - ], - "ResponseProfiles": [ - { - "Container": "string", - "AudioCodec": "string", - "VideoCodec": "string", - "Type": "Audio", - "OrgPn": "string", - "MimeType": "string", - "Conditions": [ - { - "Condition": "Equals", - "Property": "AudioChannels", - "Value": "string", - "IsRequired": True, - } - ], - } - ], - "SubtitleProfiles": [ - { - "Format": "string", - "Method": "Encode", - "DidlMode": "string", - "Language": "string", - "Container": "string", - } - ], - }, - "AppStoreUrl": "string", - "IconUrl": "string", - }, - "now_playing": { - "Name": "EPISODE", - "OriginalTitle": "string", - "ServerId": "SERVER-UUID", - "Id": "EPISODE-UUID", - "Etag": "string", - "SourceType": "string", - "PlaylistItemId": "string", - "DateCreated": "2019-08-24T14:15:22Z", - "DateLastMediaAdded": "2019-08-24T14:15:22Z", - "ExtraType": "string", - "AirsBeforeSeasonNumber": 0, - "AirsAfterSeasonNumber": 0, - "AirsBeforeEpisodeNumber": 0, - "CanDelete": True, - "CanDownload": True, - "HasSubtitles": True, - "PreferredMetadataLanguage": "string", - "PreferredMetadataCountryCode": "string", - "SupportsSync": True, - "Container": "string", - "SortName": "string", - "ForcedSortName": "string", - "Video3DFormat": "HalfSideBySide", - "PremiereDate": "2019-08-24T14:15:22Z", - "ExternalUrls": [{"Name": "string", "Url": "string"}], - "MediaSources": [ - { - "Protocol": "File", - "Id": "string", - "Path": "string", - "EncoderPath": "string", - "EncoderProtocol": "File", - "Type": "Default", - "Container": "string", - "Size": 0, - "Name": "string", - "IsRemote": True, - "ETag": "string", - "RunTimeTicks": 0, - "ReadAtNativeFramerate": True, - "IgnoreDts": True, - "IgnoreIndex": True, - "GenPtsInput": True, - "SupportsTranscoding": True, - "SupportsDirectStream": True, - "SupportsDirectPlay": True, - "IsInfiniteStream": True, - "RequiresOpening": True, - "OpenToken": "string", - "RequiresClosing": True, - "LiveStreamId": "string", - "BufferMs": 0, - "RequiresLooping": True, - "SupportsProbing": True, - "VideoType": "VideoFile", - "IsoType": "Dvd", - "Video3DFormat": "HalfSideBySide", - "MediaStreams": [ - { - "Codec": "string", - "CodecTag": "string", - "Language": "string", - "ColorRange": "string", - "ColorSpace": "string", - "ColorTransfer": "string", - "ColorPrimaries": "string", - "DvVersionMajor": 0, - "DvVersionMinor": 0, - "DvProfile": 0, - "DvLevel": 0, - "RpuPresentFlag": 0, - "ElPresentFlag": 0, - "BlPresentFlag": 0, - "DvBlSignalCompatibilityId": 0, - "Comment": "string", - "TimeBase": "string", - "CodecTimeBase": "string", - "Title": "string", - "VideoRange": "string", - "VideoRangeType": "string", - "VideoDoViTitle": "string", - "LocalizedUndefined": "string", - "LocalizedDefault": "string", - "LocalizedForced": "string", - "LocalizedExternal": "string", - "DisplayTitle": "string", - "NalLengthSize": "string", - "IsInterlaced": True, - "IsAVC": True, - "ChannelLayout": "string", - "BitRate": 0, - "BitDepth": 0, - "RefFrames": 0, - "PacketLength": 0, - "Channels": 0, - "SampleRate": 0, - "IsDefault": True, - "IsForced": True, - "Height": 0, - "Width": 0, - "AverageFrameRate": 0, - "RealFrameRate": 0, - "Profile": "string", - "Type": "Audio", - "AspectRatio": "string", - "Index": 0, - "Score": 0, - "IsExternal": True, - "DeliveryMethod": "Encode", - "DeliveryUrl": "string", - "IsExternalUrl": True, - "IsTextSubtitleStream": True, - "SupportsExternalStream": True, - "Path": "string", - "PixelFormat": "string", - "Level": 0, - "IsAnamorphic": True, - } - ], - "MediaAttachments": [ - { - "Codec": "string", - "CodecTag": "string", - "Comment": "string", - "Index": 0, - "FileName": "string", - "MimeType": "string", - "DeliveryUrl": "string", - } - ], - "Formats": ["string"], - "Bitrate": 0, - "Timestamp": "None", - "RequiredHttpHeaders": { - "property1": "string", - "property2": "string", - }, - "TranscodingUrl": "string", - "TranscodingSubProtocol": "string", - "TranscodingContainer": "string", - "AnalyzeDurationMs": 0, - "DefaultAudioStreamIndex": 0, - "DefaultSubtitleStreamIndex": 0, - } - ], - "CriticRating": 0, - "ProductionLocations": ["string"], - "Path": "string", - "EnableMediaSourceDisplay": True, - "OfficialRating": "string", - "CustomRating": "string", - "ChannelId": "04b0b2a5-93cb-474d-8ea9-3df0f84eb0ff", - "ChannelName": "string", - "Overview": "string", - "Taglines": ["string"], - "Genres": ["string"], - "CommunityRating": 0, - "CumulativeRunTimeTicks": 0, - "RunTimeTicks": 600000000, - "PlayAccess": "Full", - "AspectRatio": "string", - "ProductionYear": 0, - "IsPlaceHolder": True, - "Number": "string", - "ChannelNumber": "string", - "IndexNumber": 3, - "IndexNumberEnd": 0, - "ParentIndexNumber": 1, - "RemoteTrailers": [{"Url": "string", "Name": "string"}], - "ProviderIds": {"property1": "string", "property2": "string"}, - "IsHD": True, - "IsFolder": False, - "ParentId": "PARENT-UUID", - "Type": "Episode", - "People": [ - { - "Name": "string", - "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43", - "Role": "string", - "Type": "string", - "PrimaryImageTag": "string", - "ImageBlurHashes": { - "Primary": {"property1": "string", "property2": "string"}, - "Art": {"property1": "string", "property2": "string"}, - "Backdrop": {"property1": "string", "property2": "string"}, - "Banner": {"property1": "string", "property2": "string"}, - "Logo": {"property1": "string", "property2": "string"}, - "Thumb": {"property1": "string", "property2": "string"}, - "Disc": {"property1": "string", "property2": "string"}, - "Box": {"property1": "string", "property2": "string"}, - "Screenshot": {"property1": "string", "property2": "string"}, - "Menu": {"property1": "string", "property2": "string"}, - "Chapter": {"property1": "string", "property2": "string"}, - "BoxRear": {"property1": "string", "property2": "string"}, - "Profile": {"property1": "string", "property2": "string"}, - }, - } - ], - "Studios": [ - {"Name": "string", "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43"} - ], - "GenreItems": [ - {"Name": "string", "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43"} - ], - "ParentLogoItemId": "c78d400f-de5c-421e-8714-4fb05d387233", - "ParentBackdropItemId": "c22fd826-17fc-44f4-9b04-1eb3e8fb9173", - "ParentBackdropImageTags": ["string"], - "LocalTrailerCount": 0, - "UserData": { - "Rating": 0, - "PlayedPercentage": 0, - "UnplayedItemCount": 0, - "PlaybackPositionTicks": 0, - "PlayCount": 0, - "IsFavorite": True, - "Likes": True, - "LastPlayedDate": "2019-08-24T14:15:22Z", - "Played": True, - "Key": "string", - "ItemId": "string", - }, - "RecursiveItemCount": 0, - "ChildCount": 0, - "SeriesName": "SERIES", - "SeriesId": "SERIES-UUID", - "SeasonId": "SEASON-UUID", - "SpecialFeatureCount": 0, - "DisplayPreferencesId": "string", - "Status": "string", - "AirTime": "string", - "AirDays": ["Sunday"], - "Tags": ["string"], - "PrimaryImageAspectRatio": 0, - "Artists": ["string"], - "ArtistItems": [ - {"Name": "string", "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43"} - ], - "Album": "string", - "CollectionType": "string", - "DisplayOrder": "string", - "AlbumId": "21af9851-8e39-43a9-9c47-513d3b9e99fc", - "AlbumPrimaryImageTag": "string", - "SeriesPrimaryImageTag": "string", - "AlbumArtist": "string", - "AlbumArtists": [ - {"Name": "string", "Id": "38a5a5bb-dc30-49a2-b175-1de0d1488c43"} - ], - "SeasonName": "SEASON", - "MediaStreams": [ - { - "Codec": "string", - "CodecTag": "string", - "Language": "string", - "ColorRange": "string", - "ColorSpace": "string", - "ColorTransfer": "string", - "ColorPrimaries": "string", - "DvVersionMajor": 0, - "DvVersionMinor": 0, - "DvProfile": 0, - "DvLevel": 0, - "RpuPresentFlag": 0, - "ElPresentFlag": 0, - "BlPresentFlag": 0, - "DvBlSignalCompatibilityId": 0, - "Comment": "string", - "TimeBase": "string", - "CodecTimeBase": "string", - "Title": "string", - "VideoRange": "string", - "VideoRangeType": "string", - "VideoDoViTitle": "string", - "LocalizedUndefined": "string", - "LocalizedDefault": "string", - "LocalizedForced": "string", - "LocalizedExternal": "string", - "DisplayTitle": "string", - "NalLengthSize": "string", - "IsInterlaced": True, - "IsAVC": True, - "ChannelLayout": "string", - "BitRate": 0, - "BitDepth": 0, - "RefFrames": 0, - "PacketLength": 0, - "Channels": 0, - "SampleRate": 0, - "IsDefault": True, - "IsForced": True, - "Height": 0, - "Width": 0, - "AverageFrameRate": 0, - "RealFrameRate": 0, - "Profile": "string", - "Type": "Audio", - "AspectRatio": "string", - "Index": 0, - "Score": 0, - "IsExternal": True, - "DeliveryMethod": "Encode", - "DeliveryUrl": "string", - "IsExternalUrl": True, - "IsTextSubtitleStream": True, - "SupportsExternalStream": True, - "Path": "string", - "PixelFormat": "string", - "Level": 0, - "IsAnamorphic": True, - } - ], - "VideoType": "VideoFile", - "PartCount": 0, - "MediaSourceCount": 0, - "ImageTags": {"property1": "string", "property2": "string"}, - "BackdropImageTags": ["string"], - "ScreenshotImageTags": ["string"], - "ParentLogoImageTag": "string", - "ParentArtItemId": "10c1875b-b82c-48e8-bae9-939a5e68dc2f", - "ParentArtImageTag": "string", - "SeriesThumbImageTag": "string", - "ImageBlurHashes": { - "Primary": {"property1": "string", "property2": "string"}, - "Art": {"property1": "string", "property2": "string"}, - "Backdrop": {"property1": "string", "property2": "string"}, - "Banner": {"property1": "string", "property2": "string"}, - "Logo": {"property1": "string", "property2": "string"}, - "Thumb": {"property1": "string", "property2": "string"}, - "Disc": {"property1": "string", "property2": "string"}, - "Box": {"property1": "string", "property2": "string"}, - "Screenshot": {"property1": "string", "property2": "string"}, - "Menu": {"property1": "string", "property2": "string"}, - "Chapter": {"property1": "string", "property2": "string"}, - "BoxRear": {"property1": "string", "property2": "string"}, - "Profile": {"property1": "string", "property2": "string"}, - }, - "SeriesStudio": "HASS", - "ParentThumbItemId": "ae6ff707-333d-4994-be6d-b83ca1b35f46", - "ParentThumbImageTag": "string", - "ParentPrimaryImageItemId": "string", - "ParentPrimaryImageTag": "string", - "Chapters": [ - { - "StartPositionTicks": 0, - "Name": "string", - "ImagePath": "string", - "ImageDateModified": "2019-08-24T14:15:22Z", - "ImageTag": "string", - } - ], - "LocationType": "FileSystem", - "IsoType": "Dvd", - "MediaType": "string", - "EndDate": "2019-08-24T14:15:22Z", - "LockedFields": ["Cast"], - "TrailerCount": 0, - "MovieCount": 0, - "SeriesCount": 0, - "ProgramCount": 0, - "EpisodeCount": 0, - "SongCount": 0, - "AlbumCount": 0, - "ArtistCount": 0, - "MusicVideoCount": 0, - "LockData": True, - "Width": 0, - "Height": 0, - "CameraMake": "string", - "CameraModel": "string", - "Software": "string", - "ExposureTime": 0, - "FocalLength": 0, - "ImageOrientation": "TopLeft", - "Aperture": 0, - "ShutterSpeed": 0, - "Latitude": 0, - "Longitude": 0, - "Altitude": 0, - "IsoSpeedRating": 0, - "SeriesTimerId": "string", - "ProgramId": "string", - "ChannelPrimaryImageTag": "string", - "StartDate": "2019-08-24T14:15:22Z", - "CompletionPercentage": 0, - "IsRepeat": True, - "EpisodeTitle": "string", - "ChannelType": "TV", - "Audio": "Mono", - "IsMovie": True, - "IsSports": True, - "IsSeries": True, - "IsLive": True, - "IsNews": True, - "IsKids": True, - "IsPremiere": True, - "TimerId": "string", - "CurrentProgram": {}, - }, - "play_state": { - "PositionTicks": 100000000, - "CanSeek": True, - "IsPaused": True, - "IsMuted": True, - "VolumeLevel": 0, - "AudioStreamIndex": 0, - "SubtitleStreamIndex": 0, - "MediaSourceId": "string", - "PlayMethod": "Transcode", - "RepeatMode": "RepeatNone", - "LiveStreamId": "string", - }, - } + assert data["entry"]["data"]["client_device_id"] == init_integration.entry_id + data["entry"]["data"]["client_device_id"] = "entry-id" + + assert data == snapshot From 973928ffe9cc97cd83d584dd9626fe4df08b941f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 11:17:43 +0200 Subject: [PATCH 0666/1151] Use snapshot assertion for Airvisual diagnostics (#98728) --- tests/components/airvisual/conftest.py | 1 + .../airvisual/snapshots/test_diagnostics.ambr | 52 +++++++++++++++++++ .../components/airvisual/test_diagnostics.py | 52 +++---------------- 3 files changed, 60 insertions(+), 45 deletions(-) create mode 100644 tests/components/airvisual/snapshots/test_diagnostics.ambr diff --git a/tests/components/airvisual/conftest.py b/tests/components/airvisual/conftest.py index bdd325d4739..58b8864ea9c 100644 --- a/tests/components/airvisual/conftest.py +++ b/tests/components/airvisual/conftest.py @@ -69,6 +69,7 @@ def config_entry_fixture(hass, config, config_entry_version, integration_type): """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, + entry_id="3bd2acb0e4f0476d40865546d0d91921", unique_id=async_get_geography_id(config), data={**config, CONF_INTEGRATION_TYPE: integration_type}, options={CONF_SHOW_ON_MAP: True}, diff --git a/tests/components/airvisual/snapshots/test_diagnostics.ambr b/tests/components/airvisual/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..c805c5f9cb7 --- /dev/null +++ b/tests/components/airvisual/snapshots/test_diagnostics.ambr @@ -0,0 +1,52 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'city': '**REDACTED**', + 'country': '**REDACTED**', + 'current': dict({ + 'pollution': dict({ + 'aqicn': 18, + 'aqius': 52, + 'maincn': 'p2', + 'mainus': 'p2', + 'ts': '2021-09-04T00:00:00.000Z', + }), + 'weather': dict({ + 'hu': 45, + 'ic': '10d', + 'pr': 999, + 'tp': 23, + 'ts': '2021-09-03T21:00:00.000Z', + 'wd': 252, + 'ws': 0.45, + }), + }), + 'location': dict({ + 'coordinates': '**REDACTED**', + 'type': 'Point', + }), + 'state': '**REDACTED**', + }), + 'entry': dict({ + 'data': dict({ + 'api_key': '**REDACTED**', + 'integration_type': 'Geographical Location by Latitude/Longitude', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'airvisual', + 'entry_id': '3bd2acb0e4f0476d40865546d0d91921', + 'options': dict({ + 'show_on_map': True, + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': '**REDACTED**', + 'version': 3, + }), + }) +# --- diff --git a/tests/components/airvisual/test_diagnostics.py b/tests/components/airvisual/test_diagnostics.py index 94d22e7f61c..32a083ec985 100644 --- a/tests/components/airvisual/test_diagnostics.py +++ b/tests/components/airvisual/test_diagnostics.py @@ -1,5 +1,6 @@ """Test AirVisual diagnostics.""" -from homeassistant.components.diagnostics import REDACTED +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -11,49 +12,10 @@ async def test_entry_diagnostics( config_entry, hass_client: ClientSessionGenerator, setup_config_entry, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "entry_id": config_entry.entry_id, - "version": 3, - "domain": "airvisual", - "title": REDACTED, - "data": { - "integration_type": "Geographical Location by Latitude/Longitude", - "api_key": REDACTED, - "latitude": REDACTED, - "longitude": REDACTED, - }, - "options": {"show_on_map": True}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - }, - "data": { - "city": REDACTED, - "state": REDACTED, - "country": REDACTED, - "location": {"type": "Point", "coordinates": REDACTED}, - "current": { - "weather": { - "ts": "2021-09-03T21:00:00.000Z", - "tp": 23, - "pr": 999, - "hu": 45, - "ws": 0.45, - "wd": 252, - "ic": "10d", - }, - "pollution": { - "ts": "2021-09-04T00:00:00.000Z", - "aqius": 52, - "mainus": "p2", - "aqicn": 18, - "maincn": "p2", - }, - }, - }, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From 8f9529d376cc2b517af4bea3173ab673206495db Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 11:18:18 +0200 Subject: [PATCH 0667/1151] Use snapshot assertion for Forecast solar diagnostics (#98723) --- .../snapshots/test_diagnostics.ambr | 44 ++++++++++++++++ .../forecast_solar/test_diagnostics.py | 50 +++---------------- 2 files changed, 50 insertions(+), 44 deletions(-) create mode 100644 tests/components/forecast_solar/snapshots/test_diagnostics.ambr diff --git a/tests/components/forecast_solar/snapshots/test_diagnostics.ambr b/tests/components/forecast_solar/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..147c10c1793 --- /dev/null +++ b/tests/components/forecast_solar/snapshots/test_diagnostics.ambr @@ -0,0 +1,44 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'account': dict({ + 'rate_limit': 60, + 'timezone': 'Europe/Amsterdam', + 'type': 'public', + }), + 'data': dict({ + 'energy_current_hour': 800000, + 'energy_production_today': 100000, + 'energy_production_today_remaining': 50000, + 'energy_production_tomorrow': 200000, + 'power_production_now': 300000, + 'watts': dict({ + '2021-06-27T13:00:00-07:00': 10, + '2022-06-27T13:00:00-07:00': 100, + }), + 'wh_days': dict({ + '2021-06-27T13:00:00-07:00': 20, + '2022-06-27T13:00:00-07:00': 200, + }), + 'wh_period': dict({ + '2021-06-27T13:00:00-07:00': 30, + '2022-06-27T13:00:00-07:00': 300, + }), + }), + 'entry': dict({ + 'data': dict({ + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'options': dict({ + 'api_key': '**REDACTED**', + 'azimuth': 190, + 'damping': 0.5, + 'declination': 30, + 'inverter_size': 2000, + 'modules power': 5100, + }), + 'title': 'Green House', + }), + }) +# --- diff --git a/tests/components/forecast_solar/test_diagnostics.py b/tests/components/forecast_solar/test_diagnostics.py index 4900c3bdb32..e72f2d7d9dc 100644 --- a/tests/components/forecast_solar/test_diagnostics.py +++ b/tests/components/forecast_solar/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the Forecast.Solar integration.""" +from syrupy import SnapshotAssertion -from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -12,48 +12,10 @@ async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - assert await get_diagnostics_for_config_entry( - hass, hass_client, init_integration - ) == { - "entry": { - "title": "Green House", - "data": { - "latitude": REDACTED, - "longitude": REDACTED, - }, - "options": { - "api_key": REDACTED, - "declination": 30, - "azimuth": 190, - "modules power": 5100, - "damping": 0.5, - "inverter_size": 2000, - }, - }, - "data": { - "energy_production_today": 100000, - "energy_production_today_remaining": 50000, - "energy_production_tomorrow": 200000, - "energy_current_hour": 800000, - "power_production_now": 300000, - "watts": { - "2021-06-27T13:00:00-07:00": 10, - "2022-06-27T13:00:00-07:00": 100, - }, - "wh_days": { - "2021-06-27T13:00:00-07:00": 20, - "2022-06-27T13:00:00-07:00": 200, - }, - "wh_period": { - "2021-06-27T13:00:00-07:00": 30, - "2022-06-27T13:00:00-07:00": 300, - }, - }, - "account": { - "type": "public", - "rate_limit": 60, - "timezone": "Europe/Amsterdam", - }, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) From 1fca665b77514219948e3926c78e86918783eebd Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 21 Aug 2023 04:48:27 -0500 Subject: [PATCH 0668/1151] Use snapshot assertion for Roku diagnostics (#98731) * use snapshot assertion for Roku diagnostics * Delete roku3-diagnostics-data.json * fix state time variation. add snapshot. --- .../roku/fixtures/roku3-diagnostics-data.json | 86 ---------------- .../roku/snapshots/test_diagnostics.ambr | 98 +++++++++++++++++++ tests/components/roku/test_diagnostics.py | 33 +++---- 3 files changed, 109 insertions(+), 108 deletions(-) delete mode 100644 tests/components/roku/fixtures/roku3-diagnostics-data.json create mode 100644 tests/components/roku/snapshots/test_diagnostics.ambr diff --git a/tests/components/roku/fixtures/roku3-diagnostics-data.json b/tests/components/roku/fixtures/roku3-diagnostics-data.json deleted file mode 100644 index a3084b010c9..00000000000 --- a/tests/components/roku/fixtures/roku3-diagnostics-data.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "app": { - "app_id": null, - "name": "Roku", - "screensaver": false, - "version": null - }, - "apps": [ - { - "app_id": "11", - "name": "Roku Channel Store", - "screensaver": false, - "version": null - }, - { - "app_id": "12", - "name": "Netflix", - "screensaver": false, - "version": null - }, - { - "app_id": "13", - "name": "Amazon Video on Demand", - "screensaver": false, - "version": null - }, - { - "app_id": "14", - "name": "MLB.TV®", - "screensaver": false, - "version": null - }, - { - "app_id": "26", - "name": "Free FrameChannel Service", - "screensaver": false, - "version": null - }, - { - "app_id": "27", - "name": "Mediafly", - "screensaver": false, - "version": null - }, - { - "app_id": "28", - "name": "Pandora", - "screensaver": false, - "version": null - }, - { - "app_id": "74519", - "name": "Pluto TV - It's Free TV", - "screensaver": false, - "version": "5.2.0" - } - ], - "channel": null, - "channels": [], - "info": { - "brand": "Roku", - "device_location": null, - "device_type": "box", - "ethernet_mac": "b0:a7:37:96:4d:fa", - "ethernet_support": true, - "headphones_connected": false, - "model_name": "Roku 3", - "model_number": "4200X", - "name": "My Roku 3", - "network_name": null, - "network_type": "ethernet", - "serial_number": "1GU48T017973", - "supports_airplay": false, - "supports_find_remote": false, - "supports_private_listening": false, - "supports_wake_on_wlan": false, - "version": "7.5.0", - "wifi_mac": "b0:a7:37:96:4d:fb" - }, - "media": null, - "state": { - "at": "2022-01-23T21:05:03.154737", - "available": true, - "standby": false - } -} diff --git a/tests/components/roku/snapshots/test_diagnostics.ambr b/tests/components/roku/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..1742d1f7ee0 --- /dev/null +++ b/tests/components/roku/snapshots/test_diagnostics.ambr @@ -0,0 +1,98 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'data': dict({ + 'app': dict({ + 'app_id': None, + 'name': 'Roku', + 'screensaver': False, + 'version': None, + }), + 'apps': list([ + dict({ + 'app_id': '11', + 'name': 'Roku Channel Store', + 'screensaver': False, + 'version': None, + }), + dict({ + 'app_id': '12', + 'name': 'Netflix', + 'screensaver': False, + 'version': None, + }), + dict({ + 'app_id': '13', + 'name': 'Amazon Video on Demand', + 'screensaver': False, + 'version': None, + }), + dict({ + 'app_id': '14', + 'name': 'MLB.TV®', + 'screensaver': False, + 'version': None, + }), + dict({ + 'app_id': '26', + 'name': 'Free FrameChannel Service', + 'screensaver': False, + 'version': None, + }), + dict({ + 'app_id': '27', + 'name': 'Mediafly', + 'screensaver': False, + 'version': None, + }), + dict({ + 'app_id': '28', + 'name': 'Pandora', + 'screensaver': False, + 'version': None, + }), + dict({ + 'app_id': '74519', + 'name': "Pluto TV - It's Free TV", + 'screensaver': False, + 'version': '5.2.0', + }), + ]), + 'channel': None, + 'channels': list([ + ]), + 'info': dict({ + 'brand': 'Roku', + 'device_location': None, + 'device_type': 'box', + 'ethernet_mac': 'b0:a7:37:96:4d:fa', + 'ethernet_support': True, + 'headphones_connected': False, + 'model_name': 'Roku 3', + 'model_number': '4200X', + 'name': 'My Roku 3', + 'network_name': None, + 'network_type': 'ethernet', + 'serial_number': '1GU48T017973', + 'supports_airplay': False, + 'supports_find_remote': False, + 'supports_private_listening': False, + 'supports_wake_on_wlan': False, + 'version': '7.5.0', + 'wifi_mac': 'b0:a7:37:96:4d:fb', + }), + 'media': None, + 'state': dict({ + 'at': '2023-08-15T17:00:00+00:00', + 'available': True, + 'standby': False, + }), + }), + 'entry': dict({ + 'data': dict({ + 'host': '192.168.1.160', + }), + 'unique_id': '1GU48T017973', + }), + }) +# --- diff --git a/tests/components/roku/test_diagnostics.py b/tests/components/roku/test_diagnostics.py index 860d0424624..708e6d3f5e3 100644 --- a/tests/components/roku/test_diagnostics.py +++ b/tests/components/roku/test_diagnostics.py @@ -1,9 +1,11 @@ """Tests for the diagnostics data provided by the Roku integration.""" -import json +from rokuecp import Device as RokuDevice +from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -11,27 +13,14 @@ from tests.typing import ClientSessionGenerator async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, + mock_device: RokuDevice, init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics for config entry.""" - diagnostics_data = json.loads(load_fixture("roku/roku3-diagnostics-data.json")) + mock_device.state.at = dt_util.parse_datetime("2023-08-15 17:00:00-00:00") - result = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) - - assert isinstance(result, dict) - assert isinstance(result["entry"], dict) - assert result["entry"]["data"] == {"host": "192.168.1.160"} - assert result["entry"]["unique_id"] == "1GU48T017973" - - assert isinstance(result["data"], dict) - assert result["data"]["app"] == diagnostics_data["app"] - assert result["data"]["apps"] == diagnostics_data["apps"] - assert result["data"]["channel"] == diagnostics_data["channel"] - assert result["data"]["channels"] == diagnostics_data["channels"] - assert result["data"]["info"] == diagnostics_data["info"] - assert result["data"]["media"] == diagnostics_data["media"] - - data_state = result["data"]["state"] - assert isinstance(data_state, dict) - assert data_state["available"] == diagnostics_data["state"]["available"] - assert data_state["standby"] == diagnostics_data["state"]["standby"] + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) From 9c54c4abf5aa6282889de13796dc8a2de3caadbe Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 11:48:55 +0200 Subject: [PATCH 0669/1151] Use snapshot assertion for KNX diagnostics (#98724) Co-authored-by: farmio --- .../knx/snapshots/test_diagnostic.ambr | 95 ++++++++++++++++++ tests/components/knx/test_diagnostic.py | 97 ++++++------------- 2 files changed, 122 insertions(+), 70 deletions(-) create mode 100644 tests/components/knx/snapshots/test_diagnostic.ambr diff --git a/tests/components/knx/snapshots/test_diagnostic.ambr b/tests/components/knx/snapshots/test_diagnostic.ambr new file mode 100644 index 00000000000..4323dd113cd --- /dev/null +++ b/tests/components/knx/snapshots/test_diagnostic.ambr @@ -0,0 +1,95 @@ +# serializer version: 1 +# name: test_diagnostic_config_error[hass_config0] + dict({ + 'config_entry_data': dict({ + 'connection_type': 'automatic', + 'individual_address': '0.0.240', + 'multicast_group': '224.0.23.12', + 'multicast_port': 3671, + 'rate_limit': 0, + 'state_updater': True, + }), + 'configuration_error': "extra keys not allowed @ data['knx']['wrong_key']", + 'configuration_yaml': dict({ + 'wrong_key': dict({ + }), + }), + 'project_info': None, + 'xknx': dict({ + 'current_address': '0.0.0', + 'version': '0.0.0', + }), + }) +# --- +# name: test_diagnostic_redact[hass_config0] + dict({ + 'config_entry_data': dict({ + 'backbone_key': '**REDACTED**', + 'connection_type': 'automatic', + 'device_authentication': '**REDACTED**', + 'individual_address': '0.0.240', + 'knxkeys_password': '**REDACTED**', + 'multicast_group': '224.0.23.12', + 'multicast_port': 3671, + 'rate_limit': 0, + 'state_updater': True, + 'user_password': '**REDACTED**', + }), + 'configuration_error': None, + 'configuration_yaml': None, + 'project_info': None, + 'xknx': dict({ + 'current_address': '0.0.0', + 'version': '0.0.0', + }), + }) +# --- +# name: test_diagnostics[hass_config0] + dict({ + 'config_entry_data': dict({ + 'connection_type': 'automatic', + 'individual_address': '0.0.240', + 'multicast_group': '224.0.23.12', + 'multicast_port': 3671, + 'rate_limit': 0, + 'state_updater': True, + }), + 'configuration_error': None, + 'configuration_yaml': None, + 'project_info': None, + 'xknx': dict({ + 'current_address': '0.0.0', + 'version': '0.0.0', + }), + }) +# --- +# name: test_diagnostics_project[hass_config0] + dict({ + 'config_entry_data': dict({ + 'connection_type': 'automatic', + 'individual_address': '0.0.240', + 'multicast_group': '224.0.23.12', + 'multicast_port': 3671, + 'rate_limit': 0, + 'state_updater': True, + }), + 'configuration_error': None, + 'configuration_yaml': None, + 'project_info': dict({ + 'created_by': 'ETS5', + 'group_address_style': 'ThreeLevel', + 'guid': '6a019e80-5945-489e-95a3-378735c642d1', + 'language_code': 'de-DE', + 'last_modified': '2023-04-30T09:04:04.4043671Z', + 'name': '**REDACTED**', + 'project_id': 'P-04FF', + 'schema_version': '20', + 'tool_version': '5.7.1428.39779', + 'xknxproject_version': '3.1.0', + }), + 'xknx': dict({ + 'current_address': '0.0.0', + 'version': '0.0.0', + }), + }) +# --- diff --git a/tests/components/knx/test_diagnostic.py b/tests/components/knx/test_diagnostic.py index df8cb71d4af..0b43433c01e 100644 --- a/tests/components/knx/test_diagnostic.py +++ b/tests/components/knx/test_diagnostic.py @@ -1,6 +1,7 @@ """Tests for the diagnostics data provided by the KNX integration.""" import pytest +from syrupy import SnapshotAssertion from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from homeassistant.components.knx.const import ( @@ -36,28 +37,17 @@ async def test_diagnostics( mock_config_entry: MockConfigEntry, knx: KNXTestKit, mock_hass_config: None, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" await knx.setup_integration({}) # Overwrite the version for this test since we don't want to change this with every library bump - knx.xknx.version = "1.0.0" - assert await get_diagnostics_for_config_entry( - hass, hass_client, mock_config_entry - ) == { - "config_entry_data": { - "connection_type": "automatic", - "individual_address": "0.0.240", - "multicast_group": "224.0.23.12", - "multicast_port": 3671, - "rate_limit": 0, - "state_updater": True, - }, - "configuration_error": None, - "configuration_yaml": None, - "project_info": None, - "xknx": {"current_address": "0.0.0", "version": "1.0.0"}, - } + knx.xknx.version = "0.0.0" + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) @pytest.mark.parametrize("hass_config", [{"knx": {"wrong_key": {}}}]) @@ -67,28 +57,18 @@ async def test_diagnostic_config_error( hass_client: ClientSessionGenerator, mock_config_entry: MockConfigEntry, knx: KNXTestKit, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" await knx.setup_integration({}) # Overwrite the version for this test since we don't want to change this with every library bump - knx.xknx.version = "1.0.0" - assert await get_diagnostics_for_config_entry( - hass, hass_client, mock_config_entry - ) == { - "config_entry_data": { - "connection_type": "automatic", - "individual_address": "0.0.240", - "multicast_group": "224.0.23.12", - "multicast_port": 3671, - "rate_limit": 0, - "state_updater": True, - }, - "configuration_error": "extra keys not allowed @ data['knx']['wrong_key']", - "configuration_yaml": {"wrong_key": {}}, - "project_info": None, - "xknx": {"current_address": "0.0.0", "version": "1.0.0"}, - } + knx.xknx.version = "0.0.0" + # the snapshot will contain 'configuration_error' key with the voluptuous error message + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) @pytest.mark.parametrize("hass_config", [{}]) @@ -96,6 +76,7 @@ async def test_diagnostic_redact( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_hass_config: None, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics redacting data.""" mock_config_entry: MockConfigEntry = MockConfigEntry( @@ -118,27 +99,11 @@ async def test_diagnostic_redact( await knx.setup_integration({}) # Overwrite the version for this test since we don't want to change this with every library bump - knx.xknx.version = "1.0.0" - assert await get_diagnostics_for_config_entry( - hass, hass_client, mock_config_entry - ) == { - "config_entry_data": { - "connection_type": "automatic", - "individual_address": "0.0.240", - "multicast_group": "224.0.23.12", - "multicast_port": 3671, - "rate_limit": 0, - "state_updater": True, - "knxkeys_password": "**REDACTED**", - "user_password": "**REDACTED**", - "device_authentication": "**REDACTED**", - "backbone_key": "**REDACTED**", - }, - "configuration_error": None, - "configuration_yaml": None, - "project_info": None, - "xknx": {"current_address": "0.0.0", "version": "1.0.0"}, - } + knx.xknx.version = "0.0.0" + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) @pytest.mark.parametrize("hass_config", [{}]) @@ -149,21 +114,13 @@ async def test_diagnostics_project( knx: KNXTestKit, mock_hass_config: None, load_knxproj: None, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" await knx.setup_integration({}) - diag = await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) - - assert "config_entry_data" in diag - assert "configuration_error" in diag - assert "configuration_yaml" in diag - assert "project_info" in diag - assert "xknx" in diag - # project specific fields - assert "created_by" in diag["project_info"] - assert "group_address_style" in diag["project_info"] - assert "last_modified" in diag["project_info"] - assert "schema_version" in diag["project_info"] - assert "tool_version" in diag["project_info"] - assert "language_code" in diag["project_info"] - assert diag["project_info"]["name"] == "**REDACTED**" + knx.xknx.version = "0.0.0" + # snapshot will contain project specific fields in `project_info` + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) From 180dd3d11a9ec7788423490d11d683d237f91d3e Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Mon, 21 Aug 2023 12:01:49 +0200 Subject: [PATCH 0670/1151] Bump pyspcwebgw to 0.7.0 (#98593) --- homeassistant/components/spc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spc/manifest.json b/homeassistant/components/spc/manifest.json index 82f6ed62029..a707e1a7804 100644 --- a/homeassistant/components/spc/manifest.json +++ b/homeassistant/components/spc/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/spc", "iot_class": "local_push", "loggers": ["pyspcwebgw"], - "requirements": ["pyspcwebgw==0.4.0"] + "requirements": ["pyspcwebgw==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3f322928836..183c3293408 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2038,7 +2038,7 @@ pysnooz==0.8.3 pysoma==0.0.12 # homeassistant.components.spc -pyspcwebgw==0.4.0 +pyspcwebgw==0.7.0 # homeassistant.components.squeezebox pysqueezebox==0.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6fccaf61b1a..4c3a13ae48e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1521,7 +1521,7 @@ pysnooz==0.8.3 pysoma==0.0.12 # homeassistant.components.spc -pyspcwebgw==0.4.0 +pyspcwebgw==0.7.0 # homeassistant.components.squeezebox pysqueezebox==0.6.3 From 1a4fb908972401784717e5cd55e6ee9bf82c9901 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 21 Aug 2023 12:03:01 +0200 Subject: [PATCH 0671/1151] Clean off unnecessary logger in Workday (#98741) --- homeassistant/components/workday/__init__.py | 10 +++++----- tests/components/workday/test_binary_sensor.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index d8d31451567..84ed67a36dd 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -7,7 +7,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError -from .const import CONF_COUNTRY, CONF_PROVINCE, LOGGER, PLATFORMS +from .const import CONF_COUNTRY, CONF_PROVINCE, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -16,12 +16,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: country: str = entry.options[CONF_COUNTRY] province: str | None = entry.options.get(CONF_PROVINCE) if country and country not in list_supported_countries(): - LOGGER.error("There is no country %s", country) - raise ConfigEntryError("Selected country is not valid") + raise ConfigEntryError(f"Selected country {country} is not valid") if province and province not in list_supported_countries()[country]: - LOGGER.error("There is no subdivision %s in country %s", province, country) - raise ConfigEntryError("Selected province is not valid") + raise ConfigEntryError( + f"Selected province {province} for country {country} is not valid" + ) entry.async_on_unload(entry.add_update_listener(async_update_listener)) diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index a8cea01a864..51280c8d75c 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -200,7 +200,7 @@ async def test_setup_faulty_country( state = hass.states.get("binary_sensor.workday_sensor") assert state is None - assert "There is no country" in caplog.text + assert "Selected country ZZ is not valid" in caplog.text async def test_setup_faulty_province( @@ -215,7 +215,7 @@ async def test_setup_faulty_province( state = hass.states.get("binary_sensor.workday_sensor") assert state is None - assert "There is no subdivision" in caplog.text + assert "Selected province ZZ for country DE is not valid" in caplog.text async def test_setup_incorrect_add_remove( From 538de6d1f316bffee2b1bd40cea528ce61aa1f14 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 12:04:12 +0200 Subject: [PATCH 0672/1151] Introduce base class for Neato (#98071) Co-authored-by: Robert Resch --- .coveragerc | 1 + homeassistant/components/neato/button.py | 13 ++++-------- homeassistant/components/neato/camera.py | 25 +++++----------------- homeassistant/components/neato/entity.py | 27 ++++++++++++++++++++++++ homeassistant/components/neato/sensor.py | 18 ++++------------ homeassistant/components/neato/switch.py | 17 ++++----------- homeassistant/components/neato/vacuum.py | 21 ++++++++---------- 7 files changed, 54 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/neato/entity.py diff --git a/.coveragerc b/.coveragerc index 58aa8eb52c3..2fbfa15997b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -781,6 +781,7 @@ omit = homeassistant/components/neato/__init__.py homeassistant/components/neato/api.py homeassistant/components/neato/camera.py + homeassistant/components/neato/entity.py homeassistant/components/neato/hub.py homeassistant/components/neato/sensor.py homeassistant/components/neato/switch.py diff --git a/homeassistant/components/neato/button.py b/homeassistant/components/neato/button.py index 4bbd9196932..8b23bbe4681 100644 --- a/homeassistant/components/neato/button.py +++ b/homeassistant/components/neato/button.py @@ -7,10 +7,10 @@ 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.entity_platform import AddEntitiesCallback -from .const import NEATO_DOMAIN, NEATO_ROBOTS +from .const import NEATO_ROBOTS +from .entity import NeatoEntity async def async_setup_entry( @@ -22,10 +22,9 @@ async def async_setup_entry( async_add_entities(entities, True) -class NeatoDismissAlertButton(ButtonEntity): +class NeatoDismissAlertButton(NeatoEntity, ButtonEntity): """Representation of a dismiss_alert button entity.""" - _attr_has_entity_name = True _attr_translation_key = "dismiss_alert" _attr_entity_category = EntityCategory.CONFIG @@ -34,12 +33,8 @@ class NeatoDismissAlertButton(ButtonEntity): robot: Robot, ) -> None: """Initialize a dismiss_alert Neato button entity.""" - self.robot = robot + super().__init__(robot) self._attr_unique_id = f"{robot.serial}_dismiss_alert" - 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 5b13d12d37f..c1513bb1de6 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -12,16 +12,10 @@ 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.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - NEATO_DOMAIN, - NEATO_LOGIN, - NEATO_MAP_DATA, - NEATO_ROBOTS, - SCAN_INTERVAL_MINUTES, -) +from .const import NEATO_LOGIN, NEATO_MAP_DATA, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES +from .entity import NeatoEntity from .hub import NeatoHub _LOGGER = logging.getLogger(__name__) @@ -48,18 +42,17 @@ async def async_setup_entry( async_add_entities(dev, True) -class NeatoCleaningMap(Camera): +class NeatoCleaningMap(NeatoEntity, 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: """Initialize Neato cleaning map.""" - super().__init__() - self.robot = robot + super().__init__(robot) + Camera.__init__(self) self.neato = neato self._mapdata = mapdata self._available = neato is not None @@ -126,14 +119,6 @@ class NeatoCleaningMap(Camera): """Return if the robot is available.""" return self._available - @property - def device_info(self) -> DeviceInfo: - """Device info for neato robot.""" - return DeviceInfo( - identifiers={(NEATO_DOMAIN, self._robot_serial)}, - name=self.robot.name, - ) - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the vacuum cleaner.""" diff --git a/homeassistant/components/neato/entity.py b/homeassistant/components/neato/entity.py new file mode 100644 index 00000000000..43072f19693 --- /dev/null +++ b/homeassistant/components/neato/entity.py @@ -0,0 +1,27 @@ +"""Base entity for Neato.""" +from __future__ import annotations + +from pybotvac import Robot + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity + +from .const import NEATO_DOMAIN + + +class NeatoEntity(Entity): + """Base Neato entity.""" + + _attr_has_entity_name = True + + def __init__(self, robot: Robot) -> None: + """Initialize Neato entity.""" + self.robot = robot + + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + return DeviceInfo( + identifiers={(NEATO_DOMAIN, self.robot.serial)}, + name=self.robot.name, + ) diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 60aeb52af05..452f1bc3a9c 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -12,10 +12,10 @@ 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.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES +from .const import NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES +from .entity import NeatoEntity from .hub import NeatoHub _LOGGER = logging.getLogger(__name__) @@ -41,14 +41,12 @@ async def async_setup_entry( async_add_entities(dev, True) -class NeatoSensor(SensorEntity): +class NeatoSensor(NeatoEntity, SensorEntity): """Neato sensor.""" - _attr_has_entity_name = True - def __init__(self, neato: NeatoHub, robot: Robot) -> None: """Initialize Neato sensor.""" - self.robot = robot + super().__init__(robot) self._available: bool = False self._robot_serial: str = self.robot.serial self._state: dict[str, Any] | None = None @@ -100,11 +98,3 @@ class NeatoSensor(SensorEntity): def native_unit_of_measurement(self) -> str: """Return unit of measurement.""" return PERCENTAGE - - @property - def device_info(self) -> DeviceInfo: - """Device info for neato robot.""" - return DeviceInfo( - identifiers={(NEATO_DOMAIN, self._robot_serial)}, - name=self.robot.name, - ) diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index f6d159fcc1b..a80d05eef23 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -12,10 +12,10 @@ 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.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES +from .const import NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES +from .entity import NeatoEntity from .hub import NeatoHub _LOGGER = logging.getLogger(__name__) @@ -45,16 +45,15 @@ async def async_setup_entry( async_add_entities(dev, True) -class NeatoConnectedSwitch(SwitchEntity): +class NeatoConnectedSwitch(NeatoEntity, 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.""" + super().__init__(robot) self.type = switch_type - self.robot = robot self._available = False self._state: dict[str, Any] | None = None self._schedule_state: str | None = None @@ -109,14 +108,6 @@ class NeatoConnectedSwitch(SwitchEntity): """Device entity category.""" return EntityCategory.CONFIG - @property - def device_info(self) -> DeviceInfo: - """Device info for neato robot.""" - return DeviceInfo( - identifiers={(NEATO_DOMAIN, self._robot_serial)}, - name=self.robot.name, - ) - def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if self.type == SWITCH_TYPE_SCHEDULE: diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index f70e79f3fc0..ecc39e515c2 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -30,13 +30,13 @@ from .const import ( ALERTS, ERRORS, MODE, - NEATO_DOMAIN, NEATO_LOGIN, NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES, ) +from .entity import NeatoEntity from .hub import NeatoHub _LOGGER = logging.getLogger(__name__) @@ -91,7 +91,7 @@ async def async_setup_entry( ) -class NeatoConnectedVacuum(StateVacuumEntity): +class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity): """Representation of a Neato Connected Vacuum.""" _attr_icon = "mdi:robot-vacuum-variant" @@ -106,7 +106,6 @@ class NeatoConnectedVacuum(StateVacuumEntity): | VacuumEntityFeature.MAP | VacuumEntityFeature.LOCATE ) - _attr_has_entity_name = True _attr_name = None def __init__( @@ -117,7 +116,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): persistent_maps: dict[str, Any] | None, ) -> None: """Initialize the Neato Connected Vacuum.""" - self.robot = robot + super().__init__(robot) self._attr_available: bool = neato is not None self._mapdata = mapdata self._robot_has_map: bool = self.robot.has_persistent_maps @@ -300,14 +299,12 @@ class NeatoConnectedVacuum(StateVacuumEntity): @property def device_info(self) -> DeviceInfo: """Device info for neato robot.""" - stats = self._robot_stats - return DeviceInfo( - identifiers={(NEATO_DOMAIN, self._robot_serial)}, - manufacturer=stats["battery"]["vendor"] if stats else None, - model=stats["model"] if stats else None, - name=self.robot.name, - sw_version=stats["firmware"] if stats else None, - ) + device_info = super().device_info + if self._robot_stats: + device_info["manufacturer"] = self._robot_stats["battery"]["vendor"] + device_info["model"] = self._robot_stats["model"] + device_info["sw_version"] = self._robot_stats["firmware"] + return device_info def start(self) -> None: """Start cleaning or resume cleaning.""" From 82b3ced4f18046a13196dace008b237352d2181c Mon Sep 17 00:00:00 2001 From: Michael Arthur Date: Mon, 21 Aug 2023 22:19:55 +1200 Subject: [PATCH 0673/1151] Add lawnmower entity (#93623) Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- .core_files.yaml | 1 + .strict-typing | 1 + CODEOWNERS | 2 + .../components/kitchen_sink/__init__.py | 5 +- .../components/kitchen_sink/lawn_mower.py | 100 ++++++++++ .../components/lawn_mower/__init__.py | 120 ++++++++++++ homeassistant/components/lawn_mower/const.py | 33 ++++ .../components/lawn_mower/manifest.json | 8 + .../components/lawn_mower/services.yaml | 22 +++ .../components/lawn_mower/strings.json | 28 +++ homeassistant/const.py | 1 + homeassistant/helpers/selector.py | 2 + mypy.ini | 10 + .../snapshots/test_lawn_mower.ambr | 60 ++++++ .../kitchen_sink/test_lawn_mower.py | 116 ++++++++++++ tests/components/lawn_mower/__init__.py | 1 + tests/components/lawn_mower/test_init.py | 178 ++++++++++++++++++ 17 files changed, 686 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/kitchen_sink/lawn_mower.py create mode 100644 homeassistant/components/lawn_mower/__init__.py create mode 100644 homeassistant/components/lawn_mower/const.py create mode 100644 homeassistant/components/lawn_mower/manifest.json create mode 100644 homeassistant/components/lawn_mower/services.yaml create mode 100644 homeassistant/components/lawn_mower/strings.json create mode 100644 tests/components/kitchen_sink/snapshots/test_lawn_mower.ambr create mode 100644 tests/components/kitchen_sink/test_lawn_mower.py create mode 100644 tests/components/lawn_mower/__init__.py create mode 100644 tests/components/lawn_mower/test_init.py diff --git a/.core_files.yaml b/.core_files.yaml index 4ac65cd92c7..6fbfdf90a4b 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -30,6 +30,7 @@ base_platforms: &base_platforms - homeassistant/components/humidifier/** - homeassistant/components/image/** - homeassistant/components/image_processing/** + - homeassistant/components/lawn_mower/** - homeassistant/components/light/** - homeassistant/components/lock/** - homeassistant/components/media_player/** diff --git a/.strict-typing b/.strict-typing index c56c7d9f137..f83260c383f 100644 --- a/.strict-typing +++ b/.strict-typing @@ -194,6 +194,7 @@ homeassistant.components.lacrosse.* homeassistant.components.lacrosse_view.* homeassistant.components.lametric.* homeassistant.components.laundrify.* +homeassistant.components.lawn_mower.* homeassistant.components.lcn.* homeassistant.components.ld2410_ble.* homeassistant.components.lidarr.* diff --git a/CODEOWNERS b/CODEOWNERS index 812caea4da5..427c8290b60 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -673,6 +673,8 @@ build.json @home-assistant/supervisor /tests/components/launch_library/ @ludeeus @DurgNomis-drol /homeassistant/components/laundrify/ @xLarry /tests/components/laundrify/ @xLarry +/homeassistant/components/lawn_mower/ @home-assistant/core +/tests/components/lawn_mower/ @home-assistant/core /homeassistant/components/lcn/ @alengwenus /tests/components/lcn/ @alengwenus /homeassistant/components/ld2410_ble/ @930913 diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py index a85221108f8..5c8088823b2 100644 --- a/homeassistant/components/kitchen_sink/__init__.py +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -27,9 +27,10 @@ DOMAIN = "kitchen_sink" COMPONENTS_WITH_DEMO_PLATFORM = [ - Platform.SENSOR, - Platform.LOCK, Platform.IMAGE, + Platform.LAWN_MOWER, + Platform.LOCK, + Platform.SENSOR, Platform.WEATHER, ] diff --git a/homeassistant/components/kitchen_sink/lawn_mower.py b/homeassistant/components/kitchen_sink/lawn_mower.py new file mode 100644 index 00000000000..119b37b7569 --- /dev/null +++ b/homeassistant/components/kitchen_sink/lawn_mower.py @@ -0,0 +1,100 @@ +"""Demo platform that has a couple fake lawn mowers.""" +from __future__ import annotations + +from homeassistant.components.lawn_mower import ( + LawnMowerActivity, + LawnMowerEntity, + LawnMowerEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Demo lawn mowers.""" + async_add_entities( + [ + DemoLawnMower( + "kitchen_sink_mower_001", + "Mower can mow", + LawnMowerActivity.DOCKED, + LawnMowerEntityFeature.START_MOWING, + ), + DemoLawnMower( + "kitchen_sink_mower_002", + "Mower can dock", + LawnMowerActivity.MOWING, + LawnMowerEntityFeature.DOCK | LawnMowerEntityFeature.START_MOWING, + ), + DemoLawnMower( + "kitchen_sink_mower_003", + "Mower can pause", + LawnMowerActivity.DOCKED, + LawnMowerEntityFeature.PAUSE | LawnMowerEntityFeature.START_MOWING, + ), + DemoLawnMower( + "kitchen_sink_mower_004", + "Mower can do all", + LawnMowerActivity.DOCKED, + LawnMowerEntityFeature.DOCK + | LawnMowerEntityFeature.PAUSE + | LawnMowerEntityFeature.START_MOWING, + ), + DemoLawnMower( + "kitchen_sink_mower_005", + "Mower is paused", + LawnMowerActivity.PAUSED, + LawnMowerEntityFeature.DOCK + | LawnMowerEntityFeature.PAUSE + | LawnMowerEntityFeature.START_MOWING, + ), + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Everything but the Kitchen Sink config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + +class DemoLawnMower(LawnMowerEntity): + """Representation of a Demo lawn mower.""" + + def __init__( + self, + unique_id: str, + name: str, + activity: LawnMowerActivity, + features: LawnMowerEntityFeature = LawnMowerEntityFeature(0), + ) -> None: + """Initialize the lawn mower.""" + self._attr_name = name + self._attr_unique_id = unique_id + self._attr_supported_features = features + self._attr_activity = activity + + async def async_start_mowing(self) -> None: + """Start mowing.""" + self._attr_activity = LawnMowerActivity.MOWING + self.async_write_ha_state() + + async def async_dock(self) -> None: + """Start docking.""" + self._attr_activity = LawnMowerActivity.DOCKED + self.async_write_ha_state() + + async def async_pause(self) -> None: + """Pause mower.""" + self._attr_activity = LawnMowerActivity.PAUSED + self.async_write_ha_state() diff --git a/homeassistant/components/lawn_mower/__init__.py b/homeassistant/components/lawn_mower/__init__.py new file mode 100644 index 00000000000..5388463316f --- /dev/null +++ b/homeassistant/components/lawn_mower/__init__.py @@ -0,0 +1,120 @@ +"""The lawn mower integration.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import logging +from typing import final + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType + +from .const import ( + DOMAIN, + SERVICE_DOCK, + SERVICE_PAUSE, + SERVICE_START_MOWING, + LawnMowerActivity, + LawnMowerEntityFeature, +) + +SCAN_INTERVAL = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the lawn_mower component.""" + component = hass.data[DOMAIN] = EntityComponent[LawnMowerEntity]( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_START_MOWING, + {}, + "async_start_mowing", + [LawnMowerEntityFeature.START_MOWING], + ) + component.async_register_entity_service( + SERVICE_PAUSE, {}, "async_pause", [LawnMowerEntityFeature.PAUSE] + ) + component.async_register_entity_service( + SERVICE_DOCK, {}, "async_dock", [LawnMowerEntityFeature.DOCK] + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up lawn mower devices.""" + component: EntityComponent[LawnMowerEntity] = 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[LawnMowerEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +@dataclass +class LawnMowerEntityEntityDescription(EntityDescription): + """A class that describes lawn mower entities.""" + + +class LawnMowerEntity(Entity): + """Base class for lawn mower entities.""" + + entity_description: LawnMowerEntityEntityDescription + _attr_activity: LawnMowerActivity | None = None + _attr_supported_features: LawnMowerEntityFeature = LawnMowerEntityFeature(0) + + @final + @property + def state(self) -> str | None: + """Return the current state.""" + if (activity := self.activity) is None: + return None + return str(activity) + + @property + def activity(self) -> LawnMowerActivity | None: + """Return the current lawn mower activity.""" + return self._attr_activity + + @property + def supported_features(self) -> LawnMowerEntityFeature: + """Flag lawn mower features that are supported.""" + return self._attr_supported_features + + def start_mowing(self) -> None: + """Start or resume mowing.""" + raise NotImplementedError() + + async def async_start_mowing(self) -> None: + """Start or resume mowing.""" + await self.hass.async_add_executor_job(self.start_mowing) + + def dock(self) -> None: + """Dock the mower.""" + raise NotImplementedError() + + async def async_dock(self) -> None: + """Dock the mower.""" + await self.hass.async_add_executor_job(self.dock) + + def pause(self) -> None: + """Pause the lawn mower.""" + raise NotImplementedError() + + async def async_pause(self) -> None: + """Pause the lawn mower.""" + await self.hass.async_add_executor_job(self.pause) diff --git a/homeassistant/components/lawn_mower/const.py b/homeassistant/components/lawn_mower/const.py new file mode 100644 index 00000000000..706c9616450 --- /dev/null +++ b/homeassistant/components/lawn_mower/const.py @@ -0,0 +1,33 @@ +"""Constants for the lawn mower integration.""" +from enum import IntFlag, StrEnum + + +class LawnMowerActivity(StrEnum): + """Activity state of lawn mower devices.""" + + ERROR = "error" + """Device is in error state, needs assistance.""" + + PAUSED = "paused" + """Paused during activity.""" + + MOWING = "mowing" + """Device is mowing.""" + + DOCKED = "docked" + """Device is docked.""" + + +class LawnMowerEntityFeature(IntFlag): + """Supported features of the lawn mower entity.""" + + START_MOWING = 1 + PAUSE = 2 + DOCK = 4 + + +DOMAIN = "lawn_mower" + +SERVICE_START_MOWING = "start_mowing" +SERVICE_PAUSE = "pause" +SERVICE_DOCK = "dock" diff --git a/homeassistant/components/lawn_mower/manifest.json b/homeassistant/components/lawn_mower/manifest.json new file mode 100644 index 00000000000..43418a9440d --- /dev/null +++ b/homeassistant/components/lawn_mower/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "lawn_mower", + "name": "Lawn Mower", + "codeowners": ["@home-assistant/core"], + "documentation": "https://www.home-assistant.io/integrations/lawn_mower", + "integration_type": "entity", + "quality_scale": "internal" +} diff --git a/homeassistant/components/lawn_mower/services.yaml b/homeassistant/components/lawn_mower/services.yaml new file mode 100644 index 00000000000..8c9a2f1adcc --- /dev/null +++ b/homeassistant/components/lawn_mower/services.yaml @@ -0,0 +1,22 @@ +# Describes the format for available lawn_mower services + +start_mowing: + target: + entity: + domain: lawn_mower + supported_features: + - lawn_mower.LawnMowerEntityFeature.START_MOWING + +dock: + target: + entity: + domain: lawn_mower + supported_features: + - lawn_mower.LawnMowerEntityFeature.DOCK + +pause: + target: + entity: + domain: lawn_mower + supported_features: + - lawn_mower.LawnMowerEntityFeature.PAUSE diff --git a/homeassistant/components/lawn_mower/strings.json b/homeassistant/components/lawn_mower/strings.json new file mode 100644 index 00000000000..caf2e15df77 --- /dev/null +++ b/homeassistant/components/lawn_mower/strings.json @@ -0,0 +1,28 @@ +{ + "title": "Lawn mower", + "entity_component": { + "_": { + "name": "[%key:component::lawn_mower::title%]", + "state": { + "error": "Error", + "paused": "Paused", + "mowing": "Mowing", + "docked": "Docked" + } + } + }, + "services": { + "start_mowing": { + "name": "Start mowing", + "description": "Starts the mowing task." + }, + "dock": { + "name": "Return to dock", + "description": "Stops the mowing task and returns to the dock." + }, + "pause": { + "name": "Pause", + "description": "Pauses the mowing task." + } + } +} diff --git a/homeassistant/const.py b/homeassistant/const.py index adca3dc965c..66d05f0bd4f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -39,6 +39,7 @@ class Platform(StrEnum): HUMIDIFIER = "humidifier" IMAGE = "image" IMAGE_PROCESSING = "image_processing" + LAWN_MOWER = "lawn_mower" LIGHT = "light" LOCK = "lock" MAILBOX = "mailbox" diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index ba473758121..efb1ee0b1f1 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -92,6 +92,7 @@ def _entity_features() -> dict[str, type[IntFlag]]: from homeassistant.components.cover import CoverEntityFeature from homeassistant.components.fan import FanEntityFeature from homeassistant.components.humidifier import HumidifierEntityFeature + from homeassistant.components.lawn_mower import LawnMowerEntityFeature from homeassistant.components.light import LightEntityFeature from homeassistant.components.lock import LockEntityFeature from homeassistant.components.media_player import MediaPlayerEntityFeature @@ -110,6 +111,7 @@ def _entity_features() -> dict[str, type[IntFlag]]: "CoverEntityFeature": CoverEntityFeature, "FanEntityFeature": FanEntityFeature, "HumidifierEntityFeature": HumidifierEntityFeature, + "LawnMowerEntityFeature": LawnMowerEntityFeature, "LightEntityFeature": LightEntityFeature, "LockEntityFeature": LockEntityFeature, "MediaPlayerEntityFeature": MediaPlayerEntityFeature, diff --git a/mypy.ini b/mypy.ini index 1c47ad019a2..233539589cb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1702,6 +1702,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lawn_mower.*] +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.lcn.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/kitchen_sink/snapshots/test_lawn_mower.ambr b/tests/components/kitchen_sink/snapshots/test_lawn_mower.ambr new file mode 100644 index 00000000000..879e78d5534 --- /dev/null +++ b/tests/components/kitchen_sink/snapshots/test_lawn_mower.ambr @@ -0,0 +1,60 @@ +# serializer version: 1 +# name: test_states + set({ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mower can do all', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lawn_mower.mower_can_do_all', + 'last_changed': , + 'last_updated': , + 'state': 'docked', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mower can dock', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lawn_mower.mower_can_dock', + 'last_changed': , + 'last_updated': , + 'state': 'mowing', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mower can mow', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lawn_mower.mower_can_mow', + 'last_changed': , + 'last_updated': , + 'state': 'docked', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mower can pause', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lawn_mower.mower_can_pause', + 'last_changed': , + 'last_updated': , + 'state': 'docked', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mower is paused', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lawn_mower.mower_is_paused', + 'last_changed': , + 'last_updated': , + 'state': 'paused', + }), + }) +# --- diff --git a/tests/components/kitchen_sink/test_lawn_mower.py b/tests/components/kitchen_sink/test_lawn_mower.py new file mode 100644 index 00000000000..efd1b7485ab --- /dev/null +++ b/tests/components/kitchen_sink/test_lawn_mower.py @@ -0,0 +1,116 @@ +"""The tests for the kitchen_sink lawn mower platform.""" +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.kitchen_sink import DOMAIN +from homeassistant.components.lawn_mower import ( + DOMAIN as LAWN_MOWER_DOMAIN, + SERVICE_DOCK, + SERVICE_PAUSE, + SERVICE_START_MOWING, + LawnMowerActivity, +) +from homeassistant.const import ATTR_ENTITY_ID, EVENT_STATE_CHANGED, Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import async_capture_events, async_mock_service + +MOWER_SERVICE_ENTITY = "lawn_mower.mower_can_dock" + + +@pytest.fixture +async def lawn_mower_only() -> None: + """Enable only the lawn mower platform.""" + with patch( + "homeassistant.components.kitchen_sink.COMPONENTS_WITH_DEMO_PLATFORM", + [Platform.LAWN_MOWER], + ): + yield + + +@pytest.fixture(autouse=True) +async def setup_comp(hass: HomeAssistant, lawn_mower_only): + """Set up demo component.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + +async def test_states(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: + """Test the expected lawn mower entities are added.""" + states = hass.states.async_all() + assert set(states) == snapshot + + +@pytest.mark.parametrize( + ("entity", "service_call", "activity", "next_activity"), + [ + ( + "lawn_mower.mower_can_mow", + SERVICE_START_MOWING, + LawnMowerActivity.DOCKED, + LawnMowerActivity.MOWING, + ), + ( + "lawn_mower.mower_can_pause", + SERVICE_PAUSE, + LawnMowerActivity.DOCKED, + LawnMowerActivity.PAUSED, + ), + ( + "lawn_mower.mower_is_paused", + SERVICE_START_MOWING, + LawnMowerActivity.PAUSED, + LawnMowerActivity.MOWING, + ), + ( + "lawn_mower.mower_can_dock", + SERVICE_DOCK, + LawnMowerActivity.MOWING, + LawnMowerActivity.DOCKED, + ), + ], +) +async def test_mower( + hass: HomeAssistant, + entity: str, + service_call: str, + activity: LawnMowerActivity, + next_activity: LawnMowerActivity, +) -> None: + """Test the activity states of a lawn mower.""" + state = hass.states.get(entity) + + assert state.state == str(activity.value) + await hass.async_block_till_done() + + state_changes = async_capture_events(hass, EVENT_STATE_CHANGED) + await hass.services.async_call( + LAWN_MOWER_DOMAIN, service_call, {ATTR_ENTITY_ID: entity}, blocking=False + ) + await hass.async_block_till_done() + + assert state_changes[0].data["entity_id"] == entity + assert state_changes[0].data["new_state"].state == str(next_activity.value) + + +@pytest.mark.parametrize( + "service_call", + [ + SERVICE_DOCK, + SERVICE_START_MOWING, + SERVICE_PAUSE, + ], +) +async def test_service_calls_mocked(hass: HomeAssistant, service_call) -> None: + """Test the services of a lawn mower.""" + calls = async_mock_service(hass, LAWN_MOWER_DOMAIN, service_call) + await hass.services.async_call( + LAWN_MOWER_DOMAIN, + service_call, + {ATTR_ENTITY_ID: MOWER_SERVICE_ENTITY}, + blocking=True, + ) + assert len(calls) == 1 diff --git a/tests/components/lawn_mower/__init__.py b/tests/components/lawn_mower/__init__.py new file mode 100644 index 00000000000..0f96921206e --- /dev/null +++ b/tests/components/lawn_mower/__init__.py @@ -0,0 +1 @@ +"""Tests for the lawn mower integration.""" diff --git a/tests/components/lawn_mower/test_init.py b/tests/components/lawn_mower/test_init.py new file mode 100644 index 00000000000..39d594e1e17 --- /dev/null +++ b/tests/components/lawn_mower/test_init.py @@ -0,0 +1,178 @@ +"""The tests for the lawn mower integration.""" +from collections.abc import Generator +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.lawn_mower import ( + DOMAIN as LAWN_MOWER_DOMAIN, + LawnMowerActivity, + LawnMowerEntity, + LawnMowerEntityFeature, +) +from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from tests.common import ( + MockConfigEntry, + MockModule, + MockPlatform, + mock_config_flow, + mock_integration, + mock_platform, +) + +TEST_DOMAIN = "test" + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +class MockLawnMowerEntity(LawnMowerEntity): + """Mock lawn mower device to use in tests.""" + + def __init__( + self, + unique_id: str = "mock_lawn_mower", + name: str = "Lawn Mower", + features: LawnMowerEntityFeature = LawnMowerEntityFeature(0), + ) -> None: + """Initialize the lawn mower.""" + self._attr_name = name + self._attr_unique_id = unique_id + self._attr_supported_features = features + + def start_mowing(self) -> None: + """Start mowing.""" + self._attr_activity = LawnMowerActivity.MOWING + + +@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, MockFlow): + yield + + +async def test_lawn_mower_setup(hass: HomeAssistant) -> None: + """Test setup and tear down of lawn mower platform and entity.""" + + 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, Platform.LAWN_MOWER + ) + return True + + async def async_unload_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Unload up test config entry.""" + await hass.config_entries.async_unload_platforms( + config_entry, [Platform.LAWN_MOWER] + ) + return True + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + async_unload_entry=async_unload_entry_init, + ), + ) + + entity1 = MockLawnMowerEntity() + entity1.entity_id = "lawn_mower.mock_lawn_mower" + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test platform via config entry.""" + async_add_entities([entity1]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{LAWN_MOWER_DOMAIN}", + MockPlatform(async_setup_entry=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() + + assert config_entry.state == ConfigEntryState.LOADED + assert hass.states.get(entity1.entity_id) + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.NOT_LOADED + entity_state = hass.states.get(entity1.entity_id) + + assert entity_state + assert entity_state.state == STATE_UNAVAILABLE + + +async def test_sync_start_mowing(hass: HomeAssistant) -> None: + """Test if async mowing calls sync mowing.""" + lawn_mower = MockLawnMowerEntity() + lawn_mower.hass = hass + + lawn_mower.start_mowing = MagicMock() + await lawn_mower.async_start_mowing() + + assert lawn_mower.start_mowing.called + + +async def test_sync_dock(hass: HomeAssistant) -> None: + """Test if async dock calls sync dock.""" + lawn_mower = MockLawnMowerEntity() + lawn_mower.hass = hass + + lawn_mower.dock = MagicMock() + await lawn_mower.async_dock() + + assert lawn_mower.dock.called + + +async def test_sync_pause(hass: HomeAssistant) -> None: + """Test if async pause calls sync pause.""" + lawn_mower = MockLawnMowerEntity() + lawn_mower.hass = hass + + lawn_mower.pause = MagicMock() + await lawn_mower.async_pause() + + assert lawn_mower.pause.called + + +async def test_lawn_mower_default(hass: HomeAssistant) -> None: + """Test lawn mower entity with defaults.""" + lawn_mower = MockLawnMowerEntity() + lawn_mower.hass = hass + + assert lawn_mower.state is None + + +async def test_lawn_mower_state(hass: HomeAssistant) -> None: + """Test lawn mower entity returns state.""" + lawn_mower = MockLawnMowerEntity( + "lawn_mower_1", "Test lawn mower", LawnMowerActivity.MOWING + ) + lawn_mower.hass = hass + lawn_mower.start_mowing() + + assert lawn_mower.state == str(LawnMowerActivity.MOWING) From fb56dd0615e46182342e7b8f70acbed765a94cb7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 21 Aug 2023 12:20:48 +0200 Subject: [PATCH 0674/1151] Fix LiteJet import config issue (#97679) --- homeassistant/components/litejet/__init__.py | 21 ++------ .../components/litejet/config_flow.py | 53 +++++++++++++++++-- homeassistant/components/litejet/strings.json | 6 +++ tests/components/litejet/test_config_flow.py | 50 ++++++++++++++++- 4 files changed, 107 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index a7ea6ecd034..8c6d5ef4487 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -6,10 +6,9 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import CONF_EXCLUDE_NAMES, CONF_INCLUDE_SWITCHES, DOMAIN, PLATFORMS @@ -37,27 +36,13 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the LiteJet component.""" - if DOMAIN in config and not hass.config_entries.async_entries(DOMAIN): - # No config entry exists and configuration.yaml config exists, trigger the import flow. + if DOMAIN in config: + # Configuration.yaml config exists, trigger the import flow. hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] ) ) - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.2.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "LiteJet", - }, - ) return True diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index c469d480ca6..1062e948090 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -9,9 +9,10 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PORT -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, callback +from homeassistant.data_entry_flow import FlowResult, FlowResultType import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import CONF_DEFAULT_TRANSITION, DOMAIN @@ -53,6 +54,21 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Create a LiteJet config entry based upon user input.""" if self._async_current_entries(): + if self.context["source"] == config_entries.SOURCE_IMPORT: + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "LiteJet", + }, + ) return self.async_abort(reason="single_instance_allowed") errors = {} @@ -62,6 +78,20 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: system = await pylitejet.open(port) except SerialException: + if self.context["source"] == config_entries.SOURCE_IMPORT: + async_create_issue( + self.hass, + DOMAIN, + "deprecated_yaml_serial_exception", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.ERROR, + translation_key="deprecated_yaml_serial_exception", + translation_placeholders={ + "url": "/config/integrations/dashboard/add?domain=litejet" + }, + ) errors[CONF_PORT] = "open_failed" else: await system.close() @@ -78,7 +108,24 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_data: dict[str, Any]) -> FlowResult: """Import litejet config from configuration.yaml.""" - return self.async_create_entry(title=import_data[CONF_PORT], data=import_data) + new_data = {CONF_PORT: import_data[CONF_PORT]} + result = await self.async_step_user(new_data) + if result["type"] == FlowResultType.CREATE_ENTRY: + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.2.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "LiteJet", + }, + ) + return result @staticmethod @callback diff --git a/homeassistant/components/litejet/strings.json b/homeassistant/components/litejet/strings.json index 398f1a1e5aa..288e5f959a8 100644 --- a/homeassistant/components/litejet/strings.json +++ b/homeassistant/components/litejet/strings.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml_serial_exception": { + "title": "The LiteJet YAML configuration import failed", + "description": "Configuring LiteJet using YAML is being removed but there was an error opening the serial port when importing your YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or manually continue to [set up the integration]({url})." + } } } diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py index 69cd1d3d2e3..e2b2829de9e 100644 --- a/tests/components/litejet/test_config_flow.py +++ b/tests/components/litejet/test_config_flow.py @@ -6,7 +6,8 @@ from serial import SerialException from homeassistant import config_entries, data_entry_flow from homeassistant.components.litejet.const import CONF_DEFAULT_TRANSITION, DOMAIN from homeassistant.const import CONF_PORT -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +import homeassistant.helpers.issue_registry as ir from tests.common import MockConfigEntry @@ -67,7 +68,7 @@ async def test_flow_open_failed(hass: HomeAssistant) -> None: assert result["errors"][CONF_PORT] == "open_failed" -async def test_import_step(hass: HomeAssistant) -> None: +async def test_import_step(hass: HomeAssistant, mock_litejet) -> None: """Test initializing via import step.""" test_data = {CONF_PORT: "/dev/imported"} result = await hass.config_entries.flow.async_init( @@ -78,6 +79,51 @@ async def test_import_step(hass: HomeAssistant) -> None: assert result["title"] == test_data[CONF_PORT] assert result["data"] == test_data + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_yaml_litejet" + ) + assert issue.translation_key == "deprecated_yaml" + + +async def test_import_step_fails(hass: HomeAssistant) -> None: + """Test initializing via import step fails due to can't open port.""" + test_data = {CONF_PORT: "/dev/test"} + with patch("pylitejet.LiteJet") as mock_pylitejet: + mock_pylitejet.side_effect = SerialException + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=test_data + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"port": "open_failed"} + + issue_registry = ir.async_get(hass) + assert issue_registry.async_get_issue(DOMAIN, "deprecated_yaml_serial_exception") + + +async def test_import_step_already_exist(hass: HomeAssistant) -> None: + """Test initializing via import step when entry already exist.""" + first_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_PORT: "/dev/imported"}, + ) + first_entry.add_to_hass(hass) + + test_data = {CONF_PORT: "/dev/imported"} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=test_data + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_yaml_litejet" + ) + assert issue.translation_key == "deprecated_yaml" + async def test_options(hass: HomeAssistant) -> None: """Test updating options.""" From 604964d5f027a815af14179306daa9a7004a76ad Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 14:02:29 +0200 Subject: [PATCH 0675/1151] Use shorthand attributes in GDACS (#98173) --- homeassistant/components/gdacs/sensor.py | 36 ++++++------------------ 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py index e1535037d35..5d5589c54d6 100644 --- a/homeassistant/components/gdacs/sensor.py +++ b/homeassistant/components/gdacs/sensor.py @@ -1,6 +1,7 @@ """Feed Entity Manager Sensor support for GDACS Feed.""" from __future__ import annotations +from collections.abc import Callable import logging from homeassistant.components.sensor import SensorEntity @@ -33,21 +34,22 @@ async def async_setup_entry( ) -> None: """Set up the GDACS Feed platform.""" manager = hass.data[DOMAIN][FEED][entry.entry_id] - sensor = GdacsSensor(entry.entry_id, entry.unique_id, entry.title, manager) + sensor = GdacsSensor(entry, manager) async_add_entities([sensor]) - _LOGGER.debug("Sensor setup done") class GdacsSensor(SensorEntity): """Status sensor for the GDACS integration.""" _attr_should_poll = False + _attr_icon = DEFAULT_ICON + _attr_native_unit_of_measurement = DEFAULT_UNIT_OF_MEASUREMENT - def __init__(self, config_entry_id, config_unique_id, config_title, manager): + def __init__(self, config_entry: ConfigEntry, manager) -> None: """Initialize entity.""" - self._config_entry_id = config_entry_id - self._config_unique_id = config_unique_id - self._config_title = config_title + self._config_entry_id = config_entry.entry_id + self._attr_unique_id = config_entry.unique_id + self._attr_name = f"GDACS ({config_entry.title})" self._manager = manager self._status = None self._last_update = None @@ -57,7 +59,7 @@ class GdacsSensor(SensorEntity): self._created = None self._updated = None self._removed = None - self._remove_signal_status = None + self._remove_signal_status: Callable[[], None] | None = None async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" @@ -112,26 +114,6 @@ class GdacsSensor(SensorEntity): """Return the state of the sensor.""" return self._total - @property - def unique_id(self) -> str | None: - """Return a unique ID containing latitude/longitude.""" - return self._config_unique_id - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return f"GDACS ({self._config_title})" - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return DEFAULT_ICON - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return DEFAULT_UNIT_OF_MEASUREMENT - @property def extra_state_attributes(self): """Return the device state attributes.""" From 4a7088a996c25aa6072f3e490325754d97c3eca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 21 Aug 2023 14:10:41 +0200 Subject: [PATCH 0676/1151] Update aioairzone to v0.6.7 (#98744) --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 711da2ec993..bb1e448c8eb 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.6"] + "requirements": ["aioairzone==0.6.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 183c3293408..951a63e3be6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -188,7 +188,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.1 # homeassistant.components.airzone -aioairzone==0.6.6 +aioairzone==0.6.7 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c3a13ae48e..db0cbcbe44d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.1 # homeassistant.components.airzone -aioairzone==0.6.6 +aioairzone==0.6.7 # homeassistant.components.ambient_station aioambient==2023.04.0 From 4518dad83b47d63b76912c444ab4be159bc6af18 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 14:19:21 +0200 Subject: [PATCH 0677/1151] Use snapshot assertion for Airnow diagnostics (#98727) --- tests/components/airnow/conftest.py | 1 + .../airnow/snapshots/test_diagnostics.ambr | 39 +++++++++++++++ tests/components/airnow/test_diagnostics.py | 48 +++++-------------- 3 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 tests/components/airnow/snapshots/test_diagnostics.ambr diff --git a/tests/components/airnow/conftest.py b/tests/components/airnow/conftest.py index 47f20ccd883..15298ef3db0 100644 --- a/tests/components/airnow/conftest.py +++ b/tests/components/airnow/conftest.py @@ -16,6 +16,7 @@ def config_entry_fixture(hass, config): """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, + entry_id="3bd2acb0e4f0476d40865546d0d91921", unique_id=f"{config[CONF_LATITUDE]}-{config[CONF_LONGITUDE]}", data=config, ) diff --git a/tests/components/airnow/snapshots/test_diagnostics.ambr b/tests/components/airnow/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..ca333bbff72 --- /dev/null +++ b/tests/components/airnow/snapshots/test_diagnostics.ambr @@ -0,0 +1,39 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'AQI': 44, + 'Category.Name': 'Good', + 'Category.Number': 1, + 'DateObserved': '2020-12-20', + 'HourObserved': 15, + 'Latitude': '**REDACTED**', + 'Longitude': '**REDACTED**', + 'O3': 0.048, + 'PM10': 12, + 'PM2.5': 8.9, + 'Pollutant': 'O3', + 'ReportingArea': '**REDACTED**', + 'StateCode': '**REDACTED**', + }), + 'entry': dict({ + 'data': dict({ + 'api_key': '**REDACTED**', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'radius': 75, + }), + 'disabled_by': None, + 'domain': 'airnow', + 'entry_id': '3bd2acb0e4f0476d40865546d0d91921', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/airnow/test_diagnostics.py b/tests/components/airnow/test_diagnostics.py index 38049cfec4b..ecf6acc1c80 100644 --- a/tests/components/airnow/test_diagnostics.py +++ b/tests/components/airnow/test_diagnostics.py @@ -1,5 +1,6 @@ """Test AirNow diagnostics.""" -from homeassistant.components.diagnostics import REDACTED +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -7,41 +8,14 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, config_entry, hass_client: ClientSessionGenerator, setup_airnow + hass: HomeAssistant, + config_entry, + hass_client: ClientSessionGenerator, + setup_airnow, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "entry_id": config_entry.entry_id, - "version": 1, - "domain": "airnow", - "title": REDACTED, - "data": { - "api_key": REDACTED, - "latitude": REDACTED, - "longitude": REDACTED, - "radius": 75, - }, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - }, - "data": { - "O3": 0.048, - "PM2.5": 8.9, - "HourObserved": 15, - "DateObserved": "2020-12-20", - "StateCode": REDACTED, - "ReportingArea": REDACTED, - "Latitude": REDACTED, - "Longitude": REDACTED, - "PM10": 12, - "AQI": 44, - "Category.Number": 1, - "Category.Name": "Good", - "Pollutant": "O3", - }, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From 00904a107dce1e10524d26194fb2a5ffc527e89f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Aug 2023 08:44:45 -0500 Subject: [PATCH 0678/1151] Bump yalexs to 1.8.0 (#98751) --- 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 272a0ca4335..cd2737adca3 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.7.0", "yalexs-ble==2.2.3"] + "requirements": ["yalexs==1.8.0", "yalexs-ble==2.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 951a63e3be6..39f96a600d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2728,7 +2728,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.2.3 # homeassistant.components.august -yalexs==1.7.0 +yalexs==1.8.0 # homeassistant.components.yeelight yeelight==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db0cbcbe44d..d000c692b70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2010,7 +2010,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.2.3 # homeassistant.components.august -yalexs==1.7.0 +yalexs==1.8.0 # homeassistant.components.yeelight yeelight==0.7.13 From d9906b63b7ef296c066316a2d69eaf0d792862af Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 21 Aug 2023 15:47:11 +0200 Subject: [PATCH 0679/1151] Add `payload` to Scrape config flow (#98412) Payload to config flow --- .../components/scrape/config_flow.py | 2 + homeassistant/components/scrape/strings.json | 8 ++- tests/components/scrape/test_config_flow.py | 63 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/scrape/config_flow.py b/homeassistant/components/scrape/config_flow.py index 3ca13e56b29..1f74caf83f0 100644 --- a/homeassistant/components/scrape/config_flow.py +++ b/homeassistant/components/scrape/config_flow.py @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_PASSWORD, + CONF_PAYLOAD, CONF_RESOURCE, CONF_TIMEOUT, CONF_UNIQUE_ID, @@ -77,6 +78,7 @@ RESOURCE_SETUP = { vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): SelectSelector( SelectSelectorConfig(options=METHODS, mode=SelectSelectorMode.DROPDOWN) ), + vol.Optional(CONF_PAYLOAD): ObjectSelector(), vol.Optional(CONF_AUTHENTICATION): SelectSelector( SelectSelectorConfig( options=[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION], diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index 4301bb7d5a0..fc2d83dada4 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -16,6 +16,7 @@ "password": "[%key:common::config_flow::data::password%]", "headers": "Headers", "method": "Method", + "payload": "Payload", "timeout": "Timeout", "encoding": "Character encoding" }, @@ -25,7 +26,8 @@ "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed", "headers": "Headers to use for the web request", "timeout": "Timeout for connection to website", - "encoding": "Character encoding to use. Defaults to UTF-8" + "encoding": "Character encoding to use. Defaults to UTF-8", + "payload": "Payload to use when method is POST" } }, "sensor": { @@ -107,6 +109,7 @@ "data": { "resource": "[%key:component::scrape::config::step::user::data::resource%]", "method": "[%key:component::scrape::config::step::user::data::method%]", + "payload": "[%key:component::scrape::config::step::user::data::payload%]", "authentication": "[%key:component::scrape::config::step::user::data::authentication%]", "username": "[%key:component::scrape::config::step::user::data::username%]", "password": "[%key:component::scrape::config::step::user::data::password%]", @@ -121,7 +124,8 @@ "headers": "[%key:component::scrape::config::step::user::data_description::headers%]", "verify_ssl": "[%key:component::scrape::config::step::user::data_description::verify_ssl%]", "timeout": "[%key:component::scrape::config::step::user::data_description::timeout%]", - "encoding": "[%key:component::scrape::config::step::user::data_description::encoding%]" + "encoding": "[%key:component::scrape::config::step::user::data_description::encoding%]", + "payload": "[%key:component::scrape::config::step::user::data_description::payload%]" } } } diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py index 9c6c5e0b4de..9e1895f3a58 100644 --- a/tests/components/scrape/test_config_flow.py +++ b/tests/components/scrape/test_config_flow.py @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_PASSWORD, + CONF_PAYLOAD, CONF_RESOURCE, CONF_TIMEOUT, CONF_UNIQUE_ID, @@ -99,6 +100,68 @@ async def test_form( assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_with_post( + hass: HomeAssistant, get_data: MockRestData, mock_setup_entry: AsyncMock +) -> None: + """Test we get the form using POST method.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.rest.RestData", + return_value=get_data, + ) as mock_data: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_PAYLOAD: "POST", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10.0, + }, + ) + await hass.async_block_till_done() + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_NAME: "Current version", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0.0, + CONF_DEVICE_CLASS: NONE_SENTINEL, + CONF_STATE_CLASS: NONE_SENTINEL, + CONF_UNIT_OF_MEASUREMENT: NONE_SENTINEL, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["version"] == 1 + assert result3["options"] == { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: "GET", + CONF_PAYLOAD: "POST", + CONF_VERIFY_SSL: True, + CONF_TIMEOUT: 10.0, + CONF_ENCODING: "UTF-8", + "sensor": [ + { + CONF_NAME: "Current version", + CONF_SELECT: ".current-version h1", + CONF_INDEX: 0.0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + } + ], + } + + assert len(mock_data.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_flow_fails( hass: HomeAssistant, get_data: MockRestData, mock_setup_entry: AsyncMock ) -> None: From 207e3f90a66e1b34a9dce05fc2caafc3662150d2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 21 Aug 2023 15:48:14 +0200 Subject: [PATCH 0680/1151] Modernize template weather (#98064) * Modernize template weather * mods * adds templates * Fixes * review comments * more comments * Fix validator * Tests * Mods * Fix ruff --- homeassistant/components/template/weather.py | 167 ++++++-- tests/components/template/test_weather.py | 423 ++++++++++++++++++- 2 files changed, 563 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 81a6badfc34..85f2f82c213 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -1,6 +1,9 @@ """Template platform that aggregates meteorological data.""" from __future__ import annotations +from functools import partial +from typing import Any, Literal + import voluptuous as vol from homeassistant.components.weather import ( @@ -22,9 +25,11 @@ from homeassistant.components.weather import ( ENTITY_ID_FORMAT, Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.const import CONF_NAME, CONF_TEMPERATURE_UNIT, CONF_UNIQUE_ID -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id @@ -39,6 +44,8 @@ from homeassistant.util.unit_conversion import ( from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf +CHECK_FORECAST_KEYS = set().union(Forecast.__annotations__.keys()) + CONDITION_CLASSES = { ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -68,6 +75,9 @@ CONF_WIND_BEARING_TEMPLATE = "wind_bearing_template" CONF_OZONE_TEMPLATE = "ozone_template" CONF_VISIBILITY_TEMPLATE = "visibility_template" CONF_FORECAST_TEMPLATE = "forecast_template" +CONF_FORECAST_DAILY_TEMPLATE = "forecast_daily_template" +CONF_FORECAST_HOURLY_TEMPLATE = "forecast_hourly_template" +CONF_FORECAST_TWICE_DAILY_TEMPLATE = "forecast_twice_daily_template" CONF_PRESSURE_UNIT = "pressure_unit" CONF_WIND_SPEED_UNIT = "wind_speed_unit" CONF_VISIBILITY_UNIT = "visibility_unit" @@ -77,30 +87,40 @@ CONF_CLOUD_COVERAGE_TEMPLATE = "cloud_coverage_template" CONF_DEW_POINT_TEMPLATE = "dew_point_template" CONF_APPARENT_TEMPERATURE_TEMPLATE = "apparent_temperature_template" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_CONDITION_TEMPLATE): cv.template, - vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template, - vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template, - vol.Optional(CONF_ATTRIBUTION_TEMPLATE): cv.template, - vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template, - vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template, - vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template, - vol.Optional(CONF_OZONE_TEMPLATE): cv.template, - vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template, - vol.Optional(CONF_FORECAST_TEMPLATE): cv.template, - vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(TemperatureConverter.VALID_UNITS), - vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS), - vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(SpeedConverter.VALID_UNITS), - vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS), - vol.Optional(CONF_PRECIPITATION_UNIT): vol.In(DistanceConverter.VALID_UNITS), - vol.Optional(CONF_WIND_GUST_SPEED_TEMPLATE): cv.template, - vol.Optional(CONF_CLOUD_COVERAGE_TEMPLATE): cv.template, - vol.Optional(CONF_DEW_POINT_TEMPLATE): cv.template, - vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template, - } +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_FORECAST_TEMPLATE), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_CONDITION_TEMPLATE): cv.template, + vol.Required(CONF_TEMPERATURE_TEMPLATE): cv.template, + vol.Required(CONF_HUMIDITY_TEMPLATE): cv.template, + vol.Optional(CONF_ATTRIBUTION_TEMPLATE): cv.template, + vol.Optional(CONF_PRESSURE_TEMPLATE): cv.template, + vol.Optional(CONF_WIND_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template, + vol.Optional(CONF_OZONE_TEMPLATE): cv.template, + vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template, + vol.Optional(CONF_FORECAST_TEMPLATE): cv.template, + vol.Optional(CONF_FORECAST_DAILY_TEMPLATE): cv.template, + vol.Optional(CONF_FORECAST_HOURLY_TEMPLATE): cv.template, + vol.Optional(CONF_FORECAST_TWICE_DAILY_TEMPLATE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_TEMPERATURE_UNIT): vol.In( + TemperatureConverter.VALID_UNITS + ), + vol.Optional(CONF_PRESSURE_UNIT): vol.In(PressureConverter.VALID_UNITS), + vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(SpeedConverter.VALID_UNITS), + vol.Optional(CONF_VISIBILITY_UNIT): vol.In(DistanceConverter.VALID_UNITS), + vol.Optional(CONF_PRECIPITATION_UNIT): vol.In( + DistanceConverter.VALID_UNITS + ), + vol.Optional(CONF_WIND_GUST_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_CLOUD_COVERAGE_TEMPLATE): cv.template, + vol.Optional(CONF_DEW_POINT_TEMPLATE): cv.template, + vol.Optional(CONF_APPARENT_TEMPERATURE_TEMPLATE): cv.template, + } + ), ) @@ -151,6 +171,11 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): self._ozone_template = config.get(CONF_OZONE_TEMPLATE) self._visibility_template = config.get(CONF_VISIBILITY_TEMPLATE) self._forecast_template = config.get(CONF_FORECAST_TEMPLATE) + self._forecast_daily_template = config.get(CONF_FORECAST_DAILY_TEMPLATE) + self._forecast_hourly_template = config.get(CONF_FORECAST_HOURLY_TEMPLATE) + self._forecast_twice_daily_template = config.get( + CONF_FORECAST_TWICE_DAILY_TEMPLATE + ) self._wind_gust_speed_template = config.get(CONF_WIND_GUST_SPEED_TEMPLATE) self._cloud_coverage_template = config.get(CONF_CLOUD_COVERAGE_TEMPLATE) self._dew_point_template = config.get(CONF_DEW_POINT_TEMPLATE) @@ -180,6 +205,17 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): self._dew_point = None self._apparent_temperature = None self._forecast: list[Forecast] = [] + self._forecast_daily: list[Forecast] = [] + self._forecast_hourly: list[Forecast] = [] + self._forecast_twice_daily: list[Forecast] = [] + + self._attr_supported_features = 0 + if self._forecast_daily_template: + self._attr_supported_features |= WeatherEntityFeature.FORECAST_DAILY + if self._forecast_hourly_template: + self._attr_supported_features |= WeatherEntityFeature.FORECAST_HOURLY + if self._forecast_twice_daily_template: + self._attr_supported_features |= WeatherEntityFeature.FORECAST_TWICE_DAILY @property def condition(self) -> str | None: @@ -246,6 +282,18 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): """Return the forecast.""" return self._forecast + async def async_forecast_daily(self) -> list[Forecast]: + """Return the daily forecast in native units.""" + return self._forecast_daily + + async def async_forecast_hourly(self) -> list[Forecast]: + """Return the daily forecast in native units.""" + return self._forecast_hourly + + async def async_forecast_twice_daily(self) -> list[Forecast]: + """Return the daily forecast in native units.""" + return self._forecast_twice_daily + @property def attribution(self) -> str | None: """Return the attribution.""" @@ -327,4 +375,73 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): "_forecast", self._forecast_template, ) + + if self._forecast_daily_template: + self.add_template_attribute( + "_forecast_daily", + self._forecast_daily_template, + on_update=partial(self._update_forecast, "daily"), + validator=partial(self._validate_forecast, "daily"), + ) + if self._forecast_hourly_template: + self.add_template_attribute( + "_forecast_hourly", + self._forecast_hourly_template, + on_update=partial(self._update_forecast, "hourly"), + validator=partial(self._validate_forecast, "hourly"), + ) + if self._forecast_twice_daily_template: + self.add_template_attribute( + "_forecast_twice_daily", + self._forecast_twice_daily_template, + on_update=partial(self._update_forecast, "twice_daily"), + validator=partial(self._validate_forecast, "twice_daily"), + ) + await super().async_added_to_hass() + + @callback + def _update_forecast( + self, + forecast_type: Literal["daily", "hourly", "twice_daily"], + result: list[Forecast] | TemplateError, + ) -> None: + """Save template result and trigger forecast listener.""" + attr_result = None if isinstance(result, TemplateError) else result + setattr(self, f"_forecast_{forecast_type}", attr_result) + self.hass.create_task(self.async_update_listeners([forecast_type])) + + @callback + def _validate_forecast( + self, + forecast_type: Literal["daily", "hourly", "twice_daily"], + result: Any, + ) -> list[Forecast] | None: + """Validate the forecasts.""" + if result is None: + return None + + if not isinstance(result, list): + raise vol.Invalid( + "Forecasts is not a list, see Weather documentation https://www.home-assistant.io/integrations/weather/" + ) + for forecast in result: + if not isinstance(forecast, dict): + raise vol.Invalid( + "Forecast in list is not a dict, see Weather documentation https://www.home-assistant.io/integrations/weather/" + ) + diff_result = set().union(forecast.keys()).difference(CHECK_FORECAST_KEYS) + if diff_result: + raise vol.Invalid( + "Only valid keys in Forecast are allowed, see Weather documentation https://www.home-assistant.io/integrations/weather/" + ) + if forecast_type == "twice_daily" and "is_daytime" not in forecast: + raise vol.Invalid( + "`is_daytime` is missing in twice_daily forecast, see Weather documentation https://www.home-assistant.io/integrations/weather/" + ) + if "datetime" not in forecast: + raise vol.Invalid( + "`datetime` is required in forecasts, see Weather documentation https://www.home-assistant.io/integrations/weather/" + ) + continue + return result diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py index 38cf439987d..97965a5643e 100644 --- a/tests/components/template/test_weather.py +++ b/tests/components/template/test_weather.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components.weather import ( + ATTR_FORECAST, ATTR_WEATHER_APPARENT_TEMPERATURE, ATTR_WEATHER_CLOUD_COVERAGE, ATTR_WEATHER_DEW_POINT, @@ -13,13 +14,15 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_GUST_SPEED, ATTR_WEATHER_WIND_SPEED, - DOMAIN, + DOMAIN as WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + Forecast, ) from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant -@pytest.mark.parametrize(("count", "domain"), [(1, DOMAIN)]) +@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) @pytest.mark.parametrize( "config", [ @@ -74,3 +77,419 @@ async def test_template_state_text(hass: HomeAssistant, start_ha) -> None: assert state is not None assert state.state == "sunny" assert state.attributes.get(v_attr) == value + + +@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) +@pytest.mark.parametrize( + "config", + [ + { + "weather": [ + { + "platform": "template", + "name": "forecast", + "condition_template": "sunny", + "forecast_template": "{{ states.weather.forecast.attributes.forecast }}", + "forecast_daily_template": "{{ states.weather.forecast.attributes.forecast }}", + "forecast_hourly_template": "{{ states.weather.forecast.attributes.forecast }}", + "forecast_twice_daily_template": "{{ states.weather.forecast_twice_daily.attributes.forecast }}", + "temperature_template": "{{ states('sensor.temperature') | float }}", + "humidity_template": "{{ states('sensor.humidity') | int }}", + }, + ] + }, + ], +) +async def test_forecasts(hass: HomeAssistant, start_ha) -> None: + """Test forecast service.""" + for attr, _v_attr, value in [ + ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), + ("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60), + ]: + hass.states.async_set(attr, value) + await hass.async_block_till_done() + + hass.states.async_set( + "weather.forecast", + "sunny", + { + ATTR_FORECAST: [ + Forecast( + condition="cloudy", + datetime="2023-02-17T14:00:00+00:00", + temperature=14.2, + ) + ] + }, + ) + hass.states.async_set( + "weather.forecast_twice_daily", + "fog", + { + ATTR_FORECAST: [ + Forecast( + condition="fog", + datetime="2023-02-17T14:00:00+00:00", + temperature=14.2, + is_daytime=True, + ) + ] + }, + ) + await hass.async_block_till_done() + state = hass.states.get("weather.forecast") + assert state is not None + assert state.state == "sunny" + state2 = hass.states.get("weather.forecast_twice_daily") + assert state2 is not None + assert state2.state == "fog" + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + {"entity_id": "weather.forecast", "type": "daily"}, + blocking=True, + return_response=True, + ) + assert response == { + "forecast": [ + { + "condition": "cloudy", + "datetime": "2023-02-17T14:00:00+00:00", + "temperature": 14.2, + } + ] + } + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + {"entity_id": "weather.forecast", "type": "hourly"}, + blocking=True, + return_response=True, + ) + assert response == { + "forecast": [ + { + "condition": "cloudy", + "datetime": "2023-02-17T14:00:00+00:00", + "temperature": 14.2, + } + ] + } + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + {"entity_id": "weather.forecast", "type": "twice_daily"}, + blocking=True, + return_response=True, + ) + assert response == { + "forecast": [ + { + "condition": "fog", + "datetime": "2023-02-17T14:00:00+00:00", + "temperature": 14.2, + "is_daytime": True, + } + ] + } + + hass.states.async_set( + "weather.forecast", + "sunny", + { + ATTR_FORECAST: [ + Forecast( + condition="cloudy", + datetime="2023-02-17T14:00:00+00:00", + temperature=16.9, + ) + ] + }, + ) + await hass.async_block_till_done() + state = hass.states.get("weather.forecast") + assert state is not None + assert state.state == "sunny" + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + {"entity_id": "weather.forecast", "type": "daily"}, + blocking=True, + return_response=True, + ) + assert response == { + "forecast": [ + { + "condition": "cloudy", + "datetime": "2023-02-17T14:00:00+00:00", + "temperature": 16.9, + } + ] + } + + +@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) +@pytest.mark.parametrize( + "config", + [ + { + "weather": [ + { + "platform": "template", + "name": "forecast", + "condition_template": "sunny", + "forecast_template": "{{ states.weather.forecast.attributes.forecast }}", + "forecast_daily_template": "{{ states.weather.forecast.attributes.forecast }}", + "forecast_hourly_template": "{{ states.weather.forecast_hourly.attributes.forecast }}", + "temperature_template": "{{ states('sensor.temperature') | float }}", + "humidity_template": "{{ states('sensor.humidity') | int }}", + }, + ] + }, + ], +) +async def test_forecast_invalid( + hass: HomeAssistant, start_ha, caplog: pytest.LogCaptureFixture +) -> None: + """Test invalid forecasts.""" + for attr, _v_attr, value in [ + ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), + ("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60), + ]: + hass.states.async_set(attr, value) + await hass.async_block_till_done() + + hass.states.async_set( + "weather.forecast", + "sunny", + { + ATTR_FORECAST: [ + Forecast( + condition="cloudy", + datetime="2023-02-17T14:00:00+00:00", + temperature=14.2, + not_correct=1, + ) + ] + }, + ) + hass.states.async_set( + "weather.forecast_hourly", + "sunny", + {ATTR_FORECAST: None}, + ) + await hass.async_block_till_done() + state = hass.states.get("weather.forecast_hourly") + assert state is not None + assert state.state == "sunny" + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + {"entity_id": "weather.forecast", "type": "daily"}, + blocking=True, + return_response=True, + ) + assert response == {"forecast": []} + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + {"entity_id": "weather.forecast", "type": "hourly"}, + blocking=True, + return_response=True, + ) + assert response == {"forecast": []} + assert "Only valid keys in Forecast are allowed" in caplog.text + + +@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) +@pytest.mark.parametrize( + "config", + [ + { + "weather": [ + { + "platform": "template", + "name": "forecast", + "condition_template": "sunny", + "forecast_template": "{{ states.weather.forecast.attributes.forecast }}", + "forecast_twice_daily_template": "{{ states.weather.forecast_twice_daily.attributes.forecast }}", + "temperature_template": "{{ states('sensor.temperature') | float }}", + "humidity_template": "{{ states('sensor.humidity') | int }}", + }, + ] + }, + ], +) +async def test_forecast_invalid_is_daytime_missing_in_twice_daily( + hass: HomeAssistant, start_ha, caplog: pytest.LogCaptureFixture +) -> None: + """Test forecast service invalid when is_daytime missing in twice_daily forecast.""" + for attr, _v_attr, value in [ + ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), + ("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60), + ]: + hass.states.async_set(attr, value) + await hass.async_block_till_done() + + hass.states.async_set( + "weather.forecast_twice_daily", + "sunny", + { + ATTR_FORECAST: [ + Forecast( + condition="cloudy", + datetime="2023-02-17T14:00:00+00:00", + temperature=14.2, + ) + ] + }, + ) + await hass.async_block_till_done() + state = hass.states.get("weather.forecast_twice_daily") + assert state is not None + assert state.state == "sunny" + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + {"entity_id": "weather.forecast", "type": "twice_daily"}, + blocking=True, + return_response=True, + ) + assert response == {"forecast": []} + assert "`is_daytime` is missing in twice_daily forecast" in caplog.text + + +@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) +@pytest.mark.parametrize( + "config", + [ + { + "weather": [ + { + "platform": "template", + "name": "forecast", + "condition_template": "sunny", + "forecast_template": "{{ states.weather.forecast.attributes.forecast }}", + "forecast_twice_daily_template": "{{ states.weather.forecast_twice_daily.attributes.forecast }}", + "temperature_template": "{{ states('sensor.temperature') | float }}", + "humidity_template": "{{ states('sensor.humidity') | int }}", + }, + ] + }, + ], +) +async def test_forecast_invalid_datetime_missing( + hass: HomeAssistant, start_ha, caplog: pytest.LogCaptureFixture +) -> None: + """Test forecast service invalid when datetime missing.""" + for attr, _v_attr, value in [ + ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), + ("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60), + ]: + hass.states.async_set(attr, value) + await hass.async_block_till_done() + + hass.states.async_set( + "weather.forecast_twice_daily", + "sunny", + { + ATTR_FORECAST: [ + Forecast( + condition="cloudy", + temperature=14.2, + is_daytime=True, + ) + ] + }, + ) + await hass.async_block_till_done() + state = hass.states.get("weather.forecast_twice_daily") + assert state is not None + assert state.state == "sunny" + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + {"entity_id": "weather.forecast", "type": "twice_daily"}, + blocking=True, + return_response=True, + ) + assert response == {"forecast": []} + assert "`datetime` is required in forecasts" in caplog.text + + +@pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) +@pytest.mark.parametrize( + "config", + [ + { + "weather": [ + { + "platform": "template", + "name": "forecast", + "condition_template": "sunny", + "forecast_template": "{{ states.weather.forecast.attributes.forecast }}", + "forecast_daily_template": "{{ states.weather.forecast_daily.attributes.forecast }}", + "forecast_hourly_template": "{{ states.weather.forecast_hourly.attributes.forecast }}", + "temperature_template": "{{ states('sensor.temperature') | float }}", + "humidity_template": "{{ states('sensor.humidity') | int }}", + }, + ] + }, + ], +) +async def test_forecast_format_error( + hass: HomeAssistant, start_ha, caplog: pytest.LogCaptureFixture +) -> None: + """Test forecast service invalid on incorrect format.""" + for attr, _v_attr, value in [ + ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), + ("sensor.humidity", ATTR_WEATHER_HUMIDITY, 60), + ]: + hass.states.async_set(attr, value) + await hass.async_block_till_done() + + hass.states.async_set( + "weather.forecast_daily", + "sunny", + { + ATTR_FORECAST: [ + "cloudy", + "2023-02-17T14:00:00+00:00", + 14.2, + 1, + ] + }, + ) + hass.states.async_set( + "weather.forecast_hourly", + "sunny", + { + ATTR_FORECAST: { + "condition": "cloudy", + "temperature": 14.2, + "is_daytime": True, + } + }, + ) + + await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + {"entity_id": "weather.forecast", "type": "daily"}, + blocking=True, + return_response=True, + ) + assert "Forecasts is not a list, see Weather documentation" in caplog.text + await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + {"entity_id": "weather.forecast", "type": "hourly"}, + blocking=True, + return_response=True, + ) + assert "Forecast in list is not a dict, see Weather documentation" in caplog.text From b8086f3c21537309f4e39aeec6b28a533723c85e Mon Sep 17 00:00:00 2001 From: Marco Garzola Date: Mon, 21 Aug 2023 17:10:24 +0200 Subject: [PATCH 0681/1151] Map heatercooler rotation speed as 3 level fan speed in homekit controller (#98291) Co-authored-by: J. Nick Koston --- .../components/homekit_controller/climate.py | 58 +++++++ .../fixtures/homespan_daikin_bridge.json | 161 ++++++++++++++++++ .../test_homespan_daikin_bridge.py | 51 ++++++ .../homekit_controller/test_climate.py | 100 +++++++++++ 4 files changed, 370 insertions(+) create mode 100644 tests/components/homekit_controller/fixtures/homespan_daikin_bridge.json create mode 100644 tests/components/homekit_controller/specific_devices/test_homespan_daikin_bridge.py diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index df43d8929e9..d3e9a0f13a6 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -23,6 +23,10 @@ from homeassistant.components.climate import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, FAN_ON, SWING_OFF, SWING_VERTICAL, @@ -35,6 +39,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from . import KNOWN_DEVICES from .connection import HKDevice @@ -86,6 +94,16 @@ SWING_MODE_HASS_TO_HOMEKIT = {v: k for k, v in SWING_MODE_HOMEKIT_TO_HASS.items( DEFAULT_MIN_STEP: Final = 1.0 +ROTATION_SPEED_LOW = 33 +ROTATION_SPEED_MEDIUM = 66 +ROTATION_SPEED_HIGH = 100 + +HASS_FAN_MODE_TO_HOMEKIT_ROTATION = { + FAN_LOW: ROTATION_SPEED_LOW, + FAN_MEDIUM: ROTATION_SPEED_MEDIUM, + FAN_HIGH: ROTATION_SPEED_HIGH, +} + async def async_setup_entry( hass: HomeAssistant, @@ -170,8 +188,45 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity): CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD, CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD, CharacteristicsTypes.SWING_MODE, + CharacteristicsTypes.ROTATION_SPEED, ] + def _get_rotation_speed_range(self) -> tuple[float, float]: + rotation_speed = self.service[CharacteristicsTypes.ROTATION_SPEED] + return round(rotation_speed.minValue or 0) + 1, round( + rotation_speed.maxValue or 100 + ) + + @property + def fan_modes(self) -> list[str]: + """Return the available fan modes.""" + return [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH] + + @property + def fan_mode(self) -> str | None: + """Return the current fan mode.""" + speed_range = self._get_rotation_speed_range() + speed_percentage = ranged_value_to_percentage( + speed_range, self.service.value(CharacteristicsTypes.ROTATION_SPEED) + ) + # homekit value 0 33 66 100 + if speed_percentage > ROTATION_SPEED_MEDIUM: + return FAN_HIGH + if speed_percentage > ROTATION_SPEED_LOW: + return FAN_MEDIUM + if speed_percentage > 0: + return FAN_LOW + return FAN_OFF + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + rotation = HASS_FAN_MODE_TO_HOMEKIT_ROTATION.get(fan_mode, 0) + speed_range = self._get_rotation_speed_range() + speed = round(percentage_to_ranged_value(speed_range, rotation)) + await self.async_put_characteristics( + {CharacteristicsTypes.ROTATION_SPEED: speed} + ) + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) @@ -387,6 +442,9 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity): if self.service.has(CharacteristicsTypes.SWING_MODE): features |= ClimateEntityFeature.SWING_MODE + if self.service.has(CharacteristicsTypes.ROTATION_SPEED): + features |= ClimateEntityFeature.FAN_MODE + return features diff --git a/tests/components/homekit_controller/fixtures/homespan_daikin_bridge.json b/tests/components/homekit_controller/fixtures/homespan_daikin_bridge.json new file mode 100644 index 00000000000..b3dd6f8a84e --- /dev/null +++ b/tests/components/homekit_controller/fixtures/homespan_daikin_bridge.json @@ -0,0 +1,161 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr", "ev"], + "format": "string", + "value": "1.0.0", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "1.0.0", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "Garzola Marco", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "Daikin-fwec3a-esp32-homekit-bridge", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "Air Conditioner", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "00000001", + "description": "Serial Number", + "maxLen": 64 + } + ] + }, + { + "iid": 9, + "type": "000000BC-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000B0-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 1, + "description": "Active" + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": ["pr", "ev"], + "format": "float", + "value": 27.9, + "description": "Current Temperature", + "unit": "celsius", + "minValue": 0, + "maxValue": 99, + "minStep": 0.5 + }, + { + "type": "000000B1-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 3, + "description": "Current Heater Cooler State" + }, + { + "type": "000000B2-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 2, + "description": "Target Heater Cooler State" + }, + { + "type": "0000000D-0000-1000-8000-0026BB765291", + "iid": 14, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 24.5, + "description": "Cooling Threshold Temperature", + "unit": "celsius", + "minValue": 18, + "maxValue": 32, + "minStep": 0.5 + }, + { + "type": "00000012-0000-1000-8000-0026BB765291", + "iid": 15, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 24.5, + "description": "Heating Threshold Temperature", + "unit": "celsius", + "minValue": 13, + "maxValue": 27, + "minStep": 0.5 + }, + { + "type": "00000029-0000-1000-8000-0026BB765291", + "iid": 16, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 100, + "description": "Rotation Speed", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr"], + "format": "string", + "value": "SlaveID 1", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_homespan_daikin_bridge.py b/tests/components/homekit_controller/specific_devices/test_homespan_daikin_bridge.py new file mode 100644 index 00000000000..5bb7003e58b --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_homespan_daikin_bridge.py @@ -0,0 +1,51 @@ +"""Tests for handling accessories on a Homespan esp32 daikin bridge.""" +from homeassistant.components.climate import ClimateEntityFeature +from homeassistant.core import HomeAssistant + +from ..common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_homespan_daikin_bridge_setup(hass: HomeAssistant) -> None: + """Test that aHomespan esp32 daikin bridge can be correctly setup in HA via HomeKit.""" + accessories = await setup_accessories_from_file(hass, "homespan_daikin_bridge.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="Air Conditioner", + model="Daikin-fwec3a-esp32-homekit-bridge", + manufacturer="Garzola Marco", + sw_version="1.0.0", + hw_version="1.0.0", + serial_number="00000001", + devices=[], + entities=[ + EntityTestInfo( + entity_id="climate.air_conditioner_slaveid_1", + friendly_name="Air Conditioner SlaveID 1", + unique_id="00:00:00:00:00:00_1_9", + supported_features=( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + ), + capabilities={ + "hvac_modes": ["heat_cool", "heat", "cool", "off"], + "min_temp": 18, + "max_temp": 32, + "target_temp_step": 0.5, + "fan_modes": ["off", "low", "medium", "high"], + }, + state="cool", + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 27c675b78ec..0f6a3633bd4 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -691,6 +691,9 @@ def create_heater_cooler_service(accessory): char = service.add_char(CharacteristicsTypes.SWING_MODE) char.value = 0 + char = service.add_char(CharacteristicsTypes.ROTATION_SPEED) + char.value = 100 + # Test heater-cooler devices def create_heater_cooler_service_min_max(accessory): @@ -867,6 +870,103 @@ async def test_heater_cooler_change_thermostat_temperature( ) +async def test_heater_cooler_change_fan_speed(hass: HomeAssistant, utcnow) -> None: + """Test that we can change the target fan speed.""" + helper = await setup_test_component(hass, create_heater_cooler_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVACMode.COOL}, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {"entity_id": "climate.testdevice", "fan_mode": "low"}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ROTATION_SPEED: 33, + }, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {"entity_id": "climate.testdevice", "fan_mode": "medium"}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ROTATION_SPEED: 66, + }, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {"entity_id": "climate.testdevice", "fan_mode": "high"}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ROTATION_SPEED: 100, + }, + ) + + +async def test_heater_cooler_read_fan_speed(hass: HomeAssistant, utcnow) -> None: + """Test that we can read the state of a HomeKit thermostat accessory.""" + helper = await setup_test_component(hass, create_heater_cooler_service) + + # Simulate that fan speed is off + await helper.async_update( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ROTATION_SPEED: 0, + }, + ) + + state = await helper.poll_and_get_state() + assert state.attributes["fan_mode"] == "off" + + # Simulate that fan speed is low + await helper.async_update( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ROTATION_SPEED: 33, + }, + ) + + state = await helper.poll_and_get_state() + assert state.attributes["fan_mode"] == "low" + + # Simulate that fan speed is medium + await helper.async_update( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ROTATION_SPEED: 66, + }, + ) + + state = await helper.poll_and_get_state() + assert state.attributes["fan_mode"] == "medium" + + # Simulate that fan speed is high + await helper.async_update( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ROTATION_SPEED: 100, + }, + ) + + state = await helper.poll_and_get_state() + assert state.attributes["fan_mode"] == "high" + + async def test_heater_cooler_read_thermostat_state(hass: HomeAssistant, utcnow) -> None: """Test that we can read the state of a HomeKit thermostat accessory.""" helper = await setup_test_component(hass, create_heater_cooler_service) From af0e48081f6a4edb6a8e5602986273cfbfee4fb7 Mon Sep 17 00:00:00 2001 From: ZigStar Date: Mon, 21 Aug 2023 16:28:42 +0100 Subject: [PATCH 0682/1151] Add ZigStar UZG-01 ZHA zeroconf autodiscovery (#98657) ZigStar UZG-01 ZHA Zeconf Autodiscovery --- homeassistant/components/zha/manifest.json | 4 ++++ homeassistant/generated/zeroconf.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 29fed3a3c9f..4f23945b105 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -111,6 +111,10 @@ "type": "_zigstar_gw._tcp.local.", "name": "*zigstar*" }, + { + "type": "_uzg-01._tcp.local.", + "name": "uzg-01*" + }, { "type": "_slzb-06._tcp.local.", "name": "slzb-06*" diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 6b5676c4a25..3874a06ab4b 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -683,6 +683,12 @@ ZEROCONF = { "domain": "apple_tv", }, ], + "_uzg-01._tcp.local.": [ + { + "domain": "zha", + "name": "uzg-01*", + }, + ], "_viziocast._tcp.local.": [ { "domain": "vizio", From c86565b9bc730c33cefd8a24817d5073e52a203f Mon Sep 17 00:00:00 2001 From: Florent Thiery Date: Mon, 21 Aug 2023 17:45:15 +0200 Subject: [PATCH 0683/1151] Reduce Freebox router Raid warning to one occurence (#98740) * consider Freebox router does not support Raid if the first enumeration raised an http error, fixes #98274 * add router name to warning message * reduce log level to info, remove details --- homeassistant/components/freebox/router.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 6111eb85b4c..f42a386087f 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -71,6 +71,7 @@ class FreeboxRouter: self.devices: dict[str, dict[str, Any]] = {} self.disks: dict[int, dict[str, Any]] = {} + self.supports_raid = True self.raids: dict[int, dict[str, Any]] = {} self.sensors_temperature: dict[str, int] = {} self.sensors_connection: dict[str, float] = {} @@ -159,14 +160,21 @@ class FreeboxRouter: async def _update_raids_sensors(self) -> None: """Update Freebox raids.""" - # None at first request + if not self.supports_raid: + return + try: fbx_raids: list[dict[str, Any]] = await self._api.storage.get_raids() or [] except HttpRequestError: - _LOGGER.warning("Unable to enumerate raid disks") - else: - for fbx_raid in fbx_raids: - self.raids[fbx_raid["id"]] = fbx_raid + self.supports_raid = False + _LOGGER.info( + "Router %s API does not support RAID", + self.name, + ) + return + + for fbx_raid in fbx_raids: + self.raids[fbx_raid["id"]] = fbx_raid async def update_home_devices(self) -> None: """Update Home devices (alarm, light, sensor, switch, remote ...).""" From a42d975c490a315ab42a7a9754627bbf95eff200 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 22 Aug 2023 04:13:02 +1200 Subject: [PATCH 0684/1151] ESPHome Wake Word support (#98544) * ESPHome Wake Word support * Remove all vad code from esphome integration * Catch exception when no wake word provider found * Remove import * Remove esphome vad tests * Add tests * More tests --- homeassistant/components/esphome/manager.py | 6 +- .../components/esphome/voice_assistant.py | 163 ++++-------------- .../esphome/test_voice_assistant.py | 161 +++++++---------- 3 files changed, 105 insertions(+), 225 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 35939dc9b1f..fb3e0a1e79a 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -18,7 +18,6 @@ from aioesphomeapi import ( UserServiceArgType, VoiceAssistantEventType, ) -from aioesphomeapi.model import VoiceAssistantCommandFlag from awesomeversion import AwesomeVersion import voluptuous as vol @@ -320,7 +319,7 @@ class ESPHomeManager: self.voice_assistant_udp_server = None async def _handle_pipeline_start( - self, conversation_id: str, use_vad: int + self, conversation_id: str, flags: int ) -> int | None: """Start a voice assistant pipeline.""" if self.voice_assistant_udp_server is not None: @@ -340,8 +339,7 @@ class ESPHomeManager: voice_assistant_udp_server.run_pipeline( device_id=self.device_id, conversation_id=conversation_id or None, - use_vad=VoiceAssistantCommandFlag(use_vad) - == VoiceAssistantCommandFlag.USE_VAD, + flags=flags, ), "esphome.voice_assistant_udp_server.run_pipeline", ) diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index f870f9e42f7..a9397eda935 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -2,26 +2,23 @@ from __future__ import annotations import asyncio -from collections import deque -from collections.abc import AsyncIterable, Callable, MutableSequence, Sequence +from collections.abc import AsyncIterable, Callable import logging import socket from typing import cast -from aioesphomeapi import VoiceAssistantEventType +from aioesphomeapi import VoiceAssistantCommandFlag, VoiceAssistantEventType from homeassistant.components import stt, tts from homeassistant.components.assist_pipeline import ( PipelineEvent, PipelineEventType, PipelineNotFound, + PipelineStage, async_pipeline_from_audio_stream, select as pipeline_select, ) -from homeassistant.components.assist_pipeline.vad import ( - VadSensitivity, - VoiceCommandSegmenter, -) +from homeassistant.components.assist_pipeline.error import WakeWordDetectionError from homeassistant.components.media_player import async_process_play_media_url from homeassistant.core import Context, HomeAssistant, callback @@ -47,6 +44,8 @@ _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[ VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END: PipelineEventType.INTENT_END, VoiceAssistantEventType.VOICE_ASSISTANT_TTS_START: PipelineEventType.TTS_START, VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: PipelineEventType.TTS_END, + VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_START: PipelineEventType.WAKE_WORD_START, + VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END: PipelineEventType.WAKE_WORD_END, } ) @@ -183,121 +182,33 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): ) else: self._tts_done.set() + elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END: + assert event.data is not None + if not event.data["wake_word_output"]: + event_type = VoiceAssistantEventType.VOICE_ASSISTANT_ERROR + data_to_send = { + "code": "no_wake_word", + "message": "No wake word detected", + } + error = True elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_ERROR: assert event.data is not None data_to_send = { "code": event.data["code"], "message": event.data["message"], } - self._tts_done.set() error = True self.handle_event(event_type, data_to_send) if error: + self._tts_done.set() self.handle_finished() - async def _wait_for_speech( - self, - segmenter: VoiceCommandSegmenter, - chunk_buffer: MutableSequence[bytes], - ) -> bool: - """Buffer audio chunks until speech is detected. - - Raises asyncio.TimeoutError if no audio data is retrievable from the queue (device stops sending packets / networking issue). - - Returns True if speech was detected - Returns False if the connection was stopped gracefully (b"" put onto the queue). - """ - # Timeout if no audio comes in for a while. - async with asyncio.timeout(self.audio_timeout): - chunk = await self.queue.get() - - while chunk: - segmenter.process(chunk) - # Buffer the data we have taken from the queue - chunk_buffer.append(chunk) - if segmenter.in_command: - return True - - async with asyncio.timeout(self.audio_timeout): - chunk = await self.queue.get() - - # If chunk is falsey, `stop()` was called - return False - - async def _segment_audio( - self, - segmenter: VoiceCommandSegmenter, - chunk_buffer: Sequence[bytes], - ) -> AsyncIterable[bytes]: - """Yield audio chunks until voice command has finished. - - Raises asyncio.TimeoutError if no audio data is retrievable from the queue. - """ - # Buffered chunks first - for buffered_chunk in chunk_buffer: - yield buffered_chunk - - # Timeout if no audio comes in for a while. - async with asyncio.timeout(self.audio_timeout): - chunk = await self.queue.get() - - while chunk: - if not segmenter.process(chunk): - # Voice command is finished - break - - yield chunk - - async with asyncio.timeout(self.audio_timeout): - chunk = await self.queue.get() - - async def _iterate_packets_with_vad( - self, pipeline_timeout: float, silence_seconds: float - ) -> Callable[[], AsyncIterable[bytes]] | None: - segmenter = VoiceCommandSegmenter(silence_seconds=silence_seconds) - chunk_buffer: deque[bytes] = deque(maxlen=100) - try: - async with asyncio.timeout(pipeline_timeout): - speech_detected = await self._wait_for_speech(segmenter, chunk_buffer) - if not speech_detected: - _LOGGER.debug( - "Device stopped sending audio before speech was detected" - ) - self.handle_finished() - return None - except asyncio.TimeoutError: - self.handle_event( - VoiceAssistantEventType.VOICE_ASSISTANT_ERROR, - { - "code": "speech-timeout", - "message": "Timed out waiting for speech", - }, - ) - self.handle_finished() - return None - - async def _stream_packets() -> AsyncIterable[bytes]: - try: - async for chunk in self._segment_audio(segmenter, chunk_buffer): - yield chunk - except asyncio.TimeoutError: - self.handle_event( - VoiceAssistantEventType.VOICE_ASSISTANT_ERROR, - { - "code": "speech-timeout", - "message": "No speech detected", - }, - ) - self.handle_finished() - - return _stream_packets - async def run_pipeline( self, device_id: str, conversation_id: str | None, - use_vad: bool = False, + flags: int = 0, pipeline_timeout: float = 30.0, ) -> None: """Run the Voice Assistant pipeline.""" @@ -306,24 +217,11 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): "raw" if self.device_info.voice_assistant_version >= 2 else "mp3" ) - if use_vad: - stt_stream = await self._iterate_packets_with_vad( - pipeline_timeout, - silence_seconds=VadSensitivity.to_seconds( - pipeline_select.get_vad_sensitivity( - self.hass, - DOMAIN, - self.device_info.mac_address, - ) - ), - ) - # Error or timeout occurred and was handled already - if stt_stream is None: - return - else: - stt_stream = self._iterate_packets - _LOGGER.debug("Starting pipeline") + if flags & VoiceAssistantCommandFlag.USE_WAKE_WORD: + start_stage = PipelineStage.WAKE_WORD + else: + start_stage = PipelineStage.STT try: async with asyncio.timeout(pipeline_timeout): await async_pipeline_from_audio_stream( @@ -338,13 +236,14 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, channel=stt.AudioChannels.CHANNEL_MONO, ), - stt_stream=stt_stream(), + stt_stream=self._iterate_packets(), pipeline_id=pipeline_select.get_chosen_pipeline( self.hass, DOMAIN, self.device_info.mac_address ), conversation_id=conversation_id, device_id=device_id, tts_audio_output=tts_audio_output, + start_stage=start_stage, ) # Block until TTS is done sending @@ -356,11 +255,23 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): VoiceAssistantEventType.VOICE_ASSISTANT_ERROR, { "code": "pipeline not found", - "message": "Selected pipeline timeout", + "message": "Selected pipeline not found", }, ) _LOGGER.warning("Pipeline not found") + except WakeWordDetectionError as e: + self.handle_event( + VoiceAssistantEventType.VOICE_ASSISTANT_ERROR, + { + "code": e.code, + "message": e.message, + }, + ) + _LOGGER.warning("No Wake word provider found") except asyncio.TimeoutError: + if self.stopped: + # The pipeline was stopped gracefully + return self.handle_event( VoiceAssistantEventType.VOICE_ASSISTANT_ERROR, { @@ -397,7 +308,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): self.transport.sendto(chunk, self.remote_addr) await asyncio.sleep( - samples_in_chunk / stt.AudioSampleRates.SAMPLERATE_16000 * 0.99 + samples_in_chunk / stt.AudioSampleRates.SAMPLERATE_16000 * 0.9 ) sample_offset += samples_in_chunk diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index d6562651f0b..b7ce5670441 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -7,7 +7,13 @@ from unittest.mock import Mock, patch from aioesphomeapi import VoiceAssistantEventType import pytest -from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType +from homeassistant.components.assist_pipeline import ( + PipelineEvent, + PipelineEventType, + PipelineNotFound, + PipelineStage, +) +from homeassistant.components.assist_pipeline.error import WakeWordDetectionError from homeassistant.components.esphome import DomainData from homeassistant.components.esphome.voice_assistant import VoiceAssistantUDPServer from homeassistant.core import HomeAssistant @@ -71,6 +77,13 @@ async def test_pipeline_events( event_callback = kwargs["event_callback"] + event_callback( + PipelineEvent( + type=PipelineEventType.WAKE_WORD_END, + data={"wake_word_output": {}}, + ) + ) + # Fake events event_callback( PipelineEvent( @@ -112,6 +125,8 @@ async def test_pipeline_events( elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: assert data is not None assert data["url"] == _TEST_OUTPUT_URL + elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END: + assert data is None voice_assistant_udp_server_v1.handle_event = handle_event @@ -343,134 +358,90 @@ async def test_send_tts( voice_assistant_udp_server_v2.transport.sendto.assert_called() -async def test_speech_detection( +async def test_wake_word( hass: HomeAssistant, voice_assistant_udp_server_v2: VoiceAssistantUDPServer, ) -> None: - """Test the UDP server queues incoming data.""" + """Test that the pipeline is set to start with Wake word.""" - def is_speech(self, chunk, sample_rate): - """Anything non-zero is speech.""" - return sum(chunk) > 0 - - async def async_pipeline_from_audio_stream(*args, **kwargs): - stt_stream = kwargs["stt_stream"] - event_callback = kwargs["event_callback"] - async for _chunk in stt_stream: - pass - - # Test empty data - event_callback( - PipelineEvent( - type=PipelineEventType.STT_END, - data={"stt_output": {"text": _TEST_INPUT_TEXT}}, - ) - ) + async def async_pipeline_from_audio_stream(*args, start_stage, **kwargs): + assert start_stage == PipelineStage.WAKE_WORD with patch( - "webrtcvad.Vad.is_speech", - new=is_speech, - ), patch( "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, ): - voice_assistant_udp_server_v2.started = True - - voice_assistant_udp_server_v2.queue.put_nowait(bytes(_ONE_SECOND)) - voice_assistant_udp_server_v2.queue.put_nowait(bytes([255] * _ONE_SECOND * 2)) - voice_assistant_udp_server_v2.queue.put_nowait(bytes([255] * _ONE_SECOND * 2)) - voice_assistant_udp_server_v2.queue.put_nowait(bytes(_ONE_SECOND)) + voice_assistant_udp_server_v2.transport = Mock() await voice_assistant_udp_server_v2.run_pipeline( - device_id="", conversation_id=None, use_vad=True, pipeline_timeout=1.0 + device_id="mock-device-id", + conversation_id=None, + flags=2, + pipeline_timeout=1, ) -async def test_no_speech( +async def test_wake_word_exception( hass: HomeAssistant, voice_assistant_udp_server_v2: VoiceAssistantUDPServer, ) -> None: - """Test there is no speech.""" - - def is_speech(self, chunk, sample_rate): - """Anything non-zero is speech.""" - return sum(chunk) > 0 - - def handle_event( - event_type: VoiceAssistantEventType, data: dict[str, str] | None - ) -> None: - assert event_type == VoiceAssistantEventType.VOICE_ASSISTANT_ERROR - assert data is not None - assert data["code"] == "speech-timeout" - - voice_assistant_udp_server_v2.handle_event = handle_event - - with patch( - "webrtcvad.Vad.is_speech", - new=is_speech, - ): - voice_assistant_udp_server_v2.started = True - - voice_assistant_udp_server_v2.queue.put_nowait(bytes(_ONE_SECOND)) - - await voice_assistant_udp_server_v2.run_pipeline( - device_id="", conversation_id=None, use_vad=True, pipeline_timeout=1.0 - ) - - -async def test_speech_timeout( - hass: HomeAssistant, - voice_assistant_udp_server_v2: VoiceAssistantUDPServer, -) -> None: - """Test when speech was detected, but the pipeline times out.""" - - def is_speech(self, chunk, sample_rate): - """Anything non-zero is speech.""" - return sum(chunk) > 255 + """Test that the pipeline is set to start with Wake word.""" async def async_pipeline_from_audio_stream(*args, **kwargs): - stt_stream = kwargs["stt_stream"] - async for _chunk in stt_stream: - # Stream will end when VAD detects end of "speech" - pass - - async def segment_audio(*args, **kwargs): - raise asyncio.TimeoutError() - async for chunk in []: - yield chunk + raise WakeWordDetectionError("pipeline-not-found", "Pipeline not found") with patch( - "webrtcvad.Vad.is_speech", - new=is_speech, - ), patch( "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, - ), patch( - "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPServer._segment_audio", - new=segment_audio, ): - voice_assistant_udp_server_v2.started = True + voice_assistant_udp_server_v2.transport = Mock() - voice_assistant_udp_server_v2.queue.put_nowait(bytes([255] * (_ONE_SECOND * 2))) + def handle_event( + event_type: VoiceAssistantEventType, data: dict[str, str] | None + ) -> None: + if event_type == VoiceAssistantEventType.VOICE_ASSISTANT_ERROR: + assert data is not None + assert data["code"] == "pipeline-not-found" + assert data["message"] == "Pipeline not found" + + voice_assistant_udp_server_v2.handle_event = handle_event await voice_assistant_udp_server_v2.run_pipeline( - device_id="", conversation_id=None, use_vad=True, pipeline_timeout=1.0 + device_id="mock-device-id", + conversation_id=None, + flags=2, + pipeline_timeout=1, ) -async def test_cancelled( +async def test_pipeline_timeout( hass: HomeAssistant, voice_assistant_udp_server_v2: VoiceAssistantUDPServer, ) -> None: - """Test when the server is stopped while waiting for speech.""" + """Test that the pipeline is set to start with Wake word.""" - voice_assistant_udp_server_v2.started = True + async def async_pipeline_from_audio_stream(*args, **kwargs): + raise PipelineNotFound("not-found", "Pipeline not found") - voice_assistant_udp_server_v2.queue.put_nowait(b"") + with patch( + "homeassistant.components.esphome.voice_assistant.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ): + voice_assistant_udp_server_v2.transport = Mock() - await voice_assistant_udp_server_v2.run_pipeline( - device_id="", conversation_id=None, use_vad=True, pipeline_timeout=1.0 - ) + def handle_event( + event_type: VoiceAssistantEventType, data: dict[str, str] | None + ) -> None: + if event_type == VoiceAssistantEventType.VOICE_ASSISTANT_ERROR: + assert data is not None + assert data["code"] == "pipeline not found" + assert data["message"] == "Selected pipeline not found" - # No events should be sent if cancelled while waiting for speech - voice_assistant_udp_server_v2.handle_event.assert_not_called() + voice_assistant_udp_server_v2.handle_event = handle_event + + await voice_assistant_udp_server_v2.run_pipeline( + device_id="mock-device-id", + conversation_id=None, + flags=2, + pipeline_timeout=1, + ) From 91df9434d02b680388b168f8ed99bc9e7583d9ec Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 21 Aug 2023 18:21:34 +0200 Subject: [PATCH 0685/1151] Use storage helper in feedreader (#98754) --- .../components/feedreader/__init__.py | 228 ++++++++++-------- tests/components/feedreader/test_init.py | 182 ++++++++++++-- 2 files changed, 283 insertions(+), 127 deletions(-) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 6be0e3c219f..82312b8897c 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -1,22 +1,23 @@ """Support for RSS/Atom feeds.""" from __future__ import annotations +from calendar import timegm from datetime import datetime, timedelta from logging import getLogger -from os.path import exists +import os import pickle -from threading import Lock -from time import struct_time -from typing import cast +from time import gmtime, struct_time import feedparser import voluptuous as vol from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType +from homeassistant.util.dt import utc_from_timestamp _LOGGER = getLogger(__name__) @@ -25,10 +26,12 @@ CONF_MAX_ENTRIES = "max_entries" DEFAULT_MAX_ENTRIES = 20 DEFAULT_SCAN_INTERVAL = timedelta(hours=1) +DELAY_SAVE = 30 DOMAIN = "feedreader" EVENT_FEEDREADER = "feedreader" +STORAGE_VERSION = 1 CONFIG_SCHEMA = vol.Schema( { @@ -46,17 +49,25 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Feedreader component.""" urls: list[str] = config[DOMAIN][CONF_URLS] + if not urls: + return False + scan_interval: timedelta = config[DOMAIN][CONF_SCAN_INTERVAL] max_entries: int = config[DOMAIN][CONF_MAX_ENTRIES] - data_file = hass.config.path(f"{DOMAIN}.pickle") - storage = StoredData(data_file) + old_data_file = hass.config.path(f"{DOMAIN}.pickle") + storage = StoredData(hass, old_data_file) + await storage.async_setup() feeds = [ - FeedManager(url, scan_interval, max_entries, hass, storage) for url in urls + FeedManager(hass, url, scan_interval, max_entries, storage) for url in urls ] - return len(feeds) > 0 + + for feed in feeds: + feed.async_setup() + + return True class FeedManager: @@ -64,50 +75,47 @@ class FeedManager: def __init__( self, + hass: HomeAssistant, url: str, scan_interval: timedelta, max_entries: int, - hass: HomeAssistant, storage: StoredData, ) -> None: """Initialize the FeedManager object, poll as per scan interval.""" + self._hass = hass self._url = url self._scan_interval = scan_interval self._max_entries = max_entries self._feed: feedparser.FeedParserDict | None = None - self._hass = hass self._firstrun = True self._storage = storage self._last_entry_timestamp: struct_time | None = None - self._last_update_successful = False self._has_published_parsed = False self._has_updated_parsed = False self._event_type = EVENT_FEEDREADER self._feed_id = url - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: self._update()) - self._init_regular_updates(hass) + + @callback + def async_setup(self) -> None: + """Set up the feed manager.""" + self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, self._async_update) + async_track_time_interval( + self._hass, self._async_update, self._scan_interval, cancel_on_shutdown=True + ) def _log_no_entries(self) -> None: """Send no entries log at debug level.""" _LOGGER.debug("No new entries to be published in feed %s", self._url) - def _init_regular_updates(self, hass: HomeAssistant) -> None: - """Schedule regular updates at the top of the clock.""" - track_time_interval( - hass, - lambda now: self._update(), - self._scan_interval, - cancel_on_shutdown=True, - ) - - @property - def last_update_successful(self) -> bool: - """Return True if the last feed update was successful.""" - return self._last_update_successful - - def _update(self) -> None: + async def _async_update(self, _: datetime | Event) -> None: """Update the feed and publish new entries to the event bus.""" - _LOGGER.info("Fetching new data from feed %s", self._url) + last_entry_timestamp = await self._hass.async_add_executor_job(self._update) + if last_entry_timestamp: + self._storage.async_put_timestamp(self._feed_id, last_entry_timestamp) + + def _update(self) -> struct_time | None: + """Update the feed and publish new entries to the event bus.""" + _LOGGER.debug("Fetching new data from feed %s", self._url) self._feed: feedparser.FeedParserDict = feedparser.parse( # type: ignore[no-redef] self._url, etag=None if not self._feed else self._feed.get("etag"), @@ -115,38 +123,41 @@ class FeedManager: ) if not self._feed: _LOGGER.error("Error fetching feed data from %s", self._url) - self._last_update_successful = False - else: - # The 'bozo' flag really only indicates that there was an issue - # during the initial parsing of the XML, but it doesn't indicate - # whether this is an unrecoverable error. In this case the - # feedparser lib is trying a less strict parsing approach. - # If an error is detected here, log warning message but continue - # processing the feed entries if present. - if self._feed.bozo != 0: - _LOGGER.warning( - "Possible issue parsing feed %s: %s", - self._url, - self._feed.bozo_exception, - ) - # Using etag and modified, if there's no new data available, - # the entries list will be empty - if self._feed.entries: - _LOGGER.debug( - "%s entri(es) available in feed %s", - len(self._feed.entries), - self._url, - ) - self._filter_entries() - self._publish_new_entries() - if self._has_published_parsed or self._has_updated_parsed: - self._storage.put_timestamp( - self._feed_id, cast(struct_time, self._last_entry_timestamp) - ) - else: - self._log_no_entries() - self._last_update_successful = True - _LOGGER.info("Fetch from feed %s completed", self._url) + return None + # The 'bozo' flag really only indicates that there was an issue + # during the initial parsing of the XML, but it doesn't indicate + # whether this is an unrecoverable error. In this case the + # feedparser lib is trying a less strict parsing approach. + # If an error is detected here, log warning message but continue + # processing the feed entries if present. + if self._feed.bozo != 0: + _LOGGER.warning( + "Possible issue parsing feed %s: %s", + self._url, + self._feed.bozo_exception, + ) + # Using etag and modified, if there's no new data available, + # the entries list will be empty + _LOGGER.debug( + "%s entri(es) available in feed %s", + len(self._feed.entries), + self._url, + ) + if not self._feed.entries: + self._log_no_entries() + return None + + self._filter_entries() + self._publish_new_entries() + + _LOGGER.debug("Fetch from feed %s completed", self._url) + + if ( + self._has_published_parsed or self._has_updated_parsed + ) and self._last_entry_timestamp: + return self._last_entry_timestamp + + return None def _filter_entries(self) -> None: """Filter the entries provided and return the ones to keep.""" @@ -219,47 +230,62 @@ class FeedManager: class StoredData: - """Abstraction over pickle data storage.""" + """Represent a data storage.""" - def __init__(self, data_file: str) -> None: - """Initialize pickle data storage.""" - self._data_file = data_file - self._lock = Lock() - self._cache_outdated = True + def __init__(self, hass: HomeAssistant, legacy_data_file: str) -> None: + """Initialize data storage.""" + self._legacy_data_file = legacy_data_file self._data: dict[str, struct_time] = {} - self._fetch_data() + self._hass = hass + self._store: Store[dict[str, str]] = Store(hass, STORAGE_VERSION, DOMAIN) - def _fetch_data(self) -> None: - """Fetch data stored into pickle file.""" - if self._cache_outdated and exists(self._data_file): - try: - _LOGGER.debug("Fetching data from file %s", self._data_file) - with self._lock, open(self._data_file, "rb") as myfile: - self._data = pickle.load(myfile) or {} - self._cache_outdated = False - except Exception: # pylint: disable=broad-except - _LOGGER.error( - "Error loading data from pickled file %s", self._data_file - ) + async def async_setup(self) -> None: + """Set up storage.""" + if not os.path.exists(self._store.path): + # Remove the legacy store loading after deprecation period. + data = await self._hass.async_add_executor_job(self._legacy_fetch_data) + else: + if (store_data := await self._store.async_load()) is None: + return + # Make sure that dst is set to 0, by using gmtime() on the timestamp. + data = { + feed_id: gmtime(datetime.fromisoformat(timestamp_string).timestamp()) + for feed_id, timestamp_string in store_data.items() + } + + self._data = data + + def _legacy_fetch_data(self) -> dict[str, struct_time]: + """Fetch data stored in pickle file.""" + _LOGGER.debug("Fetching data from legacy file %s", self._legacy_data_file) + try: + with open(self._legacy_data_file, "rb") as myfile: + return pickle.load(myfile) or {} + except FileNotFoundError: + pass + except (OSError, pickle.PickleError) as err: + _LOGGER.error( + "Error loading data from pickled file %s: %s", + self._legacy_data_file, + err, + ) + + return {} def get_timestamp(self, feed_id: str) -> struct_time | None: - """Return stored timestamp for given feed id (usually the url).""" - self._fetch_data() + """Return stored timestamp for given feed id.""" return self._data.get(feed_id) - def put_timestamp(self, feed_id: str, timestamp: struct_time) -> None: - """Update timestamp for given feed id (usually the url).""" - self._fetch_data() - with self._lock, open(self._data_file, "wb") as myfile: - self._data.update({feed_id: timestamp}) - _LOGGER.debug( - "Overwriting feed %s timestamp in storage file %s: %s", - feed_id, - self._data_file, - timestamp, - ) - try: - pickle.dump(self._data, myfile) - except Exception: # pylint: disable=broad-except - _LOGGER.error("Error saving pickled data to %s", self._data_file) - self._cache_outdated = True + @callback + def async_put_timestamp(self, feed_id: str, timestamp: struct_time) -> None: + """Update timestamp for given feed id.""" + self._data[feed_id] = timestamp + self._store.async_delay_save(self._async_save_data, DELAY_SAVE) + + @callback + def _async_save_data(self) -> dict[str, str]: + """Save feed data to storage.""" + return { + feed_id: utc_from_timestamp(timegm(struct_utc)).isoformat() + for feed_id, struct_utc in self._data.items() + } diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index 61851559969..345c37dc8f1 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -1,7 +1,11 @@ """The tests for the feedreader component.""" -from datetime import timedelta +from collections.abc import Generator +from datetime import datetime, timedelta +import pickle +from time import gmtime +from typing import Any from unittest import mock -from unittest.mock import mock_open, patch +from unittest.mock import MagicMock, mock_open, patch import pytest @@ -13,7 +17,7 @@ from homeassistant.components.feedreader import ( EVENT_FEEDREADER, ) from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -27,7 +31,7 @@ VALID_CONFIG_4 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 5}} VALID_CONFIG_5 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 1}} -def load_fixture_bytes(src): +def load_fixture_bytes(src: str) -> bytes: """Return byte stream of fixture.""" feed_data = load_fixture(src) raw = bytes(feed_data, "utf-8") @@ -35,72 +39,198 @@ def load_fixture_bytes(src): @pytest.fixture(name="feed_one_event") -def fixture_feed_one_event(hass): +def fixture_feed_one_event(hass: HomeAssistant) -> bytes: """Load test feed data for one event.""" return load_fixture_bytes("feedreader.xml") @pytest.fixture(name="feed_two_event") -def fixture_feed_two_events(hass): +def fixture_feed_two_events(hass: HomeAssistant) -> bytes: """Load test feed data for two event.""" return load_fixture_bytes("feedreader1.xml") @pytest.fixture(name="feed_21_events") -def fixture_feed_21_events(hass): +def fixture_feed_21_events(hass: HomeAssistant) -> bytes: """Load test feed data for twenty one events.""" return load_fixture_bytes("feedreader2.xml") @pytest.fixture(name="feed_three_events") -def fixture_feed_three_events(hass): +def fixture_feed_three_events(hass: HomeAssistant) -> bytes: """Load test feed data for three events.""" return load_fixture_bytes("feedreader3.xml") @pytest.fixture(name="feed_atom_event") -def fixture_feed_atom_event(hass): +def fixture_feed_atom_event(hass: HomeAssistant) -> bytes: """Load test feed data for atom event.""" return load_fixture_bytes("feedreader5.xml") @pytest.fixture(name="events") -async def fixture_events(hass): +async def fixture_events(hass: HomeAssistant) -> list[Event]: """Fixture that catches alexa events.""" return async_capture_events(hass, EVENT_FEEDREADER) -@pytest.fixture(name="feed_storage", autouse=True) -def fixture_feed_storage(): +@pytest.fixture(name="storage") +def fixture_storage(request: pytest.FixtureRequest) -> Generator[None, None, None]: + """Set up the test storage environment.""" + if request.param == "legacy_storage": + with patch("os.path.exists", return_value=False): + yield + elif request.param == "json_storage": + with patch("os.path.exists", return_value=True): + yield + else: + raise RuntimeError("Invalid storage fixture") + + +@pytest.fixture(name="legacy_storage_open") +def fixture_legacy_storage_open() -> Generator[MagicMock, None, None]: """Mock builtins.open for feedreader storage.""" - with patch("homeassistant.components.feedreader.open", mock_open(), create=True): - yield - - -async def test_setup_one_feed(hass: HomeAssistant) -> None: - """Test the general setup of this component.""" with patch( - "homeassistant.components.feedreader.track_time_interval" + "homeassistant.components.feedreader.open", + mock_open(), + create=True, + ) as open_mock: + yield open_mock + + +@pytest.fixture(name="legacy_storage_load", autouse=True) +def fixture_legacy_storage_load( + legacy_storage_open, +) -> Generator[MagicMock, None, None]: + """Mock builtins.open for feedreader storage.""" + with patch( + "homeassistant.components.feedreader.pickle.load", return_value={} + ) as pickle_load: + yield pickle_load + + +async def test_setup_no_feeds(hass: HomeAssistant) -> None: + """Test config with no urls.""" + assert not await async_setup_component( + hass, feedreader.DOMAIN, {feedreader.DOMAIN: {CONF_URLS: []}} + ) + + +@pytest.mark.parametrize( + ("open_error", "load_error"), + [ + (FileNotFoundError("No file"), None), + (OSError("Boom"), None), + (None, pickle.PickleError("Bad data")), + ], +) +async def test_legacy_storage_error( + hass: HomeAssistant, + legacy_storage_open: MagicMock, + legacy_storage_load: MagicMock, + open_error: Exception | None, + load_error: Exception | None, +) -> None: + """Test legacy storage error.""" + legacy_storage_open.side_effect = open_error + legacy_storage_load.side_effect = load_error + + with patch( + "homeassistant.components.feedreader.async_track_time_interval" ) as track_method: assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_1) await hass.async_block_till_done() - track_method.assert_called_once_with( - hass, mock.ANY, DEFAULT_SCAN_INTERVAL, cancel_on_shutdown=True - ) + track_method.assert_called_once_with( + hass, mock.ANY, DEFAULT_SCAN_INTERVAL, cancel_on_shutdown=True + ) + + +@pytest.mark.parametrize("storage", ["legacy_storage", "json_storage"], indirect=True) +async def test_storage_data_loading( + hass: HomeAssistant, + events: list[Event], + feed_one_event: bytes, + legacy_storage_load: MagicMock, + hass_storage: dict[str, Any], + storage: None, +) -> None: + """Test loading existing storage data.""" + storage_data: dict[str, str] = {URL: "2018-04-30T05:10:00+00:00"} + hass_storage[feedreader.DOMAIN] = { + "version": 1, + "minor_version": 1, + "key": feedreader.DOMAIN, + "data": storage_data, + } + legacy_storage_data = { + URL: gmtime(datetime.fromisoformat(storage_data[URL]).timestamp()) + } + legacy_storage_load.return_value = legacy_storage_data + + with patch( + "feedparser.http.get", + return_value=feed_one_event, + ): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + # no new events + assert not events + + +async def test_storage_data_writing( + hass: HomeAssistant, + events: list[Event], + feed_one_event: bytes, + hass_storage: dict[str, Any], +) -> None: + """Test writing to storage.""" + storage_data: dict[str, str] = {URL: "2018-04-30T05:10:00+00:00"} + + with patch( + "feedparser.http.get", + return_value=feed_one_event, + ), patch("homeassistant.components.feedreader.DELAY_SAVE", new=0): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + # one new event + assert len(events) == 1 + + # storage data updated + assert hass_storage[feedreader.DOMAIN]["data"] == storage_data + + +@pytest.mark.parametrize("storage", ["legacy_storage", "json_storage"], indirect=True) +async def test_setup_one_feed(hass: HomeAssistant, storage: None) -> None: + """Test the general setup of this component.""" + with patch( + "homeassistant.components.feedreader.async_track_time_interval" + ) as track_method: + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_1) + await hass.async_block_till_done() + + track_method.assert_called_once_with( + hass, mock.ANY, DEFAULT_SCAN_INTERVAL, cancel_on_shutdown=True + ) async def test_setup_scan_interval(hass: HomeAssistant) -> None: """Test the setup of this component with scan interval.""" with patch( - "homeassistant.components.feedreader.track_time_interval" + "homeassistant.components.feedreader.async_track_time_interval" ) as track_method: assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) await hass.async_block_till_done() - track_method.assert_called_once_with( - hass, mock.ANY, timedelta(seconds=60), cancel_on_shutdown=True - ) + track_method.assert_called_once_with( + hass, mock.ANY, timedelta(seconds=60), cancel_on_shutdown=True + ) async def test_setup_max_entries(hass: HomeAssistant) -> None: From 6023ee0cc4b88e87fdca8178b004c68fb3d90d04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Aug 2023 12:08:26 -0500 Subject: [PATCH 0686/1151] Bump dbus-fast to 1.93.0 (#98758) --- 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 1ae23633bdf..84453344c3c 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.8.0", - "dbus-fast==1.92.0" + "dbus-fast==1.93.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 78b14aaa590..986a86b38a9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.8.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 -dbus-fast==1.92.0 +dbus-fast==1.93.0 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 39f96a600d1..02e53524a1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.92.0 +dbus-fast==1.93.0 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d000c692b70..1b54f2326d0 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.92.0 +dbus-fast==1.93.0 # homeassistant.components.debugpy debugpy==1.6.7 From 2d46b589b9235d9c2e31cd4a3d32ace9a25b2075 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 19:09:51 +0200 Subject: [PATCH 0687/1151] Add entity translations to Kraken (#98765) --- homeassistant/components/kraken/sensor.py | 37 +++++++------- homeassistant/components/kraken/strings.json | 52 ++++++++++++++++++++ 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index 87ad8dc258f..a6c00e62b62 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -47,93 +47,93 @@ class KrakenSensorEntityDescription(SensorEntityDescription, KrakenRequiredKeysM SENSOR_TYPES: tuple[KrakenSensorEntityDescription, ...] = ( KrakenSensorEntityDescription( key="ask", - name="Ask", + translation_key="ask", value_fn=lambda x, y: x.data[y]["ask"][0], ), KrakenSensorEntityDescription( key="ask_volume", - name="Ask Volume", + translation_key="ask_volume", value_fn=lambda x, y: x.data[y]["ask"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="bid", - name="Bid", + translation_key="bid", value_fn=lambda x, y: x.data[y]["bid"][0], ), KrakenSensorEntityDescription( key="bid_volume", - name="Bid Volume", + translation_key="bid_volume", value_fn=lambda x, y: x.data[y]["bid"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_today", - name="Volume Today", + translation_key="volume_today", value_fn=lambda x, y: x.data[y]["volume"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_last_24h", - name="Volume last 24h", + translation_key="volume_last_24h", value_fn=lambda x, y: x.data[y]["volume"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_weighted_average_today", - name="Volume weighted average today", + translation_key="volume_weighted_average_today", value_fn=lambda x, y: x.data[y]["volume_weighted_average"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_weighted_average_last_24h", - name="Volume weighted average last 24h", + translation_key="volume_weighted_average_last_24h", value_fn=lambda x, y: x.data[y]["volume_weighted_average"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="number_of_trades_today", - name="Number of trades today", + translation_key="number_of_trades_today", value_fn=lambda x, y: x.data[y]["number_of_trades"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="number_of_trades_last_24h", - name="Number of trades last 24h", + translation_key="number_of_trades_last_24h", value_fn=lambda x, y: x.data[y]["number_of_trades"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="last_trade_closed", - name="Last trade closed", + translation_key="last_trade_closed", value_fn=lambda x, y: x.data[y]["last_trade_closed"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="low_today", - name="Low today", + translation_key="low_today", value_fn=lambda x, y: x.data[y]["low"][0], ), KrakenSensorEntityDescription( key="low_last_24h", - name="Low last 24h", + translation_key="low_last_24h", value_fn=lambda x, y: x.data[y]["low"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="high_today", - name="High today", + translation_key="high_today", value_fn=lambda x, y: x.data[y]["high"][0], ), KrakenSensorEntityDescription( key="high_last_24h", - name="High last 24h", + translation_key="high_last_24h", value_fn=lambda x, y: x.data[y]["high"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="opening_price_today", - name="Opening price today", + translation_key="opening_price_today", value_fn=lambda x, y: x.data[y]["opening_price"], entity_registry_enabled_default=False, ), @@ -207,6 +207,9 @@ class KrakenSensor( entity_description: KrakenSensorEntityDescription + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_has_entity_name = True + def __init__( self, kraken_data: KrakenData, @@ -233,7 +236,6 @@ class KrakenSensor( ).lower() self._received_data_at_least_once = False self._available = True - self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_device_info = DeviceInfo( configuration_url="https://www.kraken.com/", @@ -242,7 +244,6 @@ class KrakenSensor( manufacturer="Kraken.com", name=self._device_name, ) - self._attr_has_entity_name = True async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" diff --git a/homeassistant/components/kraken/strings.json b/homeassistant/components/kraken/strings.json index e8ad5ffb98c..c636dbf8d1f 100644 --- a/homeassistant/components/kraken/strings.json +++ b/homeassistant/components/kraken/strings.json @@ -18,5 +18,57 @@ } } } + }, + "entity": { + "sensor": { + "ask": { + "name": "Ask" + }, + "ask_volume": { + "name": "Ask volume" + }, + "bid": { + "name": "Bid" + }, + "bid_volume": { + "name": "Bid volume" + }, + "volume_today": { + "name": "Volume today" + }, + "volume_last_24h": { + "name": "Volume last 24h" + }, + "volume_weighted_average_today": { + "name": "Volume weighted average today" + }, + "volume_weighted_average_last_24h": { + "name": "Volume weighted average last 24h" + }, + "number_of_trades_today": { + "name": "Number of trades today" + }, + "number_of_trades_last_24h": { + "name": "Number of trades last 24h" + }, + "last_trade_closed": { + "name": "Last trade closed" + }, + "low_today": { + "name": "Low today" + }, + "low_last_24h": { + "name": "Low last 24h" + }, + "high_today": { + "name": "High today" + }, + "high_last_24h": { + "name": "High last 24h" + }, + "opening_price_today": { + "name": "Opening price today" + } + } } } From faf0f5f19b4d062182b15ee35cd13416995ebc39 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 21 Aug 2023 19:10:35 +0200 Subject: [PATCH 0688/1151] Fix default values in Scrape (#98755) --- homeassistant/components/scrape/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/scrape/config_flow.py b/homeassistant/components/scrape/config_flow.py index 1f74caf83f0..dc0254cc642 100644 --- a/homeassistant/components/scrape/config_flow.py +++ b/homeassistant/components/scrape/config_flow.py @@ -106,7 +106,7 @@ SENSOR_SETUP = { ), vol.Optional(CONF_ATTRIBUTE): TextSelector(), vol.Optional(CONF_VALUE_TEMPLATE): TemplateSelector(), - vol.Required(CONF_DEVICE_CLASS): SelectSelector( + vol.Required(CONF_DEVICE_CLASS, default=NONE_SENTINEL): SelectSelector( SelectSelectorConfig( options=[NONE_SENTINEL] + sorted( @@ -120,14 +120,14 @@ SENSOR_SETUP = { translation_key="device_class", ) ), - vol.Required(CONF_STATE_CLASS): SelectSelector( + vol.Required(CONF_STATE_CLASS, default=NONE_SENTINEL): SelectSelector( SelectSelectorConfig( options=[NONE_SENTINEL] + sorted([cls.value for cls in SensorStateClass]), mode=SelectSelectorMode.DROPDOWN, translation_key="state_class", ) ), - vol.Required(CONF_UNIT_OF_MEASUREMENT): SelectSelector( + vol.Required(CONF_UNIT_OF_MEASUREMENT, default=NONE_SENTINEL): SelectSelector( SelectSelectorConfig( options=[NONE_SENTINEL] + sorted([cls.value for cls in UnitOfTemperature]), custom_value=True, From 2399cd283a3ebddb6e5b6eaea9fc69abc04228d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 21 Aug 2023 20:14:07 +0300 Subject: [PATCH 0689/1151] Python 3.10 support cleanups (#98640) --- homeassistant/components/datetime/__init__.py | 4 +-- homeassistant/components/demo/datetime.py | 4 +-- .../components/gardena_bluetooth/sensor.py | 4 +-- .../components/google_mail/sensor.py | 4 +-- homeassistant/components/radarr/sensor.py | 4 +-- homeassistant/components/sensor/__init__.py | 26 ++++--------------- .../components/system_bridge/sensor.py | 6 ++--- homeassistant/components/whois/sensor.py | 4 +-- homeassistant/components/zha/sensor.py | 3 +-- homeassistant/util/async_.py | 4 +-- homeassistant/util/dt.py | 4 +-- pyproject.toml | 2 -- script/gen_requirements_all.py | 3 +-- tests/common.py | 6 ++--- tests/components/datetime/test_init.py | 4 +-- .../environment_canada/test_diagnostics.py | 6 ++--- .../components/forecast_solar/test_energy.py | 6 ++--- .../components/google_assistant/test_http.py | 6 ++--- .../home_plus_control/test_switch.py | 10 +++---- tests/components/ipma/__init__.py | 6 ++--- tests/components/melnor/conftest.py | 4 +-- tests/components/octoprint/test_sensor.py | 4 +-- tests/components/prusalink/test_sensor.py | 4 +-- tests/components/recorder/test_util.py | 4 +-- tests/components/sensor/test_init.py | 12 ++++----- tests/components/subaru/api_responses.py | 4 +-- .../components/template/test_binary_sensor.py | 10 +++---- tests/components/template/test_button.py | 2 +- tests/components/uvc/test_camera.py | 4 +-- tests/components/whirlpool/test_sensor.py | 10 +++---- .../custom_components/test/datetime.py | 4 +-- 31 files changed, 75 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/datetime/__init__.py b/homeassistant/components/datetime/__init__.py index fb67f4b1ffb..b04008672ae 100644 --- a/homeassistant/components/datetime/__init__.py +++ b/homeassistant/components/datetime/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import logging from typing import final @@ -110,7 +110,7 @@ class DateTimeEntity(Entity): "which is missing timezone information" ) - return value.astimezone(timezone.utc).isoformat(timespec="seconds") + return value.astimezone(UTC).isoformat(timespec="seconds") @property def native_value(self) -> datetime | None: diff --git a/homeassistant/components/demo/datetime.py b/homeassistant/components/demo/datetime.py index e7f72b66a87..63c8a5a7873 100644 --- a/homeassistant/components/demo/datetime.py +++ b/homeassistant/components/demo/datetime.py @@ -1,7 +1,7 @@ """Demo platform that offers a fake date/time entity.""" from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from homeassistant.components.datetime import DateTimeEntity from homeassistant.config_entries import ConfigEntry @@ -23,7 +23,7 @@ async def async_setup_entry( DemoDateTime( "datetime", "Date and Time", - datetime(2020, 1, 1, 12, 0, 0, tzinfo=timezone.utc), + datetime(2020, 1, 1, 12, 0, 0, tzinfo=UTC), "mdi:calendar-clock", False, ), diff --git a/homeassistant/components/gardena_bluetooth/sensor.py b/homeassistant/components/gardena_bluetooth/sensor.py index ebc83ae88af..dd2bde43cc4 100644 --- a/homeassistant/components/gardena_bluetooth/sensor.py +++ b/homeassistant/components/gardena_bluetooth/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from gardena_bluetooth.const import Battery, Valve from gardena_bluetooth.parse import Characteristic @@ -106,7 +106,7 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity): super()._handle_coordinator_update() return - time = datetime.now(timezone.utc) + timedelta(seconds=value) + time = datetime.now(UTC) + timedelta(seconds=value) if not self._attr_native_value: self._attr_native_value = time super()._handle_coordinator_update() diff --git a/homeassistant/components/google_mail/sensor.py b/homeassistant/components/google_mail/sensor.py index a65e845095c..dc1ee33c16e 100644 --- a/homeassistant/components/google_mail/sensor.py +++ b/homeassistant/components/google_mail/sensor.py @@ -1,7 +1,7 @@ """Support for Google Mail Sensors.""" from __future__ import annotations -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from googleapiclient.http import HttpRequest @@ -46,7 +46,7 @@ class GoogleMailSensor(GoogleMailEntity, SensorEntity): data: dict = await self.hass.async_add_executor_job(settings.execute) if data["enableAutoReply"] and (end := data.get("endTime")): - value = datetime.fromtimestamp(int(end) / 1000, tz=timezone.utc) + value = datetime.fromtimestamp(int(end) / 1000, tz=UTC) else: value = None self._attr_native_value = value diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 367e302d56f..803b6de44a4 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from copy import deepcopy from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any, Generic from aiopyarr import Diskspace, RootFolder, SystemStatus @@ -88,7 +88,7 @@ SENSOR_TYPES: dict[str, RadarrSensorEntityDescription[Any]] = { device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - value_fn=lambda data, _: data.startTime.replace(tzinfo=timezone.utc), + value_fn=lambda data, _: data.startTime.replace(tzinfo=UTC), ), } diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index cbdaa24ec83..c00d20d51ef 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -5,12 +5,10 @@ import asyncio from collections.abc import Mapping from contextlib import suppress from dataclasses import dataclass -from datetime import date, datetime, timedelta, timezone +from datetime import UTC, date, datetime, timedelta from decimal import Decimal, InvalidOperation as DecimalInvalidOperation import logging from math import ceil, floor, log10 -import re -import sys from typing import Any, Final, Self, cast, final from homeassistant.config_entries import ConfigEntry @@ -89,10 +87,6 @@ _LOGGER: Final = logging.getLogger(__name__) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" -NEGATIVE_ZERO_PATTERN = re.compile(r"^-(0\.?0*)$") - -PY_311 = sys.version_info >= (3, 11, 0) - SCAN_INTERVAL: Final = timedelta(seconds=30) __all__ = [ @@ -534,8 +528,8 @@ class SensorEntity(Entity): "which is missing timezone information" ) - if value.tzinfo != timezone.utc: - value = value.astimezone(timezone.utc) + if value.tzinfo != UTC: + value = value.astimezone(UTC) return value.isoformat(timespec="seconds") except (AttributeError, OverflowError, TypeError) as err: @@ -636,12 +630,7 @@ class SensorEntity(Entity): ) precision = precision + floor(ratio_log) - if PY_311: - value = f"{converted_numerical_value:z.{precision}f}" - else: - value = f"{converted_numerical_value:.{precision}f}" - if value.startswith("-0") and NEGATIVE_ZERO_PATTERN.match(value): - value = value[1:] + value = f"{converted_numerical_value:z.{precision}f}" else: value = converted_numerical_value @@ -903,11 +892,6 @@ def async_rounded_state(hass: HomeAssistant, entity_id: str, state: State) -> st with suppress(TypeError, ValueError): numerical_value = float(value) - if PY_311: - value = f"{numerical_value:z.{precision}f}" - else: - value = f"{numerical_value:.{precision}f}" - if value.startswith("-0") and NEGATIVE_ZERO_PATTERN.match(value): - value = value[1:] + value = f"{numerical_value:z.{precision}f}" return value diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index 9290ebeacd5..4e0cbb9d2b9 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from typing import Final, cast from homeassistant.components.sensor import ( @@ -146,9 +146,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( name="Boot time", device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:av-timer", - value=lambda data: datetime.fromtimestamp( - data.system.boot_time, tz=timezone.utc - ), + value=lambda data: datetime.fromtimestamp(data.system.boot_time, tz=UTC), ), SystemBridgeSensorEntityDescription( key="cpu_power_package", diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 5163e0b3a6e..72c366bb0bc 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import cast from whois import Domain @@ -55,7 +55,7 @@ def _ensure_timezone(timestamp: datetime | None) -> datetime | None: # If timezone info isn't provided by the Whois, assume UTC. if timestamp.tzinfo is None: - return timestamp.replace(tzinfo=timezone.utc) + return timestamp.replace(tzinfo=UTC) return timestamp diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 0e520d98b52..c514e02ec57 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations import enum import functools import numbers -import sys from typing import TYPE_CHECKING, Any, Self from zigpy import types @@ -485,7 +484,7 @@ class SmartEnergyMetering(Sensor): if self._cluster_handler.device_type is not None: attrs["device_type"] = self._cluster_handler.device_type if (status := self._cluster_handler.status) is not None: - if isinstance(status, enum.IntFlag) and sys.version_info >= (3, 11): + if isinstance(status, enum.IntFlag): attrs["status"] = str( status.name if status.name is not None else status.value ) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 4caf074b879..ce1105cff75 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -21,9 +21,7 @@ _P = ParamSpec("_P") def cancelling(task: Future[Any]) -> bool: - """Return True if task is done or cancelling.""" - # https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancelling - # is new in Python 3.11 + """Return True if task is cancelling.""" return bool((cancelling_ := getattr(task, "cancelling", None)) and cancelling_()) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 4f49ec44ca7..34a81728d14 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -14,8 +14,8 @@ import zoneinfo import ciso8601 DATE_STR_FORMAT = "%Y-%m-%d" -UTC = dt.timezone.utc -DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc +UTC = dt.UTC +DEFAULT_TIME_ZONE: dt.tzinfo = dt.UTC CLOCK_MONOTONIC_COARSE = 6 # EPOCHORDINAL is not exposed as a constant diff --git a/pyproject.toml b/pyproject.toml index fcc47ed2c31..3c0ea4c4e34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -521,8 +521,6 @@ filterwarnings = [ ] [tool.ruff] -target-version = "py310" - select = [ "B002", # Python does not support the unary prefix increment "B007", # Loop control variable {name} not used within loop body diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 5a683660efe..101a57e419d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -9,9 +9,8 @@ from pathlib import Path import pkgutil import re import sys -from typing import Any - import tomllib +from typing import Any from homeassistant.util.yaml.loader import load_yaml from script.hassfest.model import Integration diff --git a/tests/common.py b/tests/common.py index 6f2209276ce..df8722a563c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -5,7 +5,7 @@ import asyncio from collections import OrderedDict from collections.abc import Generator, Mapping, Sequence from contextlib import contextmanager -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import functools as ft from functools import lru_cache from io import StringIO @@ -384,7 +384,7 @@ def async_fire_time_changed_exact( approach, as this is only for testing. """ if datetime_ is None: - utc_datetime = datetime.now(timezone.utc) + utc_datetime = datetime.now(UTC) else: utc_datetime = dt_util.as_utc(datetime_) @@ -406,7 +406,7 @@ def async_fire_time_changed( for an exact microsecond, use async_fire_time_changed_exact. """ if datetime_ is None: - utc_datetime = datetime.now(timezone.utc) + utc_datetime = datetime.now(UTC) else: utc_datetime = dt_util.as_utc(datetime_) diff --git a/tests/components/datetime/test_init.py b/tests/components/datetime/test_init.py index 66390c8d90f..6f2e2db29a1 100644 --- a/tests/components/datetime/test_init.py +++ b/tests/components/datetime/test_init.py @@ -1,5 +1,5 @@ """The tests for the datetime component.""" -from datetime import datetime, timezone +from datetime import UTC, datetime from zoneinfo import ZoneInfo import pytest @@ -14,7 +14,7 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, CONF_PLATFOR from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -DEFAULT_VALUE = datetime(2020, 1, 1, 12, 0, 0, tzinfo=timezone.utc) +DEFAULT_VALUE = datetime(2020, 1, 1, 12, 0, 0, tzinfo=UTC) class MockDateTimeEntity(DateTimeEntity): diff --git a/tests/components/environment_canada/test_diagnostics.py b/tests/components/environment_canada/test_diagnostics.py index f85de2cb97c..6044c9e778b 100644 --- a/tests/components/environment_canada/test_diagnostics.py +++ b/tests/components/environment_canada/test_diagnostics.py @@ -1,5 +1,5 @@ """Test Environment Canada diagnostics.""" -from datetime import datetime, timezone +from datetime import UTC, datetime import json from unittest.mock import AsyncMock, MagicMock, patch @@ -43,7 +43,7 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: ) weather_mock = mock_ec() - ec_data["metadata"]["timestamp"] = datetime(2022, 10, 4, tzinfo=timezone.utc) + ec_data["metadata"]["timestamp"] = datetime(2022, 10, 4, tzinfo=UTC) weather_mock.conditions = ec_data["conditions"] weather_mock.alerts = ec_data["alerts"] weather_mock.daily_forecasts = ec_data["daily_forecasts"] @@ -51,7 +51,7 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: radar_mock = mock_ec() radar_mock.image = b"GIF..." - radar_mock.timestamp = datetime(2022, 10, 4, tzinfo=timezone.utc) + radar_mock.timestamp = datetime(2022, 10, 4, tzinfo=UTC) with patch( "homeassistant.components.environment_canada.ECWeather", diff --git a/tests/components/forecast_solar/test_energy.py b/tests/components/forecast_solar/test_energy.py index 3ca89d33faa..7d3a853b8a7 100644 --- a/tests/components/forecast_solar/test_energy.py +++ b/tests/components/forecast_solar/test_energy.py @@ -1,5 +1,5 @@ """Test forecast solar energy platform.""" -from datetime import datetime, timezone +from datetime import UTC, datetime from unittest.mock import MagicMock from homeassistant.components.forecast_solar import energy @@ -16,8 +16,8 @@ async def test_energy_solar_forecast( ) -> None: """Test the Forecast.Solar energy platform solar forecast.""" mock_forecast_solar.estimate.return_value.wh_period = { - datetime(2021, 6, 27, 13, 0, tzinfo=timezone.utc): 12, - datetime(2021, 6, 27, 14, 0, tzinfo=timezone.utc): 8, + datetime(2021, 6, 27, 13, 0, tzinfo=UTC): 12, + datetime(2021, 6, 27, 14, 0, tzinfo=UTC): 8, } mock_config_entry.add_to_hass(hass) diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index b7dc880ede0..44dc40f5a47 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -1,5 +1,5 @@ """Test Google http services.""" -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from http import HTTPStatus from typing import Any from unittest.mock import ANY, patch @@ -51,7 +51,7 @@ async def test_get_jwt(hass: HomeAssistant) -> None: jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkdW1teUBkdW1teS5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInNjb3BlIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9ob21lZ3JhcGgiLCJhdWQiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW4iLCJpYXQiOjE1NzEwMTEyMDAsImV4cCI6MTU3MTAxNDgwMH0.akHbMhOflXdIDHVvUVwO0AoJONVOPUdCghN6hAdVz4gxjarrQeGYc_Qn2r84bEvCU7t6EvimKKr0fyupyzBAzfvKULs5mTHO3h2CwSgvOBMv8LnILboJmbO4JcgdnRV7d9G3ktQs7wWSCXJsI5i5jUr1Wfi9zWwxn2ebaAAgrp8" res = _get_homegraph_jwt( - datetime(2019, 10, 14, tzinfo=timezone.utc), + datetime(2019, 10, 14, tzinfo=UTC), DUMMY_CONFIG["service_account"]["client_email"], DUMMY_CONFIG["service_account"]["private_key"], ) @@ -85,7 +85,7 @@ async def test_update_access_token(hass: HomeAssistant) -> None: config = GoogleConfig(hass, DUMMY_CONFIG) await config.async_initialize() - base_time = datetime(2019, 10, 14, tzinfo=timezone.utc) + base_time = datetime(2019, 10, 14, tzinfo=UTC) with patch( "homeassistant.components.google_assistant.http._get_homegraph_token" ) as mock_get_token, patch( diff --git a/tests/components/home_plus_control/test_switch.py b/tests/components/home_plus_control/test_switch.py index ead1f83cb94..d41977d57a9 100644 --- a/tests/components/home_plus_control/test_switch.py +++ b/tests/components/home_plus_control/test_switch.py @@ -143,7 +143,7 @@ async def test_plant_topology_reduction_change( return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) + hass, dt.datetime.now(dt.UTC) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -205,7 +205,7 @@ async def test_plant_topology_increase_change( return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) + hass, dt.datetime.now(dt.UTC) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -267,7 +267,7 @@ async def test_module_status_unavailable( return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) + hass, dt.datetime.now(dt.UTC) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -338,7 +338,7 @@ async def test_module_status_available( return_value=mock_modules, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) + hass, dt.datetime.now(dt.UTC) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 @@ -442,7 +442,7 @@ async def test_update_with_api_error( side_effect=HomePlusControlApiError, ) as mock_check: async_fire_time_changed( - hass, dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=400) + hass, dt.datetime.now(dt.UTC) + dt.timedelta(seconds=400) ) await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 diff --git a/tests/components/ipma/__init__.py b/tests/components/ipma/__init__.py index ba172fc7bb8..827481c60de 100644 --- a/tests/components/ipma/__init__.py +++ b/tests/components/ipma/__init__.py @@ -1,6 +1,6 @@ """Tests for the IPMA component.""" from collections import namedtuple -from datetime import datetime, timezone +from datetime import UTC, datetime from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME @@ -87,7 +87,7 @@ class MockLocation: return [ Forecast( "7.7", - datetime(2020, 1, 15, 1, 0, 0, tzinfo=timezone.utc), + datetime(2020, 1, 15, 1, 0, 0, tzinfo=UTC), 1, "86.9", 12.0, @@ -101,7 +101,7 @@ class MockLocation: ), Forecast( "5.7", - datetime(2020, 1, 15, 2, 0, 0, tzinfo=timezone.utc), + datetime(2020, 1, 15, 2, 0, 0, tzinfo=UTC), 1, "86.9", 12.0, diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index ab51bf44a57..3e87a4e646f 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Generator -from datetime import datetime, time, timedelta, timezone +from datetime import UTC, datetime, time, timedelta from unittest.mock import AsyncMock, patch from melnor_bluetooth.device import Device @@ -73,7 +73,7 @@ class MockFrequency: self._interval = 0 self._is_watering = False self._start_time = time(12, 0) - self._next_run_time = datetime(2021, 1, 1, 12, 0, tzinfo=timezone.utc) + self._next_run_time = datetime(2021, 1, 1, 12, 0, tzinfo=UTC) @property def duration_minutes(self) -> int: diff --git a/tests/components/octoprint/test_sensor.py b/tests/components/octoprint/test_sensor.py index a388aeae106..2ba657c77d5 100644 --- a/tests/components/octoprint/test_sensor.py +++ b/tests/components/octoprint/test_sensor.py @@ -1,5 +1,5 @@ """The tests for Octoptint binary sensor module.""" -from datetime import datetime, timezone +from datetime import UTC, datetime from unittest.mock import patch from homeassistant.core import HomeAssistant @@ -24,7 +24,7 @@ async def test_sensors(hass: HomeAssistant) -> None: } with patch( "homeassistant.util.dt.utcnow", - return_value=datetime(2020, 2, 20, 9, 10, 13, 543, tzinfo=timezone.utc), + return_value=datetime(2020, 2, 20, 9, 10, 13, 543, tzinfo=UTC), ): await init_integration(hass, "sensor", printer=printer, job=job) diff --git a/tests/components/prusalink/test_sensor.py b/tests/components/prusalink/test_sensor.py index 6a0944bdf36..0f2a966b4e4 100644 --- a/tests/components/prusalink/test_sensor.py +++ b/tests/components/prusalink/test_sensor.py @@ -1,6 +1,6 @@ """Test Prusalink sensors.""" -from datetime import datetime, timezone +from datetime import UTC, datetime from unittest.mock import PropertyMock, patch import pytest @@ -125,7 +125,7 @@ async def test_sensors_active_job( """Test sensors while active job.""" with patch( "homeassistant.components.prusalink.sensor.utcnow", - return_value=datetime(2022, 8, 27, 14, 0, 0, tzinfo=timezone.utc), + return_value=datetime(2022, 8, 27, 14, 0, 0, tzinfo=UTC), ): assert await async_setup_component(hass, "prusalink", {}) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index ecfd188db8e..a7b15a7f12d 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -1,6 +1,6 @@ """Test util methods.""" from collections.abc import Callable -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import os from pathlib import Path import sqlite3 @@ -948,7 +948,7 @@ def test_execute_stmt_lambda_element( assert rows == ["mock_row"] -@pytest.mark.freeze_time(datetime(2022, 10, 21, 7, 25, tzinfo=timezone.utc)) +@pytest.mark.freeze_time(datetime(2022, 10, 21, 7, 25, tzinfo=UTC)) async def test_resolve_period(hass: HomeAssistant) -> None: """Test statistic_during_period.""" diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index c5406a85fc0..530e8cb4209 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Generator -from datetime import date, datetime, timezone +from datetime import UTC, date, datetime from decimal import Decimal from typing import Any @@ -177,7 +177,7 @@ async def test_datetime_conversion( enable_custom_integrations: None, ) -> None: """Test conversion of datetime.""" - test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=timezone.utc) + test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=UTC) test_local_timestamp = test_timestamp.astimezone( dt_util.get_time_zone("Europe/Amsterdam") ) @@ -233,7 +233,7 @@ async def test_a_sensor_with_a_non_numeric_device_class( A non numeric sensor with a valid device class should never be handled as numeric because it has a device class. """ - test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=timezone.utc) + test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=UTC) test_local_timestamp = test_timestamp.astimezone( dt_util.get_time_zone("Europe/Amsterdam") ) @@ -334,7 +334,7 @@ RESTORE_DATA = { "native_unit_of_measurement": None, "native_value": { "__type": "", - "isoformat": datetime(2020, 2, 8, 15, tzinfo=timezone.utc).isoformat(), + "isoformat": datetime(2020, 2, 8, 15, tzinfo=UTC).isoformat(), }, }, "Decimal": { @@ -375,7 +375,7 @@ RESTORE_DATA = { ), (date(2020, 2, 8), dict, RESTORE_DATA["date"], SensorDeviceClass.DATE, None), ( - datetime(2020, 2, 8, 15, tzinfo=timezone.utc), + datetime(2020, 2, 8, 15, tzinfo=UTC), dict, RESTORE_DATA["datetime"], SensorDeviceClass.TIMESTAMP, @@ -433,7 +433,7 @@ async def test_restore_sensor_save_state( (123.0, float, RESTORE_DATA["float"], SensorDeviceClass.TEMPERATURE, "°F"), (date(2020, 2, 8), date, RESTORE_DATA["date"], SensorDeviceClass.DATE, None), ( - datetime(2020, 2, 8, 15, tzinfo=timezone.utc), + datetime(2020, 2, 8, 15, tzinfo=UTC), datetime, RESTORE_DATA["datetime"], SensorDeviceClass.TIMESTAMP, diff --git a/tests/components/subaru/api_responses.py b/tests/components/subaru/api_responses.py index e2fdf9ae508..52c57e7348a 100644 --- a/tests/components/subaru/api_responses.py +++ b/tests/components/subaru/api_responses.py @@ -1,6 +1,6 @@ """Sample API response data for tests.""" -from datetime import datetime, timezone +from datetime import UTC, datetime from homeassistant.components.subaru.const import ( API_GEN_1, @@ -58,7 +58,7 @@ VEHICLE_DATA = { }, } -MOCK_DATETIME = datetime.fromtimestamp(1595560000, timezone.utc) +MOCK_DATETIME = datetime.fromtimestamp(1595560000, UTC) VEHICLE_STATUS_EV = { VEHICLE_STATUS: { diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 1e6b2cc3840..e43163f66fc 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -1,5 +1,5 @@ """The tests for the Template Binary sensor platform.""" -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import logging from unittest.mock import patch @@ -1276,9 +1276,7 @@ async def test_trigger_entity_restore_state_auto_off( fake_extra_data = { "auto_off_time": { "__type": "", - "isoformat": datetime( - 2022, 2, 2, 12, 2, 2, tzinfo=timezone.utc - ).isoformat(), + "isoformat": datetime(2022, 2, 2, 12, 2, 2, tzinfo=UTC).isoformat(), }, } mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) @@ -1336,9 +1334,7 @@ async def test_trigger_entity_restore_state_auto_off_expired( fake_extra_data = { "auto_off_time": { "__type": "", - "isoformat": datetime( - 2022, 2, 2, 12, 2, 0, tzinfo=timezone.utc - ).isoformat(), + "isoformat": datetime(2022, 2, 2, 12, 2, 0, tzinfo=UTC).isoformat(), }, } mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) diff --git a/tests/components/template/test_button.py b/tests/components/template/test_button.py index f3fd3e03ce0..bfdb9352767 100644 --- a/tests/components/template/test_button.py +++ b/tests/components/template/test_button.py @@ -97,7 +97,7 @@ async def test_all_optional_config(hass: HomeAssistant, calls) -> None: _TEST_OPTIONS_BUTTON, ) - now = dt.datetime.now(dt.timezone.utc) + now = dt.datetime.now(dt.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): await hass.services.async_call( diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index 0c532d9007d..dd42cfc2977 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -1,5 +1,5 @@ """The tests for UVC camera module.""" -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from unittest.mock import call, patch import pytest @@ -368,7 +368,7 @@ async def test_motion_recording_mode_properties( assert state assert state.state != STATE_RECORDING assert state.attributes["last_recording_start_time"] == datetime( - 2021, 1, 8, 1, 56, 32, 367000, tzinfo=timezone.utc + 2021, 1, 8, 1, 56, 32, 367000, tzinfo=UTC ) mock_remote.return_value.get_camera.return_value["recordingIndicator"] = "DISABLED" diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index 8e8c5513097..3155d588e14 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -1,5 +1,5 @@ """Test the Whirlpool Sensor domain.""" -from datetime import datetime, timezone +from datetime import UTC, datetime from unittest.mock import MagicMock import pytest @@ -50,7 +50,7 @@ async def test_dryer_sensor_values( ) -> None: """Test the sensor value callbacks.""" hass.state = CoreState.not_running - thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, UTC) mock_restore_cache_with_extra_data( hass, ( @@ -114,7 +114,7 @@ async def test_washer_sensor_values( ) -> None: """Test the sensor value callbacks.""" hass.state = CoreState.not_running - thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, UTC) mock_restore_cache_with_extra_data( hass, ( @@ -281,7 +281,7 @@ async def test_restore_state( """Test sensor restore state.""" # Home assistant is not running yet hass.state = CoreState.not_running - thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, UTC) mock_restore_cache_with_extra_data( hass, ( @@ -334,7 +334,7 @@ async def test_callback( ) -> None: """Test callback timestamp callback function.""" hass.state = CoreState.not_running - thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, UTC) mock_restore_cache_with_extra_data( hass, ( diff --git a/tests/testing_config/custom_components/test/datetime.py b/tests/testing_config/custom_components/test/datetime.py index 7fca8d57881..ba511e81648 100644 --- a/tests/testing_config/custom_components/test/datetime.py +++ b/tests/testing_config/custom_components/test/datetime.py @@ -2,7 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ -from datetime import datetime, timezone +from datetime import UTC, datetime from homeassistant.components.datetime import DateTimeEntity @@ -37,7 +37,7 @@ def init(empty=False): MockDateTimeEntity( name="test", unique_id=UNIQUE_DATETIME, - native_value=datetime(2020, 1, 1, 1, 2, 3, tzinfo=timezone.utc), + native_value=datetime(2020, 1, 1, 1, 2, 3, tzinfo=UTC), ), ] ) From 2369964f27e9b1f5bff871b7ef510f4a5dd57975 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Aug 2023 19:40:21 +0200 Subject: [PATCH 0690/1151] Update aws boto dependencies (#98619) --- homeassistant/components/amazon_polly/manifest.json | 2 +- homeassistant/components/aws/manifest.json | 2 +- homeassistant/components/route53/manifest.json | 2 +- pyproject.toml | 3 --- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index aeda26c9b23..57971899cc0 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "iot_class": "cloud_push", "loggers": ["boto3", "botocore", "s3transfer"], - "requirements": ["boto3==1.20.24"] + "requirements": ["boto3==1.28.17"] } diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index 35d20258ead..c93a8493845 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/aws", "iot_class": "cloud_push", "loggers": ["aiobotocore", "botocore"], - "requirements": ["aiobotocore==2.1.0"] + "requirements": ["aiobotocore==2.6.0"] } diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index c04a193d2e1..644dcd499a0 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/route53", "iot_class": "cloud_push", "loggers": ["boto3", "botocore", "s3transfer"], - "requirements": ["boto3==1.20.24"] + "requirements": ["boto3==1.28.17"] } diff --git a/pyproject.toml b/pyproject.toml index 3c0ea4c4e34..cdbcf851a1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -481,9 +481,6 @@ filterwarnings = [ "ignore:ssl.PROTOCOL_TLSv1_2 is deprecated:DeprecationWarning:pylutron_caseta.smartbridge", # https://github.com/Danielhiversen/pyMillLocal/pull/8 - >=0.3.0 "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:mill_local", - # https://github.com/home-assistant/core/pull/98619 - update botocore to >=1.31.17 - "ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning:botocore.utils", - "ignore:'urllib3.contrib.pyopenssl' module is deprecated and will be removed in a future release of urllib3 2.x:DeprecationWarning:botocore.httpsession", # -- not helpful # pyatmo.__init__ imports deprecated moduls from itself - v7.5.0 diff --git a/requirements_all.txt b/requirements_all.txt index 02e53524a1d..a88b5a6423c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ aioazuredevops==1.3.5 aiobafi6==0.8.2 # homeassistant.components.aws -aiobotocore==2.1.0 +aiobotocore==2.6.0 # homeassistant.components.comelit aiocomelit==0.0.5 @@ -553,7 +553,7 @@ boschshcpy==0.2.57 # homeassistant.components.amazon_polly # homeassistant.components.route53 -boto3==1.20.24 +boto3==1.28.17 # homeassistant.components.broadlink broadlink==0.18.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b54f2326d0..36cb0cb766a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -187,7 +187,7 @@ aioazuredevops==1.3.5 aiobafi6==0.8.2 # homeassistant.components.aws -aiobotocore==2.1.0 +aiobotocore==2.6.0 # homeassistant.components.comelit aiocomelit==0.0.5 From 07ffbe82c183d792443ec90eef972882cfb13901 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 19:46:36 +0200 Subject: [PATCH 0691/1151] Add snapshot assertion to Ambient Station (#98764) --- tests/components/ambient_station/conftest.py | 6 +- .../snapshots/test_diagnostics.ambr | 65 +++++++++++++++++++ .../ambient_station/test_diagnostics.py | 63 ++---------------- 3 files changed, 77 insertions(+), 57 deletions(-) create mode 100644 tests/components/ambient_station/snapshots/test_diagnostics.ambr diff --git a/tests/components/ambient_station/conftest.py b/tests/components/ambient_station/conftest.py index aa849922b34..ab5eb6239c8 100644 --- a/tests/components/ambient_station/conftest.py +++ b/tests/components/ambient_station/conftest.py @@ -28,7 +28,11 @@ def config_fixture(hass): @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, data=config) + entry = MockConfigEntry( + domain=DOMAIN, + data=config, + entry_id="382cf7643f016fd48b3fe52163fe8877", + ) entry.add_to_hass(hass) return entry diff --git a/tests/components/ambient_station/snapshots/test_diagnostics.ambr b/tests/components/ambient_station/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..4b231660c4b --- /dev/null +++ b/tests/components/ambient_station/snapshots/test_diagnostics.ambr @@ -0,0 +1,65 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'entry': dict({ + 'data': dict({ + 'api_key': '**REDACTED**', + 'app_key': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'ambient_station', + 'entry_id': '382cf7643f016fd48b3fe52163fe8877', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': '**REDACTED**', + 'version': 2, + }), + 'stations': dict({ + 'devices': list([ + dict({ + 'apiKey': '**REDACTED**', + 'info': dict({ + 'location': '**REDACTED**', + 'name': 'Side Yard', + }), + 'lastData': dict({ + 'baromabsin': 25.016, + 'baromrelin': 29.953, + 'batt_co2': 1, + 'dailyrainin': 0, + 'date': '2022-01-19T22:38:00.000Z', + 'dateutc': 1642631880000, + 'deviceId': '**REDACTED**', + 'dewPoint': 17.75, + 'dewPointin': 37, + 'eventrainin': 0, + 'feelsLike': 21, + 'feelsLikein': 69.1, + 'hourlyrainin': 0, + 'humidity': 87, + 'humidityin': 29, + 'lastRain': '2022-01-07T19:45:00.000Z', + 'maxdailygust': 9.2, + 'monthlyrainin': 0.409, + 'solarradiation': 11.62, + 'tempf': 21, + 'tempinf': 70.9, + 'totalrainin': 35.398, + 'tz': '**REDACTED**', + 'uv': 0, + 'weeklyrainin': 0, + 'winddir': 25, + 'windgustmph': 1.1, + 'windspeedmph': 0.2, + }), + 'macAddress': '**REDACTED**', + }), + ]), + 'method': 'subscribe', + }), + }) +# --- diff --git a/tests/components/ambient_station/test_diagnostics.py b/tests/components/ambient_station/test_diagnostics.py index 61e974f4d0b..4c7a0f66f6a 100644 --- a/tests/components/ambient_station/test_diagnostics.py +++ b/tests/components/ambient_station/test_diagnostics.py @@ -1,6 +1,7 @@ """Test Ambient PWS diagnostics.""" +from syrupy import SnapshotAssertion + from homeassistant.components.ambient_station import DOMAIN -from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -13,62 +14,12 @@ async def test_entry_diagnostics( hass_client: ClientSessionGenerator, data_station, setup_config_entry, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" ambient = hass.data[DOMAIN][config_entry.entry_id] ambient.stations = data_station - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "entry_id": config_entry.entry_id, - "version": 2, - "domain": "ambient_station", - "title": REDACTED, - "data": {"api_key": REDACTED, "app_key": REDACTED}, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - }, - "stations": { - "devices": [ - { - "macAddress": REDACTED, - "lastData": { - "dateutc": 1642631880000, - "tempinf": 70.9, - "humidityin": 29, - "baromrelin": 29.953, - "baromabsin": 25.016, - "tempf": 21, - "humidity": 87, - "winddir": 25, - "windspeedmph": 0.2, - "windgustmph": 1.1, - "maxdailygust": 9.2, - "hourlyrainin": 0, - "eventrainin": 0, - "dailyrainin": 0, - "weeklyrainin": 0, - "monthlyrainin": 0.409, - "totalrainin": 35.398, - "solarradiation": 11.62, - "uv": 0, - "batt_co2": 1, - "feelsLike": 21, - "dewPoint": 17.75, - "feelsLikein": 69.1, - "dewPointin": 37, - "lastRain": "2022-01-07T19:45:00.000Z", - "deviceId": REDACTED, - "tz": REDACTED, - "date": "2022-01-19T22:38:00.000Z", - }, - "info": {"name": "Side Yard", "location": REDACTED}, - "apiKey": REDACTED, - } - ], - "method": "subscribe", - }, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From 3eb2b7010d2e39210bcdce6f712f951c575b969a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 19:54:17 +0200 Subject: [PATCH 0692/1151] Add device info to LG Soundbar (#98771) --- homeassistant/components/lg_soundbar/media_player.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 577b4a8811a..31176d82d6a 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -11,8 +11,11 @@ from homeassistant.components.media_player import ( 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 DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .const import DOMAIN + async def async_setup_entry( hass: HomeAssistant, @@ -66,6 +69,9 @@ class LGDevice(MediaPlayerEntity): self._bass = 0 self._treble = 0 self._device = None + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, name=host + ) async def async_added_to_hass(self) -> None: """Register the callback after hass is ready for it.""" From c39fc0766e3037add319da87b531a1df898445d4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 21 Aug 2023 20:03:19 +0200 Subject: [PATCH 0693/1151] Remove repair issue for MQTT discovered items (#98768) --- homeassistant/components/mqtt/mixins.py | 14 ++++---------- homeassistant/components/mqtt/strings.json | 8 -------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index fc87971064e..97ba96f0207 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -1144,11 +1144,8 @@ class MqttEntity( ) elif (device_name := config[CONF_DEVICE][CONF_NAME]) == entity_name: self._attr_name = None - self._issue_key = ( - "entity_name_is_device_name_discovery" - if self._discovery - else "entity_name_is_device_name_yaml" - ) + if not self._discovery: + self._issue_key = "entity_name_is_device_name_yaml" _LOGGER.warning( "MQTT device name is equal to entity name in your config %s, " "this is not expected. Please correct your configuration. " @@ -1162,11 +1159,8 @@ class MqttEntity( if device_name[:1].isupper(): # Ensure a capital if the device name first char is a capital new_entity_name = new_entity_name[:1].upper() + new_entity_name[1:] - self._issue_key = ( - "entity_name_startswith_device_name_discovery" - if self._discovery - else "entity_name_startswith_device_name_yaml" - ) + if not self._discovery: + self._issue_key = "entity_name_startswith_device_name_yaml" _LOGGER.warning( "MQTT entity name starts with the device name in your config %s, " "this is not expected. Please correct your configuration. " diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 516672c88ab..ae6033de5f9 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -15,14 +15,6 @@ "entity_name_startswith_device_name_yaml": { "title": "Manual configured MQTT entities with a name that starts with the device name", "description": "Some MQTT entities have an entity name that starts with the device name. This is not expected. To avoid a duplicate name the device name prefix is stripped off the entity name as a work-a-round. Please update your configuration and restart Home Assistant to fix this issue. \n\nList of affected entities:\n\n{config}" - }, - "entity_name_is_device_name_discovery": { - "title": "Discovered MQTT entities with a name that is equal to the device name", - "description": "Some MQTT entities have an entity name equal to the device name. This is not expected. The entity name is set to `null` as a work-a-round to avoid a duplicate name. Please inform the maintainer of the software application that supplies the affected entities to fix this issue.\n\nList of affected entities:\n\n{config}" - }, - "entity_name_startswith_device_name_discovery": { - "title": "Discovered entities with a name that starts with the device name", - "description": "Some MQTT entities have an entity name that starts with the device name. This is not expected. To avoid a duplicate name the device name prefix is stripped off the entity name as a work-a-round. Please inform the maintainer of the software application that supplies the affected entities to fix this issue. \n\nList of affected entities:\n\n{config}" } }, "config": { From 365dc47740a54557296d303d1f5e4ca423e5401b Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 21 Aug 2023 20:59:58 +0200 Subject: [PATCH 0694/1151] Add update platform to devolo Home Network (#86003) * Add update platform * Take care of progress * Adapt to recent development * Only add platform if supported * Avoid unneeded line change * Fix ruff in tests * Handle update failures like in button platform * Apply suggestions * Fix tests * Remove unused logger --- .../devolo_home_network/__init__.py | 25 ++- .../components/devolo_home_network/const.py | 1 + .../components/devolo_home_network/update.py | 132 ++++++++++++++ tests/components/devolo_home_network/const.py | 6 + tests/components/devolo_home_network/mock.py | 5 + .../devolo_home_network/test_init.py | 10 +- .../devolo_home_network/test_update.py | 166 ++++++++++++++++++ 7 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/devolo_home_network/update.py create mode 100644 tests/components/devolo_home_network/test_update.py diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index ed070abf0c8..181c47aac61 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -9,6 +9,7 @@ from devolo_plc_api import Device from devolo_plc_api.device_api import ( ConnectedStationInfo, NeighborAPInfo, + UpdateFirmwareCheck, WifiGuestAccessGet, ) from devolo_plc_api.exceptions.device import ( @@ -37,6 +38,7 @@ from .const import ( DOMAIN, LONG_UPDATE_INTERVAL, NEIGHBORING_WIFI_NETWORKS, + REGULAR_FIRMWARE, SHORT_UPDATE_INTERVAL, SWITCH_GUEST_WIFI, SWITCH_LEDS, @@ -45,7 +47,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Set up devolo Home Network from a config entry.""" hass.data.setdefault(DOMAIN, {}) zeroconf_instance = await zeroconf.async_get_async_instance(hass) @@ -66,6 +70,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {"device": device} + async def async_update_firmware_available() -> UpdateFirmwareCheck: + """Fetch data from API endpoint.""" + assert device.device + try: + async with asyncio.timeout(10): + return await device.device.async_check_firmware_available() + except DeviceUnavailable as err: + raise UpdateFailed(err) from err + async def async_update_connected_plc_devices() -> LogicalNetwork: """Fetch data from API endpoint.""" assert device.plcnet @@ -134,6 +147,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_led_status, update_interval=SHORT_UPDATE_INTERVAL, ) + if device.device and "update" in device.device.features: + coordinators[REGULAR_FIRMWARE] = DataUpdateCoordinator( + hass, + _LOGGER, + name=REGULAR_FIRMWARE, + update_method=async_update_firmware_available, + update_interval=LONG_UPDATE_INTERVAL, + ) if device.device and "wifi1" in device.device.features: coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator( hass, @@ -192,4 +213,6 @@ def platforms(device: Device) -> set[Platform]: supported_platforms.add(Platform.BINARY_SENSOR) if device.device and "wifi1" in device.device.features: supported_platforms.add(Platform.DEVICE_TRACKER) + if device.device and "update" in device.device.features: + supported_platforms.add(Platform.UPDATE) return supported_platforms diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index 39016ac7916..53019e28a23 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -23,6 +23,7 @@ CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" IDENTIFY = "identify" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" PAIRING = "pairing" +REGULAR_FIRMWARE = "regular_firmware" RESTART = "restart" START_WPS = "start_wps" SWITCH_GUEST_WIFI = "switch_guest_wifi" diff --git a/homeassistant/components/devolo_home_network/update.py b/homeassistant/components/devolo_home_network/update.py new file mode 100644 index 00000000000..21f6edd862c --- /dev/null +++ b/homeassistant/components/devolo_home_network/update.py @@ -0,0 +1,132 @@ +"""Platform for update integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from devolo_plc_api.device import Device +from devolo_plc_api.device_api import UpdateFirmwareCheck +from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityDescription, + UpdateEntityFeature, +) +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_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, REGULAR_FIRMWARE +from .entity import DevoloCoordinatorEntity + + +@dataclass +class DevoloUpdateRequiredKeysMixin: + """Mixin for required keys.""" + + latest_version: Callable[[UpdateFirmwareCheck], str] + update_func: Callable[[Device], Awaitable[bool]] + + +@dataclass +class DevoloUpdateEntityDescription( + UpdateEntityDescription, DevoloUpdateRequiredKeysMixin +): + """Describes devolo update entity.""" + + +UPDATE_TYPES: dict[str, DevoloUpdateEntityDescription] = { + REGULAR_FIRMWARE: DevoloUpdateEntityDescription( + key=REGULAR_FIRMWARE, + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + latest_version=lambda data: data.new_firmware_version.split("_")[0], + update_func=lambda device: device.device.async_start_firmware_update(), # type: ignore[union-attr] + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Get all devices and sensors and setup them via config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + coordinators: dict[str, DataUpdateCoordinator[Any]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinators"] + + async_add_entities( + [ + DevoloUpdateEntity( + entry, + coordinators[REGULAR_FIRMWARE], + UPDATE_TYPES[REGULAR_FIRMWARE], + device, + ) + ] + ) + + +class DevoloUpdateEntity(DevoloCoordinatorEntity, UpdateEntity): + """Representation of a devolo update.""" + + _attr_supported_features = ( + UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS + ) + + entity_description: DevoloUpdateEntityDescription + + def __init__( + self, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator, + description: DevoloUpdateEntityDescription, + device: Device, + ) -> None: + """Initialize entity.""" + self.entity_description = description + super().__init__(entry, coordinator, device) + self._attr_translation_key = None + self._in_progress_old_version: str | None = None + + @property + def installed_version(self) -> str: + """Version currently in use.""" + return self.device.firmware_version + + @property + def latest_version(self) -> str: + """Latest version available for install.""" + if latest_version := self.entity_description.latest_version( + self.coordinator.data + ): + return latest_version + return self.device.firmware_version + + @property + def in_progress(self) -> bool: + """Update installation in progress.""" + return self._in_progress_old_version == self.installed_version + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Turn the entity on.""" + self._in_progress_old_version = self.installed_version + try: + await self.entity_description.update_func(self.device) + except DevicePasswordProtected as ex: + self.entry.async_start_reauth(self.hass) + raise HomeAssistantError( + f"Device {self.entry.title} require re-authenticatication to set or change the password" + ) from ex + except DeviceUnavailable as ex: + raise HomeAssistantError( + f"Device {self.entry.title} did not respond" + ) from ex diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index fe11a55eb85..f4cc372660c 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -1,11 +1,13 @@ """Constants used for mocking data.""" from devolo_plc_api.device_api import ( + UPDATE_AVAILABLE, WIFI_BAND_2G, WIFI_BAND_5G, WIFI_VAP_MAIN_AP, ConnectedStationInfo, NeighborAPInfo, + UpdateFirmwareCheck, WifiGuestAccessGet, ) from devolo_plc_api.plcnet_api import LogicalNetwork @@ -79,6 +81,10 @@ DISCOVERY_INFO_WRONG_DEVICE = ZeroconfServiceInfo( type="mock_type", ) +FIRMWARE_UPDATE_AVAILABLE = UpdateFirmwareCheck( + result=UPDATE_AVAILABLE, new_firmware_version="5.6.2_2023-01-15" +) + GUEST_WIFI = WifiGuestAccessGet( ssid="devolo-guest-930", key="HMANPGBA", diff --git a/tests/components/devolo_home_network/mock.py b/tests/components/devolo_home_network/mock.py index 1cced53a520..80d1348cf0f 100644 --- a/tests/components/devolo_home_network/mock.py +++ b/tests/components/devolo_home_network/mock.py @@ -13,6 +13,7 @@ from zeroconf.asyncio import AsyncZeroconf from .const import ( CONNECTED_STATIONS, DISCOVERY_INFO, + FIRMWARE_UPDATE_AVAILABLE, GUEST_WIFI, IP, NEIGHBOR_ACCESS_POINTS, @@ -50,6 +51,9 @@ class MockDevice(Device): """Reset mock to starting point.""" self.async_disconnect = AsyncMock() self.device = DeviceApi(IP, None, DISCOVERY_INFO) + self.device.async_check_firmware_available = AsyncMock( + return_value=FIRMWARE_UPDATE_AVAILABLE + ) self.device.async_get_led_setting = AsyncMock(return_value=False) self.device.async_restart = AsyncMock(return_value=True) self.device.async_start_wps = AsyncMock(return_value=True) @@ -60,6 +64,7 @@ class MockDevice(Device): self.device.async_get_wifi_neighbor_access_points = AsyncMock( return_value=NEIGHBOR_ACCESS_POINTS ) + self.device.async_start_firmware_update = AsyncMock(return_value=True) self.plcnet = PlcNetApi(IP, None, DISCOVERY_INFO) self.plcnet.async_get_network_overview = AsyncMock(return_value=PLCNET) self.plcnet.async_identify_device_start = AsyncMock(return_value=True) diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index 99b6053e1ba..ba34eb18490 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -10,6 +10,7 @@ from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.devolo_home_network.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.components.update import DOMAIN as UPDATE from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant @@ -84,9 +85,12 @@ async def test_hass_stop(hass: HomeAssistant, mock_device: MockDevice) -> None: @pytest.mark.parametrize( ("device", "expected_platforms"), [ - ["mock_device", (BINARY_SENSOR, BUTTON, DEVICE_TRACKER, SENSOR, SWITCH)], - ["mock_repeater_device", (BUTTON, DEVICE_TRACKER, SENSOR, SWITCH)], - ["mock_nonwifi_device", (BINARY_SENSOR, BUTTON, SENSOR, SWITCH)], + [ + "mock_device", + (BINARY_SENSOR, BUTTON, DEVICE_TRACKER, SENSOR, SWITCH, UPDATE), + ], + ["mock_repeater_device", (BUTTON, DEVICE_TRACKER, SENSOR, SWITCH, UPDATE)], + ["mock_nonwifi_device", (BINARY_SENSOR, BUTTON, SENSOR, SWITCH, UPDATE)], ], ) async def test_platforms( diff --git a/tests/components/devolo_home_network/test_update.py b/tests/components/devolo_home_network/test_update.py new file mode 100644 index 00000000000..f5ef0bc9381 --- /dev/null +++ b/tests/components/devolo_home_network/test_update.py @@ -0,0 +1,166 @@ +"""Tests for the devolo Home Network update.""" +from devolo_plc_api.device_api import UPDATE_NOT_AVAILABLE, UpdateFirmwareCheck +from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable +import pytest + +from homeassistant.components.devolo_home_network.const import ( + DOMAIN, + LONG_UPDATE_INTERVAL, +) +from homeassistant.components.update import ( + DOMAIN as PLATFORM, + SERVICE_INSTALL, + UpdateDeviceClass, +) +from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory +from homeassistant.util import dt as dt_util + +from . import configure_integration +from .const import FIRMWARE_UPDATE_AVAILABLE +from .mock import MockDevice + +from tests.common import async_fire_time_changed + + +@pytest.mark.usefixtures("mock_device") +async def test_update_setup(hass: HomeAssistant) -> None: + """Test default setup of the update component.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{PLATFORM}.{device_name}_firmware") is not None + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_update_firmware( + hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry +) -> None: + """Test updating a device.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_firmware" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + assert state.attributes["device_class"] == UpdateDeviceClass.FIRMWARE + assert state.attributes["installed_version"] == mock_device.firmware_version + assert ( + state.attributes["latest_version"] + == FIRMWARE_UPDATE_AVAILABLE.new_firmware_version.split("_")[0] + ) + + assert entity_registry.async_get(state_key).entity_category == EntityCategory.CONFIG + + await hass.services.async_call( + PLATFORM, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: state_key}, + blocking=True, + ) + assert mock_device.device.async_start_firmware_update.call_count == 1 + + # Emulate state change + mock_device.device.async_check_firmware_available.return_value = ( + UpdateFirmwareCheck(result=UPDATE_NOT_AVAILABLE) + ) + async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_device_failure_check( + hass: HomeAssistant, mock_device: MockDevice +) -> None: + """Test device failure during check.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_firmware" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + + mock_device.device.async_check_firmware_available.side_effect = DeviceUnavailable + async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_device_failure_update( + hass: HomeAssistant, + mock_device: MockDevice, +) -> None: + """Test device failure when starting update.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_firmware" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_device.device.async_start_firmware_update.side_effect = DeviceUnavailable + + # Emulate update start + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + PLATFORM, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: state_key}, + blocking=True, + ) + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_auth_failed(hass: HomeAssistant, mock_device: MockDevice) -> None: + """Test updating unautherized triggers the reauth flow.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_firmware" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_device.device.async_start_firmware_update.side_effect = DevicePasswordProtected + + with pytest.raises(HomeAssistantError): + assert await hass.services.async_call( + PLATFORM, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: state_key}, + blocking=True, + ) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.config_entries.async_unload(entry.entry_id) From baf32658e5d220280b237cf982a5707524fb18c5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 21:30:23 +0200 Subject: [PATCH 0695/1151] Set battery device class in Logi Circle (#98774) --- homeassistant/components/logi_circle/sensor.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index 148dd88b41a..cd1dcfa2ede 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -4,7 +4,11 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_CHARGING, @@ -17,7 +21,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant 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 from homeassistant.util.dt import as_local @@ -29,9 +32,8 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="battery_level", - name="Battery", native_unit_of_measurement=PERCENTAGE, - icon="mdi:battery-50", + device_class=SensorDeviceClass.BATTERY, ), SensorEntityDescription( key="last_activity_time", @@ -135,10 +137,6 @@ class LogiSensor(SensorEntity): def icon(self): """Icon to use in the frontend, if any.""" sensor_type = self.entity_description.key - if sensor_type == "battery_level" and self._attr_native_value is not None: - return icon_for_battery_level( - battery_level=int(self._attr_native_value), charging=False - ) if sensor_type == "recording_mode" and self._attr_native_value is not None: return "mdi:eye" if self._attr_native_value == STATE_ON else "mdi:eye-off" if sensor_type == "streaming_mode" and self._attr_native_value is not None: From a1d554d1cb0cd65a836a177bac422c297cd744b7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 21:40:01 +0200 Subject: [PATCH 0696/1151] Add entity translations to Hyperion (#98635) --- homeassistant/components/hyperion/camera.py | 10 ++----- homeassistant/components/hyperion/const.py | 3 -- homeassistant/components/hyperion/light.py | 12 ++------ .../components/hyperion/strings.json | 28 +++++++++++++++++++ homeassistant/components/hyperion/switch.py | 27 +++++++++--------- 5 files changed, 46 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index f36b84170a9..9c9e509947d 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -44,7 +44,6 @@ from .const import ( DOMAIN, HYPERION_MANUFACTURER_NAME, HYPERION_MODEL_NAME, - NAME_SUFFIX_HYPERION_CAMERA, SIGNAL_ENTITY_REMOVE, TYPE_HYPERION_CAMERA, ) @@ -107,6 +106,9 @@ async def async_setup_entry( class HyperionCamera(Camera): """ComponentBinarySwitch switch class.""" + _attr_has_entity_name = True + _attr_name = None + def __init__( self, server_id: str, @@ -120,7 +122,6 @@ class HyperionCamera(Camera): self._unique_id = get_hyperion_unique_id( server_id, instance_num, TYPE_HYPERION_CAMERA ) - self._name = f"{instance_name} {NAME_SUFFIX_HYPERION_CAMERA}".strip() self._device_id = get_hyperion_device_id(server_id, instance_num) self._instance_name = instance_name self._client = hyperion_client @@ -140,11 +141,6 @@ class HyperionCamera(Camera): """Return a unique id for this instance.""" return self._unique_id - @property - def name(self) -> str: - """Return the name of the switch.""" - return self._name - @property def is_on(self) -> bool: """Return true if the camera is on.""" diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 4585b8bedaa..77e16df4d72 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -21,9 +21,6 @@ HYPERION_MODEL_NAME = f"{HYPERION_MANUFACTURER_NAME}-NG" HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases" HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" -NAME_SUFFIX_HYPERION_COMPONENT_SWITCH = "Component" -NAME_SUFFIX_HYPERION_CAMERA = "" - SIGNAL_INSTANCE_ADD = f"{DOMAIN}_instance_add_signal.{{}}" SIGNAL_INSTANCE_REMOVE = f"{DOMAIN}_instance_remove_signal.{{}}" SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal.{{}}" diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 54f9a3a27ff..105e577efad 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -116,6 +116,8 @@ async def async_setup_entry( class HyperionLight(LightEntity): """A Hyperion light that acts as a client for the configured priority.""" + _attr_has_entity_name = True + _attr_name = None _attr_color_mode = ColorMode.HS _attr_should_poll = False _attr_supported_color_modes = {ColorMode.HS} @@ -131,7 +133,6 @@ class HyperionLight(LightEntity): ) -> None: """Initialize the light.""" self._unique_id = self._compute_unique_id(server_id, instance_num) - self._name = self._compute_name(instance_name) self._device_id = get_hyperion_device_id(server_id, instance_num) self._instance_name = instance_name self._options = options @@ -157,20 +158,11 @@ class HyperionLight(LightEntity): """Compute a unique id for this instance.""" return get_hyperion_unique_id(server_id, instance_num, TYPE_HYPERION_LIGHT) - def _compute_name(self, instance_name: str) -> str: - """Compute the name of the light.""" - return f"{instance_name}".strip() - @property def entity_registry_enabled_default(self) -> bool: """Whether or not the entity is enabled by default.""" return True - @property - def name(self) -> str: - """Return the name of the light.""" - return self._name - @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/hyperion/strings.json b/homeassistant/components/hyperion/strings.json index 54beb7704c9..a2f8838e2ea 100644 --- a/homeassistant/components/hyperion/strings.json +++ b/homeassistant/components/hyperion/strings.json @@ -50,5 +50,33 @@ } } } + }, + "entity": { + "switch": { + "all": { + "name": "Component all" + }, + "smoothing": { + "name": "Component smoothing" + }, + "blackbar_detection": { + "name": "Component blackbar detection" + }, + "forwarder": { + "name": "Component forwarder" + }, + "boblight_server": { + "name": "Component boblight server" + }, + "platform_capture": { + "name": "Component platform capture" + }, + "led_device": { + "name": "Component LED device" + }, + "usb_capture": { + "name": "Component USB capture" + } + } } } diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index 95f14b9b888..11e1dc199be 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -46,7 +46,6 @@ from .const import ( DOMAIN, HYPERION_MANUFACTURER_NAME, HYPERION_MODEL_NAME, - NAME_SUFFIX_HYPERION_COMPONENT_SWITCH, SIGNAL_ENTITY_REMOVE, TYPE_HYPERION_COMPONENT_SWITCH_BASE, ) @@ -74,13 +73,17 @@ def _component_to_unique_id(server_id: str, component: str, instance_num: int) - ) -def _component_to_switch_name(component: str, instance_name: str) -> str: - """Convert a component to a switch name.""" - return ( - f"{instance_name} " - f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} " - f"{KEY_COMPONENTID_TO_NAME.get(component, component.capitalize())}" - ) +def _component_to_translation_key(component: str) -> str: + return { + KEY_COMPONENTID_ALL: "all", + KEY_COMPONENTID_SMOOTHING: "smoothing", + KEY_COMPONENTID_BLACKBORDER: "blackbar_detection", + KEY_COMPONENTID_FORWARDER: "forwarder", + KEY_COMPONENTID_BOBLIGHTSERVER: "boblight_server", + KEY_COMPONENTID_GRABBER: "platform_capture", + KEY_COMPONENTID_LEDDEVICE: "led_device", + KEY_COMPONENTID_V4L: "usb_capture", + }[component] async def async_setup_entry( @@ -129,6 +132,7 @@ class HyperionComponentSwitch(SwitchEntity): _attr_entity_category = EntityCategory.CONFIG _attr_should_poll = False + _attr_has_entity_name = True def __init__( self, @@ -143,7 +147,7 @@ class HyperionComponentSwitch(SwitchEntity): server_id, component_name, instance_num ) self._device_id = get_hyperion_device_id(server_id, instance_num) - self._name = _component_to_switch_name(component_name, instance_name) + self._attr_translation_key = _component_to_translation_key(component_name) self._instance_name = instance_name self._component_name = component_name self._client = hyperion_client @@ -162,11 +166,6 @@ class HyperionComponentSwitch(SwitchEntity): """Return a unique id for this instance.""" return self._unique_id - @property - def name(self) -> str: - """Return the name of the switch.""" - return self._name - @property def is_on(self) -> bool: """Return true if the switch is on.""" From 30d3df2d962c73274c82c5331eb8b5f38c67708f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 21:43:09 +0200 Subject: [PATCH 0697/1151] Add morning and evening damping to Forecast solar (#98721) Co-authored-by: Franck Nijhof --- .../components/forecast_solar/__init__.py | 28 ++++++++++++++- .../components/forecast_solar/config_flow.py | 17 ++++++--- .../components/forecast_solar/const.py | 4 ++- .../components/forecast_solar/coordinator.py | 6 ++-- .../components/forecast_solar/strings.json | 5 +-- tests/components/forecast_solar/conftest.py | 7 ++-- .../snapshots/test_diagnostics.ambr | 5 +-- .../forecast_solar/snapshots/test_init.ambr | 27 ++++++++++++++ .../forecast_solar/test_config_flow.py | 18 ++++++---- tests/components/forecast_solar/test_init.py | 36 ++++++++++++++++++- 10 files changed, 132 insertions(+), 21 deletions(-) create mode 100644 tests/components/forecast_solar/snapshots/test_init.ambr diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index e10d9651c3b..c6d4236c219 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -5,12 +5,38 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import ( + CONF_DAMPING, + CONF_DAMPING_EVENING, + CONF_DAMPING_MORNING, + CONF_MODULES_POWER, + DOMAIN, +) from .coordinator import ForecastSolarDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old config entry.""" + + if entry.version == 1: + new_options = entry.options.copy() + new_options |= { + CONF_MODULES_POWER: new_options.pop("modules power"), + CONF_DAMPING_MORNING: new_options.get(CONF_DAMPING, 0.0), + CONF_DAMPING_EVENING: new_options.pop(CONF_DAMPING, 0.0), + } + + entry.version = 2 + + hass.config_entries.async_update_entry( + entry, data=entry.data, options=new_options + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Forecast.Solar from a config entry.""" coordinator = ForecastSolarDataUpdateCoordinator(hass, entry) diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py index e74585da35b..47e1afaec7b 100644 --- a/homeassistant/components/forecast_solar/config_flow.py +++ b/homeassistant/components/forecast_solar/config_flow.py @@ -14,7 +14,8 @@ from homeassistant.helpers import config_validation as cv from .const import ( CONF_AZIMUTH, - CONF_DAMPING, + CONF_DAMPING_EVENING, + CONF_DAMPING_MORNING, CONF_DECLINATION, CONF_INVERTER_SIZE, CONF_MODULES_POWER, @@ -27,7 +28,7 @@ RE_API_KEY = re.compile(r"^[a-zA-Z0-9]{16}$") class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Forecast.Solar.""" - VERSION = 1 + VERSION = 2 @staticmethod @callback @@ -127,8 +128,16 @@ class ForecastSolarOptionFlowHandler(OptionsFlow): default=self.config_entry.options[CONF_MODULES_POWER], ): vol.Coerce(int), vol.Optional( - CONF_DAMPING, - default=self.config_entry.options.get(CONF_DAMPING, 0.0), + CONF_DAMPING_MORNING, + default=self.config_entry.options.get( + CONF_DAMPING_MORNING, 0.0 + ), + ): vol.Coerce(float), + vol.Optional( + CONF_DAMPING_EVENING, + default=self.config_entry.options.get( + CONF_DAMPING_EVENING, 0.0 + ), ): vol.Coerce(float), vol.Optional( CONF_INVERTER_SIZE, diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index e566733413b..24273f32405 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -8,6 +8,8 @@ LOGGER = logging.getLogger(__package__) CONF_DECLINATION = "declination" CONF_AZIMUTH = "azimuth" -CONF_MODULES_POWER = "modules power" +CONF_MODULES_POWER = "modules_power" CONF_DAMPING = "damping" +CONF_DAMPING_MORNING = "damping_morning" +CONF_DAMPING_EVENING = "damping_evening" CONF_INVERTER_SIZE = "inverter_size" diff --git a/homeassistant/components/forecast_solar/coordinator.py b/homeassistant/components/forecast_solar/coordinator.py index 273d3a49a2f..2ef6912e5a2 100644 --- a/homeassistant/components/forecast_solar/coordinator.py +++ b/homeassistant/components/forecast_solar/coordinator.py @@ -13,7 +13,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( CONF_AZIMUTH, - CONF_DAMPING, + CONF_DAMPING_EVENING, + CONF_DAMPING_MORNING, CONF_DECLINATION, CONF_INVERTER_SIZE, CONF_MODULES_POWER, @@ -48,7 +49,8 @@ class ForecastSolarDataUpdateCoordinator(DataUpdateCoordinator[Estimate]): declination=entry.options[CONF_DECLINATION], azimuth=(entry.options[CONF_AZIMUTH] - 180), kwp=(entry.options[CONF_MODULES_POWER] / 1000), - damping=entry.options.get(CONF_DAMPING, 0), + damping_morning=entry.options.get(CONF_DAMPING_MORNING, 0.0), + damping_evening=entry.options.get(CONF_DAMPING_EVENING, 0.0), inverter=inverter_size, ) diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index 43e6fca4ada..1413dba23d4 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -24,10 +24,11 @@ "data": { "api_key": "Forecast.Solar API Key (optional)", "azimuth": "[%key:component::forecast_solar::config::step::user::data::azimuth%]", - "damping": "Damping factor: adjusts the results in the morning and evening", + "damping_morning": "Damping factor: adjusts the results in the morning", + "damping_evening": "Damping factor: adjusts the results in the evening", "inverter_size": "Inverter size (Watt)", "declination": "[%key:component::forecast_solar::config::step::user::data::declination%]", - "modules power": "[%key:component::forecast_solar::config::step::user::data::modules_power%]" + "modules_power": "[%key:component::forecast_solar::config::step::user::data::modules_power%]" } } } diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index 60e9c9dc5d0..06cf39b4875 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -9,7 +9,8 @@ import pytest from homeassistant.components.forecast_solar.const import ( CONF_AZIMUTH, - CONF_DAMPING, + CONF_DAMPING_EVENING, + CONF_DAMPING_MORNING, CONF_DECLINATION, CONF_INVERTER_SIZE, CONF_MODULES_POWER, @@ -37,6 +38,7 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( title="Green House", unique_id="unique", + version=2, domain=DOMAIN, data={ CONF_LATITUDE: 52.42, @@ -47,7 +49,8 @@ def mock_config_entry() -> MockConfigEntry: CONF_DECLINATION: 30, CONF_AZIMUTH: 190, CONF_MODULES_POWER: 5100, - CONF_DAMPING: 0.5, + CONF_DAMPING_MORNING: 0.5, + CONF_DAMPING_EVENING: 0.5, CONF_INVERTER_SIZE: 2000, }, ) diff --git a/tests/components/forecast_solar/snapshots/test_diagnostics.ambr b/tests/components/forecast_solar/snapshots/test_diagnostics.ambr index 147c10c1793..686721a9d4a 100644 --- a/tests/components/forecast_solar/snapshots/test_diagnostics.ambr +++ b/tests/components/forecast_solar/snapshots/test_diagnostics.ambr @@ -33,10 +33,11 @@ 'options': dict({ 'api_key': '**REDACTED**', 'azimuth': 190, - 'damping': 0.5, + 'damping_evening': 0.5, + 'damping_morning': 0.5, 'declination': 30, 'inverter_size': 2000, - 'modules power': 5100, + 'modules_power': 5100, }), 'title': 'Green House', }), diff --git a/tests/components/forecast_solar/snapshots/test_init.ambr b/tests/components/forecast_solar/snapshots/test_init.ambr new file mode 100644 index 00000000000..a009105e2e6 --- /dev/null +++ b/tests/components/forecast_solar/snapshots/test_init.ambr @@ -0,0 +1,27 @@ +# serializer version: 1 +# name: test_migration + ConfigEntrySnapshot({ + 'data': dict({ + 'latitude': 52.42, + 'longitude': 4.42, + }), + 'disabled_by': None, + 'domain': 'forecast_solar', + 'entry_id': , + 'options': dict({ + 'api_key': 'abcdef12345', + 'azimuth': 190, + 'damping_evening': 0.5, + 'damping_morning': 0.5, + 'declination': 30, + 'inverter_size': 2000, + 'modules_power': 5100, + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Green House', + 'unique_id': 'unique', + 'version': 2, + }) +# --- diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index 2129821217e..06aeb94542e 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -3,7 +3,8 @@ from unittest.mock import AsyncMock from homeassistant.components.forecast_solar.const import ( CONF_AZIMUTH, - CONF_DAMPING, + CONF_DAMPING_EVENING, + CONF_DAMPING_MORNING, CONF_DECLINATION, CONF_INVERTER_SIZE, CONF_MODULES_POWER, @@ -75,7 +76,8 @@ async def test_options_flow_invalid_api( CONF_DECLINATION: 21, CONF_AZIMUTH: 22, CONF_MODULES_POWER: 2122, - CONF_DAMPING: 0.25, + CONF_DAMPING_MORNING: 0.25, + CONF_DAMPING_EVENING: 0.25, CONF_INVERTER_SIZE: 2000, }, ) @@ -108,7 +110,8 @@ async def test_options_flow( CONF_DECLINATION: 21, CONF_AZIMUTH: 22, CONF_MODULES_POWER: 2122, - CONF_DAMPING: 0.25, + CONF_DAMPING_MORNING: 0.25, + CONF_DAMPING_EVENING: 0.25, CONF_INVERTER_SIZE: 2000, }, ) @@ -120,7 +123,8 @@ async def test_options_flow( CONF_DECLINATION: 21, CONF_AZIMUTH: 22, CONF_MODULES_POWER: 2122, - CONF_DAMPING: 0.25, + CONF_DAMPING_MORNING: 0.25, + CONF_DAMPING_EVENING: 0.25, CONF_INVERTER_SIZE: 2000, } @@ -147,7 +151,8 @@ async def test_options_flow_without_key( CONF_DECLINATION: 21, CONF_AZIMUTH: 22, CONF_MODULES_POWER: 2122, - CONF_DAMPING: 0.25, + CONF_DAMPING_MORNING: 0.25, + CONF_DAMPING_EVENING: 0.25, CONF_INVERTER_SIZE: 2000, }, ) @@ -159,6 +164,7 @@ async def test_options_flow_without_key( CONF_DECLINATION: 21, CONF_AZIMUTH: 22, CONF_MODULES_POWER: 2122, - CONF_DAMPING: 0.25, + CONF_DAMPING_MORNING: 0.25, + CONF_DAMPING_EVENING: 0.25, CONF_INVERTER_SIZE: 2000, } diff --git a/tests/components/forecast_solar/test_init.py b/tests/components/forecast_solar/test_init.py index a7696fe8f53..25dcb41c976 100644 --- a/tests/components/forecast_solar/test_init.py +++ b/tests/components/forecast_solar/test_init.py @@ -2,9 +2,17 @@ from unittest.mock import MagicMock, patch from forecast_solar import ForecastSolarConnectionError +from syrupy import SnapshotAssertion -from homeassistant.components.forecast_solar.const import DOMAIN +from homeassistant.components.forecast_solar.const import ( + CONF_AZIMUTH, + CONF_DAMPING, + CONF_DECLINATION, + CONF_INVERTER_SIZE, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -44,3 +52,29 @@ async def test_config_entry_not_ready( assert mock_request.call_count == 1 assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_migration(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: + """Test config entry version 1 -> 2 migration.""" + mock_config_entry = MockConfigEntry( + title="Green House", + unique_id="unique", + domain=DOMAIN, + data={ + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.42, + }, + options={ + CONF_API_KEY: "abcdef12345", + CONF_DECLINATION: 30, + CONF_AZIMUTH: 190, + "modules power": 5100, + CONF_DAMPING: 0.5, + CONF_INVERTER_SIZE: 2000, + }, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.config_entries.async_get_entry(mock_config_entry.entry_id) == snapshot From 41db088f5db742138236442b0061f1400fc4711c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 21 Aug 2023 21:58:21 +0200 Subject: [PATCH 0698/1151] Update to 1.3.0 of gardena bluetooth (#98776) --- .../components/gardena_bluetooth/manifest.json | 2 +- homeassistant/components/gardena_bluetooth/number.py | 10 +++++----- .../components/gardena_bluetooth/strings.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json index af72ff7f69d..5d1c1888586 100644 --- a/homeassistant/components/gardena_bluetooth/manifest.json +++ b/homeassistant/components/gardena_bluetooth/manifest.json @@ -13,5 +13,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth", "iot_class": "local_polling", - "requirements": ["gardena_bluetooth==1.2.0"] + "requirements": ["gardena_bluetooth==1.3.0"] } diff --git a/homeassistant/components/gardena_bluetooth/number.py b/homeassistant/components/gardena_bluetooth/number.py index ec887458586..f53a7720577 100644 --- a/homeassistant/components/gardena_bluetooth/number.py +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -71,15 +71,15 @@ DESCRIPTIONS = ( char=DeviceConfiguration.rain_pause, ), GardenaBluetoothNumberEntityDescription( - key=DeviceConfiguration.season_pause.uuid, - translation_key="season_pause", + key=DeviceConfiguration.seasonal_adjust.uuid, + translation_key="seasonal_adjust", native_unit_of_measurement=UnitOfTime.DAYS, mode=NumberMode.BOX, - native_min_value=0.0, - native_max_value=365.0, + native_min_value=-128.0, + native_max_value=127.0, native_step=1.0, entity_category=EntityCategory.CONFIG, - char=DeviceConfiguration.season_pause, + char=DeviceConfiguration.seasonal_adjust, ), ) diff --git a/homeassistant/components/gardena_bluetooth/strings.json b/homeassistant/components/gardena_bluetooth/strings.json index 1fc6e10b5a6..538f97ffdb3 100644 --- a/homeassistant/components/gardena_bluetooth/strings.json +++ b/homeassistant/components/gardena_bluetooth/strings.json @@ -43,8 +43,8 @@ "rain_pause": { "name": "Rain pause" }, - "season_pause": { - "name": "Season pause" + "seasonal_adjust": { + "name": "Seasonal adjust" } }, "sensor": { diff --git a/requirements_all.txt b/requirements_all.txt index a88b5a6423c..33964c6d953 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -829,7 +829,7 @@ fritzconnection[qr]==1.12.2 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena_bluetooth==1.2.0 +gardena_bluetooth==1.3.0 # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36cb0cb766a..a1f8173cfa2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ fritzconnection[qr]==1.12.2 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena_bluetooth==1.2.0 +gardena_bluetooth==1.3.0 # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 From 78f0d8bc9c0170f5f17056bb4a2b05b6a631c5db Mon Sep 17 00:00:00 2001 From: Dennis Date: Mon, 21 Aug 2023 21:59:56 +0200 Subject: [PATCH 0699/1151] Add/Modify tomorrow.io sensor entity icons (#98648) --- homeassistant/components/tomorrowio/sensor.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index 7ccb4f673cd..119a3dfe582 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -118,6 +118,7 @@ SENSOR_TYPES = ( TomorrowioSensorEntityDescription( key=TMRW_ATTR_DEW_POINT, name="Dew Point", + icon="mdi:thermometer-water", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), @@ -142,6 +143,7 @@ SENSOR_TYPES = ( TomorrowioSensorEntityDescription( key=TMRW_ATTR_CLOUD_BASE, name="Cloud Base", + icon="mdi:cloud-arrow-down", unit_imperial=UnitOfLength.MILES, unit_metric=UnitOfLength.KILOMETERS, imperial_conversion=lambda val: DistanceConverter.convert( @@ -154,6 +156,7 @@ SENSOR_TYPES = ( TomorrowioSensorEntityDescription( key=TMRW_ATTR_CLOUD_CEILING, name="Cloud Ceiling", + icon="mdi:cloud-arrow-up", unit_imperial=UnitOfLength.MILES, unit_metric=UnitOfLength.KILOMETERS, imperial_conversion=lambda val: DistanceConverter.convert( @@ -165,12 +168,14 @@ SENSOR_TYPES = ( TomorrowioSensorEntityDescription( key=TMRW_ATTR_CLOUD_COVER, name="Cloud Cover", + icon="mdi:cloud-percent", native_unit_of_measurement=PERCENTAGE, ), # Data comes in as m/s, convert to mi/h for imperial TomorrowioSensorEntityDescription( key=TMRW_ATTR_WIND_GUST, name="Wind Gust", + icon="mdi:weather-windy", unit_imperial=UnitOfSpeed.MILES_PER_HOUR, unit_metric=UnitOfSpeed.METERS_PER_SECOND, imperial_conversion=lambda val: SpeedConverter.convert( @@ -270,9 +275,9 @@ SENSOR_TYPES = ( TomorrowioSensorEntityDescription( key=TMRW_ATTR_POLLEN_TREE, name="Tree Pollen Index", + icon="mdi:tree", value_map=PollenIndex, translation_key="pollen_index", - icon="mdi:flower-pollen", ), TomorrowioSensorEntityDescription( key=TMRW_ATTR_POLLEN_WEED, @@ -284,9 +289,9 @@ SENSOR_TYPES = ( TomorrowioSensorEntityDescription( key=TMRW_ATTR_POLLEN_GRASS, name="Grass Pollen Index", + icon="mdi:grass", value_map=PollenIndex, translation_key="pollen_index", - icon="mdi:flower-pollen", ), TomorrowioSensorEntityDescription( TMRW_ATTR_FIRE_INDEX, @@ -304,7 +309,7 @@ SENSOR_TYPES = ( name="UV Radiation Health Concern", value_map=UVDescription, translation_key="uv_index", - icon="mdi:sun-wireless", + icon="mdi:weather-sunny-alert", ), ) From d0d160f11cb4d40db132470be2db74bc2f539528 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 21 Aug 2023 22:01:44 +0200 Subject: [PATCH 0700/1151] Unifi add port forward control to switch platform (#98309) --- homeassistant/components/unifi/entity.py | 16 ++--- homeassistant/components/unifi/switch.py | 50 +++++++++++++- tests/components/unifi/test_switch.py | 87 ++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index 05ad2f56a8c..28a7b557b16 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -53,12 +53,12 @@ def async_wlan_available_fn(controller: UniFiController, obj_id: str) -> bool: @callback -def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: +def async_device_device_info_fn(controller: UniFiController, obj_id: str) -> DeviceInfo: """Create device registry entry for device.""" if "_" in obj_id: # Sub device (outlet or port) obj_id = obj_id.partition("_")[0] - device = api.devices[obj_id] + device = controller.api.devices[obj_id] return DeviceInfo( connections={(CONNECTION_NETWORK_MAC, device.mac)}, manufacturer=ATTR_MANUFACTURER, @@ -70,9 +70,9 @@ def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> Device @callback -def async_wlan_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: +def async_wlan_device_info_fn(controller: UniFiController, obj_id: str) -> DeviceInfo: """Create device registry entry for WLAN.""" - wlan = api.wlans[obj_id] + wlan = controller.api.wlans[obj_id] return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, wlan.id)}, @@ -83,9 +83,9 @@ def async_wlan_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceIn @callback -def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: +def async_client_device_info_fn(controller: UniFiController, obj_id: str) -> DeviceInfo: """Create device registry entry for client.""" - client = api.clients[obj_id] + client = controller.api.clients[obj_id] return DeviceInfo( connections={(CONNECTION_NETWORK_MAC, obj_id)}, default_manufacturer=client.oui, @@ -100,7 +100,7 @@ class UnifiDescription(Generic[HandlerT, ApiItemT]): allowed_fn: Callable[[UniFiController, str], bool] api_handler_fn: Callable[[aiounifi.Controller], HandlerT] available_fn: Callable[[UniFiController, str], bool] - device_info_fn: Callable[[aiounifi.Controller, str], DeviceInfo | None] + device_info_fn: Callable[[UniFiController, str], DeviceInfo | None] event_is_on: tuple[EventKey, ...] | None event_to_subscribe: tuple[EventKey, ...] | None name_fn: Callable[[ApiItemT], str | None] @@ -137,7 +137,7 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): self._removed = False self._attr_available = description.available_fn(controller, obj_id) - self._attr_device_info = description.device_info_fn(controller.api, obj_id) + self._attr_device_info = description.device_info_fn(controller, obj_id) self._attr_should_poll = description.should_poll self._attr_unique_id = description.unique_id_fn(controller, obj_id) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index e2b4dda3912..046aa3a1abd 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -17,6 +17,7 @@ from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups from aiounifi.interfaces.outlets import Outlets +from aiounifi.interfaces.port_forwarding import PortForwarding from aiounifi.interfaces.ports import Ports from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT @@ -30,6 +31,7 @@ from aiounifi.models.dpi_restriction_group import DPIRestrictionGroup from aiounifi.models.event import Event, EventKey from aiounifi.models.outlet import Outlet from aiounifi.models.port import Port +from aiounifi.models.port_forward import PortForward, PortForwardEnableRequest from aiounifi.models.wlan import Wlan, WlanEnableRequest from homeassistant.components.switch import ( @@ -75,7 +77,9 @@ def async_dpi_group_is_on_fn( @callback -def async_dpi_group_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: +def async_dpi_group_device_info_fn( + controller: UniFiController, obj_id: str +) -> DeviceInfo: """Create device registry entry for DPI group.""" return DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -86,6 +90,22 @@ def async_dpi_group_device_info_fn(api: aiounifi.Controller, obj_id: str) -> Dev ) +@callback +def async_port_forward_device_info_fn( + controller: UniFiController, obj_id: str +) -> DeviceInfo: + """Create device registry entry for port forward.""" + unique_id = controller.config_entry.unique_id + assert unique_id is not None + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + model="UniFi Network", + name="UniFi Network", + ) + + async def async_block_client_control_fn( api: aiounifi.Controller, obj_id: str, target: bool ) -> None: @@ -136,6 +156,14 @@ async def async_poe_port_control_fn( await api.request(DeviceSetPoePortModeRequest.create(device, int(index), state)) +async def async_port_forward_control_fn( + api: aiounifi.Controller, obj_id: str, target: bool +) -> None: + """Control port forward state.""" + port_forward = api.port_forwarding[obj_id] + await api.request(PortForwardEnableRequest.create(port_forward, target)) + + async def async_wlan_control_fn( api: aiounifi.Controller, obj_id: str, target: bool ) -> None: @@ -222,6 +250,26 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = ( supported_fn=async_outlet_supports_switching_fn, unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-outlet-{obj_id.split('_', 1)[1]}", ), + UnifiSwitchEntityDescription[PortForwarding, PortForward]( + key="Port forward control", + device_class=SwitchDeviceClass.SWITCH, + entity_category=EntityCategory.CONFIG, + has_entity_name=True, + icon="mdi:upload-network", + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.port_forwarding, + available_fn=lambda controller, obj_id: controller.available, + control_fn=async_port_forward_control_fn, + device_info_fn=async_port_forward_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + is_on_fn=lambda controller, port_forward: port_forward.enabled, + name_fn=lambda port_forward: f"{port_forward.name}", + object_fn=lambda api, obj_id: api.port_forwarding[obj_id], + should_poll=False, + supported_fn=lambda controller, obj_id: True, + unique_id_fn=lambda controller, obj_id: f"port_forward-{obj_id}", + ), UnifiSwitchEntityDescription[Ports, Port]( key="PoE port control", device_class=SwitchDeviceClass.OUTLET, diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index c091fc5cc59..8e3e215e717 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1518,3 +1518,90 @@ async def test_wlan_switches( mock_unifi_websocket(state=WebsocketState.RUNNING) await hass.async_block_till_done() assert hass.states.get("switch.ssid_1").state == STATE_OFF + + +async def test_port_forwarding_switches( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_unifi_websocket +) -> None: + """Test control of UniFi port forwarding.""" + _data = { + "_id": "5a32aa4ee4b0412345678911", + "dst_port": "12345", + "enabled": True, + "fwd_port": "23456", + "fwd": "10.0.0.2", + "name": "plex", + "pfwd_interface": "wan", + "proto": "tcp_udp", + "site_id": "5a32aa4ee4b0412345678910", + "src": "any", + } + config_entry = await setup_unifi_integration( + hass, aioclient_mock, port_forward_response=[_data.copy()] + ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 + + ent_reg = er.async_get(hass) + ent_reg_entry = ent_reg.async_get("switch.unifi_network_plex") + assert ent_reg_entry.unique_id == "port_forward-5a32aa4ee4b0412345678911" + assert ent_reg_entry.entity_category is EntityCategory.CONFIG + + # Validate state object + switch_1 = hass.states.get("switch.unifi_network_plex") + assert switch_1 is not None + assert switch_1.state == STATE_ON + assert switch_1.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH + + # Update state object + data = _data.copy() + data["enabled"] = False + mock_unifi_websocket(message=MessageKey.PORT_FORWARD_UPDATED, data=data) + await hass.async_block_till_done() + assert hass.states.get("switch.unifi_network_plex").state == STATE_OFF + + # Disable port forward + aioclient_mock.clear_requests() + aioclient_mock.put( + f"https://{controller.host}:1234/api/s/{controller.site}" + + f"/rest/portforward/{data['_id']}", + ) + + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_off", + {"entity_id": "switch.unifi_network_plex"}, + blocking=True, + ) + assert aioclient_mock.call_count == 1 + data = _data.copy() + data["enabled"] = False + assert aioclient_mock.mock_calls[0][2] == data + + # Enable port forward + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_on", + {"entity_id": "switch.unifi_network_plex"}, + blocking=True, + ) + assert aioclient_mock.call_count == 2 + assert aioclient_mock.mock_calls[1][2] == _data + + # Availability signalling + + # Controller disconnects + mock_unifi_websocket(state=WebsocketState.DISCONNECTED) + await hass.async_block_till_done() + assert hass.states.get("switch.unifi_network_plex").state == STATE_UNAVAILABLE + + # Controller reconnects + mock_unifi_websocket(state=WebsocketState.RUNNING) + await hass.async_block_till_done() + assert hass.states.get("switch.unifi_network_plex").state == STATE_OFF + + # Remove entity on deleted message + mock_unifi_websocket(message=MessageKey.PORT_FORWARD_DELETED, data=_data) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 From d582e60a6e1b527e6876b172c3391ced9d535ccb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Aug 2023 15:22:25 -0500 Subject: [PATCH 0701/1151] Enable strict typing for doorbird (#98778) --- .strict-typing | 1 + homeassistant/components/doorbird/__init__.py | 4 ++- homeassistant/components/doorbird/camera.py | 2 +- .../components/doorbird/config_flow.py | 26 +++++++++++++------ homeassistant/components/doorbird/device.py | 4 +-- homeassistant/components/doorbird/logbook.py | 11 ++++++-- mypy.ini | 10 +++++++ 7 files changed, 44 insertions(+), 14 deletions(-) diff --git a/.strict-typing b/.strict-typing index f83260c383f..5ecdc54826b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -104,6 +104,7 @@ homeassistant.components.dhcp.* homeassistant.components.diagnostics.* homeassistant.components.dlna_dmr.* homeassistant.components.dnsip.* +homeassistant.components.doorbird.* homeassistant.components.dormakaba_dkey.* homeassistant.components.dsmr.* homeassistant.components.dunehd.* diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index bf5fdeb1f60..58ab5eee1ac 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -157,7 +157,9 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: @callback -def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): +def _async_import_options_from_data_if_missing( + hass: HomeAssistant, entry: ConfigEntry +) -> None: options = dict(entry.options) modified = False for importable_option in (CONF_EVENTS,): diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index a29272168d4..a4133f2da2c 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -87,7 +87,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): self._last_update = datetime.datetime.min self._attr_unique_id = f"{self._mac_addr}_{camera_id}" - async def stream_source(self): + async def stream_source(self) -> str | None: """Return the stream source.""" return self._stream_url diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index d2197de93c9..56a02f49042 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -23,7 +23,9 @@ from .util import get_mac_address_from_door_station_info _LOGGER = logging.getLogger(__name__) -def _schema_with_defaults(host=None, name=None): +def _schema_with_defaults( + host: str | None = None, name: str | None = None +) -> vol.Schema: return vol.Schema( { vol.Required(CONF_HOST, default=host): str, @@ -39,7 +41,9 @@ def _check_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]: return device.ready(), device.info() -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate the user input allows us to connect.""" device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) try: @@ -78,13 +82,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the DoorBird config flow.""" - self.discovery_schema = {} + self.discovery_schema: vol.Schema | None = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: info, errors = await self._async_validate_or_error(user_input) if not errors: @@ -128,7 +134,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() - async def _async_validate_or_error(self, user_input): + async def _async_validate_or_error( + self, user_input: dict[str, Any] + ) -> tuple[dict[str, Any], dict[str, Any]]: """Validate doorbird or error.""" errors = {} info = {} @@ -159,7 +167,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle options flow.""" if user_input is not None: events = [event.strip() for event in user_input[CONF_EVENTS].split(",")] diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index 3a50700fa37..767a80a7857 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast from doorbirdpy import DoorBird @@ -131,7 +131,7 @@ class ConfiguredDoorBird: for fav_id in favs["http"]: if favs["http"][fav_id]["value"] == url: - return fav_id + return cast(str, fav_id) return None diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index 7c8e3cd3c51..84497a312ae 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -1,6 +1,8 @@ """Describe logbook events.""" from __future__ import annotations +from collections.abc import Callable + from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, @@ -14,11 +16,16 @@ from .models import DoorBirdData @callback -def async_describe_events(hass: HomeAssistant, async_describe_event): +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[ + [str, str, Callable[[Event], dict[str, str | None]]], None + ], +) -> None: """Describe logbook events.""" @callback - def async_describe_logbook_event(event: Event): + def async_describe_logbook_event(event: Event) -> dict[str, str | None]: """Describe a logbook event.""" return { LOGBOOK_ENTRY_NAME: "Doorbird", diff --git a/mypy.ini b/mypy.ini index 233539589cb..883a5ec2f26 100644 --- a/mypy.ini +++ b/mypy.ini @@ -802,6 +802,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.doorbird.*] +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.dormakaba_dkey.*] check_untyped_defs = true disallow_incomplete_defs = true From 9123e13774435da5ea4662c491abd86512c61256 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Aug 2023 15:23:16 -0500 Subject: [PATCH 0702/1151] Remove unused code in doorbird (#98779) --- homeassistant/components/doorbird/__init__.py | 14 -------------- homeassistant/components/doorbird/view.py | 3 --- 2 files changed, 17 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 58ab5eee1ac..d7800a26fc8 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -7,7 +7,6 @@ from typing import Any from doorbirdpy import DoorBird import requests -import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.config_entries import ConfigEntry @@ -32,19 +31,6 @@ _LOGGER = logging.getLogger(__name__) CONF_CUSTOM_URL = "hass_url_override" - -DEVICE_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_EVENTS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_CUSTOM_URL): cv.string, - vol.Optional(CONF_NAME): cv.string, - } -) - CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) diff --git a/homeassistant/components/doorbird/view.py b/homeassistant/components/doorbird/view.py index fca72d36fc1..396db79bf4c 100644 --- a/homeassistant/components/doorbird/view.py +++ b/homeassistant/components/doorbird/view.py @@ -2,7 +2,6 @@ from __future__ import annotations from http import HTTPStatus -import logging from aiohttp import web @@ -13,8 +12,6 @@ from .const import API_URL, DOMAIN from .device import async_reset_device_favorites from .util import get_door_station_by_token -_LOGGER = logging.getLogger(__name__) - class DoorBirdRequestView(HomeAssistantView): """Provide a page for the device to call.""" From f97f33fff78439543e01123e923c0fa11a223d91 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 21 Aug 2023 20:27:36 +0000 Subject: [PATCH 0703/1151] Only create an issue if push updates fail 5 times in a row for Shelly gen1 devices (#98747) --- .../components/shelly/coordinator.py | 27 +++++++++++++------ tests/components/shelly/conftest.py | 8 ++++++ tests/components/shelly/test_coordinator.py | 22 +++++++-------- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 7829cd76567..d645b09799f 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -274,8 +274,23 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): except InvalidAuthError: self.entry.async_start_reauth(self.hass) else: + device_update_info(self.hass, self.device, self.entry) + + @callback + def _async_handle_update( + self, device_: BlockDevice, update_type: BlockUpdateType + ) -> None: + """Handle device update.""" + if update_type == BlockUpdateType.COAP_PERIODIC: + self._push_update_failures = 0 + ir.async_delete_issue( + self.hass, + DOMAIN, + PUSH_UPDATE_ISSUE_ID.format(unique=self.mac), + ) + elif update_type == BlockUpdateType.COAP_REPLY: self._push_update_failures += 1 - if self._push_update_failures > MAX_PUSH_UPDATE_FAILURES: + if self._push_update_failures == MAX_PUSH_UPDATE_FAILURES: LOGGER.debug( "Creating issue %s", PUSH_UPDATE_ISSUE_ID.format(unique=self.mac) ) @@ -293,13 +308,9 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): "ip_address": self.device.ip_address, }, ) - device_update_info(self.hass, self.device, self.entry) - - @callback - def _async_handle_update( - self, device_: BlockDevice, update_type: BlockUpdateType - ) -> None: - """Handle device update.""" + LOGGER.debug( + "Push update failures for %s: %s", self.name, self._push_update_failures + ) self.async_set_updated_data(None) def async_setup(self) -> None: diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 2f33e76d336..de12adefaf1 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -251,6 +251,11 @@ async def mock_block_device(): {}, BlockUpdateType.COAP_PERIODIC ) + def update_reply(): + block_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, BlockUpdateType.COAP_REPLY + ) + device = Mock( spec=BlockDevice, blocks=MOCK_BLOCKS, @@ -265,6 +270,9 @@ async def mock_block_device(): type(device).name = PropertyMock(return_value="Test name") block_device_mock.return_value = device block_device_mock.return_value.mock_update = Mock(side_effect=update) + block_device_mock.return_value.mock_update_reply = Mock( + side_effect=update_reply + ) yield block_device_mock.return_value diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index eb546ce5835..5a8bb234f30 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -36,7 +36,6 @@ from . import ( mock_rest_update, register_entity, ) -from .conftest import MOCK_BLOCKS from tests.common import async_fire_time_changed @@ -259,24 +258,25 @@ async def test_block_device_push_updates_failure( """Test block device with push updates failure.""" issue_registry: ir.IssueRegistry = ir.async_get(hass) - monkeypatch.setattr( - mock_block_device, - "update", - AsyncMock(return_value=MOCK_BLOCKS), - ) await init_integration(hass, 1) - # Move time to force polling - for _ in range(MAX_PUSH_UPDATE_FAILURES + 1): - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15) - ) + # Updates with COAP_REPLAY type should create an issue + for _ in range(MAX_PUSH_UPDATE_FAILURES): + mock_block_device.mock_update_reply() await hass.async_block_till_done() assert issue_registry.async_get_issue( domain=DOMAIN, issue_id=f"push_update_{MOCK_MAC}" ) + # An update with COAP_PERIODIC type should clear the issue + mock_block_device.mock_update() + await hass.async_block_till_done() + + assert not issue_registry.async_get_issue( + domain=DOMAIN, issue_id=f"push_update_{MOCK_MAC}" + ) + async def test_block_button_click_event( hass: HomeAssistant, mock_block_device, events, monkeypatch From 5a835e703f6c5411d3d00bfbda299e63009a1986 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 22:28:20 +0200 Subject: [PATCH 0704/1151] Add entity translations to Honeywell Lyric (#98775) --- homeassistant/components/lyric/__init__.py | 2 ++ homeassistant/components/lyric/climate.py | 2 ++ homeassistant/components/lyric/sensor.py | 12 +++++------ homeassistant/components/lyric/strings.json | 22 +++++++++++++++++++++ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index a407afaa207..3e83fedb72a 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -118,6 +118,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class LyricEntity(CoordinatorEntity[DataUpdateCoordinator[Lyric]]): """Defines a base Honeywell Lyric entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator[Lyric], diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index df90ebcd6cf..ef662d061e8 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -138,6 +138,8 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): coordinator: DataUpdateCoordinator[Lyric] entity_description: ClimateEntityDescription + _attr_name = None + def __init__( self, coordinator: DataUpdateCoordinator[Lyric], diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 1e15ff58b18..d628a108183 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -86,7 +86,7 @@ async def async_setup_entry( coordinator, LyricSensorEntityDescription( key=f"{device.macID}_indoor_temperature", - name="Indoor Temperature", + translation_key="indoor_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=native_temperature_unit, @@ -102,7 +102,7 @@ async def async_setup_entry( coordinator, LyricSensorEntityDescription( key=f"{device.macID}_indoor_humidity", - name="Indoor Humidity", + translation_key="indoor_humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -123,7 +123,7 @@ async def async_setup_entry( coordinator, LyricSensorEntityDescription( key=f"{device.macID}_outdoor_temperature", - name="Outdoor Temperature", + translation_key="outdoor_temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=native_temperature_unit, @@ -139,7 +139,7 @@ async def async_setup_entry( coordinator, LyricSensorEntityDescription( key=f"{device.macID}_outdoor_humidity", - name="Outdoor Humidity", + translation_key="outdoor_humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -156,7 +156,7 @@ async def async_setup_entry( coordinator, LyricSensorEntityDescription( key=f"{device.macID}_next_period_time", - name="Next Period Time", + translation_key="next_period_time", device_class=SensorDeviceClass.TIMESTAMP, value=lambda device: get_datetime_from_future_time( device.changeableValues.nextPeriodTime @@ -172,7 +172,7 @@ async def async_setup_entry( coordinator, LyricSensorEntityDescription( key=f"{device.macID}_setpoint_status", - name="Setpoint Status", + translation_key="setpoint_status", icon="mdi:thermostat", value=lambda device: get_setpoint_status( device.changeableValues.thermostatSetpointStatus, diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 2271d4201f6..219530a9747 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -18,6 +18,28 @@ "default": "[%key:common::config_flow::create_entry::authenticated%]" } }, + "entity": { + "sensor": { + "indoor_temperature": { + "name": "Indoor temperature" + }, + "indoor_humidity": { + "name": "Indoor humidity" + }, + "outdoor_temperature": { + "name": "Outdoor temperature" + }, + "outdoor_humidity": { + "name": "Outdoor humidity" + }, + "next_period_time": { + "name": "Next period time" + }, + "setpoint_status": { + "name": "Setpoint status" + } + } + }, "services": { "set_hold_time": { "name": "Set Hold Time", From 53c118f6522d278ce87ed9153aac2918147fd189 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 22:29:09 +0200 Subject: [PATCH 0705/1151] Migrate LG Soundbar to has entity name (#98773) --- homeassistant/components/lg_soundbar/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 31176d82d6a..54d9be78df9 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -45,6 +45,8 @@ class LGDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.SELECT_SOUND_MODE ) + _attr_has_entity_name = True + _attr_name = None def __init__(self, host, port, unique_id): """Initialize the LG speakers.""" From 52cabed98f6d009448fbd389b9bad6ca0289eda5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 21 Aug 2023 22:31:04 +0200 Subject: [PATCH 0706/1151] Migrate LastFM to has entity name (#98766) --- homeassistant/components/lastfm/sensor.py | 3 ++- tests/components/lastfm/snapshots/test_sensor.ambr | 8 ++++---- tests/components/lastfm/test_init.py | 4 ++-- tests/components/lastfm/test_sensor.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index f0f3af3b672..40d6521bdc9 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -87,6 +87,8 @@ class LastFmSensor(CoordinatorEntity[LastFMDataUpdateCoordinator], SensorEntity) _attr_attribution = "Data provided by Last.fm" _attr_icon = "mdi:radio-fm" + _attr_has_entity_name = True + _attr_name = None def __init__( self, @@ -98,7 +100,6 @@ class LastFmSensor(CoordinatorEntity[LastFMDataUpdateCoordinator], SensorEntity) super().__init__(coordinator) self._username = username self._attr_unique_id = hashlib.sha256(username.encode("utf-8")).hexdigest() - self._attr_name = username self._attr_device_info = DeviceInfo( configuration_url="https://www.last.fm", entry_type=DeviceEntryType.SERVICE, diff --git a/tests/components/lastfm/snapshots/test_sensor.ambr b/tests/components/lastfm/snapshots/test_sensor.ambr index e64cf6b2629..30ad40df428 100644 --- a/tests/components/lastfm/snapshots/test_sensor.ambr +++ b/tests/components/lastfm/snapshots/test_sensor.ambr @@ -4,14 +4,14 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Last.fm', 'entity_picture': 'image', - 'friendly_name': 'testaccount1', + 'friendly_name': 'LastFM testaccount1', 'icon': 'mdi:radio-fm', 'last_played': 'artist - title', 'play_count': 1, 'top_played': 'artist - title', }), 'context': , - 'entity_id': 'sensor.testaccount1', + 'entity_id': 'sensor.lastfm_testaccount1', 'last_changed': , 'last_updated': , 'state': 'artist - title', @@ -22,14 +22,14 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Last.fm', 'entity_picture': 'image', - 'friendly_name': 'testaccount1', + 'friendly_name': 'LastFM testaccount1', 'icon': 'mdi:radio-fm', 'last_played': None, 'play_count': 0, 'top_played': None, }), 'context': , - 'entity_id': 'sensor.testaccount1', + 'entity_id': 'sensor.lastfm_testaccount1', 'last_changed': , 'last_updated': , 'state': 'Not Scrobbling', diff --git a/tests/components/lastfm/test_init.py b/tests/components/lastfm/test_init.py index 8f731385e6f..2f126af11a3 100644 --- a/tests/components/lastfm/test_init.py +++ b/tests/components/lastfm/test_init.py @@ -20,11 +20,11 @@ async def test_load_unload_entry( await setup_integration(config_entry, default_user) entry = hass.config_entries.async_entries(DOMAIN)[0] - state = hass.states.get("sensor.testaccount1") + state = hass.states.get("sensor.lastfm_testaccount1") assert state await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.testaccount1") + state = hass.states.get("sensor.lastfm_testaccount1") assert not state diff --git a/tests/components/lastfm/test_sensor.py b/tests/components/lastfm/test_sensor.py index fa29862d012..f5723215e2a 100644 --- a/tests/components/lastfm/test_sensor.py +++ b/tests/components/lastfm/test_sensor.py @@ -55,7 +55,7 @@ async def test_sensors( user = request.getfixturevalue(fixture) await setup_integration(config_entry, user) - entity_id = "sensor.testaccount1" + entity_id = "sensor.lastfm_testaccount1" state = hass.states.get(entity_id) From 07fb47b849fc6978232ba0cc0de3ff956f85c8e3 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Mon, 21 Aug 2023 22:45:29 +0200 Subject: [PATCH 0707/1151] Use VehicleType enum for Garages Amsterdam integration (#98780) --- homeassistant/components/garages_amsterdam/__init__.py | 4 ++-- homeassistant/components/garages_amsterdam/config_flow.py | 4 ++-- homeassistant/components/garages_amsterdam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py index 35d177b2cca..82e0c832e7b 100644 --- a/homeassistant/components/garages_amsterdam/__init__.py +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -3,7 +3,7 @@ import asyncio from datetime import timedelta import logging -from odp_amsterdam import ODPAmsterdam +from odp_amsterdam import ODPAmsterdam, VehicleType from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -45,7 +45,7 @@ async def get_coordinator( garage.garage_name: garage for garage in await ODPAmsterdam( session=aiohttp_client.async_get_clientsession(hass) - ).all_garages(vehicle="car") + ).all_garages(vehicle=VehicleType.CAR) } coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/garages_amsterdam/config_flow.py b/homeassistant/components/garages_amsterdam/config_flow.py index 7799630ddee..65a2d359747 100644 --- a/homeassistant/components/garages_amsterdam/config_flow.py +++ b/homeassistant/components/garages_amsterdam/config_flow.py @@ -5,7 +5,7 @@ import logging from typing import Any from aiohttp import ClientResponseError -from odp_amsterdam import ODPAmsterdam +from odp_amsterdam import ODPAmsterdam, VehicleType import voluptuous as vol from homeassistant import config_entries @@ -32,7 +32,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: api_data = await ODPAmsterdam( session=aiohttp_client.async_get_clientsession(self.hass) - ).all_garages(vehicle="car") + ).all_garages(vehicle=VehicleType.CAR) except ClientResponseError: _LOGGER.error("Unexpected response from server") return self.async_abort(reason="cannot_connect") diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json index e67bdaa04d0..3f4ffc7fae1 100644 --- a/homeassistant/components/garages_amsterdam/manifest.json +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", "iot_class": "cloud_polling", - "requirements": ["odp-amsterdam==5.3.0"] + "requirements": ["odp-amsterdam==5.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 33964c6d953..9d7222e75e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1317,7 +1317,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==5.3.0 +odp-amsterdam==5.3.1 # homeassistant.components.oem oemthermostat==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1f8173cfa2..3198be864ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1007,7 +1007,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==5.3.0 +odp-amsterdam==5.3.1 # homeassistant.components.omnilogic omnilogic==0.4.5 From 4a03f6482a2457a8f7a7ab6fa70bc72c98868ea4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 21 Aug 2023 22:46:15 +0200 Subject: [PATCH 0708/1151] Set thread dataset's preferred router on add if not set (#98639) --- .../components/thread/dataset_store.py | 17 +++++++++++-- tests/components/thread/test_dataset_store.py | 25 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thread/dataset_store.py b/homeassistant/components/thread/dataset_store.py index 22e2c1822c1..f814fbffbd0 100644 --- a/homeassistant/components/thread/dataset_store.py +++ b/homeassistant/components/thread/dataset_store.py @@ -202,8 +202,17 @@ class DatasetStore: raise HomeAssistantError("Invalid dataset") # Bail out if the dataset already exists - if any(entry for entry in self.datasets.values() if entry.dataset == dataset): - return + entry: DatasetEntry | None + for entry in self.datasets.values(): + if entry.dataset == dataset: + if ( + preferred_border_agent_id + and entry.preferred_border_agent_id is None + ): + self.async_set_preferred_border_agent_id( + entry.id, preferred_border_agent_id + ) + return # Update if dataset with same extended pan id exists and the timestamp # is newer @@ -248,6 +257,10 @@ class DatasetStore: self.datasets[entry.id], tlv=tlv ) self.async_schedule_save() + if preferred_border_agent_id and entry.preferred_border_agent_id is None: + self.async_set_preferred_border_agent_id( + entry.id, preferred_border_agent_id + ) return entry = DatasetEntry( diff --git a/tests/components/thread/test_dataset_store.py b/tests/components/thread/test_dataset_store.py index 77102f92019..d8822a7d536 100644 --- a/tests/components/thread/test_dataset_store.py +++ b/tests/components/thread/test_dataset_store.py @@ -546,9 +546,32 @@ async def test_set_preferred_border_agent_id(hass: HomeAssistant) -> None: assert await dataset_store.async_get_preferred_dataset(hass) is None await dataset_store.async_add_dataset( - hass, "source", DATASET_1, preferred_border_agent_id="blah" + hass, "source", DATASET_3, preferred_border_agent_id="blah" ) store = await dataset_store.async_get_store(hass) assert len(store.datasets) == 1 assert list(store.datasets.values())[0].preferred_border_agent_id == "blah" + + await dataset_store.async_add_dataset( + hass, "source", DATASET_3, preferred_border_agent_id="bleh" + ) + assert list(store.datasets.values())[0].preferred_border_agent_id == "blah" + + await dataset_store.async_add_dataset(hass, "source", DATASET_2) + assert len(store.datasets) == 2 + assert list(store.datasets.values())[1].preferred_border_agent_id is None + + await dataset_store.async_add_dataset( + hass, "source", DATASET_2, preferred_border_agent_id="blah" + ) + assert list(store.datasets.values())[1].preferred_border_agent_id == "blah" + + await dataset_store.async_add_dataset(hass, "source", DATASET_1) + assert len(store.datasets) == 3 + assert list(store.datasets.values())[2].preferred_border_agent_id is None + + await dataset_store.async_add_dataset( + hass, "source", DATASET_1_LARGER_TIMESTAMP, preferred_border_agent_id="blah" + ) + assert list(store.datasets.values())[1].preferred_border_agent_id == "blah" From 92258b8e6ffe8f6e216746ecea401bcac7a4f6ac Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 21 Aug 2023 22:55:50 +0200 Subject: [PATCH 0709/1151] Correct modbus swap/datatype error message (#98698) --- homeassistant/components/modbus/validators.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index b2e33a0f1f1..f5f88ea5f59 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -124,8 +124,7 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: if count < regs_needed or (count % regs_needed) != 0: raise vol.Invalid( f"Error in sensor {name} swap({swap_type}) " - "not possible due to the registers " - f"count: {count}, needed: {regs_needed}" + f"impossible because datatype({data_type}) is too small" ) structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" if slave_count > 1: From 3e7ec88703cf724ef29b00732b467704cf66c5e2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 21 Aug 2023 23:10:16 +0200 Subject: [PATCH 0710/1151] Add CoordinatorWeatherEntity (#98777) --- homeassistant/components/aemet/weather.py | 16 +++--------- .../components/environment_canada/weather.py | 16 +++--------- homeassistant/components/met/weather.py | 16 +++--------- .../components/met_eireann/weather.py | 20 +++------------ .../components/tomorrowio/weather.py | 15 +++-------- homeassistant/components/weather/__init__.py | 25 ++++++++++++++++++- 6 files changed, 40 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index f9b0f7ef6ca..6affc39c7a8 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -11,8 +11,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, DOMAIN as WEATHER_DOMAIN, + CoordinatorWeatherEntity, Forecast, - WeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -22,10 +22,9 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_API_CONDITION, @@ -111,7 +110,7 @@ async def async_setup_entry( async_add_entities(entities, False) -class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): +class AemetWeather(CoordinatorWeatherEntity[WeatherUpdateCoordinator]): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION @@ -139,15 +138,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): self._attr_name = name self._attr_unique_id = unique_id - @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): """Return the current condition.""" diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index bdc300dc9a3..67cb2df5473 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -22,8 +22,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, DOMAIN as WEATHER_DOMAIN, + CoordinatorWeatherEntity, Forecast, - WeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -33,10 +33,9 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from . import device_info @@ -87,7 +86,7 @@ def _calculate_unique_id(config_entry_unique_id: str | None, hourly: bool) -> st return f"{config_entry_unique_id}{'-hourly' if hourly else '-daily'}" -class ECWeather(CoordinatorEntity, WeatherEntity): +class ECWeather(CoordinatorWeatherEntity): """Representation of a weather condition.""" _attr_has_entity_name = True @@ -112,15 +111,6 @@ class ECWeather(CoordinatorEntity, WeatherEntity): self._hourly = hourly self._attr_device_info = device_info(coordinator.config_entry) - @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 native_temperature(self): """Return the temperature.""" diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index e7aea21875a..c697befd01f 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -16,8 +16,8 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_GUST_SPEED, ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, + CoordinatorWeatherEntity, Forecast, - WeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -30,11 +30,10 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er 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 from . import MetDataUpdateCoordinator @@ -92,7 +91,7 @@ def format_condition(condition: str) -> str: return condition -class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): +class MetWeather(CoordinatorWeatherEntity[MetDataUpdateCoordinator]): """Implementation of a Met.no weather condition.""" _attr_attribution = ( @@ -148,15 +147,6 @@ 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.""" diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index c40c89892c9..a69c1f24c08 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -7,8 +7,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TIME, DOMAIN as WEATHER_DOMAIN, + CoordinatorWeatherEntity, Forecast, - WeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -21,14 +21,11 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt as dt_util from . import MetEireannWeatherData @@ -78,7 +75,7 @@ def _calculate_unique_id(config: MappingProxyType[str, Any], hourly: bool) -> st class MetEireannWeather( - CoordinatorEntity[DataUpdateCoordinator[MetEireannWeatherData]], WeatherEntity + CoordinatorWeatherEntity[DataUpdateCoordinator[MetEireannWeatherData]] ): """Implementation of a Met Éireann weather condition.""" @@ -98,15 +95,6 @@ class MetEireannWeather( 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 name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index ec77a2c8040..f88887e64dd 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -17,8 +17,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, DOMAIN as WEATHER_DOMAIN, + CoordinatorWeatherEntity, Forecast, - WeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -31,7 +31,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import is_up @@ -93,7 +93,7 @@ def _calculate_unique_id(config_entry_unique_id: str | None, forecast_type: str) return f"{config_entry_unique_id}_{forecast_type}" -class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): +class TomorrowioWeatherEntity(TomorrowioEntity, CoordinatorWeatherEntity): """Entity that talks to Tomorrow.io v4 API to retrieve weather data.""" _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS @@ -123,15 +123,6 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): config_entry.unique_id, forecast_type ) - @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")) - ) - def _forecast_dict( self, forecast_dt: datetime, diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 74671f0c1df..8652f947f7c 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from datetime import timedelta import inspect import logging -from typing import Any, Final, Literal, Required, TypedDict, final +from typing import Any, Final, Literal, Required, TypedDict, TypeVar, final import voluptuous as vol @@ -37,6 +37,10 @@ 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.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from homeassistant.util.json import JsonValueType from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM @@ -117,6 +121,10 @@ ROUNDING_PRECISION = 2 SERVICE_GET_FORECAST: Final = "get_forecast" +_DataUpdateCoordinatorT = TypeVar( + "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" +) + # mypy: disallow-any-generics @@ -1155,3 +1163,18 @@ async def async_get_forecast_service( return { "forecast": converted_forecast_list, } + + +class CoordinatorWeatherEntity( + CoordinatorEntity[_DataUpdateCoordinatorT], WeatherEntity +): + """A class for weather entities using a single DataUpdateCoordinator.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + super()._handle_coordinator_update() + assert self.coordinator.config_entry + self.coordinator.config_entry.async_create_task( + self.hass, self.async_update_listeners(None) + ) From c93fcc37c46bf6e8779df10fa5cf0d8452fa76bb Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 22 Aug 2023 00:48:48 -0500 Subject: [PATCH 0711/1151] Update pyipp to 0.14.4 (#98791) * update pyipp to 0.14.4 * hassfest --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index e8bd4425ef3..cedf0521f95 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_polling", "loggers": ["deepmerge", "pyipp"], "quality_scale": "platinum", - "requirements": ["pyipp==0.14.3"], + "requirements": ["pyipp==0.14.4"], "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9d7222e75e1..ae624186d00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1749,7 +1749,7 @@ pyintesishome==1.8.0 pyipma==3.0.6 # homeassistant.components.ipp -pyipp==0.14.3 +pyipp==0.14.4 # homeassistant.components.iqvia pyiqvia==2022.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3198be864ab..64fc1751883 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1292,7 +1292,7 @@ pyinsteon==1.4.3 pyipma==3.0.6 # homeassistant.components.ipp -pyipp==0.14.3 +pyipp==0.14.4 # homeassistant.components.iqvia pyiqvia==2022.04.0 From 0f2b8570d269b4fc713cdd66ce760f4aea51107a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 09:20:30 +0200 Subject: [PATCH 0712/1151] Add device to Dexcom (#98574) --- homeassistant/components/dexcom/sensor.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/dexcom/sensor.py b/homeassistant/components/dexcom/sensor.py index cbe24088378..1a3c2a21011 100644 --- a/homeassistant/components/dexcom/sensor.py +++ b/homeassistant/components/dexcom/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -25,8 +26,10 @@ async def async_setup_entry( unit_of_measurement = config_entry.options[CONF_UNIT_OF_MEASUREMENT] async_add_entities( [ - DexcomGlucoseTrendSensor(coordinator, username), - DexcomGlucoseValueSensor(coordinator, username, unit_of_measurement), + DexcomGlucoseTrendSensor(coordinator, username, config_entry.entry_id), + DexcomGlucoseValueSensor( + coordinator, username, config_entry.entry_id, unit_of_measurement + ), ], False, ) @@ -36,11 +39,15 @@ class DexcomSensorEntity(CoordinatorEntity, SensorEntity): """Base Dexcom sensor entity.""" def __init__( - self, coordinator: DataUpdateCoordinator, username: str, key: str + self, coordinator: DataUpdateCoordinator, username: str, entry_id: str, key: str ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self._attr_unique_id = f"{username}-{key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry_id)}, + name=username, + ) class DexcomGlucoseValueSensor(DexcomSensorEntity): @@ -52,10 +59,11 @@ class DexcomGlucoseValueSensor(DexcomSensorEntity): self, coordinator: DataUpdateCoordinator, username: str, + entry_id: str, unit_of_measurement: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, username, "value") + super().__init__(coordinator, username, entry_id, "value") self._attr_native_unit_of_measurement = unit_of_measurement self._key = "mg_dl" if unit_of_measurement == MG_DL else "mmol_l" self._attr_name = f"{DOMAIN}_{username}_glucose_value" @@ -71,9 +79,11 @@ class DexcomGlucoseValueSensor(DexcomSensorEntity): class DexcomGlucoseTrendSensor(DexcomSensorEntity): """Representation of a Dexcom glucose trend sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, username: str) -> None: + def __init__( + self, coordinator: DataUpdateCoordinator, username: str, entry_id: str + ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, username, "trend") + super().__init__(coordinator, username, entry_id, "trend") self._attr_name = f"{DOMAIN}_{username}_glucose_trend" @property From 2e0038b9813ef71728502865c2c29ee233db6a9b Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 22 Aug 2023 02:22:46 -0500 Subject: [PATCH 0713/1151] Require device id for Roku entities (#98734) --- homeassistant/components/roku/__init__.py | 7 ++- .../components/roku/binary_sensor.py | 3 +- homeassistant/components/roku/coordinator.py | 2 + homeassistant/components/roku/entity.py | 52 ++++++++----------- homeassistant/components/roku/media_player.py | 3 +- homeassistant/components/roku/remote.py | 3 +- homeassistant/components/roku/select.py | 3 -- homeassistant/components/roku/sensor.py | 3 +- tests/components/roku/conftest.py | 6 ++- tests/components/roku/test_init.py | 19 +++++++ 10 files changed, 58 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 583d26a4a5b..f31a07feb29 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -22,7 +22,12 @@ PLATFORMS = [ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" - coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) + if (device_id := entry.unique_id) is None: + device_id = entry.entry_id + + coordinator = RokuDataUpdateCoordinator( + hass, host=entry.data[CONF_HOST], device_id=device_id + ) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator diff --git a/homeassistant/components/roku/binary_sensor.py b/homeassistant/components/roku/binary_sensor.py index 4bc36d0f7e5..1ac5f38b2c5 100644 --- a/homeassistant/components/roku/binary_sensor.py +++ b/homeassistant/components/roku/binary_sensor.py @@ -71,10 +71,9 @@ async def async_setup_entry( ) -> None: """Set up a Roku binary sensors based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = coordinator.data.info.serial_number + async_add_entities( RokuBinarySensorEntity( - device_id=unique_id, coordinator=coordinator, description=description, ) diff --git a/homeassistant/components/roku/coordinator.py b/homeassistant/components/roku/coordinator.py index f084302841e..a0bd9df238c 100644 --- a/homeassistant/components/roku/coordinator.py +++ b/homeassistant/components/roku/coordinator.py @@ -32,8 +32,10 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): hass: HomeAssistant, *, host: str, + device_id: str, ) -> None: """Initialize global Roku data updater.""" + self.device_id = device_id self.roku = Roku(host=host, session=async_get_clientsession(hass)) self.full_update_interval = timedelta(minutes=15) diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py index b6343d0dae1..b783831d4ec 100644 --- a/homeassistant/components/roku/entity.py +++ b/homeassistant/components/roku/entity.py @@ -12,45 +12,37 @@ from .const import DOMAIN class RokuEntity(CoordinatorEntity[RokuDataUpdateCoordinator]): """Defines a base Roku entity.""" + _attr_has_entity_name = True + def __init__( self, *, - device_id: str | None, coordinator: RokuDataUpdateCoordinator, description: EntityDescription | None = None, ) -> None: """Initialize the Roku entity.""" super().__init__(coordinator) - self._device_id = device_id if description is not None: self.entity_description = description + self._attr_unique_id = f"{coordinator.device_id}_{description.key}" + else: + self._attr_unique_id = coordinator.device_id - if device_id is None: - self._attr_name = f"{coordinator.data.info.name} {description.name}" - - if device_id is not None: - self._attr_has_entity_name = True - - if description is not None: - self._attr_unique_id = f"{device_id}_{description.key}" - else: - self._attr_unique_id = device_id - - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device_id)}, - connections={ - (CONNECTION_NETWORK_MAC, mac_address) - for mac_address in ( - self.coordinator.data.info.wifi_mac, - self.coordinator.data.info.ethernet_mac, - ) - if mac_address is not None - }, - name=self.coordinator.data.info.name, - manufacturer=self.coordinator.data.info.brand, - model=self.coordinator.data.info.model_name, - hw_version=self.coordinator.data.info.model_number, - sw_version=self.coordinator.data.info.version, - suggested_area=self.coordinator.data.info.device_location, - ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.device_id)}, + connections={ + (CONNECTION_NETWORK_MAC, mac_address) + for mac_address in ( + self.coordinator.data.info.wifi_mac, + self.coordinator.data.info.ethernet_mac, + ) + if mac_address is not None + }, + name=self.coordinator.data.info.name, + manufacturer=self.coordinator.data.info.brand, + model=self.coordinator.data.info.model_name, + hw_version=self.coordinator.data.info.model_number, + sw_version=self.coordinator.data.info.version, + suggested_area=self.coordinator.data.info.device_location, + ) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index a8c1cf4698c..05f782b37c4 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -85,11 +85,10 @@ async def async_setup_entry( ) -> None: """Set up the Roku config entry.""" coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = coordinator.data.info.serial_number + async_add_entities( [ RokuMediaPlayer( - device_id=unique_id, coordinator=coordinator, ) ], diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 0271e4a0f73..ef5350eb741 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -22,11 +22,10 @@ async def async_setup_entry( ) -> None: """Load Roku remote based on a config entry.""" coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = coordinator.data.info.serial_number + async_add_entities( [ RokuRemote( - device_id=unique_id, coordinator=coordinator, ) ], diff --git a/homeassistant/components/roku/select.py b/homeassistant/components/roku/select.py index e11748114d1..f915fdef9b0 100644 --- a/homeassistant/components/roku/select.py +++ b/homeassistant/components/roku/select.py @@ -122,14 +122,12 @@ async def async_setup_entry( """Set up Roku select based on a config entry.""" coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] device: RokuDevice = coordinator.data - unique_id = device.info.serial_number entities: list[RokuSelectEntity] = [] for description in ENTITIES: entities.append( RokuSelectEntity( - device_id=unique_id, coordinator=coordinator, description=description, ) @@ -138,7 +136,6 @@ async def async_setup_entry( if len(device.channels) > 0: entities.append( RokuSelectEntity( - device_id=unique_id, coordinator=coordinator, description=CHANNEL_ENTITY, ) diff --git a/homeassistant/components/roku/sensor.py b/homeassistant/components/roku/sensor.py index 0f0e87205b9..562501b4013 100644 --- a/homeassistant/components/roku/sensor.py +++ b/homeassistant/components/roku/sensor.py @@ -56,10 +56,9 @@ async def async_setup_entry( ) -> None: """Set up Roku sensor based on a config entry.""" coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = coordinator.data.info.serial_number + async_add_entities( RokuSensorEntity( - device_id=unique_id, coordinator=coordinator, description=description, ) diff --git a/tests/components/roku/conftest.py b/tests/components/roku/conftest.py index 677a10c697c..c1ceb23934e 100644 --- a/tests/components/roku/conftest.py +++ b/tests/components/roku/conftest.py @@ -81,9 +81,13 @@ def mock_roku( @pytest.fixture async def init_integration( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_roku: MagicMock + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_device: RokuDevice, + mock_roku: MagicMock, ) -> MockConfigEntry: """Set up the Roku integration for testing.""" + mock_config_entry.unique_id = mock_device.info.serial_number mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py index f8820e711a2..2ebad1e0b2b 100644 --- a/tests/components/roku/test_init.py +++ b/tests/components/roku/test_init.py @@ -26,6 +26,25 @@ async def test_config_entry_not_ready( assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY +async def test_config_entry_no_unique_id( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_roku: AsyncMock, +) -> None: + """Test the Roku configuration entry with missing unique id.""" + mock_config_entry.unique_id = None + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.entry_id in hass.data[DOMAIN] + assert mock_config_entry.state is ConfigEntryState.LOADED + assert ( + hass.data[DOMAIN][mock_config_entry.entry_id].device_id + == mock_config_entry.entry_id + ) + + async def test_load_unload_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, From a0a06f16a7c653e68aa5bea9bcf9f72dc639703a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 09:26:49 +0200 Subject: [PATCH 0714/1151] Add entity translations to Bosch SHC (#98750) --- .../components/bosch_shc/binary_sensor.py | 3 +- homeassistant/components/bosch_shc/cover.py | 1 + homeassistant/components/bosch_shc/entity.py | 2 +- homeassistant/components/bosch_shc/sensor.py | 22 +++++++------- .../components/bosch_shc/strings.json | 30 +++++++++++++++++++ homeassistant/components/bosch_shc/switch.py | 2 +- 6 files changed, 46 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bosch_shc/binary_sensor.py b/homeassistant/components/bosch_shc/binary_sensor.py index 348bfe80701..c9969fcf415 100644 --- a/homeassistant/components/bosch_shc/binary_sensor.py +++ b/homeassistant/components/bosch_shc/binary_sensor.py @@ -62,6 +62,8 @@ async def async_setup_entry( class ShutterContactSensor(SHCEntity, BinarySensorEntity): """Representation of an SHC shutter contact sensor.""" + _attr_name = None + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC shutter contact sensor..""" super().__init__(device, parent_id, entry_id) @@ -89,7 +91,6 @@ class BatterySensor(SHCEntity, BinarySensorEntity): def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC battery reporting sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Battery" self._attr_unique_id = f"{device.serial}_battery" @property diff --git a/homeassistant/components/bosch_shc/cover.py b/homeassistant/components/bosch_shc/cover.py index 3f1a9eccb93..8b2a2f65c12 100644 --- a/homeassistant/components/bosch_shc/cover.py +++ b/homeassistant/components/bosch_shc/cover.py @@ -42,6 +42,7 @@ async def async_setup_entry( class ShutterControlCover(SHCEntity, CoverEntity): """Representation of a SHC shutter control device.""" + _attr_name = None _attr_device_class = CoverDeviceClass.SHUTTER _attr_supported_features = ( CoverEntityFeature.OPEN diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index 5af77f8ee87..8c26d2e6d5a 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -24,6 +24,7 @@ class SHCBaseEntity(Entity): """Base representation of a SHC entity.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__( self, device: SHCDevice | SHCIntrusionSystem, parent_id: str, entry_id: str @@ -31,7 +32,6 @@ class SHCBaseEntity(Entity): """Initialize the generic SHC device.""" self._device = device self._entry_id = entry_id - self._attr_name = device.name async def async_added_to_hass(self) -> None: """Subscribe to SHC events.""" diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index 73307d9ea0a..df216ed0ff2 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -170,7 +170,6 @@ class TemperatureSensor(SHCEntity, SensorEntity): def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC temperature reporting sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Temperature" self._attr_unique_id = f"{device.serial}_temperature" @property @@ -188,7 +187,6 @@ class HumiditySensor(SHCEntity, SensorEntity): def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC humidity reporting sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Humidity" self._attr_unique_id = f"{device.serial}_humidity" @property @@ -200,13 +198,13 @@ class HumiditySensor(SHCEntity, SensorEntity): class PuritySensor(SHCEntity, SensorEntity): """Representation of an SHC purity reporting sensor.""" + _attr_translation_key = "purity" _attr_icon = "mdi:molecule-co2" _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC purity reporting sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Purity" self._attr_unique_id = f"{device.serial}_purity" @property @@ -218,10 +216,11 @@ class PuritySensor(SHCEntity, SensorEntity): class AirQualitySensor(SHCEntity, SensorEntity): """Representation of an SHC airquality reporting sensor.""" + _attr_translation_key = "air_quality" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC airquality reporting sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Air Quality" self._attr_unique_id = f"{device.serial}_airquality" @property @@ -240,10 +239,11 @@ class AirQualitySensor(SHCEntity, SensorEntity): class TemperatureRatingSensor(SHCEntity, SensorEntity): """Representation of an SHC temperature rating sensor.""" + _attr_translation_key = "temperature_rating" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC temperature rating sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Temperature Rating" self._attr_unique_id = f"{device.serial}_temperature_rating" @property @@ -255,12 +255,12 @@ class TemperatureRatingSensor(SHCEntity, SensorEntity): class CommunicationQualitySensor(SHCEntity, SensorEntity): """Representation of an SHC communication quality reporting sensor.""" + _attr_translation_key = "communication_quality" _attr_icon = "mdi:wifi" def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC communication quality reporting sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Communication Quality" self._attr_unique_id = f"{device.serial}_communication_quality" @property @@ -272,10 +272,11 @@ class CommunicationQualitySensor(SHCEntity, SensorEntity): class HumidityRatingSensor(SHCEntity, SensorEntity): """Representation of an SHC humidity rating sensor.""" + _attr_translation_key = "humidity_rating" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC humidity rating sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Humidity Rating" self._attr_unique_id = f"{device.serial}_humidity_rating" @property @@ -287,10 +288,11 @@ class HumidityRatingSensor(SHCEntity, SensorEntity): class PurityRatingSensor(SHCEntity, SensorEntity): """Representation of an SHC purity rating sensor.""" + _attr_translation_key = "purity_rating" + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC purity rating sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Purity Rating" self._attr_unique_id = f"{device.serial}_purity_rating" @property @@ -308,7 +310,6 @@ class PowerSensor(SHCEntity, SensorEntity): def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC power reporting sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Power" self._attr_unique_id = f"{device.serial}_power" @property @@ -327,7 +328,6 @@ class EnergySensor(SHCEntity, SensorEntity): def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC energy reporting sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{self._device.name} Energy" self._attr_unique_id = f"{self._device.serial}_energy" @property @@ -340,13 +340,13 @@ class ValveTappetSensor(SHCEntity, SensorEntity): """Representation of an SHC valve tappet reporting sensor.""" _attr_icon = "mdi:gauge" + _attr_translation_key = "valvetappet" _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC valve tappet reporting sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Valvetappet" self._attr_unique_id = f"{device.serial}_valvetappet" @property diff --git a/homeassistant/components/bosch_shc/strings.json b/homeassistant/components/bosch_shc/strings.json index 2b5720f0849..67462b78bec 100644 --- a/homeassistant/components/bosch_shc/strings.json +++ b/homeassistant/components/bosch_shc/strings.json @@ -36,5 +36,35 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "flow_title": "Bosch SHC: {name}" + }, + "entity": { + "sensor": { + "purity_rating": { + "name": "Purity rating" + }, + "purity": { + "name": "Purity" + }, + "valvetappet": { + "name": "Valvetappet" + }, + "air_quality": { + "name": "Air quality" + }, + "temperature_rating": { + "name": "Temperature rating" + }, + "humidity_rating": { + "name": "Humidity rating" + }, + "communication_quality": { + "name": "Communication quality" + } + }, + "switch": { + "routing": { + "name": "Routing" + } + } } } diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index 3b3b6e2ffd4..25af0628780 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -200,12 +200,12 @@ class SHCRoutingSwitch(SHCEntity, SwitchEntity): """Representation of a SHC routing switch.""" _attr_icon = "mdi:wifi" + _attr_translation_key = "routing" _attr_entity_category = EntityCategory.CONFIG def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: """Initialize an SHC communication quality reporting sensor.""" super().__init__(device, parent_id, entry_id) - self._attr_name = f"{device.name} Routing" self._attr_unique_id = f"{device.serial}_routing" @property From a89c0c944aedaf402cdff5b433d3d4756384bd2a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 09:28:47 +0200 Subject: [PATCH 0715/1151] Add device info to Life360 (#98772) --- homeassistant/components/life360/device_tracker.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index f16e7215a22..27b4ce291fd 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -10,6 +10,7 @@ from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_CHARGING from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -111,6 +112,7 @@ class Life360DeviceTracker( self._prev_data = self._data self._attr_name = self._data.name + self._name = self._data.name self._attr_entity_picture = self._data.entity_picture # Server sends a pair of address values on alternate updates. Keep the pair of @@ -124,6 +126,11 @@ class Life360DeviceTracker( address = None self._addresses = [address] + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + return DeviceInfo(identifiers={(DOMAIN, self._attr_unique_id)}, name=self._name) + @property def _options(self) -> Mapping[str, Any]: """Shortcut to config entry options.""" From d4b49726f428feb007b89cdd11bc19b4d299a6cb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 09:30:43 +0200 Subject: [PATCH 0716/1151] Add snapshot assertion to Airzone cloud (#98761) --- .../snapshots/test_diagnostics.ambr | 231 ++++++++++++++++++ .../airzone_cloud/test_diagnostics.py | 53 +--- tests/components/airzone_cloud/util.py | 1 + 3 files changed, 239 insertions(+), 46 deletions(-) create mode 100644 tests/components/airzone_cloud/snapshots/test_diagnostics.ambr diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..abdcc90978d --- /dev/null +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -0,0 +1,231 @@ +# serializer version: 1 +# name: test_config_entry_diagnostics + dict({ + 'api_data': dict({ + 'devices-config': dict({ + 'device1': dict({ + }), + }), + 'devices-status': dict({ + 'device1': dict({ + }), + }), + 'installations': dict({ + 'installation1': dict({ + 'groups': list([ + dict({ + 'devices': list([ + dict({ + 'device_id': 'device1', + 'ws_id': 'webserver1', + }), + ]), + 'group_id': 'group1', + }), + ]), + 'plugins': dict({ + 'schedules': dict({ + 'calendar_ws_ids': list([ + 'webserver1', + ]), + }), + }), + }), + }), + 'installations-list': dict({ + }), + 'test_cov': dict({ + '1': None, + '2': list([ + 'foo', + 'bar', + ]), + '3': list([ + list([ + 'foo', + 'bar', + ]), + ]), + }), + 'webservers': dict({ + 'webserver1': dict({ + }), + }), + }), + 'config_entry': dict({ + 'data': dict({ + 'id': 'installation1', + 'password': '**REDACTED**', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'airzone_cloud', + 'entry_id': 'd186e31edb46d64d14b9b2f11f1ebd9f', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': 'installation1', + 'version': 1, + }), + 'coord_data': dict({ + 'aidoos': dict({ + 'aidoo1': dict({ + 'action': 6, + 'active': False, + 'available': True, + 'id': 'aidoo1', + 'installation': 'installation1', + 'is-connected': True, + 'mode': None, + 'name': 'Bron', + 'power': None, + 'problems': False, + 'temperature': 21.0, + 'temperature-step': 0.5, + 'web-server': '11:22:33:44:55:67', + 'ws-connected': True, + }), + }), + 'groups': dict({ + 'group1': dict({ + 'action': 6, + 'active': True, + 'available': True, + 'humidity': 27, + 'installation': 'installation1', + 'mode': 0, + 'name': 'Group', + 'num-devices': 2, + 'power': None, + 'systems': list([ + 'system1', + ]), + 'temperature': 22.5, + 'temperature-step': 0.5, + 'zones': list([ + 'zone1', + 'zone2', + ]), + }), + 'grp2': dict({ + 'action': 6, + 'active': False, + 'aidoos': list([ + 'aidoo1', + ]), + 'available': True, + 'installation': 'installation1', + 'mode': 0, + 'name': 'Aidoo Group', + 'num-devices': 1, + 'power': None, + 'temperature': 21.0, + 'temperature-step': 0.5, + }), + }), + 'installations': dict({ + 'installation1': dict({ + 'id': 'installation1', + 'name': 'House', + 'web-servers': list([ + 'webserver1', + '11:22:33:44:55:67', + ]), + }), + }), + 'systems': dict({ + 'system1': dict({ + 'available': True, + 'id': 'system1', + 'installation': 'installation1', + 'is-connected': True, + 'mode': None, + 'name': 'System 1', + 'problems': False, + 'system': 1, + 'web-server': 'webserver1', + 'ws-connected': True, + }), + }), + 'web-servers': dict({ + '11:22:33:44:55:67': dict({ + 'available': True, + 'connection-date': '2023-05-24 17:00:52 +0200', + 'disconnection-date': '2023-05-24 17:00:25 +0200', + 'firmware': '3.13', + 'id': '11:22:33:44:55:67', + 'installation': 'installation1', + 'name': 'WebServer 11:22:33:44:55:67', + 'type': 'ws_aidoo', + 'wifi-channel': 1, + 'wifi-mac': '**REDACTED**', + 'wifi-quality': 4, + 'wifi-rssi': -77, + 'wifi-ssid': 'Wifi', + }), + 'webserver1': dict({ + 'available': True, + 'connection-date': '2023-05-07T12:55:51.000Z', + 'disconnection-date': '2023-01-01T22:26:55.376Z', + 'firmware': '3.44', + 'id': 'webserver1', + 'installation': 'installation1', + 'name': 'WebServer 11:22:33:44:55:66', + 'type': 'ws_az', + 'wifi-channel': 36, + 'wifi-mac': '**REDACTED**', + 'wifi-quality': 4, + 'wifi-rssi': -56, + 'wifi-ssid': 'Wifi', + }), + }), + 'zones': dict({ + 'zone1': dict({ + 'action': 6, + 'active': True, + 'available': True, + 'humidity': 30, + 'id': 'zone1', + 'installation': 'installation1', + 'is-connected': True, + 'master': None, + 'mode': None, + 'name': 'Salon', + 'power': None, + 'problems': False, + 'system': 1, + 'system-id': 'system1', + 'temperature': 20.0, + 'temperature-step': 0.5, + 'web-server': 'webserver1', + 'ws-connected': True, + 'zone': 1, + }), + 'zone2': dict({ + 'action': 6, + 'active': False, + 'available': True, + 'humidity': 24, + 'id': 'zone2', + 'installation': 'installation1', + 'is-connected': True, + 'master': None, + 'mode': None, + 'name': 'Dormitorio', + 'power': None, + 'problems': False, + 'system': 1, + 'system-id': 'system1', + 'temperature': 25.0, + 'temperature-step': 0.5, + 'web-server': 'webserver1', + 'ws-connected': True, + 'zone': 2, + }), + }), + }), + }) +# --- diff --git a/tests/components/airzone_cloud/test_diagnostics.py b/tests/components/airzone_cloud/test_diagnostics.py index 6c8ae366518..8bef70501e7 100644 --- a/tests/components/airzone_cloud/test_diagnostics.py +++ b/tests/components/airzone_cloud/test_diagnostics.py @@ -8,22 +8,16 @@ from aioairzone_cloud.const import ( API_GROUP_ID, API_GROUPS, API_WS_ID, - AZD_AIDOOS, - AZD_GROUPS, - AZD_INSTALLATIONS, - AZD_SYSTEMS, - AZD_WEBSERVERS, - AZD_ZONES, RAW_DEVICES_CONFIG, RAW_DEVICES_STATUS, RAW_INSTALLATIONS, RAW_INSTALLATIONS_LIST, RAW_WEBSERVERS, ) +from syrupy import SnapshotAssertion from homeassistant.components.airzone_cloud.const import DOMAIN -from homeassistant.components.diagnostics import REDACTED -from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant from .util import CONFIG, WS_ID, async_init_integration @@ -78,7 +72,9 @@ RAW_DATA_MOCK = { async def test_config_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" await async_init_integration(hass) @@ -89,40 +85,5 @@ async def test_config_entry_diagnostics( "homeassistant.components.airzone_cloud.AirzoneCloudApi.raw_data", return_value=RAW_DATA_MOCK, ): - diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - - assert list(diag["api_data"]) >= list(RAW_DATA_MOCK) - assert "dev1" not in diag["api_data"][RAW_DEVICES_CONFIG] - assert "device1" in diag["api_data"][RAW_DEVICES_CONFIG] - assert ( - diag["api_data"][RAW_INSTALLATIONS]["installation1"][API_GROUPS][0][ - API_GROUP_ID - ] - == "group1" - ) - assert "inst1" not in diag["api_data"][RAW_INSTALLATIONS] - assert "installation1" in diag["api_data"][RAW_INSTALLATIONS] - assert WS_ID not in diag["api_data"][RAW_WEBSERVERS] - assert "webserver1" in diag["api_data"][RAW_WEBSERVERS] - - assert ( - diag["config_entry"].items() - >= { - "data": { - CONF_ID: "installation1", - CONF_PASSWORD: REDACTED, - CONF_USERNAME: REDACTED, - }, - "domain": DOMAIN, - "unique_id": "installation1", - }.items() - ) - - assert list(diag["coord_data"]) >= [ - AZD_AIDOOS, - AZD_GROUPS, - AZD_INSTALLATIONS, - AZD_SYSTEMS, - AZD_WEBSERVERS, - AZD_ZONES, - ] + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert result == snapshot diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 0c26755f948..21459be60e4 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -223,6 +223,7 @@ async def async_init_integration( config_entry = MockConfigEntry( data=CONFIG, + entry_id="d186e31edb46d64d14b9b2f11f1ebd9f", domain=DOMAIN, unique_id=CONFIG[CONF_ID], ) From 6f7c3c949cb0119b1b14f38f7823e3cf11120d07 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 09:31:31 +0200 Subject: [PATCH 0717/1151] Add snapshot assertion to Airvisual Pro (#98759) --- tests/components/airvisual_pro/conftest.py | 7 +- .../snapshots/test_diagnostics.ambr | 106 ++++++++++++++++++ .../airvisual_pro/test_diagnostics.py | 86 ++------------ 3 files changed, 119 insertions(+), 80 deletions(-) create mode 100644 tests/components/airvisual_pro/snapshots/test_diagnostics.ambr diff --git a/tests/components/airvisual_pro/conftest.py b/tests/components/airvisual_pro/conftest.py index caff9571812..4376db23366 100644 --- a/tests/components/airvisual_pro/conftest.py +++ b/tests/components/airvisual_pro/conftest.py @@ -24,7 +24,12 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id="XXXXXXX", data=config) + entry = MockConfigEntry( + domain=DOMAIN, + entry_id="6a2b3770e53c28dc1eeb2515e906b0ce", + unique_id="XXXXXXX", + data=config, + ) entry.add_to_hass(hass) return entry diff --git a/tests/components/airvisual_pro/snapshots/test_diagnostics.ambr b/tests/components/airvisual_pro/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..96cda8e012f --- /dev/null +++ b/tests/components/airvisual_pro/snapshots/test_diagnostics.ambr @@ -0,0 +1,106 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'date_and_time': dict({ + 'date': '2022/10/06', + 'time': '16:00:44', + 'timestamp': '1665072044', + }), + 'history': dict({ + }), + 'last_measurement_timestamp': 1665072044, + 'measurements': dict({ + 'aqi_cn': '0', + 'aqi_us': '0', + 'co2': '472', + 'humidity': '57', + 'pm0_1': '0', + 'pm1_0': '0', + 'pm2_5': '0', + 'temperature_C': '23.0', + 'temperature_F': '73.4', + 'voc': '-1', + }), + 'serial_number': '**REDACTED**', + 'settings': dict({ + 'follow_mode': 'station', + 'followed_station': '0', + 'is_aqi_usa': True, + 'is_concentration_showed': True, + 'is_indoor': True, + 'is_lcd_on': True, + 'is_network_time': True, + 'is_temperature_celsius': False, + 'language': 'en-US', + 'lcd_brightness': 80, + 'node_name': 'Office', + 'power_saving': dict({ + '2slots': list([ + dict({ + 'hour_off': 9, + 'hour_on': 7, + }), + dict({ + 'hour_off': 22, + 'hour_on': 18, + }), + ]), + 'mode': 'yes', + 'running_time': 99, + 'yes': list([ + dict({ + 'hour': 8, + 'minute': 0, + }), + dict({ + 'hour': 21, + 'minute': 0, + }), + ]), + }), + 'sensor_mode': dict({ + 'custom_mode_interval': 3, + 'mode': 1, + }), + 'speed_unit': 'mph', + 'timezone': 'America/New York', + 'tvoc_unit': 'ppb', + }), + 'status': dict({ + 'app_version': '1.1826', + 'battery': 100, + 'datetime': 1665072044, + 'device_name': 'AIRVISUAL-XXXXXXX', + 'ip_address': '192.168.1.101', + 'mac_address': '**REDACTED**', + 'model': 20, + 'sensor_life': dict({ + 'pm2_5': 1567924345130, + }), + 'sensor_pm25_serial': '00000005050224011145', + 'sync_time': 250000, + 'system_version': 'KBG63F84', + 'used_memory': 3, + 'wifi_strength': 4, + }), + }), + 'entry': dict({ + 'data': dict({ + 'ip_address': '192.168.1.101', + 'password': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'airvisual_pro', + 'entry_id': '6a2b3770e53c28dc1eeb2515e906b0ce', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': 'XXXXXXX', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/airvisual_pro/test_diagnostics.py b/tests/components/airvisual_pro/test_diagnostics.py index 5141782e574..7c69a7e636f 100644 --- a/tests/components/airvisual_pro/test_diagnostics.py +++ b/tests/components/airvisual_pro/test_diagnostics.py @@ -1,5 +1,6 @@ """Test AirVisual Pro diagnostics.""" -from homeassistant.components.diagnostics import REDACTED +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -11,83 +12,10 @@ async def test_entry_diagnostics( config_entry, hass_client: ClientSessionGenerator, setup_airvisual_pro, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "entry_id": config_entry.entry_id, - "version": 1, - "domain": "airvisual_pro", - "title": "Mock Title", - "data": {"ip_address": "192.168.1.101", "password": REDACTED}, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": "XXXXXXX", - "disabled_by": None, - }, - "data": { - "date_and_time": { - "date": "2022/10/06", - "time": "16:00:44", - "timestamp": "1665072044", - }, - "history": {}, - "measurements": { - "co2": "472", - "humidity": "57", - "pm0_1": "0", - "pm1_0": "0", - "aqi_cn": "0", - "aqi_us": "0", - "pm2_5": "0", - "temperature_C": "23.0", - "temperature_F": "73.4", - "voc": "-1", - }, - "serial_number": REDACTED, - "settings": { - "follow_mode": "station", - "followed_station": "0", - "is_aqi_usa": True, - "is_concentration_showed": True, - "is_indoor": True, - "is_lcd_on": True, - "is_network_time": True, - "is_temperature_celsius": False, - "language": "en-US", - "lcd_brightness": 80, - "node_name": "Office", - "power_saving": { - "2slots": [ - {"hour_off": 9, "hour_on": 7}, - {"hour_off": 22, "hour_on": 18}, - ], - "mode": "yes", - "running_time": 99, - "yes": [{"hour": 8, "minute": 0}, {"hour": 21, "minute": 0}], - }, - "sensor_mode": {"custom_mode_interval": 3, "mode": 1}, - "speed_unit": "mph", - "timezone": "America/New York", - "tvoc_unit": "ppb", - }, - "status": { - "app_version": "1.1826", - "battery": 100, - "datetime": 1665072044, - "device_name": "AIRVISUAL-XXXXXXX", - "ip_address": "192.168.1.101", - "mac_address": REDACTED, - "model": 20, - "sensor_life": {"pm2_5": 1567924345130}, - "sensor_pm25_serial": "00000005050224011145", - "sync_time": 250000, - "system_version": "KBG63F84", - "used_memory": 3, - "wifi_strength": 4, - }, - "last_measurement_timestamp": 1665072044, - }, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From 2a78d7fa2d4bfc390d6f528c40bb723ba5f6945e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 22 Aug 2023 09:33:32 +0200 Subject: [PATCH 0718/1151] Add Reolink zoom in/out buttons (#97638) --- homeassistant/components/reolink/button.py | 25 +++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/button.py b/homeassistant/components/reolink/button.py index 7a6e2486c71..93ea5810bb6 100644 --- a/homeassistant/components/reolink/button.py +++ b/homeassistant/components/reolink/button.py @@ -36,6 +36,7 @@ class ReolinkButtonEntityDescription( """A class that describes button entities for a camera channel.""" supported: Callable[[Host, int], bool] = lambda api, ch: True + enabled_default: Callable[[Host, int], bool] | None = None @dataclass @@ -59,7 +60,9 @@ BUTTON_ENTITIES = ( key="ptz_stop", name="PTZ stop", icon="mdi:pan", - supported=lambda api, ch: api.supported(ch, "pan_tilt"), + enabled_default=lambda api, ch: api.supported(ch, "pan_tilt"), + supported=lambda api, ch: api.supported(ch, "pan_tilt") + or api.supported(ch, "zoom_basic"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.stop.value), ), ReolinkButtonEntityDescription( @@ -90,6 +93,22 @@ BUTTON_ENTITIES = ( supported=lambda api, ch: api.supported(ch, "tilt"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.down.value), ), + ReolinkButtonEntityDescription( + key="ptz_zoom_in", + name="PTZ zoom in", + icon="mdi:magnify", + entity_registry_enabled_default=False, + supported=lambda api, ch: api.supported(ch, "zoom_basic"), + method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.zoomin.value), + ), + ReolinkButtonEntityDescription( + key="ptz_zoom_out", + name="PTZ zoom out", + icon="mdi:magnify", + entity_registry_enabled_default=False, + supported=lambda api, ch: api.supported(ch, "zoom_basic"), + method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.zoomout.value), + ), ReolinkButtonEntityDescription( key="ptz_calibrate", name="PTZ calibrate", @@ -169,6 +188,10 @@ class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity): self._attr_unique_id = ( f"{self._host.unique_id}_{channel}_{entity_description.key}" ) + if entity_description.enabled_default is not None: + self._attr_entity_registry_enabled_default = ( + entity_description.enabled_default(self._host.api, self._channel) + ) async def async_press(self) -> None: """Execute the button action.""" From 3e56d27bf7df758ba9e141957a95cc6334e64aef Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 09:37:37 +0200 Subject: [PATCH 0719/1151] Add device info to FOSCAM (#98167) --- homeassistant/components/foscam/camera.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index ae28fd8d111..384aea4c5fa 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -11,9 +11,17 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import CONF_RTSP_PORT, CONF_STREAM, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET +from .const import ( + CONF_RTSP_PORT, + CONF_STREAM, + DOMAIN, + LOGGER, + SERVICE_PTZ, + SERVICE_PTZ_PRESET, +) DIR_UP = "up" DIR_DOWN = "down" @@ -94,12 +102,14 @@ async def async_setup_entry( class HassFoscamCamera(Camera): """An implementation of a Foscam IP camera.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, camera: FoscamCamera, config_entry: ConfigEntry) -> None: """Initialize a Foscam camera.""" super().__init__() self._foscam_session = camera - self._attr_name = config_entry.title self._username = config_entry.data[CONF_USERNAME] self._password = config_entry.data[CONF_PASSWORD] self._stream = config_entry.data[CONF_STREAM] @@ -107,6 +117,10 @@ class HassFoscamCamera(Camera): self._rtsp_port = config_entry.data[CONF_RTSP_PORT] if self._rtsp_port: self._attr_supported_features = CameraEntityFeature.STREAM + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, config_entry.entry_id)}, + manufacturer="Foscam", + ) async def async_added_to_hass(self) -> None: """Handle entity addition to hass.""" From 79811984f0e0c3fd714b89d127abdd0ed9326c0c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Aug 2023 09:43:33 +0200 Subject: [PATCH 0720/1151] Modernize open_meteo weather (#98504) --- homeassistant/components/open_meteo/weather.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index b1785ed0ef5..b874e066031 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -3,16 +3,17 @@ from __future__ import annotations from open_meteo import Forecast as OpenMeteoForecast -from homeassistant.components.weather import Forecast, WeatherEntity +from homeassistant.components.weather import ( + CoordinatorWeatherEntity, + Forecast, + WeatherEntityFeature, +) 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, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, WMO_TO_HA_CONDITION_MAP @@ -28,7 +29,7 @@ async def async_setup_entry( class OpenMeteoWeatherEntity( - CoordinatorEntity[DataUpdateCoordinator[OpenMeteoForecast]], WeatherEntity + CoordinatorWeatherEntity[DataUpdateCoordinator[OpenMeteoForecast]] ): """Defines an Open-Meteo weather entity.""" @@ -37,6 +38,7 @@ class OpenMeteoWeatherEntity( _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS _attr_native_temperature_unit = UnitOfTemperature.CELSIUS _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR + _attr_supported_features = WeatherEntityFeature.FORECAST_DAILY def __init__( self, @@ -121,3 +123,7 @@ class OpenMeteoWeatherEntity( forecasts.append(forecast) return forecasts + + async def async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + return self.forecast From 68e2809c36fb3423373f387634ba7c211b1a42a6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Aug 2023 10:01:17 +0200 Subject: [PATCH 0721/1151] Modernize nws weather (#98748) --- homeassistant/components/nws/__init__.py | 18 +- homeassistant/components/nws/const.py | 3 +- homeassistant/components/nws/weather.py | 183 +++++++++++--- homeassistant/components/weather/__init__.py | 21 ++ .../nws/snapshots/test_weather.ambr | 229 ++++++++++++++++++ tests/components/nws/test_weather.py | 212 +++++++++++++++- 6 files changed, 619 insertions(+), 47 deletions(-) create mode 100644 tests/components/nws/snapshots/test_weather.ambr diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index f0f2a12cfec..a6af045776f 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -90,7 +90,6 @@ class NwsDataUpdateCoordinator(DataUpdateCoordinator[None]): # the base class allows None, but this one doesn't assert self.update_interval is not None update_interval = self.update_interval - self.last_update_success_time = utcnow() else: update_interval = self.failed_update_interval self._unsub_refresh = async_track_point_in_utc_time( @@ -99,6 +98,23 @@ class NwsDataUpdateCoordinator(DataUpdateCoordinator[None]): utcnow().replace(microsecond=0) + update_interval, ) + async def _async_refresh( + self, + log_failures: bool = True, + raise_on_auth_failed: bool = False, + scheduled: bool = False, + raise_on_entry_error: bool = False, + ) -> None: + """Refresh data.""" + await super()._async_refresh( + log_failures, + raise_on_auth_failed, + scheduled, + raise_on_entry_error, + ) + if self.last_update_success: + self.last_update_success_time = utcnow() + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a National Weather Service entry.""" diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index 5db541106b9..1e028649d89 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Final from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, @@ -25,7 +26,7 @@ CONF_STATION = "station" ATTRIBUTION = "Data from National Weather Service/NOAA" -ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description" +ATTR_FORECAST_DETAILED_DESCRIPTION: Final = "detailed_description" CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_EXCEPTIONAL: [ diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 0e5fd412e0c..dec7e9bf3b3 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -1,8 +1,9 @@ """Support for NWS weather service.""" from __future__ import annotations +from collections.abc import Callable from types import MappingProxyType -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal, cast from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, @@ -16,8 +17,10 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + DOMAIN as WEATHER_DOMAIN, Forecast, WeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -29,13 +32,19 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er 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 -from homeassistant.util.unit_system import UnitSystem -from . import NWSData, base_unique_id, device_info +from . import ( + DEFAULT_SCAN_INTERVAL, + NWSData, + NwsDataUpdateCoordinator, + base_unique_id, + device_info, +) from .const import ( ATTR_FORECAST_DETAILED_DESCRIPTION, ATTRIBUTION, @@ -80,15 +89,20 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the NWS weather platform.""" + entity_registry = er.async_get(hass) nws_data: NWSData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - [ - NWSWeather(entry.data, nws_data, DAYNIGHT, hass.config.units), - NWSWeather(entry.data, nws_data, HOURLY, hass.config.units), - ], - False, - ) + entities = [NWSWeather(entry.data, nws_data, DAYNIGHT)] + + # Add hourly entity to legacy config entries + if entity_registry.async_get_entity_id( + WEATHER_DOMAIN, + DOMAIN, + _calculate_unique_id(entry.data, HOURLY), + ): + entities.append(NWSWeather(entry.data, nws_data, HOURLY)) + + async_add_entities(entities, False) if TYPE_CHECKING: @@ -99,34 +113,51 @@ if TYPE_CHECKING: detailed_description: str | None +def _calculate_unique_id(entry_data: MappingProxyType[str, Any], mode: str) -> str: + """Calculate unique ID.""" + latitude = entry_data[CONF_LATITUDE] + longitude = entry_data[CONF_LONGITUDE] + return f"{base_unique_id(latitude, longitude)}_{mode}" + + class NWSWeather(WeatherEntity): """Representation of a weather condition.""" _attr_attribution = ATTRIBUTION _attr_should_poll = False + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_HOURLY | WeatherEntityFeature.FORECAST_TWICE_DAILY + ) def __init__( self, entry_data: MappingProxyType[str, Any], nws_data: NWSData, mode: str, - units: UnitSystem, ) -> None: """Initialise the platform with a data instance and station name.""" self.nws = nws_data.api self.latitude = entry_data[CONF_LATITUDE] self.longitude = entry_data[CONF_LONGITUDE] + self.coordinator_forecast_hourly = nws_data.coordinator_forecast_hourly + self.coordinator_forecast_twice_daily = nws_data.coordinator_forecast self.coordinator_observation = nws_data.coordinator_observation if mode == DAYNIGHT: - self.coordinator_forecast = nws_data.coordinator_forecast + self.coordinator_forecast_legacy = nws_data.coordinator_forecast else: - self.coordinator_forecast = nws_data.coordinator_forecast_hourly + self.coordinator_forecast_legacy = nws_data.coordinator_forecast_hourly self.station = self.nws.station + self._unsub_hourly_forecast: Callable[[], None] | None = None + self._unsub_twice_daily_forecast: Callable[[], None] | None = None self.mode = mode - self.observation = None - self._forecast = None + self.observation: dict[str, Any] | None = None + self._forecast_hourly: list[dict[str, Any]] | None = None + self._forecast_legacy: list[dict[str, Any]] | None = None + self._forecast_twice_daily: list[dict[str, Any]] | None = None + + self._attr_unique_id = _calculate_unique_id(entry_data, mode) async def async_added_to_hass(self) -> None: """Set up a listener and load data.""" @@ -134,20 +165,72 @@ class NWSWeather(WeatherEntity): self.coordinator_observation.async_add_listener(self._update_callback) ) self.async_on_remove( - self.coordinator_forecast.async_add_listener(self._update_callback) + self.coordinator_forecast_legacy.async_add_listener(self._update_callback) ) + self.async_on_remove(self._remove_hourly_forecast_listener) + self.async_on_remove(self._remove_twice_daily_forecast_listener) self._update_callback() + def _remove_hourly_forecast_listener(self) -> None: + """Remove hourly forecast listener.""" + if self._unsub_hourly_forecast: + self._unsub_hourly_forecast() + self._unsub_hourly_forecast = None + + def _remove_twice_daily_forecast_listener(self) -> None: + """Remove hourly forecast listener.""" + if self._unsub_twice_daily_forecast: + self._unsub_twice_daily_forecast() + self._unsub_twice_daily_forecast = None + + @callback + def _async_subscription_started( + self, + forecast_type: Literal["daily", "hourly", "twice_daily"], + ) -> None: + """Start subscription to forecast_type.""" + if forecast_type == "hourly" and self.mode == DAYNIGHT: + self._unsub_hourly_forecast = ( + self.coordinator_forecast_hourly.async_add_listener( + self._update_callback + ) + ) + return + if forecast_type == "twice_daily" and self.mode == HOURLY: + self._unsub_twice_daily_forecast = ( + self.coordinator_forecast_twice_daily.async_add_listener( + self._update_callback + ) + ) + return + + @callback + def _async_subscription_ended( + self, + forecast_type: Literal["daily", "hourly", "twice_daily"], + ) -> None: + """End subscription to forecast_type.""" + if forecast_type == "hourly" and self.mode == DAYNIGHT: + self._remove_hourly_forecast_listener() + if forecast_type == "twice_daily" and self.mode == HOURLY: + self._remove_twice_daily_forecast_listener() + @callback def _update_callback(self) -> None: """Load data from integration.""" self.observation = self.nws.observation + self._forecast_hourly = self.nws.forecast_hourly + self._forecast_twice_daily = self.nws.forecast if self.mode == DAYNIGHT: - self._forecast = self.nws.forecast + self._forecast_legacy = self.nws.forecast else: - self._forecast = self.nws.forecast_hourly + self._forecast_legacy = self.nws.forecast_hourly self.async_write_ha_state() + assert self.platform.config_entry + self.platform.config_entry.async_create_task( + self.hass, self.async_update_listeners(("hourly", "twice_daily")) + ) @property def name(self) -> str: @@ -210,7 +293,7 @@ class NWSWeather(WeatherEntity): weather = None if self.observation: weather = self.observation.get("iconWeather") - time = self.observation.get("iconTime") + time = cast(str, self.observation.get("iconTime")) if weather: return convert_condition(time, weather) @@ -228,18 +311,19 @@ class NWSWeather(WeatherEntity): """Return visibility unit.""" return UnitOfLength.METERS - @property - def forecast(self) -> list[Forecast] | None: + def _forecast( + self, nws_forecast: list[dict[str, Any]] | None, mode: str + ) -> list[Forecast] | None: """Return forecast.""" - if self._forecast is None: + if nws_forecast is None: return None - forecast: list[NWSForecast] = [] - for forecast_entry in self._forecast: - data = { + forecast: list[Forecast] = [] + for forecast_entry in nws_forecast: + data: NWSForecast = { ATTR_FORECAST_DETAILED_DESCRIPTION: forecast_entry.get( "detailedForecast" ), - ATTR_FORECAST_TIME: forecast_entry.get("startTime"), + ATTR_FORECAST_TIME: cast(str, forecast_entry.get("startTime")), } if (temp := forecast_entry.get("temperature")) is not None: @@ -262,7 +346,7 @@ class NWSWeather(WeatherEntity): data[ATTR_FORECAST_HUMIDITY] = forecast_entry.get("relativeHumidity") - if self.mode == DAYNIGHT: + if mode == DAYNIGHT: data[ATTR_FORECAST_IS_DAYTIME] = forecast_entry.get("isDaytime") time = forecast_entry.get("iconTime") @@ -285,25 +369,56 @@ class NWSWeather(WeatherEntity): return forecast @property - def unique_id(self) -> str: - """Return a unique_id for this entity.""" - return f"{base_unique_id(self.latitude, self.longitude)}_{self.mode}" + def forecast(self) -> list[Forecast] | None: + """Return forecast.""" + return self._forecast(self._forecast_legacy, self.mode) + + async def _async_forecast( + self, + coordinator: NwsDataUpdateCoordinator, + nws_forecast: list[dict[str, Any]] | None, + mode: str, + ) -> list[Forecast] | None: + """Refresh stale forecast and return it in native units.""" + if ( + not (last_success_time := coordinator.last_update_success_time) + or utcnow() - last_success_time >= DEFAULT_SCAN_INTERVAL + ): + await coordinator.async_refresh() + if ( + not (last_success_time := coordinator.last_update_success_time) + or utcnow() - last_success_time >= FORECAST_VALID_TIME + ): + return None + return self._forecast(nws_forecast, mode) + + async def async_forecast_hourly(self) -> list[Forecast] | None: + """Return the hourly forecast in native units.""" + coordinator = self.coordinator_forecast_hourly + return await self._async_forecast(coordinator, self._forecast_hourly, HOURLY) + + async def async_forecast_twice_daily(self) -> list[Forecast] | None: + """Return the twice daily forecast in native units.""" + coordinator = self.coordinator_forecast_twice_daily + return await self._async_forecast( + coordinator, self._forecast_twice_daily, DAYNIGHT + ) @property def available(self) -> bool: """Return if state is available.""" last_success = ( self.coordinator_observation.last_update_success - and self.coordinator_forecast.last_update_success + and self.coordinator_forecast_legacy.last_update_success ) if ( self.coordinator_observation.last_update_success_time - and self.coordinator_forecast.last_update_success_time + and self.coordinator_forecast_legacy.last_update_success_time ): last_success_time = ( utcnow() - self.coordinator_observation.last_update_success_time < OBSERVATION_VALID_TIME - and utcnow() - self.coordinator_forecast.last_update_success_time + and utcnow() - self.coordinator_forecast_legacy.last_update_success_time < FORECAST_VALID_TIME ) else: @@ -316,7 +431,7 @@ class NWSWeather(WeatherEntity): Only used by the generic entity update service. """ await self.coordinator_observation.async_request_refresh() - await self.coordinator_forecast.async_request_refresh() + await self.coordinator_forecast_legacy.async_request_refresh() @property def entity_registry_enabled_default(self) -> bool: diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 8652f947f7c..eb137f06d7b 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1079,6 +1079,22 @@ class WeatherEntity(Entity, PostInit): ) and custom_unit_visibility in VALID_UNITS[ATTR_WEATHER_VISIBILITY_UNIT]: self._weather_option_visibility_unit = custom_unit_visibility + @callback + def _async_subscription_started( + self, + forecast_type: Literal["daily", "hourly", "twice_daily"], + ) -> None: + """Start subscription to forecast_type.""" + return None + + @callback + def _async_subscription_ended( + self, + forecast_type: Literal["daily", "hourly", "twice_daily"], + ) -> None: + """End subscription to forecast_type.""" + return None + @final @callback def async_subscribe_forecast( @@ -1090,11 +1106,16 @@ class WeatherEntity(Entity, PostInit): Called by websocket API. """ + subscription_started = not self._forecast_listeners[forecast_type] self._forecast_listeners[forecast_type].append(forecast_listener) + if subscription_started: + self._async_subscription_started(forecast_type) @callback def unsubscribe() -> None: self._forecast_listeners[forecast_type].remove(forecast_listener) + if not self._forecast_listeners[forecast_type]: + self._async_subscription_ended(forecast_type) return unsubscribe diff --git a/tests/components/nws/snapshots/test_weather.ambr b/tests/components/nws/snapshots/test_weather.ambr new file mode 100644 index 00000000000..0dddca954be --- /dev/null +++ b/tests/components/nws/snapshots/test_weather.ambr @@ -0,0 +1,229 @@ +# serializer version: 1 +# name: test_forecast_service + dict({ + 'forecast': list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'is_daytime': False, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]), + }) +# --- +# name: test_forecast_service.1 + dict({ + 'forecast': list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]), + }) +# --- +# name: test_forecast_service.2 + dict({ + 'forecast': list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'is_daytime': False, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]), + }) +# --- +# name: test_forecast_service.3 + dict({ + 'forecast': list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]), + }) +# --- +# name: test_forecast_service.4 + dict({ + 'forecast': list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]), + }) +# --- +# name: test_forecast_service.5 + dict({ + 'forecast': list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]), + }) +# --- +# name: test_forecast_subscription[hourly-weather.abc_daynight] + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]) +# --- +# name: test_forecast_subscription[hourly-weather.abc_daynight].1 + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]) +# --- +# name: test_forecast_subscription[hourly] + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]) +# --- +# name: test_forecast_subscription[hourly].1 + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]) +# --- +# name: test_forecast_subscription[twice_daily-weather.abc_hourly] + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'is_daytime': False, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]) +# --- +# name: test_forecast_subscription[twice_daily-weather.abc_hourly].1 + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'is_daytime': False, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]) +# --- +# name: test_forecast_subscription[twice_daily] + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'is_daytime': False, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]) +# --- +# name: test_forecast_subscription[twice_daily].1 + list([ + dict({ + 'condition': 'lightning-rainy', + 'datetime': '2019-08-12T20:00:00-04:00', + 'detailed_description': 'A detailed forecast.', + 'dew_point': -15.6, + 'humidity': 75, + 'is_daytime': False, + 'precipitation_probability': 89, + 'temperature': -12.2, + 'wind_bearing': 180, + 'wind_speed': 16.09, + }), + ]) +# --- diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 06d2c2006d8..54069eec02c 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -3,7 +3,9 @@ from datetime import timedelta from unittest.mock import patch import aiohttp +from freezegun.api import FrozenDateTimeFactory import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import nws from homeassistant.components.weather import ( @@ -11,6 +13,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_FORECAST, DOMAIN as WEATHER_DOMAIN, + SERVICE_GET_FORECAST, ) from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -31,6 +34,7 @@ from .const import ( ) from tests.common import MockConfigEntry, async_fire_time_changed +from tests.typing import WebSocketGenerator @pytest.mark.parametrize( @@ -354,10 +358,10 @@ async def test_error_forecast_hourly( assert state.state == ATTR_CONDITION_SUNNY -async def test_forecast_hourly_disable_enable( - hass: HomeAssistant, mock_simple_nws, no_sensor -) -> None: - """Test error during update forecast hourly.""" +async def test_new_config_entry(hass: HomeAssistant, no_sensor) -> None: + """Test the expected entities are created.""" + registry = er.async_get(hass) + entry = MockConfigEntry( domain=nws.DOMAIN, data=NWS_CONFIG, @@ -367,17 +371,203 @@ async def test_forecast_hourly_disable_enable( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + assert len(hass.states.async_entity_ids("weather")) == 1 + entry = hass.config_entries.async_entries()[0] + assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 1 + + +async def test_legacy_config_entry(hass: HomeAssistant, no_sensor) -> None: + """Test the expected entities are created.""" registry = er.async_get(hass) - entry = registry.async_get_or_create( + # Pre-create the hourly entity + registry.async_get_or_create( WEATHER_DOMAIN, nws.DOMAIN, "35_-75_hourly", ) - assert entry.disabled is True - # Test enabling entity - updated_entry = registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, ) - assert updated_entry != entry - assert updated_entry.disabled is False + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("weather")) == 2 + entry = hass.config_entries.async_entries()[0] + assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 2 + + +async def test_forecast_service( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, + mock_simple_nws, + no_sensor, +) -> None: + """Test multiple forecast.""" + instance = mock_simple_nws.return_value + + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + instance.update_observation.assert_called_once() + instance.update_forecast.assert_called_once() + instance.update_forecast_hourly.assert_called_once() + + for forecast_type in ("twice_daily", "hourly"): + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": "weather.abc_daynight", + "type": forecast_type, + }, + blocking=True, + return_response=True, + ) + assert response["forecast"] != [] + assert response == snapshot + + # Calling the services should use cached data + instance.update_observation.assert_called_once() + instance.update_forecast.assert_called_once() + instance.update_forecast_hourly.assert_called_once() + + # Trigger data refetch + freezer.tick(nws.DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert instance.update_observation.call_count == 2 + assert instance.update_forecast.call_count == 2 + assert instance.update_forecast_hourly.call_count == 1 + + for forecast_type in ("twice_daily", "hourly"): + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": "weather.abc_daynight", + "type": forecast_type, + }, + blocking=True, + return_response=True, + ) + assert response["forecast"] != [] + assert response == snapshot + + # Calling the services should update the hourly forecast + assert instance.update_observation.call_count == 2 + assert instance.update_forecast.call_count == 2 + assert instance.update_forecast_hourly.call_count == 2 + + # third update fails, but data is cached + instance.update_forecast_hourly.side_effect = aiohttp.ClientError + freezer.tick(nws.DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": "weather.abc_daynight", + "type": "hourly", + }, + blocking=True, + return_response=True, + ) + assert response["forecast"] != [] + assert response == snapshot + + # after additional 35 minutes data caching expires, data is no longer shown + freezer.tick(timedelta(minutes=35)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": "weather.abc_daynight", + "type": "hourly", + }, + blocking=True, + return_response=True, + ) + assert response["forecast"] == [] + + +@pytest.mark.parametrize( + ("forecast_type", "entity_id"), + [("hourly", "weather.abc_daynight"), ("twice_daily", "weather.abc_hourly")], +) +async def test_forecast_subscription( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, + mock_simple_nws, + no_sensor, + forecast_type: str, + entity_id: str, +) -> None: + """Test multiple forecast.""" + client = await hass_ws_client(hass) + + registry = er.async_get(hass) + # Pre-create the hourly entity + registry.async_get_or_create( + WEATHER_DOMAIN, + nws.DOMAIN, + "35_-75_hourly", + suggested_object_id="abc_hourly", + ) + + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + 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 != [] + assert forecast1 == snapshot + + freezer.tick(nws.DEFAULT_SCAN_INTERVAL + 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 != [] + assert forecast2 == snapshot From 00b75ce58d485efa50c1775032b3336aa8931dcb Mon Sep 17 00:00:00 2001 From: Florian Bachmann <834350+baflo@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:14:21 +0200 Subject: [PATCH 0722/1151] Allows the supervisor to send a session's user to addon with header X-Remote-User (#88472) * Working draft for x-remote-user * Adds comment * Submits user id instead of its name * Move lines out of try-catch block * Updates payload attribute * Removes unnecessary user data from user info API * revert changes --- homeassistant/components/hassio/const.py | 1 + homeassistant/components/hassio/websocket_api.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 0735f2645cc..5712f5d1bea 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -9,6 +9,7 @@ ATTR_ADMIN = "admin" ATTR_COMPRESSED = "compressed" ATTR_CONFIG = "config" ATTR_DATA = "data" +ATTR_SESSION_DATA_USER_ID = "user_id" ATTR_DISCOVERY = "discovery" ATTR_ENABLE = "enable" ATTR_ENDPOINT = "endpoint" diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index c8fefe65e1f..ac0395ebd9f 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -22,6 +22,7 @@ from .const import ( ATTR_ENDPOINT, ATTR_METHOD, ATTR_RESULT, + ATTR_SESSION_DATA_USER_ID, ATTR_TIMEOUT, ATTR_WS_EVENT, DOMAIN, @@ -115,12 +116,21 @@ async def websocket_supervisor_api( ): raise Unauthorized() supervisor: HassIO = hass.data[DOMAIN] + + command = msg[ATTR_ENDPOINT] + payload = msg.get(ATTR_DATA, {}) + + if command == "/ingress/session": + # Send user ID on session creation, so the supervisor can correlate session tokens with users + # for every request that is authenticated with the given ingress session token. + payload[ATTR_SESSION_DATA_USER_ID] = connection.user.id + try: result = await supervisor.send_command( - msg[ATTR_ENDPOINT], + command, method=msg[ATTR_METHOD], timeout=msg.get(ATTR_TIMEOUT, 10), - payload=msg.get(ATTR_DATA, {}), + payload=payload, source="core.websocket_api", ) From 52b1e34af07ed055953414e5c33f062da0bf6ef3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Aug 2023 10:27:34 +0200 Subject: [PATCH 0723/1151] Migrate openweathermap weather to CoordinatorEntity (#98799) --- .../components/openweathermap/weather.py | 42 +++++++------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 631a4cceb0b..c6f95555954 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -29,6 +29,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_API_CLOUDS, @@ -95,7 +96,7 @@ async def async_setup_entry( async_add_entities([owm_weather], False) -class OpenWeatherMapWeather(WeatherEntity): +class OpenWeatherMapWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an OpenWeatherMap sensor.""" _attr_attribution = ATTRIBUTION @@ -113,6 +114,7 @@ class OpenWeatherMapWeather(WeatherEntity): weather_coordinator: WeatherUpdateCoordinator, ) -> None: """Initialize the sensor.""" + super().__init__(weather_coordinator) self._attr_name = name self._attr_unique_id = unique_id self._attr_device_info = DeviceInfo( @@ -121,62 +123,61 @@ class OpenWeatherMapWeather(WeatherEntity): manufacturer=MANUFACTURER, name=DEFAULT_NAME, ) - self._weather_coordinator = weather_coordinator @property def condition(self) -> str | None: """Return the current condition.""" - return self._weather_coordinator.data[ATTR_API_CONDITION] + return self.coordinator.data[ATTR_API_CONDITION] @property def cloud_coverage(self) -> float | None: """Return the Cloud coverage in %.""" - return self._weather_coordinator.data[ATTR_API_CLOUDS] + return self.coordinator.data[ATTR_API_CLOUDS] @property def native_apparent_temperature(self) -> float | None: """Return the apparent temperature.""" - return self._weather_coordinator.data[ATTR_API_FEELS_LIKE_TEMPERATURE] + return self.coordinator.data[ATTR_API_FEELS_LIKE_TEMPERATURE] @property def native_temperature(self) -> float | None: """Return the temperature.""" - return self._weather_coordinator.data[ATTR_API_TEMPERATURE] + return self.coordinator.data[ATTR_API_TEMPERATURE] @property def native_pressure(self) -> float | None: """Return the pressure.""" - return self._weather_coordinator.data[ATTR_API_PRESSURE] + return self.coordinator.data[ATTR_API_PRESSURE] @property def humidity(self) -> float | None: """Return the humidity.""" - return self._weather_coordinator.data[ATTR_API_HUMIDITY] + return self.coordinator.data[ATTR_API_HUMIDITY] @property def native_dew_point(self) -> float | None: """Return the dew point.""" - return self._weather_coordinator.data[ATTR_API_DEW_POINT] + return self.coordinator.data[ATTR_API_DEW_POINT] @property def native_wind_gust_speed(self) -> float | None: """Return the wind gust speed.""" - return self._weather_coordinator.data[ATTR_API_WIND_GUST] + return self.coordinator.data[ATTR_API_WIND_GUST] @property def native_wind_speed(self) -> float | None: """Return the wind speed.""" - return self._weather_coordinator.data[ATTR_API_WIND_SPEED] + return self.coordinator.data[ATTR_API_WIND_SPEED] @property def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" - return self._weather_coordinator.data[ATTR_API_WIND_BEARING] + return self.coordinator.data[ATTR_API_WIND_BEARING] @property def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" - api_forecasts = self._weather_coordinator.data[ATTR_API_FORECAST] + api_forecasts = self.coordinator.data[ATTR_API_FORECAST] forecasts = [ { ha_key: forecast[api_key] @@ -186,18 +187,3 @@ class OpenWeatherMapWeather(WeatherEntity): for forecast in api_forecasts ] return cast(list[Forecast], forecasts) - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._weather_coordinator.last_update_success - - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self._weather_coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self) -> None: - """Get the latest data from OWM and updates the states.""" - await self._weather_coordinator.async_request_refresh() From b885dfa5a803f869f6787ca1fd261b860a18d484 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Aug 2023 10:29:16 +0200 Subject: [PATCH 0724/1151] Add preview to sensor group config and option flows (#83638) --- homeassistant/components/group/config_flow.py | 87 ++++++++- homeassistant/components/group/sensor.py | 24 ++- homeassistant/config_entries.py | 43 +++-- homeassistant/data_entry_flow.py | 19 ++ homeassistant/helpers/entity.py | 52 +++--- .../helpers/schema_config_entry_flow.py | 18 +- tests/components/cloud/test_repairs.py | 2 + .../components/config/test_config_entries.py | 6 + .../snapshots/test_config_flow.ambr | 5 + tests/components/group/test_config_flow.py | 172 ++++++++++++++++++ tests/components/hassio/test_repairs.py | 4 + tests/components/kitchen_sink/test_init.py | 1 + .../components/repairs/test_websocket_api.py | 1 + tests/components/subaru/test_config_flow.py | 1 + tests/test_config_entries.py | 93 ++++++++++ 15 files changed, 483 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 6cdc47f9e85..d8c983f83db 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -7,8 +7,10 @@ from typing import Any, cast import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.const import CONF_ENTITIES, CONF_TYPE from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.schema_config_entry_flow import ( SchemaCommonFlowHandler, @@ -22,6 +24,7 @@ from homeassistant.helpers.schema_config_entry_flow import ( from . import DOMAIN from .binary_sensor import CONF_ALL from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC +from .sensor import SensorGroup _STATISTIC_MEASURES = [ "min", @@ -36,15 +39,22 @@ _STATISTIC_MEASURES = [ async def basic_group_options_schema( - domain: str | list[str], handler: SchemaCommonFlowHandler + domain: str | list[str], handler: SchemaCommonFlowHandler | None ) -> vol.Schema: """Generate options schema.""" + if handler is None: + entity_selector = selector.selector( + {"entity": {"domain": domain, "multiple": True}} + ) + else: + entity_selector = entity_selector_without_own_entities( + cast(SchemaOptionsFlowHandler, handler.parent_handler), + selector.EntitySelectorConfig(domain=domain, multiple=True), + ) + return vol.Schema( { - vol.Required(CONF_ENTITIES): entity_selector_without_own_entities( - cast(SchemaOptionsFlowHandler, handler.parent_handler), - selector.EntitySelectorConfig(domain=domain, multiple=True), - ), + vol.Required(CONF_ENTITIES): entity_selector, vol.Required(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(), } ) @@ -96,7 +106,7 @@ SENSOR_OPTIONS = { async def sensor_options_schema( - domain: str, handler: SchemaCommonFlowHandler + domain: str, handler: SchemaCommonFlowHandler | None ) -> vol.Schema: """Generate options schema.""" return ( @@ -184,6 +194,7 @@ CONFIG_FLOW = { "sensor": SchemaFlowFormStep( SENSOR_CONFIG_SCHEMA, validate_user_input=set_group_type("sensor"), + preview="group_sensor", ), "switch": SchemaFlowFormStep( basic_group_config_schema("switch"), @@ -202,7 +213,10 @@ OPTIONS_FLOW = { "media_player": SchemaFlowFormStep( partial(basic_group_options_schema, "media_player") ), - "sensor": SchemaFlowFormStep(partial(sensor_options_schema, "sensor")), + "sensor": SchemaFlowFormStep( + partial(sensor_options_schema, "sensor"), + preview="group_sensor", + ), "switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")), } @@ -241,6 +255,12 @@ class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): ) _async_hide_members(hass, options[CONF_ENTITIES], hidden_by) + @callback + @staticmethod + def async_setup_preview(hass: HomeAssistant) -> None: + """Set up preview WS API.""" + websocket_api.async_register_command(hass, ws_preview_sensor) + def _async_hide_members( hass: HomeAssistant, members: list[str], hidden_by: er.RegistryEntryHider | None @@ -253,3 +273,56 @@ def _async_hide_members( if entity_id not in registry.entities: continue registry.async_update_entity(entity_id, hidden_by=hidden_by) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "group/sensor/start_preview", + vol.Required("flow_id"): str, + vol.Required("flow_type"): vol.Any("config_flow", "options_flow"), + vol.Required("user_input"): dict, + } +) +@websocket_api.async_response +async def ws_preview_sensor( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Generate a preview.""" + if msg["flow_type"] == "config_flow": + validated = SENSOR_CONFIG_SCHEMA(msg["user_input"]) + ignore_non_numeric = False + name = validated["name"] + else: + validated = (await sensor_options_schema("sensor", None))(msg["user_input"]) + flow_status = hass.config_entries.options.async_get(msg["flow_id"]) + config_entry = hass.config_entries.async_get_entry(flow_status["handler"]) + if not config_entry: + raise HomeAssistantError + ignore_non_numeric = validated[CONF_IGNORE_NON_NUMERIC] + name = config_entry.options["name"] + + @callback + def async_preview_updated(state: str, attributes: Mapping[str, Any]) -> None: + """Forward config entry state events to websocket.""" + connection.send_message( + websocket_api.event_message( + msg["id"], {"state": state, "attributes": attributes} + ) + ) + + sensor = SensorGroup( + None, + name, + validated[CONF_ENTITIES], + ignore_non_numeric, + validated[CONF_TYPE], + None, + None, + None, + ) + sensor.hass = hass + + connection.send_result(msg["id"]) + connection.subscriptions[msg["id"]] = sensor.async_start_preview( + async_preview_updated + ) diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index d62447d9947..48175b55358 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -1,7 +1,7 @@ """Platform allowing several sensors to be grouped into one sensor to provide numeric combinations.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from datetime import datetime import logging import statistics @@ -33,7 +33,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( @@ -303,6 +303,26 @@ class SensorGroup(GroupEntity, SensorEntity): self._state_incorrect: set[str] = set() self._extra_state_attribute: dict[str, Any] = {} + @callback + def async_start_preview( + self, + preview_callback: Callable[[str, Mapping[str, Any]], None], + ) -> CALLBACK_TYPE: + """Render a preview.""" + + @callback + def async_state_changed_listener( + event: EventType[EventStateChangedData] | None, + ) -> None: + """Handle child updates.""" + self.async_update_group_state() + preview_callback(*self._async_generate_attributes()) + + async_state_changed_listener(None) + return async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) + async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1e3af81395a..d3ff741e3e6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1814,6 +1814,14 @@ class ConfigFlow(data_entry_flow.FlowHandler): class OptionsFlowManager(data_entry_flow.FlowManager): """Flow to set options for a configuration entry.""" + def _async_get_config_entry(self, config_entry_id: str) -> ConfigEntry: + """Return config entry or raise if not found.""" + entry = self.hass.config_entries.async_get_entry(config_entry_id) + if entry is None: + raise UnknownEntry(config_entry_id) + + return entry + async def async_create_flow( self, handler_key: str, @@ -1825,10 +1833,7 @@ class OptionsFlowManager(data_entry_flow.FlowManager): Entry_id and flow.handler is the same thing to map entry with flow. """ - entry = self.hass.config_entries.async_get_entry(handler_key) - if entry is None: - raise UnknownEntry(handler_key) - + entry = self._async_get_config_entry(handler_key) handler = await _async_get_flow_handler(self.hass, entry.domain, {}) return handler.async_get_options_flow(entry) @@ -1853,6 +1858,14 @@ class OptionsFlowManager(data_entry_flow.FlowManager): result["result"] = True return result + async def _async_setup_preview(self, flow: data_entry_flow.FlowHandler) -> None: + """Set up preview for an option flow handler.""" + entry = self._async_get_config_entry(flow.handler) + await _load_integration(self.hass, entry.domain, {}) + if entry.domain not in self._preview: + self._preview.add(entry.domain) + flow.async_setup_preview(self.hass) + class OptionsFlow(data_entry_flow.FlowHandler): """Base class for config options flows.""" @@ -2016,15 +2029,9 @@ async def support_remove_from_device(hass: HomeAssistant, domain: str) -> bool: return hasattr(component, "async_remove_config_entry_device") -async def _async_get_flow_handler( +async def _load_integration( hass: HomeAssistant, domain: str, hass_config: ConfigType -) -> type[ConfigFlow]: - """Get a flow handler for specified domain.""" - - # First check if there is a handler registered for the domain - if domain in hass.config.components and (handler := HANDLERS.get(domain)): - return handler - +) -> None: try: integration = await loader.async_get_integration(hass, domain) except loader.IntegrationNotFound as err: @@ -2044,6 +2051,18 @@ async def _async_get_flow_handler( ) raise data_entry_flow.UnknownHandler + +async def _async_get_flow_handler( + hass: HomeAssistant, domain: str, hass_config: ConfigType +) -> type[ConfigFlow]: + """Get a flow handler for specified domain.""" + + # First check if there is a handler registered for the domain + if domain in hass.config.components and (handler := HANDLERS.get(domain)): + return handler + + await _load_integration(hass, domain, hass_config) + if handler := HANDLERS.get(domain): return handler diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index e0408a24b2e..04876590d2b 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -95,6 +95,7 @@ class FlowResult(TypedDict, total=False): last_step: bool | None menu_options: list[str] | dict[str, str] options: Mapping[str, Any] + preview: str | None progress_action: str reason: str required: bool @@ -135,6 +136,7 @@ class FlowManager(abc.ABC): ) -> None: """Initialize the flow manager.""" self.hass = hass + self._preview: set[str] = set() self._progress: dict[str, FlowHandler] = {} self._handler_progress_index: dict[str, set[str]] = {} self._init_data_process_index: dict[type, set[str]] = {} @@ -395,6 +397,10 @@ class FlowManager(abc.ABC): flow.flow_id, flow.handler, err.reason, err.description_placeholders ) + # Setup the flow handler's preview if needed + if result.get("preview") is not None: + await self._async_setup_preview(flow) + if not isinstance(result["type"], FlowResultType): result["type"] = FlowResultType(result["type"]) # type: ignore[unreachable] report( @@ -429,6 +435,12 @@ class FlowManager(abc.ABC): return result + async def _async_setup_preview(self, flow: FlowHandler) -> None: + """Set up preview for a flow handler.""" + if flow.handler not in self._preview: + self._preview.add(flow.handler) + flow.async_setup_preview(self.hass) + class FlowHandler: """Handle a data entry flow.""" @@ -504,6 +516,7 @@ class FlowHandler: errors: dict[str, str] | None = None, description_placeholders: Mapping[str, str | None] | None = None, last_step: bool | None = None, + preview: str | None = None, ) -> FlowResult: """Return the definition of a form to gather user input.""" return FlowResult( @@ -515,6 +528,7 @@ class FlowHandler: errors=errors, description_placeholders=description_placeholders, last_step=last_step, # Display next or submit button in frontend + preview=preview, # Display preview component in frontend ) @callback @@ -635,6 +649,11 @@ class FlowHandler: def async_remove(self) -> None: """Notification that the flow has been removed.""" + @callback + @staticmethod + def async_setup_preview(hass: HomeAssistant) -> None: + """Set up preview.""" + @callback def _create_abort_data( diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 9338346fc8b..29a944874ab 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -756,31 +756,10 @@ class Entity(ABC): return f"{device_name} {name}" if device_name else name @callback - def _async_write_ha_state(self) -> None: - """Write the state to the state machine.""" - if self._platform_state == EntityPlatformState.REMOVED: - # Polling returned after the entity has already been removed - return - - hass = self.hass - entity_id = self.entity_id + def _async_generate_attributes(self) -> tuple[str, dict[str, Any]]: + """Calculate state string and attribute mapping.""" entry = self.registry_entry - if entry and entry.disabled_by: - if not self._disabled_reported: - self._disabled_reported = True - _LOGGER.warning( - ( - "Entity %s is incorrectly being triggered for updates while it" - " is disabled. This is a bug in the %s integration" - ), - entity_id, - self.platform.platform_name, - ) - return - - start = timer() - attr = self.capability_attributes attr = dict(attr) if attr else {} @@ -818,6 +797,33 @@ class Entity(ABC): if (supported_features := self.supported_features) is not None: attr[ATTR_SUPPORTED_FEATURES] = supported_features + return (state, attr) + + @callback + def _async_write_ha_state(self) -> None: + """Write the state to the state machine.""" + if self._platform_state == EntityPlatformState.REMOVED: + # Polling returned after the entity has already been removed + return + + hass = self.hass + entity_id = self.entity_id + + if (entry := self.registry_entry) and entry.disabled_by: + if not self._disabled_reported: + self._disabled_reported = True + _LOGGER.warning( + ( + "Entity %s is incorrectly being triggered for updates while it" + " is disabled. This is a bug in the %s integration" + ), + entity_id, + self.platform.platform_name, + ) + return + + start = timer() + state, attr = self._async_generate_attributes() end = timer() if end - start > 0.4 and not self._slow_reported: diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 653594f2808..18d59f4f90d 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -78,6 +78,9 @@ class SchemaFlowFormStep(SchemaFlowStep): have priority over the suggested values. """ + preview: str | None = None + """Optional preview component.""" + @dataclass(slots=True) class SchemaFlowMenuStep(SchemaFlowStep): @@ -237,6 +240,7 @@ class SchemaCommonFlowHandler: data_schema=data_schema, errors=errors, last_step=last_step, + preview=form_step.preview, ) async def _async_menu_step( @@ -271,7 +275,10 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): raise UnknownHandler return SchemaOptionsFlowHandler( - config_entry, cls.options_flow, cls.async_options_flow_finished + config_entry, + cls.options_flow, + cls.async_options_flow_finished, + cls.async_setup_preview, ) # Create an async_get_options_flow method @@ -285,6 +292,11 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): """Initialize config flow.""" self._common_handler = SchemaCommonFlowHandler(self, self.config_flow, None) + @callback + @staticmethod + def async_setup_preview(hass: HomeAssistant) -> None: + """Set up preview.""" + @classmethod @callback def async_supports_options_flow( @@ -357,6 +369,7 @@ class SchemaOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): options_flow: Mapping[str, SchemaFlowStep], async_options_flow_finished: Callable[[HomeAssistant, Mapping[str, Any]], None] | None = None, + async_setup_preview: Callable[[HomeAssistant], None] | None = None, ) -> None: """Initialize options flow. @@ -378,6 +391,9 @@ class SchemaOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): types.MethodType(self._async_step(step), self), ) + if async_setup_preview: + setattr(self, "async_setup_preview", async_setup_preview) + @staticmethod def _async_step(step_id: str) -> Callable: """Generate a step handler.""" diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py index d010cac77ad..f83de408bcc 100644 --- a/tests/components/cloud/test_repairs.py +++ b/tests/components/cloud/test_repairs.py @@ -123,6 +123,7 @@ async def test_legacy_subscription_repair_flow( "errors": None, "description_placeholders": None, "last_step": None, + "preview": None, } resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") @@ -205,6 +206,7 @@ async def test_legacy_subscription_repair_flow_timeout( "errors": None, "description_placeholders": None, "last_step": None, + "preview": None, } with patch("homeassistant.components.cloud.repairs.MAX_RETRIES", new=0): diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 4684b4148b1..4239e031893 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -396,6 +396,7 @@ async def test_initialize_flow(hass: HomeAssistant, client) -> None: }, "errors": {"username": "Should be unique."}, "last_step": None, + "preview": None, } @@ -571,6 +572,7 @@ async def test_two_step_flow( "description_placeholders": None, "errors": None, "last_step": None, + "preview": None, } with patch.dict(HANDLERS, {"test": TestFlow}): @@ -647,6 +649,7 @@ async def test_continue_flow_unauth( "description_placeholders": None, "errors": None, "last_step": None, + "preview": None, } hass_admin_user.groups = [] @@ -822,6 +825,7 @@ async def test_options_flow(hass: HomeAssistant, client) -> None: "description_placeholders": {"enabled": "Set to true to be true"}, "errors": None, "last_step": None, + "preview": None, } @@ -917,6 +921,7 @@ async def test_two_step_options_flow(hass: HomeAssistant, client) -> None: "description_placeholders": None, "errors": None, "last_step": None, + "preview": None, } with patch.dict(HANDLERS, {"test": TestFlow}): @@ -998,6 +1003,7 @@ async def test_options_flow_with_invalid_data(hass: HomeAssistant, client) -> No "description_placeholders": None, "errors": None, "last_step": None, + "preview": None, } with patch.dict(HANDLERS, {"test": TestFlow}): diff --git a/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr b/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr index 24cef3c349e..31925e2d626 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_config_flow.ambr @@ -9,6 +9,7 @@ 'flow_id': , 'handler': 'gardena_bluetooth', 'last_step': None, + 'preview': None, 'step_id': 'confirm', 'type': , }) @@ -136,6 +137,7 @@ 'flow_id': , 'handler': 'gardena_bluetooth', 'last_step': None, + 'preview': None, 'step_id': 'user', 'type': , }) @@ -150,6 +152,7 @@ 'flow_id': , 'handler': 'gardena_bluetooth', 'last_step': None, + 'preview': None, 'step_id': 'confirm', 'type': , }) @@ -198,6 +201,7 @@ 'flow_id': , 'handler': 'gardena_bluetooth', 'last_step': None, + 'preview': None, 'step_id': 'user', 'type': , }) @@ -212,6 +216,7 @@ 'flow_id': , 'handler': 'gardena_bluetooth', 'last_step': None, + 'preview': None, 'step_id': 'confirm', 'type': , }) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 8202601fc18..a2c5ad64b1d 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator @pytest.mark.parametrize( @@ -446,3 +447,174 @@ async def test_options_flow_hides_members( assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by + + +async def test_config_flow_sensor_preview( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test the config flow preview.""" + client = await hass_ws_client(hass) + + input_sensors = ["sensor.input_one", "sensor.input_two"] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "sensor"}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "sensor" + assert result["errors"] is None + assert result["preview"] == "group_sensor" + + await client.send_json_auto_id( + { + "type": "group/sensor/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": { + "name": "My sensor group", + "entities": input_sensors, + "type": "max", + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + msg = await client.receive_json() + assert msg["event"] == { + "attributes": { + "friendly_name": "My sensor group", + "icon": "mdi:calculator", + }, + "state": "unavailable", + } + + hass.states.async_set("sensor.input_one", "10") + hass.states.async_set("sensor.input_two", "20") + + msg = await client.receive_json() + assert msg["event"] == { + "attributes": { + "entity_id": ["sensor.input_one", "sensor.input_two"], + "friendly_name": "My sensor group", + "icon": "mdi:calculator", + "max_entity_id": "sensor.input_two", + }, + "state": "20.0", + } + + +async def test_option_flow_sensor_preview( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test the option flow preview.""" + client = await hass_ws_client(hass) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entities": ["sensor.input_one", "sensor.input_two"], + "group_type": "sensor", + "hide_members": False, + "name": "My sensor group", + "type": "min", + }, + title="My min_max", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + input_sensors = ["sensor.input_one", "sensor.input_two"] + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + assert result["preview"] == "group_sensor" + + hass.states.async_set("sensor.input_one", "10") + hass.states.async_set("sensor.input_two", "20") + + await client.send_json_auto_id( + { + "type": "group/sensor/start_preview", + "flow_id": result["flow_id"], + "flow_type": "options_flow", + "user_input": { + "entities": input_sensors, + "type": "min", + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + msg = await client.receive_json() + assert msg["event"] == { + "attributes": { + "entity_id": ["sensor.input_one", "sensor.input_two"], + "friendly_name": "My sensor group", + "icon": "mdi:calculator", + "min_entity_id": "sensor.input_one", + }, + "state": "10.0", + } + + +async def test_option_flow_sensor_preview_config_entry_removed( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test the option flow preview where the config entry is removed.""" + client = await hass_ws_client(hass) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entities": ["sensor.input_one", "sensor.input_two"], + "group_type": "sensor", + "hide_members": False, + "name": "My sensor group", + "type": "min", + }, + title="My min_max", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + input_sensors = ["sensor.input_one", "sensor.input_two"] + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + assert result["preview"] == "group_sensor" + + await hass.config_entries.async_remove(config_entry.entry_id) + + await client.send_json_auto_id( + { + "type": "group/sensor/start_preview", + "flow_id": result["flow_id"], + "flow_type": "options_flow", + "user_input": { + "entities": input_sensors, + "type": "min", + }, + } + ) + msg = await client.receive_json() + assert not msg["success"] + assert msg["error"] == {"code": "unknown_error", "message": "Unknown error"} diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 237c20a5272..21bf7e5b47a 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -84,6 +84,7 @@ async def test_supervisor_issue_repair_flow( "errors": None, "description_placeholders": {"reference": "/dev/sda1"}, "last_step": True, + "preview": None, } resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") @@ -292,6 +293,7 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions_and_confir "errors": None, "description_placeholders": None, "last_step": True, + "preview": None, } resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") @@ -371,6 +373,7 @@ async def test_supervisor_issue_repair_flow_skip_confirmation( "errors": None, "description_placeholders": None, "last_step": True, + "preview": None, } resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") @@ -580,6 +583,7 @@ async def test_supervisor_issue_docker_config_repair_flow( "errors": None, "description_placeholders": {"components": "Home Assistant\n- test"}, "last_step": True, + "preview": None, } resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") diff --git a/tests/components/kitchen_sink/test_init.py b/tests/components/kitchen_sink/test_init.py index 88f2de5b394..ebd0f781d22 100644 --- a/tests/components/kitchen_sink/test_init.py +++ b/tests/components/kitchen_sink/test_init.py @@ -212,6 +212,7 @@ async def test_issues_created( "flow_id": ANY, "handler": DOMAIN, "last_step": None, + "preview": None, "step_id": "confirm", "type": "form", } diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index c82337b484f..6c9b51a7cf6 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -313,6 +313,7 @@ async def test_fix_issue( "flow_id": ANY, "handler": domain, "last_step": None, + "preview": None, "step_id": step, "type": "form", } diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index fc959fc434d..c3df10ed618 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -264,6 +264,7 @@ async def test_pin_form_init(pin_form) -> None: "step_id": "pin", "type": "form", "last_step": None, + "preview": None, } assert pin_form == expected diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 3485162cbb3..f04f033b49f 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1178,6 +1178,27 @@ async def test_entry_options_abort( ) +async def test_entry_options_unknown_config_entry( + hass: HomeAssistant, manager: config_entries.ConfigEntries +) -> None: + """Test that we can abort options flow.""" + mock_integration(hass, MockModule("test")) + mock_entity_platform(hass, "config_flow.test", None) + + class TestFlow: + """Test flow.""" + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Test options flow.""" + + with pytest.raises(config_entries.UnknownEntry): + await manager.options.async_create_flow( + "blah", context={"source": "test"}, data=None + ) + + async def test_entry_setup_succeed( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: @@ -3919,3 +3940,75 @@ async def test_task_tracking(hass: HomeAssistant) -> None: hass.loop.call_soon(event.set) await entry._async_process_on_unload(hass) assert results == ["on_unload", "background", "normal"] + + +async def test_preview_supported( + hass: HomeAssistant, manager: config_entries.ConfigEntries +) -> None: + """Test preview support.""" + + preview_calls = [] + + class MockFlowHandler(config_entries.ConfigFlow): + """Define a mock flow handler.""" + + VERSION = 1 + + async def async_step_test1(self, data): + """Mock Reauth.""" + return self.async_show_form(step_id="next") + + async def async_step_test2(self, data): + """Mock Reauth.""" + return self.async_show_form(step_id="next", preview="test") + + @callback + @staticmethod + def async_setup_preview(hass: HomeAssistant) -> None: + """Set up preview.""" + preview_calls.append(None) + + mock_integration(hass, MockModule("test")) + mock_entity_platform(hass, "config_flow.test", None) + + assert len(preview_calls) == 0 + + with patch.dict( + config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler} + ): + result = await manager.flow.async_init("test", context={"source": "test1"}) + + assert len(preview_calls) == 0 + assert result["preview"] is None + + result = await manager.flow.async_init("test", context={"source": "test2"}) + + assert len(preview_calls) == 1 + assert result["preview"] == "test" + + +async def test_preview_not_supported( + hass: HomeAssistant, manager: config_entries.ConfigEntries +) -> None: + """Test preview support.""" + + class MockFlowHandler(config_entries.ConfigFlow): + """Define a mock flow handler.""" + + VERSION = 1 + + async def async_step_user(self, data): + """Mock Reauth.""" + return self.async_show_form(step_id="user_confirm") + + mock_integration(hass, MockModule("test")) + mock_entity_platform(hass, "config_flow.test", None) + + with patch.dict( + config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler} + ): + result = await manager.flow.async_init( + "test", context={"source": config_entries.SOURCE_USER} + ) + + assert result["preview"] is None From c025244ac10639059926e19e93343af5e72bbcaa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 10:34:39 +0200 Subject: [PATCH 0725/1151] Add entity translations to Modem callerID (#98798) --- homeassistant/components/modem_callerid/button.py | 5 ++++- homeassistant/components/modem_callerid/sensor.py | 8 +++++--- homeassistant/components/modem_callerid/strings.json | 7 +++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modem_callerid/button.py b/homeassistant/components/modem_callerid/button.py index 4b149deece3..5f9e4cf489c 100644 --- a/homeassistant/components/modem_callerid/button.py +++ b/homeassistant/components/modem_callerid/button.py @@ -7,6 +7,7 @@ from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_KEY_API, DOMAIN @@ -32,13 +33,15 @@ class PhoneModemButton(ButtonEntity): """Implementation of USB modem caller ID button.""" _attr_icon = "mdi:phone-hangup" - _attr_name = "Phone Modem Reject" + _attr_translation_key = "phone_modem_reject" + _attr_has_entity_name = True def __init__(self, api: PhoneModem, device: str, server_unique_id: str) -> None: """Initialize the button.""" self.device = device self.api = api self._attr_unique_id = server_unique_id + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, server_unique_id)}) async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 1cb1043a5e0..c7c4403300a 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_IDLE from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CID, DATA_KEY_API, DOMAIN, ICON @@ -21,7 +22,6 @@ async def async_setup_entry( [ ModemCalleridSensor( api, - entry.title, entry.entry_id, ) ] @@ -42,11 +42,12 @@ class ModemCalleridSensor(SensorEntity): _attr_icon = ICON _attr_should_poll = False + _attr_has_entity_name = True + _attr_name = None - def __init__(self, api: PhoneModem, name: str, server_unique_id: str) -> None: + def __init__(self, api: PhoneModem, server_unique_id: str) -> None: """Initialize the sensor.""" self.api = api - self._attr_name = name self._attr_unique_id = server_unique_id self._attr_native_value = STATE_IDLE self._attr_extra_state_attributes = { @@ -54,6 +55,7 @@ class ModemCalleridSensor(SensorEntity): CID.CID_NUMBER: "", CID.CID_NAME: "", } + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, server_unique_id)}) async def async_added_to_hass(self) -> None: """Call when the modem sensor is added to Home Assistant.""" diff --git a/homeassistant/components/modem_callerid/strings.json b/homeassistant/components/modem_callerid/strings.json index 2e18ba3654f..dd0af40fac1 100644 --- a/homeassistant/components/modem_callerid/strings.json +++ b/homeassistant/components/modem_callerid/strings.json @@ -20,5 +20,12 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_devices_found": "No remaining devices found" } + }, + "entity": { + "button": { + "phone_modem_reject": { + "name": "Phone modem reject" + } + } } } From 17050a328652d0aab8311afdd0e50acf4b8e3ae1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 22 Aug 2023 08:53:52 +0000 Subject: [PATCH 0726/1151] Add support for Shelly Gas Valve addon (#98705) * Support Gas Valve * Treat opening and closing as open * Use set_state() * Change entity icon and name * Add valve state sensor * Closing == closed * Add translations for valve state entity * Valve state -> Valve status * Add tests; use control_result * Fix mypy error * Add missing "valve" to the Mock * Improve docstrings * Fix climate platform tests * Increase test coverage * Add mising return --- homeassistant/components/shelly/const.py | 2 + homeassistant/components/shelly/sensor.py | 18 +++++ homeassistant/components/shelly/strings.json | 11 +++ homeassistant/components/shelly/switch.py | 83 +++++++++++++++++++- tests/components/shelly/conftest.py | 10 +++ tests/components/shelly/test_climate.py | 6 ++ tests/components/shelly/test_switch.py | 51 ++++++++++++ 7 files changed, 179 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index cc82f0ad700..33b4caa5034 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -179,3 +179,5 @@ MAX_PUSH_UPDATE_FAILURES = 5 PUSH_UPDATE_ISSUE_ID = "push_update_{unique}" NOT_CALIBRATED_ISSUE_ID = "not_calibrated_{unique}" + +GAS_VALVE_OPEN_STATES = ("opening", "opened") diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index cd9980921c8..abcca888005 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -312,6 +312,24 @@ SENSORS: Final = { value=lambda value: value, extra_state_attributes=lambda block: {"self_test": block.selfTest}, ), + ("valve", "valve"): BlockSensorDescription( + key="valve|valve", + name="Valve status", + translation_key="valve_status", + icon="mdi:valve", + device_class=SensorDeviceClass.ENUM, + options=[ + "checking", + "closed", + "closing", + "failure", + "opened", + "opening", + "unknown", + ], + entity_category=EntityCategory.DIAGNOSTIC, + removal_condition=lambda _, block: block.valve == "not_connected", + ), } REST_SENSORS: Final = { diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 6ff48f5b85b..043ff419742 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -116,6 +116,17 @@ } } } + }, + "valve_status": { + "state": { + "checking": "Checking", + "closed": "Closed", + "closing": "Closing", + "failure": "Failure", + "opened": "Opened", + "opening": "Opening", + "unknown": "[%key:component::shelly::entity::sensor::operation::state::unknown%]" + } } } }, diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 3f5186a2017..395b386993a 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -1,17 +1,25 @@ """Switch for Shelly.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any, cast from aioshelly.block_device import Block -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .const import GAS_VALVE_OPEN_STATES from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data -from .entity import ShellyBlockEntity, ShellyRpcEntity +from .entity import ( + BlockEntityDescription, + ShellyBlockAttributeEntity, + ShellyBlockEntity, + ShellyRpcEntity, + async_setup_block_attribute_entities, +) from .utils import ( async_remove_shelly_entity, get_device_entry_gen, @@ -21,6 +29,19 @@ from .utils import ( ) +@dataclass +class BlockSwitchDescription(BlockEntityDescription, SwitchEntityDescription): + """Class to describe a BLOCK switch.""" + + +GAS_VALVE_SWITCH = BlockSwitchDescription( + key="valve|valve", + name="Valve", + available=lambda block: block.valve not in ("failure", "checking"), + removal_condition=lambda _, block: block.valve in ("not_connected", "unknown"), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -43,6 +64,17 @@ def async_setup_block_entry( coordinator = get_entry_data(hass)[config_entry.entry_id].block assert coordinator + # Add Shelly Gas Valve as a switch + if coordinator.model == "SHGS-1": + async_setup_block_attribute_entities( + hass, + async_add_entities, + coordinator, + {("valve", "valve"): GAS_VALVE_SWITCH}, + BlockValveSwitch, + ) + return + # In roller mode the relay blocks exist but do not contain required info if ( coordinator.model in ["SHSW-21", "SHSW-25"] @@ -94,6 +126,53 @@ def async_setup_rpc_entry( async_add_entities(RpcRelaySwitch(coordinator, id_) for id_ in switch_ids) +class BlockValveSwitch(ShellyBlockAttributeEntity, SwitchEntity): + """Entity that controls a Gas Valve on Block based Shelly devices.""" + + entity_description: BlockSwitchDescription + + def __init__( + self, + coordinator: ShellyBlockCoordinator, + block: Block, + attribute: str, + description: BlockSwitchDescription, + ) -> None: + """Initialize valve.""" + super().__init__(coordinator, block, attribute, description) + self.control_result: dict[str, Any] | None = None + + @property + def is_on(self) -> bool: + """If valve is open.""" + if self.control_result: + return self.control_result["state"] in GAS_VALVE_OPEN_STATES + + return self.attribute_value in GAS_VALVE_OPEN_STATES + + @property + def icon(self) -> str: + """Return the icon.""" + return "mdi:valve-open" if self.is_on else "mdi:valve-closed" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Open valve.""" + self.control_result = await self.set_state(go="open") + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Close valve.""" + self.control_result = await self.set_state(go="close") + self.async_write_ha_state() + + @callback + def _update_callback(self) -> None: + """When device updates, clear control result that overrides state.""" + self.control_result = None + + super()._update_callback() + + class BlockRelaySwitch(ShellyBlockEntity, SwitchEntity): """Entity that controls a relay on Block based Shelly devices.""" diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index de12adefaf1..797673265a6 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -131,6 +131,16 @@ MOCK_BLOCKS = [ description="emeter_0", type="emeter", ), + Mock( + sensor_ids={"valve": "closed"}, + valve="closed", + channel="0", + description="valve_0", + type="valve", + set_state=AsyncMock( + side_effect=lambda go: {"state": "opening" if go == "open" else "closing"} + ), + ), ] MOCK_CONFIG = { diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index c806cb5e742..08ec548d3f0 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -32,6 +32,7 @@ from tests.common import mock_restore_cache, mock_restore_cache_with_extra_data SENSOR_BLOCK_ID = 3 DEVICE_BLOCK_ID = 4 EMETER_BLOCK_ID = 5 +GAS_VALVE_BLOCK_ID = 6 ENTITY_ID = f"{CLIMATE_DOMAIN}.test_name" @@ -47,6 +48,7 @@ async def test_climate_hvac_mode( ) monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp") + monkeypatch.delattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "targetTemp") await init_integration(hass, 1, sleep_period=1000, model="SHTRV-01") # Make device online @@ -103,6 +105,7 @@ async def test_climate_set_temperature( """Test climate set temperature service.""" monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) + monkeypatch.delattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "targetTemp") await init_integration(hass, 1, sleep_period=1000) # Make device online @@ -144,6 +147,7 @@ async def test_climate_set_preset_mode( ) -> None: """Test climate set preset mode service.""" monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.delattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "targetTemp") monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "mode", None) await init_integration(hass, 1, sleep_period=1000, model="SHTRV-01") @@ -198,6 +202,7 @@ async def test_block_restored_climate( ) -> None: """Test block restored climate.""" monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.delattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "targetTemp") monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp") entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) @@ -261,6 +266,7 @@ async def test_block_restored_climate_us_customery( """Test block restored climate with US CUSTOMATY unit system.""" hass.config.units = US_CUSTOMARY_SYSTEM monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") + monkeypatch.delattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "targetTemp") monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "valveError", 0) monkeypatch.delattr(mock_block_device.blocks[EMETER_BLOCK_ID], "targetTemp") entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index a93d752f9e2..a53c5dc185b 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -9,6 +9,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_ICON, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -16,10 +17,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from . import init_integration RELAY_BLOCK_ID = 0 +GAS_VALVE_BLOCK_ID = 6 async def test_block_device_services(hass: HomeAssistant, mock_block_device) -> None: @@ -226,3 +229,51 @@ async def test_rpc_auth_error( assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH assert flow["context"].get("entry_id") == entry.entry_id + + +async def test_block_device_gas_valve( + hass: HomeAssistant, mock_block_device, monkeypatch +) -> None: + """Test block device Shelly Gas with Valve addon.""" + registry = er.async_get(hass) + await init_integration(hass, 1, "SHGS-1") + entity_id = "switch.test_name_valve" + + entry = registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-valve_0-valve" + + assert hass.states.get(entity_id).state == STATE_OFF # valve is closed + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON # valve is open + assert state.attributes.get(ATTR_ICON) == "mdi:valve-open" + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF # valve is closed + assert state.attributes.get(ATTR_ICON) == "mdi:valve-closed" + + monkeypatch.setattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "valve", "opened") + mock_block_device.mock_update() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON # valve is open + assert state.attributes.get(ATTR_ICON) == "mdi:valve-open" From 5ad97827cf30ece4d734eea8814fc442fb6c1740 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 12:17:43 +0200 Subject: [PATCH 0727/1151] Use snapshot assertion for Airly diagnostics (#98726) --- tests/components/airly/__init__.py | 1 + .../airly/fixtures/diagnostics_data.json | 28 ---------- .../airly/snapshots/test_diagnostics.ambr | 52 +++++++++++++++++++ tests/components/airly/test_diagnostics.py | 28 ++-------- 4 files changed, 57 insertions(+), 52 deletions(-) delete mode 100644 tests/components/airly/fixtures/diagnostics_data.json create mode 100644 tests/components/airly/snapshots/test_diagnostics.ambr diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index 452df6d9c27..ca26dbaf87f 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -14,6 +14,7 @@ async def init_integration(hass, aioclient_mock) -> MockConfigEntry: entry = MockConfigEntry( domain=DOMAIN, title="Home", + entry_id="3bd2acb0e4f0476d40865546d0d91921", unique_id="123-456", data={ "api_key": "foo", diff --git a/tests/components/airly/fixtures/diagnostics_data.json b/tests/components/airly/fixtures/diagnostics_data.json deleted file mode 100644 index 0f225fd4a20..00000000000 --- a/tests/components/airly/fixtures/diagnostics_data.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "PM1": 2.83, - "PM25": 4.37, - "PM10": 6.06, - "CO": 162.49, - "NO2": 16.04, - "O3": 41.52, - "SO2": 13.97, - "PRESSURE": 1019.86, - "HUMIDITY": 68.35, - "TEMPERATURE": 14.37, - "PM25_LIMIT": 15.0, - "PM25_PERCENT": 29.13, - "PM10_LIMIT": 45.0, - "PM10_PERCENT": 14.5, - "CO_LIMIT": 4000, - "CO_PERCENT": 4.06, - "NO2_LIMIT": 25, - "NO2_PERCENT": 64.17, - "O3_LIMIT": 100, - "O3_PERCENT": 41.52, - "SO2_LIMIT": 40, - "SO2_PERCENT": 34.93, - "CAQI": 7.29, - "LEVEL": "very low", - "DESCRIPTION": "Great air here today!", - "ADVICE": "Catch your breath!" -} diff --git a/tests/components/airly/snapshots/test_diagnostics.ambr b/tests/components/airly/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..a224ea07d46 --- /dev/null +++ b/tests/components/airly/snapshots/test_diagnostics.ambr @@ -0,0 +1,52 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config_entry': dict({ + 'data': dict({ + 'api_key': '**REDACTED**', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'Home', + }), + 'disabled_by': None, + 'domain': 'airly', + 'entry_id': '3bd2acb0e4f0476d40865546d0d91921', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Home', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + 'coordinator_data': dict({ + 'ADVICE': 'Catch your breath!', + 'CAQI': 7.29, + 'CO': 162.49, + 'CO_LIMIT': 4000, + 'CO_PERCENT': 4.06, + 'DESCRIPTION': 'Great air here today!', + 'HUMIDITY': 68.35, + 'LEVEL': 'very low', + 'NO2': 16.04, + 'NO2_LIMIT': 25, + 'NO2_PERCENT': 64.17, + 'O3': 41.52, + 'O3_LIMIT': 100, + 'O3_PERCENT': 41.52, + 'PM1': 2.83, + 'PM10': 6.06, + 'PM10_LIMIT': 45, + 'PM10_PERCENT': 14.5, + 'PM25': 4.37, + 'PM25_LIMIT': 15, + 'PM25_PERCENT': 29.13, + 'PRESSURE': 1019.86, + 'SO2': 13.97, + 'SO2_LIMIT': 40, + 'SO2_PERCENT': 34.93, + 'TEMPERATURE': 14.37, + }), + }) +# --- diff --git a/tests/components/airly/test_diagnostics.py b/tests/components/airly/test_diagnostics.py index 611f7910ae7..7364824e594 100644 --- a/tests/components/airly/test_diagnostics.py +++ b/tests/components/airly/test_diagnostics.py @@ -1,12 +1,11 @@ """Test Airly diagnostics.""" -import json -from homeassistant.components.diagnostics import REDACTED +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from . import init_integration -from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator @@ -16,30 +15,11 @@ async def test_entry_diagnostics( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" entry = await init_integration(hass, aioclient_mock) - coordinator_data = json.loads(load_fixture("diagnostics_data.json", "airly")) - result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert result["config_entry"] == { - "entry_id": entry.entry_id, - "version": 1, - "domain": "airly", - "title": "Home", - "data": { - "latitude": REDACTED, - "longitude": REDACTED, - "name": "Home", - "api_key": REDACTED, - }, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - } - assert result["coordinator_data"] == coordinator_data + assert result == snapshot From 08707b4abd96f59ec5cf346b93be0bc7850f9aec Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 12:24:23 +0200 Subject: [PATCH 0728/1151] Add entity translations to Logi circle (#98797) --- .../components/logi_circle/camera.py | 7 ++++--- .../components/logi_circle/sensor.py | 12 ++++++------ .../components/logi_circle/strings.json | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 027009669e5..77c0f2f24c8 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -53,7 +53,7 @@ async def async_setup_entry( devices = await hass.data[LOGI_CIRCLE_DOMAIN].cameras ffmpeg = get_ffmpeg_manager(hass) - cameras = [LogiCam(device, entry, ffmpeg) for device in devices] + cameras = [LogiCam(device, ffmpeg) for device in devices] async_add_entities(cameras, True) @@ -64,12 +64,13 @@ class LogiCam(Camera): _attr_attribution = ATTRIBUTION _attr_should_poll = True # Cameras default to False _attr_supported_features = CameraEntityFeature.ON_OFF + _attr_has_entity_name = True + _attr_name = None - def __init__(self, camera, device_info, ffmpeg): + def __init__(self, camera, ffmpeg): """Initialize Logi Circle camera.""" super().__init__() self._camera = camera - self._name = self._camera.name self._id = self._camera.mac_address self._has_battery = self._camera.supports_feature("battery_level") self._ffmpeg = ffmpeg diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index cd1dcfa2ede..32082b794b7 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -37,28 +37,28 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="last_activity_time", - name="Last Activity", + translation_key="last_activity", icon="mdi:history", ), SensorEntityDescription( key="recording", - name="Recording Mode", + translation_key="recording_mode", icon="mdi:eye", ), SensorEntityDescription( key="signal_strength_category", - name="WiFi Signal Category", + translation_key="wifi_signal_category", icon="mdi:wifi", ), SensorEntityDescription( key="signal_strength_percentage", - name="WiFi Signal Strength", + translation_key="wifi_signal_strength", native_unit_of_measurement=PERCENTAGE, icon="mdi:wifi", ), SensorEntityDescription( key="streaming", - name="Streaming Mode", + translation_key="streaming_mode", icon="mdi:camera", ), ) @@ -97,13 +97,13 @@ class LogiSensor(SensorEntity): """A sensor implementation for a Logi Circle camera.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__(self, camera, time_zone, description: SensorEntityDescription) -> None: """Initialize a sensor for Logi Circle camera.""" self.entity_description = description self._camera = camera self._attr_unique_id = f"{camera.mac_address}-{description.key}" - self._attr_name = f"{camera.name} {description.name}" self._activity: dict[Any, Any] = {} self._tz = time_zone diff --git a/homeassistant/components/logi_circle/strings.json b/homeassistant/components/logi_circle/strings.json index 4f641238a49..188139e6c29 100644 --- a/homeassistant/components/logi_circle/strings.json +++ b/homeassistant/components/logi_circle/strings.json @@ -25,6 +25,25 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]" } }, + "entity": { + "sensor": { + "last_activity": { + "name": "Last activity" + }, + "recording_mode": { + "name": "Recording mode" + }, + "wifi_signal_category": { + "name": "Wi-Fi signal category" + }, + "wifi_signal_strength": { + "name": "Wi-Fi signal strength" + }, + "streaming_mode": { + "name": "Streaming mode" + } + } + }, "services": { "set_config": { "name": "Set config", From 32d8d65addeb1c870e9b9bdf053738a2f03a7c8d Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 22 Aug 2023 03:28:19 -0700 Subject: [PATCH 0729/1151] Bump androidtvremote2 to 0.0.14 (#98801) --- homeassistant/components/androidtv_remote/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv_remote/manifest.json b/homeassistant/components/androidtv_remote/manifest.json index cb7a969379e..f45dee34afe 100644 --- a/homeassistant/components/androidtv_remote/manifest.json +++ b/homeassistant/components/androidtv_remote/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_push", "loggers": ["androidtvremote2"], "quality_scale": "platinum", - "requirements": ["androidtvremote2==0.0.13"], + "requirements": ["androidtvremote2==0.0.14"], "zeroconf": ["_androidtvremote2._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index ae624186d00..d5771e354d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -402,7 +402,7 @@ amcrest==1.9.7 androidtv[async]==0.0.70 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.13 +androidtvremote2==0.0.14 # homeassistant.components.anel_pwrctrl anel-pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64fc1751883..5d704df04bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -368,7 +368,7 @@ amberelectric==1.0.4 androidtv[async]==0.0.70 # homeassistant.components.androidtv_remote -androidtvremote2==0.0.13 +androidtvremote2==0.0.14 # homeassistant.components.anova anova-wifi==0.10.0 From 1369874348ee277f2820712f5b5dedc4e42c3a01 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Tue, 22 Aug 2023 14:34:26 +0200 Subject: [PATCH 0730/1151] Add text sensor to BTHome (#98355) --- homeassistant/components/bthome/manifest.json | 2 +- homeassistant/components/bthome/sensor.py | 15 ++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bthome/test_sensor.py | 20 +++++++++++++++++-- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 418c7b8e3e3..7f53a5b5f06 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bthome", "iot_class": "local_push", - "requirements": ["bthome-ble==3.0.0"] + "requirements": ["bthome-ble==3.1.0"] } diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index caa652715bf..06f205246c8 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -2,6 +2,9 @@ from __future__ import annotations from bthome_ble import SensorDeviceClass as BTHomeSensorDeviceClass, SensorUpdate, Units +from bthome_ble.const import ( + ExtendedSensorDeviceClass as BTHomeExtendedSensorDeviceClass, +) from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -66,7 +69,7 @@ SENSOR_DESCRIPTIONS = { ), # Count (-) (BTHomeSensorDeviceClass.COUNT, None): SensorEntityDescription( - key=f"{BTHomeSensorDeviceClass.COUNT}", + key=str(BTHomeSensorDeviceClass.COUNT), state_class=SensorStateClass.MEASUREMENT, ), # CO2 (parts per million) @@ -186,7 +189,7 @@ SENSOR_DESCRIPTIONS = { ), # Packet Id (-) (BTHomeSensorDeviceClass.PACKET_ID, None): SensorEntityDescription( - key=f"{BTHomeSensorDeviceClass.PACKET_ID}", + key=str(BTHomeSensorDeviceClass.PACKET_ID), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -260,12 +263,16 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), + # Text (-) + (BTHomeExtendedSensorDeviceClass.TEXT, None): SensorEntityDescription( + key=str(BTHomeExtendedSensorDeviceClass.TEXT), + ), # Timestamp (datetime object) ( BTHomeSensorDeviceClass.TIMESTAMP, None, ): SensorEntityDescription( - key=f"{BTHomeSensorDeviceClass.TIMESTAMP}", + key=str(BTHomeSensorDeviceClass.TIMESTAMP), device_class=SensorDeviceClass.TIMESTAMP, state_class=SensorStateClass.MEASUREMENT, ), @@ -274,7 +281,7 @@ SENSOR_DESCRIPTIONS = { BTHomeSensorDeviceClass.UV_INDEX, None, ): SensorEntityDescription( - key=f"{BTHomeSensorDeviceClass.UV_INDEX}", + key=str(BTHomeSensorDeviceClass.UV_INDEX), state_class=SensorStateClass.MEASUREMENT, ), # Volatile organic Compounds (VOC) (µg/m3) diff --git a/requirements_all.txt b/requirements_all.txt index d5771e354d4..5f975c78e0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -571,7 +571,7 @@ brunt==1.2.0 bt-proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==3.0.0 +bthome-ble==3.1.0 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d704df04bf..fd63156cdd9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -475,7 +475,7 @@ brottsplatskartan==0.0.1 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==3.0.0 +bthome-ble==3.1.0 # homeassistant.components.buienradar buienradar==1.0.5 diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index 7474e3ba890..831f7811972 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -869,7 +869,6 @@ async def test_v1_sensors( { "sensor_entity": "sensor.test_device_18b2_timestamp", "friendly_name": "Test Device 18B2 Timestamp", - "unit_of_measurement": "s", "state_class": "measurement", "expected_state": "2023-05-14T19:41:17+00:00", }, @@ -943,6 +942,21 @@ async def test_v1_sensors( }, ], ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x44\x53\x0C\x48\x65\x6C\x6C\x6F\x20\x57\x6F\x72\x6C\x64\x21", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_text", + "friendly_name": "Test Device 18B2 Text", + "expected_state": "Hello World!", + }, + ], + ), ( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( @@ -1080,7 +1094,9 @@ async def test_v2_sensors( if ATTR_UNIT_OF_MEASUREMENT in sensor_attr: # Some sensors don't have a unit of measurement assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == meas["unit_of_measurement"] - assert sensor_attr[ATTR_STATE_CLASS] == meas["state_class"] + if ATTR_STATE_CLASS in sensor_attr: + # Some sensors have state class None + assert sensor_attr[ATTR_STATE_CLASS] == meas["state_class"] assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() From 09efd1c972d98452503082861d57758e1fbe91ff Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 14:37:33 +0200 Subject: [PATCH 0731/1151] Migrate Oncue to has entity name (#98812) --- homeassistant/components/oncue/entity.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/oncue/entity.py b/homeassistant/components/oncue/entity.py index 0572cf6fb99..6d988d4aaaf 100644 --- a/homeassistant/components/oncue/entity.py +++ b/homeassistant/components/oncue/entity.py @@ -20,6 +20,8 @@ class OncueEntity( ): """Representation of an Oncue entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator[dict[str, OncueDevice]], @@ -33,7 +35,7 @@ class OncueEntity( self.entity_description = description self._device_id = device_id self._attr_unique_id = f"{device_id}_{description.key}" - self._attr_name = f"{device.name} {sensor.display_name}" + self._attr_name = sensor.display_name self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device_id)}, name=device.name, From 097c7fbfef95e29a3d2ef90910f0cd15154e4471 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 14:41:15 +0200 Subject: [PATCH 0732/1151] Add entity translations to Nexia (#98803) --- .../components/nexia/binary_sensor.py | 8 ++-- homeassistant/components/nexia/climate.py | 6 +-- homeassistant/components/nexia/entity.py | 15 +++---- homeassistant/components/nexia/number.py | 2 +- homeassistant/components/nexia/scene.py | 4 +- homeassistant/components/nexia/sensor.py | 28 ++++++------ homeassistant/components/nexia/strings.json | 43 +++++++++++++++++++ homeassistant/components/nexia/switch.py | 5 ++- tests/components/nexia/test_binary_sensor.py | 4 +- tests/components/nexia/test_init.py | 2 +- tests/components/nexia/test_sensor.py | 18 ++++---- 11 files changed, 90 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/nexia/binary_sensor.py b/homeassistant/components/nexia/binary_sensor.py index 1b398610ac2..02c3fef1162 100644 --- a/homeassistant/components/nexia/binary_sensor.py +++ b/homeassistant/components/nexia/binary_sensor.py @@ -23,7 +23,7 @@ async def async_setup_entry( thermostat = nexia_home.get_thermostat_by_id(thermostat_id) entities.append( NexiaBinarySensor( - coordinator, thermostat, "is_blower_active", "Blower Active" + coordinator, thermostat, "is_blower_active", "blower_active" ) ) if thermostat.has_emergency_heat(): @@ -32,7 +32,7 @@ async def async_setup_entry( coordinator, thermostat, "is_emergency_heat_active", - "Emergency Heat Active", + "emergency_heat_active", ) ) @@ -42,16 +42,16 @@ async def async_setup_entry( class NexiaBinarySensor(NexiaThermostatEntity, BinarySensorEntity): """Provices Nexia BinarySensor support.""" - def __init__(self, coordinator, thermostat, sensor_call, sensor_name): + def __init__(self, coordinator, thermostat, sensor_call, translation_key): """Initialize the nexia sensor.""" super().__init__( coordinator, thermostat, - name=f"{thermostat.get_name()} {sensor_name}", unique_id=f"{thermostat.thermostat_id}_{sensor_call}", ) self._call = sensor_call self._state = None + self._attr_translation_key = translation_key @property def is_on(self): diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index fe31263a86c..e331108f6ba 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -150,13 +150,13 @@ async def async_setup_entry( class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Provides Nexia Climate support.""" + _attr_name = None + def __init__( self, coordinator: NexiaDataUpdateCoordinator, zone: NexiaThermostatZone ) -> None: """Initialize the thermostat.""" - super().__init__( - coordinator, zone, name=zone.get_name(), unique_id=zone.zone_id - ) + super().__init__(coordinator, zone, zone.zone_id) unit = self._thermostat.get_unit() min_humidity, max_humidity = self._thermostat.get_humidity_setpoint_limits() min_setpoint, max_setpoint = self._thermostat.get_setpoint_limits() diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index 2a09ec877c8..dfb2366d34a 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -31,21 +31,20 @@ class NexiaEntity(CoordinatorEntity[NexiaDataUpdateCoordinator]): _attr_attribution = ATTRIBUTION - def __init__( - self, coordinator: NexiaDataUpdateCoordinator, name: str, unique_id: str - ) -> None: + def __init__(self, coordinator: NexiaDataUpdateCoordinator, unique_id: str) -> None: """Initialize the entity.""" super().__init__(coordinator) self._attr_unique_id = unique_id - self._attr_name = name class NexiaThermostatEntity(NexiaEntity): """Base class for nexia devices attached to a thermostat.""" - def __init__(self, coordinator, thermostat, name, unique_id): + _attr_has_entity_name = True + + def __init__(self, coordinator, thermostat, unique_id): """Initialize the entity.""" - super().__init__(coordinator, name, unique_id) + super().__init__(coordinator, unique_id) self._thermostat: NexiaThermostat = thermostat self._attr_device_info = DeviceInfo( configuration_url=self.coordinator.nexia_home.root_url, @@ -89,9 +88,9 @@ class NexiaThermostatEntity(NexiaEntity): class NexiaThermostatZoneEntity(NexiaThermostatEntity): """Base class for nexia devices attached to a thermostat.""" - def __init__(self, coordinator, zone, name, unique_id): + def __init__(self, coordinator, zone, unique_id): """Initialize the entity.""" - super().__init__(coordinator, zone.thermostat, name, unique_id) + super().__init__(coordinator, zone.thermostat, unique_id) self._zone: NexiaThermostatZone = zone zone_name = self._zone.get_name() self._attr_device_info |= { diff --git a/homeassistant/components/nexia/number.py b/homeassistant/components/nexia/number.py index acb99c2ed01..b44c6a4c48f 100644 --- a/homeassistant/components/nexia/number.py +++ b/homeassistant/components/nexia/number.py @@ -42,6 +42,7 @@ class NexiaFanSpeedEntity(NexiaThermostatEntity, NumberEntity): _attr_native_unit_of_measurement = PERCENTAGE _attr_icon = "mdi:fan" + _attr_translation_key = "fan_speed" def __init__( self, @@ -53,7 +54,6 @@ class NexiaFanSpeedEntity(NexiaThermostatEntity, NumberEntity): super().__init__( coordinator, thermostat, - name=f"{thermostat.get_name()} Fan speed", unique_id=f"{thermostat.thermostat_id}_fan_speed_setpoint", ) min_value, max_value = valid_range diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py index 941785f8221..3a21c61badd 100644 --- a/homeassistant/components/nexia/scene.py +++ b/homeassistant/components/nexia/scene.py @@ -43,9 +43,9 @@ class NexiaAutomationScene(NexiaEntity, Scene): """Initialize the automation scene.""" super().__init__( coordinator, - name=automation.name, - unique_id=automation.automation_id, + automation.automation_id, ) + self._attr_name = automation.name self._automation: NexiaAutomation = automation self._attr_extra_state_attributes = {ATTR_DESCRIPTION: automation.description} diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index a67ac681199..79e07bc71b4 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -40,7 +40,7 @@ async def async_setup_entry( coordinator, thermostat, "get_system_status", - "System Status", + "system_status", None, None, None, @@ -52,7 +52,7 @@ async def async_setup_entry( coordinator, thermostat, "get_air_cleaner_mode", - "Air Cleaner Mode", + "air_cleaner_mode", None, None, None, @@ -65,7 +65,7 @@ async def async_setup_entry( coordinator, thermostat, "get_current_compressor_speed", - "Current Compressor Speed", + "current_compressor_speed", None, PERCENTAGE, SensorStateClass.MEASUREMENT, @@ -77,7 +77,7 @@ async def async_setup_entry( coordinator, thermostat, "get_requested_compressor_speed", - "Requested Compressor Speed", + "requested_compressor_speed", None, PERCENTAGE, SensorStateClass.MEASUREMENT, @@ -95,7 +95,7 @@ async def async_setup_entry( coordinator, thermostat, "get_outdoor_temperature", - "Outdoor Temperature", + "outdoor_temperature", SensorDeviceClass.TEMPERATURE, unit, SensorStateClass.MEASUREMENT, @@ -108,7 +108,7 @@ async def async_setup_entry( coordinator, thermostat, "get_relative_humidity", - "Relative Humidity", + None, SensorDeviceClass.HUMIDITY, PERCENTAGE, SensorStateClass.MEASUREMENT, @@ -129,7 +129,7 @@ async def async_setup_entry( coordinator, zone, "get_temperature", - "Temperature", + None, SensorDeviceClass.TEMPERATURE, unit, SensorStateClass.MEASUREMENT, @@ -139,7 +139,7 @@ async def async_setup_entry( # Zone Status entities.append( NexiaThermostatZoneSensor( - coordinator, zone, "get_status", "Zone Status", None, None, None + coordinator, zone, "get_status", "zone_status", None, None, None ) ) # Setpoint Status @@ -148,7 +148,7 @@ async def async_setup_entry( coordinator, zone, "get_setpoint_status", - "Zone Setpoint Status", + "zone_setpoint_status", None, None, None, @@ -166,7 +166,7 @@ class NexiaThermostatSensor(NexiaThermostatEntity, SensorEntity): coordinator, thermostat, sensor_call, - sensor_name, + translation_key, sensor_class, sensor_unit, state_class, @@ -176,7 +176,6 @@ class NexiaThermostatSensor(NexiaThermostatEntity, SensorEntity): super().__init__( coordinator, thermostat, - name=f"{thermostat.get_name()} {sensor_name}", unique_id=f"{thermostat.thermostat_id}_{sensor_call}", ) self._call = sensor_call @@ -184,6 +183,8 @@ class NexiaThermostatSensor(NexiaThermostatEntity, SensorEntity): self._attr_device_class = sensor_class self._attr_native_unit_of_measurement = sensor_unit self._attr_state_class = state_class + if translation_key is not None: + self._attr_translation_key = translation_key @property def native_value(self): @@ -204,7 +205,7 @@ class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity, SensorEntity): coordinator, zone, sensor_call, - sensor_name, + translation_key, sensor_class, sensor_unit, state_class, @@ -215,7 +216,6 @@ class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity, SensorEntity): super().__init__( coordinator, zone, - name=f"{zone.get_name()} {sensor_name}", unique_id=f"{zone.zone_id}_{sensor_call}", ) self._call = sensor_call @@ -223,6 +223,8 @@ class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity, SensorEntity): self._attr_device_class = sensor_class self._attr_native_unit_of_measurement = sensor_unit self._attr_state_class = state_class + if translation_key is not None: + self._attr_translation_key = translation_key @property def native_value(self): diff --git a/homeassistant/components/nexia/strings.json b/homeassistant/components/nexia/strings.json index f3d343ffda3..9e49f4bb793 100644 --- a/homeassistant/components/nexia/strings.json +++ b/homeassistant/components/nexia/strings.json @@ -18,6 +18,49 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "entity": { + "binary_sensor": { + "blower_active": { + "name": "Blower active" + }, + "emergency_heat_active": { + "name": "Emergency heat active" + } + }, + "number": { + "fan_speed": { + "name": "Fan speed" + } + }, + "sensor": { + "system_status": { + "name": "System status" + }, + "air_cleaner_mode": { + "name": "Air cleaner mode" + }, + "current_compressor_speed": { + "name": "Current compressor speed" + }, + "requested_compressor_speed": { + "name": "Requested compressor speed" + }, + "outdoor_temperature": { + "name": "Outdoor temperature" + }, + "zone_status": { + "name": "Zone status" + }, + "zone_setpoint_status": { + "name": "Zone setpoint status" + } + }, + "switch": { + "hold": { + "name": "Hold" + } + } + }, "services": { "set_aircleaner_mode": { "name": "Set air cleaner mode", diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py index 643a4d585c4..7f191d39c73 100644 --- a/homeassistant/components/nexia/switch.py +++ b/homeassistant/components/nexia/switch.py @@ -39,13 +39,14 @@ async def async_setup_entry( class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): """Provides Nexia hold switch support.""" + _attr_translation_key = "hold" + def __init__( self, coordinator: NexiaDataUpdateCoordinator, zone: NexiaThermostatZone ) -> None: """Initialize the hold mode switch.""" - switch_name = f"{zone.get_name()} Hold" zone_id = zone.zone_id - super().__init__(coordinator, zone, name=switch_name, unique_id=zone_id) + super().__init__(coordinator, zone, zone_id) @property def is_on(self) -> bool: diff --git a/tests/components/nexia/test_binary_sensor.py b/tests/components/nexia/test_binary_sensor.py index 78753383b03..f59e968d634 100644 --- a/tests/components/nexia/test_binary_sensor.py +++ b/tests/components/nexia/test_binary_sensor.py @@ -14,7 +14,7 @@ async def test_create_binary_sensors(hass: HomeAssistant) -> None: assert state.state == STATE_ON expected_attributes = { "attribution": "Data provided by Trane Technologies", - "friendly_name": "Master Suite Blower Active", + "friendly_name": "Master Suite Blower active", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -26,7 +26,7 @@ async def test_create_binary_sensors(hass: HomeAssistant) -> None: assert state.state == STATE_OFF expected_attributes = { "attribution": "Data provided by Trane Technologies", - "friendly_name": "Downstairs East Wing Blower Active", + "friendly_name": "Downstairs East Wing Blower active", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears diff --git a/tests/components/nexia/test_init.py b/tests/components/nexia/test_init.py index 5409181f00e..f920592f8a6 100644 --- a/tests/components/nexia/test_init.py +++ b/tests/components/nexia/test_init.py @@ -53,7 +53,7 @@ async def test_device_remove_devices( is False ) - entity = registry.entities["sensor.master_suite_relative_humidity"] + entity = registry.entities["sensor.master_suite_humidity"] live_thermostat_device_entry = device_registry.async_get(entity.device_id) assert ( await remove_device( diff --git a/tests/components/nexia/test_sensor.py b/tests/components/nexia/test_sensor.py index 4d693261a9d..23a92af71c8 100644 --- a/tests/components/nexia/test_sensor.py +++ b/tests/components/nexia/test_sensor.py @@ -29,7 +29,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: assert state.state == "Permanent Hold" expected_attributes = { "attribution": "Data provided by Trane Technologies", - "friendly_name": "Nick Office Zone Setpoint Status", + "friendly_name": "Nick Office Zone setpoint status", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -42,7 +42,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: expected_attributes = { "attribution": "Data provided by Trane Technologies", - "friendly_name": "Nick Office Zone Status", + "friendly_name": "Nick Office Zone status", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -55,7 +55,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: expected_attributes = { "attribution": "Data provided by Trane Technologies", - "friendly_name": "Master Suite Air Cleaner Mode", + "friendly_name": "Master Suite Air cleaner mode", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -68,7 +68,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: expected_attributes = { "attribution": "Data provided by Trane Technologies", - "friendly_name": "Master Suite Current Compressor Speed", + "friendly_name": "Master Suite Current compressor speed", "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case @@ -83,7 +83,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: expected_attributes = { "attribution": "Data provided by Trane Technologies", "device_class": "temperature", - "friendly_name": "Master Suite Outdoor Temperature", + "friendly_name": "Master Suite Outdoor temperature", "unit_of_measurement": UnitOfTemperature.CELSIUS, } # Only test for a subset of attributes in case @@ -92,13 +92,13 @@ async def test_create_sensors(hass: HomeAssistant) -> None: state.attributes[key] == expected_attributes[key] for key in expected_attributes ) - state = hass.states.get("sensor.master_suite_relative_humidity") + state = hass.states.get("sensor.master_suite_humidity") assert state.state == "52.0" expected_attributes = { "attribution": "Data provided by Trane Technologies", "device_class": "humidity", - "friendly_name": "Master Suite Relative Humidity", + "friendly_name": "Master Suite Humidity", "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case @@ -112,7 +112,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: expected_attributes = { "attribution": "Data provided by Trane Technologies", - "friendly_name": "Master Suite Requested Compressor Speed", + "friendly_name": "Master Suite Requested compressor speed", "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case @@ -126,7 +126,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: expected_attributes = { "attribution": "Data provided by Trane Technologies", - "friendly_name": "Master Suite System Status", + "friendly_name": "Master Suite System status", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears From 406f06f0fc7b2380a51675ecc7e7742c480a4aa5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 22 Aug 2023 15:41:50 +0300 Subject: [PATCH 0733/1151] Abort Shelly setup if MAC address mismatch (#98807) --- homeassistant/components/shelly/__init__.py | 10 +++++++--- tests/components/shelly/test_init.py | 22 ++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 65b60546f61..09d9e3655f0 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -6,7 +6,11 @@ from typing import Any, Final from aioshelly.block_device import BlockDevice, BlockUpdateType from aioshelly.common import ConnectionOptions -from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError +from aioshelly.exceptions import ( + DeviceConnectionError, + InvalidAuthError, + MacAddressMismatchError, +) from aioshelly.rpc_device import RpcDevice, RpcUpdateType import voluptuous as vol @@ -185,7 +189,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b LOGGER.debug("Setting up online block device %s", entry.title) try: await device.initialize() - except DeviceConnectionError as err: + except (DeviceConnectionError, MacAddressMismatchError) as err: raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err @@ -271,7 +275,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo LOGGER.debug("Setting up online RPC device %s", entry.title) try: await device.initialize() - except DeviceConnectionError as err: + except (DeviceConnectionError, MacAddressMismatchError) as err: raise ConfigEntryNotReady(repr(err)) from err except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index a62dfda82f9..1fdfc9d4304 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -3,7 +3,11 @@ from __future__ import annotations from unittest.mock import AsyncMock, patch -from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError +from aioshelly.exceptions import ( + DeviceConnectionError, + InvalidAuthError, + MacAddressMismatchError, +) import pytest from homeassistant.components.shelly.const import ( @@ -86,6 +90,22 @@ async def test_device_connection_error( assert entry.state == ConfigEntryState.SETUP_RETRY +@pytest.mark.parametrize("gen", [1, 2]) +async def test_mac_mismatch_error( + hass: HomeAssistant, gen, mock_block_device, mock_rpc_device, monkeypatch +) -> None: + """Test device MAC address mismatch error.""" + monkeypatch.setattr( + mock_block_device, "initialize", AsyncMock(side_effect=MacAddressMismatchError) + ) + monkeypatch.setattr( + mock_rpc_device, "initialize", AsyncMock(side_effect=MacAddressMismatchError) + ) + + entry = await init_integration(hass, gen) + assert entry.state == ConfigEntryState.SETUP_RETRY + + @pytest.mark.parametrize("gen", [1, 2]) async def test_device_auth_error( hass: HomeAssistant, gen, mock_block_device, mock_rpc_device, monkeypatch From 890efd58e067afe48357fb75888a972e4751ba2e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 16:59:56 +0200 Subject: [PATCH 0734/1151] Add entity translations to Roku (#96083) * Add entity translations to Roku * Add entity translations to Roku --- .../components/roku/binary_sensor.py | 8 ++--- homeassistant/components/roku/select.py | 4 +-- homeassistant/components/roku/sensor.py | 4 +-- homeassistant/components/roku/strings.json | 32 +++++++++++++++++++ tests/components/roku/test_sensor.py | 8 ++--- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/roku/binary_sensor.py b/homeassistant/components/roku/binary_sensor.py index 1ac5f38b2c5..b08933dcd91 100644 --- a/homeassistant/components/roku/binary_sensor.py +++ b/homeassistant/components/roku/binary_sensor.py @@ -36,27 +36,27 @@ class RokuBinarySensorEntityDescription( BINARY_SENSORS: tuple[RokuBinarySensorEntityDescription, ...] = ( RokuBinarySensorEntityDescription( key="headphones_connected", - name="Headphones connected", + translation_key="headphones_connected", icon="mdi:headphones", value_fn=lambda device: device.info.headphones_connected, ), RokuBinarySensorEntityDescription( key="supports_airplay", - name="Supports AirPlay", + translation_key="supports_airplay", icon="mdi:cast-variant", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.supports_airplay, ), RokuBinarySensorEntityDescription( key="supports_ethernet", - name="Supports ethernet", + translation_key="supports_ethernet", icon="mdi:ethernet", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.ethernet_support, ), RokuBinarySensorEntityDescription( key="supports_find_remote", - name="Supports find remote", + translation_key="supports_find_remote", icon="mdi:remote", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.supports_find_remote, diff --git a/homeassistant/components/roku/select.py b/homeassistant/components/roku/select.py index f915fdef9b0..430133b7f77 100644 --- a/homeassistant/components/roku/select.py +++ b/homeassistant/components/roku/select.py @@ -95,7 +95,7 @@ class RokuSelectEntityDescription( ENTITIES: tuple[RokuSelectEntityDescription, ...] = ( RokuSelectEntityDescription( key="application", - name="Application", + translation_key="application", icon="mdi:application", set_fn=_launch_application, value_fn=_get_application_name, @@ -106,7 +106,7 @@ ENTITIES: tuple[RokuSelectEntityDescription, ...] = ( CHANNEL_ENTITY = RokuSelectEntityDescription( key="channel", - name="Channel", + translation_key="channel", icon="mdi:television", set_fn=_tune_channel, value_fn=_get_channel_name, diff --git a/homeassistant/components/roku/sensor.py b/homeassistant/components/roku/sensor.py index 562501b4013..69b8c34d312 100644 --- a/homeassistant/components/roku/sensor.py +++ b/homeassistant/components/roku/sensor.py @@ -34,14 +34,14 @@ class RokuSensorEntityDescription( SENSORS: tuple[RokuSensorEntityDescription, ...] = ( RokuSensorEntityDescription( key="active_app", - name="Active App", + translation_key="active_app", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:application", value_fn=lambda device: device.app.name if device.app else None, ), RokuSensorEntityDescription( key="active_app_id", - name="Active App ID", + translation_key="active_app_id", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:application-cog", value_fn=lambda device: device.app.app_id if device.app else None, diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 3510a43c604..818b43930f4 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -21,6 +21,38 @@ "unknown": "[%key:common::config_flow::error::unknown%]" } }, + "entity": { + "binary_sensor": { + "headphones_connected": { + "name": "Headphones connected" + }, + "supports_airplay": { + "name": "Supports AirPlay" + }, + "supports_ethernet": { + "name": "Supports ethernet" + }, + "supports_find_remote": { + "name": "Supports find remote" + } + }, + "select": { + "application": { + "name": "Application" + }, + "channel": { + "name": "Channel" + } + }, + "sensor": { + "active_app": { + "name": "Active app" + }, + "active_app_id": { + "name": "Active app ID" + } + } + }, "services": { "search": { "name": "Search", diff --git a/tests/components/roku/test_sensor.py b/tests/components/roku/test_sensor.py index 7cfb6f7f7c9..ab7b9ac00f5 100644 --- a/tests/components/roku/test_sensor.py +++ b/tests/components/roku/test_sensor.py @@ -34,7 +34,7 @@ async def test_roku_sensors( assert entry.unique_id == f"{UPNP_SERIAL}_active_app" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "Roku" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Active App" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Active app" assert state.attributes.get(ATTR_ICON) == "mdi:application" assert ATTR_DEVICE_CLASS not in state.attributes @@ -45,7 +45,7 @@ async def test_roku_sensors( assert entry.unique_id == f"{UPNP_SERIAL}_active_app_id" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Active App ID" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Active app ID" assert state.attributes.get(ATTR_ICON) == "mdi:application-cog" assert ATTR_DEVICE_CLASS not in state.attributes @@ -83,7 +83,7 @@ async def test_rokutv_sensors( assert entry.unique_id == "YN00H5555555_active_app" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "Antenna TV" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Active App' + assert state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Active app' assert state.attributes.get(ATTR_ICON) == "mdi:application" assert ATTR_DEVICE_CLASS not in state.attributes @@ -94,7 +94,7 @@ async def test_rokutv_sensors( assert entry.unique_id == "YN00H5555555_active_app_id" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "tvinput.dtv" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Active App ID' + assert state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Active app ID' assert state.attributes.get(ATTR_ICON) == "mdi:application-cog" assert ATTR_DEVICE_CLASS not in state.attributes From 426fd62ee30157cf688d756933fcd6b46d22f2ca Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 22 Aug 2023 17:05:53 +0200 Subject: [PATCH 0735/1151] Adjust hassfest to require translations for core services (#98814) Co-authored-by: Franck Nijhof --- script/hassfest/services.py | 66 +++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index b3f59ab66a3..4a826f7cad9 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -25,10 +25,8 @@ def exists(value: Any) -> Any: return value -FIELD_SCHEMA = vol.Schema( +CORE_INTEGRATION_FIELD_SCHEMA = vol.Schema( { - vol.Optional("description"): str, - vol.Optional("name"): str, vol.Optional("example"): exists, vol.Optional("default"): exists, vol.Optional("values"): exists, @@ -46,7 +44,26 @@ FIELD_SCHEMA = vol.Schema( } ) -SERVICE_SCHEMA = vol.Any( +CUSTOM_INTEGRATION_FIELD_SCHEMA = CORE_INTEGRATION_FIELD_SCHEMA.extend( + { + vol.Optional("description"): str, + vol.Optional("name"): str, + } +) + +CORE_INTEGRATION_SERVICE_SCHEMA = vol.Any( + vol.Schema( + { + vol.Optional("target"): vol.Any( + selector.TargetSelector.CONFIG_SCHEMA, None + ), + vol.Optional("fields"): vol.Schema({str: CORE_INTEGRATION_FIELD_SCHEMA}), + } + ), + None, +) + +CUSTOM_INTEGRATION_SERVICE_SCHEMA = vol.Any( vol.Schema( { vol.Optional("description"): str, @@ -54,13 +71,23 @@ SERVICE_SCHEMA = vol.Any( vol.Optional("target"): vol.Any( selector.TargetSelector.CONFIG_SCHEMA, None ), - vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}), + vol.Optional("fields"): vol.Schema({str: CUSTOM_INTEGRATION_FIELD_SCHEMA}), } ), None, ) -SERVICES_SCHEMA = vol.Schema({cv.slug: SERVICE_SCHEMA}) +CORE_INTEGRATION_SERVICES_SCHEMA = vol.Schema( + {cv.slug: CORE_INTEGRATION_SERVICE_SCHEMA} +) +CUSTOM_INTEGRATION_SERVICES_SCHEMA = vol.Schema( + {cv.slug: CUSTOM_INTEGRATION_SERVICE_SCHEMA} +) + +VALIDATE_AS_CUSTOM_INTEGRATION = { + # Adding translations would be a breaking change + "foursquare", +} def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> bool: @@ -99,7 +126,13 @@ def validate_services(config: Config, integration: Integration) -> None: return try: - services = SERVICES_SCHEMA(data) + if ( + integration.core + and integration.domain not in VALIDATE_AS_CUSTOM_INTEGRATION + ): + services = CORE_INTEGRATION_SERVICES_SCHEMA(data) + else: + services = CUSTOM_INTEGRATION_SERVICES_SCHEMA(data) except vol.Invalid as err: integration.add_error( "services", f"Invalid services.yaml: {humanize_error(data, err)}" @@ -118,6 +151,10 @@ def validate_services(config: Config, integration: Integration) -> None: with contextlib.suppress(ValueError): strings = json.loads(strings_file.read_text()) + error_msg_suffix = "in the translations file" + if not integration.core: + error_msg_suffix = f"and is not {error_msg_suffix}" + # For each service in the integration, check if the description if set, # if not, check if it's in the strings file. If not, add an error. for service_name, service_schema in services.items(): @@ -129,7 +166,7 @@ def validate_services(config: Config, integration: Integration) -> None: except KeyError: integration.add_error( "services", - f"Service {service_name} has no name and is not in the translations file", + f"Service {service_name} has no name {error_msg_suffix}", ) if "description" not in service_schema: @@ -138,12 +175,21 @@ def validate_services(config: Config, integration: Integration) -> None: except KeyError: integration.add_error( "services", - f"Service {service_name} has no description and is not in the translations file", + f"Service {service_name} has no description {error_msg_suffix}", ) # The same check is done for the description in each of the fields of the # service schema. for field_name, field_schema in service_schema.get("fields", {}).items(): + if "name" not in field_schema: + try: + strings["services"][service_name]["fields"][field_name]["name"] + except KeyError: + integration.add_error( + "services", + f"Service {service_name} has a field {field_name} with no name {error_msg_suffix}", + ) + if "description" not in field_schema: try: strings["services"][service_name]["fields"][field_name][ @@ -152,7 +198,7 @@ def validate_services(config: Config, integration: Integration) -> None: except KeyError: integration.add_error( "services", - f"Service {service_name} has a field {field_name} with no description and is not in the translations file", + f"Service {service_name} has a field {field_name} with no description {error_msg_suffix}", ) if "selector" in field_schema: From d0fc0aea40f00c80ba1357ec0bcdb9df6de865bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Aug 2023 10:17:15 -0500 Subject: [PATCH 0736/1151] Retry lifx setup later if device has an unexpected serial (#98783) --- homeassistant/components/lifx/__init__.py | 12 ++++++++++- tests/components/lifx/test_binary_sensor.py | 3 +-- tests/components/lifx/test_button.py | 5 ++--- tests/components/lifx/test_diagnostics.py | 12 +++++------ tests/components/lifx/test_init.py | 22 +++++++++++++++++++++ tests/components/lifx/test_select.py | 15 +++++++------- tests/components/lifx/test_sensor.py | 6 +++--- 7 files changed, 52 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 41aa58fb962..76d4b7e36c5 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -30,7 +30,7 @@ from .coordinator import LIFXUpdateCoordinator from .discovery import async_discover_devices, async_trigger_discovery from .manager import LIFXManager from .migration import async_migrate_entities_devices, async_migrate_legacy_entries -from .util import async_entry_is_legacy, async_get_legacy_entry +from .util import async_entry_is_legacy, async_get_legacy_entry, formatted_serial CONF_SERVER = "server" CONF_BROADCAST = "broadcast" @@ -218,6 +218,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: connection.async_stop() raise + serial = formatted_serial(coordinator.serial_number) + if serial != entry.unique_id: + # If the serial number of the device does not match the unique_id + # of the config entry, it likely means the DHCP lease has expired + # and the device has been assigned a new IP address. We need to + # wait for the next discovery to find the device at its new address + # and update the config entry so we do not mix up devices. + raise ConfigEntryNotReady( + f"Unexpected device found at {host}; expected {entry.unique_id}, found {serial}" + ) domain_data[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/tests/components/lifx/test_binary_sensor.py b/tests/components/lifx/test_binary_sensor.py index 4b583eed475..d71a7eeaf0b 100644 --- a/tests/components/lifx/test_binary_sensor.py +++ b/tests/components/lifx/test_binary_sensor.py @@ -21,7 +21,6 @@ from homeassistant.util import dt as dt_util from . import ( DEFAULT_ENTRY_TITLE, IP_ADDRESS, - MAC_ADDRESS, SERIAL, _mocked_clean_bulb, _patch_config_flow_try_connect, @@ -38,7 +37,7 @@ async def test_hev_cycle_state(hass: HomeAssistant) -> None: domain=lifx.DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_clean_bulb() diff --git a/tests/components/lifx/test_button.py b/tests/components/lifx/test_button.py index b166aa05d66..d527229fe78 100644 --- a/tests/components/lifx/test_button.py +++ b/tests/components/lifx/test_button.py @@ -14,7 +14,6 @@ from homeassistant.setup import async_setup_component from . import ( DEFAULT_ENTRY_TITLE, IP_ADDRESS, - MAC_ADDRESS, SERIAL, _mocked_bulb, _patch_config_flow_try_connect, @@ -38,7 +37,7 @@ async def test_button_restart(hass: HomeAssistant) -> None: domain=DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -70,7 +69,7 @@ async def test_button_identify(hass: HomeAssistant) -> None: domain=DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() diff --git a/tests/components/lifx/test_diagnostics.py b/tests/components/lifx/test_diagnostics.py index 581f0516184..a72695502a4 100644 --- a/tests/components/lifx/test_diagnostics.py +++ b/tests/components/lifx/test_diagnostics.py @@ -7,7 +7,7 @@ from homeassistant.setup import async_setup_component from . import ( DEFAULT_ENTRY_TITLE, IP_ADDRESS, - MAC_ADDRESS, + SERIAL, _mocked_bulb, _mocked_clean_bulb, _mocked_infrared_bulb, @@ -30,7 +30,7 @@ async def test_bulb_diagnostics( domain=lifx.DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -77,7 +77,7 @@ async def test_clean_bulb_diagnostics( domain=lifx.DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_clean_bulb() @@ -129,7 +129,7 @@ async def test_infrared_bulb_diagnostics( domain=lifx.DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() @@ -177,7 +177,7 @@ async def test_legacy_multizone_bulb_diagnostics( domain=lifx.DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_light_strip() @@ -288,7 +288,7 @@ async def test_multizone_bulb_diagnostics( domain=lifx.DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_light_strip() diff --git a/tests/components/lifx/test_init.py b/tests/components/lifx/test_init.py index 3c813840faf..3f16cc44f41 100644 --- a/tests/components/lifx/test_init.py +++ b/tests/components/lifx/test_init.py @@ -5,6 +5,8 @@ from datetime import timedelta import socket from unittest.mock import patch +import pytest + from homeassistant.components import lifx from homeassistant.components.lifx import DOMAIN, discovery from homeassistant.config_entries import ConfigEntryState @@ -149,3 +151,23 @@ async def test_dns_error_at_startup(hass: HomeAssistant) -> None: await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_wrong_serial( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test config entry enters setup retry when serial mismatches.""" + mismatched_serial = f"{SERIAL[:-1]}0" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=mismatched_serial + ) + already_migrated_config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + + assert ( + "Unexpected device found at 127.0.0.1; expected aa:bb:cc:dd:ee:c0, found aa:bb:cc:dd:ee:cc" + in caplog.text + ) diff --git a/tests/components/lifx/test_select.py b/tests/components/lifx/test_select.py index d190cbe6b10..aa705418d55 100644 --- a/tests/components/lifx/test_select.py +++ b/tests/components/lifx/test_select.py @@ -13,7 +13,6 @@ from homeassistant.util import dt as dt_util from . import ( DEFAULT_ENTRY_TITLE, IP_ADDRESS, - MAC_ADDRESS, SERIAL, MockLifxCommand, _mocked_infrared_bulb, @@ -32,7 +31,7 @@ async def test_theme_select(hass: HomeAssistant) -> None: domain=DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_light_strip() @@ -70,7 +69,7 @@ async def test_infrared_brightness(hass: HomeAssistant) -> None: domain=DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() @@ -100,7 +99,7 @@ async def test_set_infrared_brightness_25_percent(hass: HomeAssistant) -> None: domain=DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() @@ -139,7 +138,7 @@ async def test_set_infrared_brightness_50_percent(hass: HomeAssistant) -> None: domain=DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() @@ -178,7 +177,7 @@ async def test_set_infrared_brightness_100_percent(hass: HomeAssistant) -> None: domain=DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() @@ -217,7 +216,7 @@ async def test_disable_infrared(hass: HomeAssistant) -> None: domain=DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() @@ -256,7 +255,7 @@ async def test_invalid_infrared_brightness(hass: HomeAssistant) -> None: domain=DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() diff --git a/tests/components/lifx/test_sensor.py b/tests/components/lifx/test_sensor.py index a36e151849b..5fe69c8dabc 100644 --- a/tests/components/lifx/test_sensor.py +++ b/tests/components/lifx/test_sensor.py @@ -20,7 +20,7 @@ from homeassistant.util import dt as dt_util from . import ( DEFAULT_ENTRY_TITLE, IP_ADDRESS, - MAC_ADDRESS, + SERIAL, _mocked_bulb, _mocked_bulb_old_firmware, _patch_config_flow_try_connect, @@ -38,7 +38,7 @@ async def test_rssi_sensor(hass: HomeAssistant) -> None: domain=lifx.DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -89,7 +89,7 @@ async def test_rssi_sensor_old_firmware(hass: HomeAssistant) -> None: domain=lifx.DOMAIN, title=DEFAULT_ENTRY_TITLE, data={CONF_HOST: IP_ADDRESS}, - unique_id=MAC_ADDRESS, + unique_id=SERIAL, ) config_entry.add_to_hass(hass) bulb = _mocked_bulb_old_firmware() From 07884026c63c89c6960e0ad0472e150012f74fa5 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 22 Aug 2023 10:31:09 -0500 Subject: [PATCH 0737/1151] Detect wake word services in hassio discovery (#98827) --- homeassistant/components/wyoming/config_flow.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wyoming/config_flow.py b/homeassistant/components/wyoming/config_flow.py index 3fccbaea9c4..f6b8ed73890 100644 --- a/homeassistant/components/wyoming/config_flow.py +++ b/homeassistant/components/wyoming/config_flow.py @@ -93,9 +93,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: uri = urlparse(self._hassio_discovery.config["uri"]) if service := await WyomingService.create(uri.hostname, uri.port): - if not any( - asr for asr in service.info.asr if asr.installed - ) and not any(tts for tts in service.info.tts if tts.installed): + if ( + not any(asr for asr in service.info.asr if asr.installed) + and not any(tts for tts in service.info.tts if tts.installed) + and not any(wake for wake in service.info.wake if wake.installed) + ): return self.async_abort(reason="no_services") return self.async_create_entry( From 10b3cc4dd63bb16fb1f4e5bf89acda5777c00c03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Aug 2023 10:58:29 -0500 Subject: [PATCH 0738/1151] Bump zeroconf to 0.81.0 (#98826) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 0d63e87db17..5b605721782 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.80.0"] + "requirements": ["zeroconf==0.81.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 986a86b38a9..da38d4abd50 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.80.0 +zeroconf==0.81.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 5f975c78e0c..bfefe1385eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2755,7 +2755,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.80.0 +zeroconf==0.81.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd63156cdd9..ac8d36b46b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2028,7 +2028,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.80.0 +zeroconf==0.81.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 59900a49e298a9722e4f25fee7e24e43d4af4db0 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 22 Aug 2023 18:02:44 +0200 Subject: [PATCH 0739/1151] Add Reolink AI detection delay time (#98398) --- homeassistant/components/reolink/number.py | 64 ++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/homeassistant/components/reolink/number.py b/homeassistant/components/reolink/number.py index bb19974114d..1be6cd24027 100644 --- a/homeassistant/components/reolink/number.py +++ b/homeassistant/components/reolink/number.py @@ -175,6 +175,70 @@ NUMBER_ENTITIES = ( value=lambda api, ch: api.ai_sensitivity(ch, "dog_cat"), method=lambda api, ch, value: api.set_ai_sensitivity(ch, int(value), "dog_cat"), ), + ReolinkNumberEntityDescription( + key="ai_face_delay", + name="AI face delay", + icon="mdi:face-recognition", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + native_step=1, + native_unit_of_measurement=UnitOfTime.SECONDS, + native_min_value=0, + native_max_value=8, + supported=lambda api, ch: ( + api.supported(ch, "ai_delay") and api.ai_supported(ch, "face") + ), + value=lambda api, ch: api.ai_delay(ch, "face"), + method=lambda api, ch, value: api.set_ai_delay(ch, int(value), "face"), + ), + ReolinkNumberEntityDescription( + key="ai_person_delay", + name="AI person delay", + icon="mdi:account", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + native_step=1, + native_unit_of_measurement=UnitOfTime.SECONDS, + native_min_value=0, + native_max_value=8, + supported=lambda api, ch: ( + api.supported(ch, "ai_delay") and api.ai_supported(ch, "people") + ), + value=lambda api, ch: api.ai_delay(ch, "people"), + method=lambda api, ch, value: api.set_ai_delay(ch, int(value), "people"), + ), + ReolinkNumberEntityDescription( + key="ai_vehicle_delay", + name="AI vehicle delay", + icon="mdi:car", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + native_step=1, + native_unit_of_measurement=UnitOfTime.SECONDS, + native_min_value=0, + native_max_value=8, + supported=lambda api, ch: ( + api.supported(ch, "ai_delay") and api.ai_supported(ch, "vehicle") + ), + value=lambda api, ch: api.ai_delay(ch, "vehicle"), + method=lambda api, ch, value: api.set_ai_delay(ch, int(value), "vehicle"), + ), + ReolinkNumberEntityDescription( + key="ai_pet_delay", + name="AI pet delay", + icon="mdi:dog-side", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + native_step=1, + native_unit_of_measurement=UnitOfTime.SECONDS, + native_min_value=0, + native_max_value=8, + supported=lambda api, ch: ( + api.supported(ch, "ai_delay") and api.ai_supported(ch, "dog_cat") + ), + value=lambda api, ch: api.ai_delay(ch, "dog_cat"), + method=lambda api, ch, value: api.set_ai_delay(ch, int(value), "dog_cat"), + ), ReolinkNumberEntityDescription( key="auto_quick_reply_time", name="Auto quick reply time", From 19576e6c956bf28d864af3b8673f819c2f0ed4c3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 18:06:19 +0200 Subject: [PATCH 0740/1151] Add options flow to OpenSky (#98177) --- homeassistant/components/opensky/__init__.py | 24 +++- .../components/opensky/config_flow.py | 84 +++++++++++- homeassistant/components/opensky/const.py | 1 + .../components/opensky/coordinator.py | 6 +- homeassistant/components/opensky/strings.json | 20 +++ tests/components/opensky/__init__.py | 11 ++ tests/components/opensky/conftest.py | 34 ++++- tests/components/opensky/test_config_flow.py | 128 +++++++++++++++++- tests/components/opensky/test_init.py | 17 +++ 9 files changed, 314 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/opensky/__init__.py b/homeassistant/components/opensky/__init__.py index 81f348b5911..cb9c6173694 100644 --- a/homeassistant/components/opensky/__init__.py +++ b/homeassistant/components/opensky/__init__.py @@ -1,13 +1,17 @@ """The opensky component.""" from __future__ import annotations +from aiohttp import BasicAuth from python_opensky import OpenSky +from python_opensky.exceptions import OpenSkyUnauthenticatedError from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN, PLATFORMS +from .const import CONF_CONTRIBUTING_USER, DOMAIN, PLATFORMS from .coordinator import OpenSkyDataUpdateCoordinator @@ -15,11 +19,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up opensky from a config entry.""" client = OpenSky(session=async_get_clientsession(hass)) + if CONF_USERNAME in entry.options and CONF_PASSWORD in entry.options: + try: + await client.authenticate( + BasicAuth( + login=entry.options[CONF_USERNAME], + password=entry.options[CONF_PASSWORD], + ), + contributing_user=entry.options.get(CONF_CONTRIBUTING_USER, False), + ) + except OpenSkyUnauthenticatedError as exc: + raise ConfigEntryNotReady from exc + 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) + entry.async_on_unload(entry.add_update_listener(update_listener)) return True @@ -28,3 +45,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload opensky 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/opensky/config_flow.py b/homeassistant/components/opensky/config_flow.py index 12827dfd6ba..a0cd6bc54c2 100644 --- a/homeassistant/components/opensky/config_flow.py +++ b/homeassistant/components/opensky/config_flow.py @@ -3,21 +3,45 @@ from __future__ import annotations from typing import Any +from aiohttp import BasicAuth +from python_opensky import OpenSky +from python_opensky.exceptions import OpenSkyUnauthenticatedError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + OptionsFlowWithConfigEntry, +) +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_PASSWORD, + CONF_RADIUS, + CONF_USERNAME, +) +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DEFAULT_NAME, DOMAIN +from .const import CONF_CONTRIBUTING_USER, DEFAULT_NAME, DOMAIN from .sensor import CONF_ALTITUDE, DEFAULT_ALTITUDE class OpenSkyConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow handler for OpenSky.""" + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OpenSkyOptionsFlowHandler: + """Get the options flow for this handler.""" + return OpenSkyOptionsFlowHandler(config_entry) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -70,3 +94,57 @@ class OpenSkyConfigFlowHandler(ConfigFlow, domain=DOMAIN): CONF_ALTITUDE: import_config.get(CONF_ALTITUDE, DEFAULT_ALTITUDE), }, ) + + +class OpenSkyOptionsFlowHandler(OptionsFlowWithConfigEntry): + """OpenSky Options flow handler.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Initialize form.""" + errors: dict[str, str] = {} + if user_input is not None: + authentication = CONF_USERNAME in user_input or CONF_PASSWORD in user_input + if authentication and CONF_USERNAME not in user_input: + errors["base"] = "username_missing" + if authentication and CONF_PASSWORD not in user_input: + errors["base"] = "password_missing" + if user_input[CONF_CONTRIBUTING_USER] and not authentication: + errors["base"] = "no_authentication" + if authentication and not errors: + async with OpenSky( + session=async_get_clientsession(self.hass) + ) as opensky: + try: + await opensky.authenticate( + BasicAuth( + login=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ), + contributing_user=user_input[CONF_CONTRIBUTING_USER], + ) + except OpenSkyUnauthenticatedError: + errors["base"] = "invalid_auth" + if not errors: + return self.async_create_entry( + title=self.options.get(CONF_NAME, "OpenSky"), + data=user_input, + ) + + return self.async_show_form( + step_id="init", + errors=errors, + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_RADIUS): vol.Coerce(float), + vol.Optional(CONF_ALTITUDE): vol.Coerce(float), + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Optional(CONF_CONTRIBUTING_USER, default=False): bool, + } + ), + user_input or self.options, + ), + ) diff --git a/homeassistant/components/opensky/const.py b/homeassistant/components/opensky/const.py index 4f4eb8a142c..7fe26b424d3 100644 --- a/homeassistant/components/opensky/const.py +++ b/homeassistant/components/opensky/const.py @@ -10,6 +10,7 @@ DEFAULT_NAME = "OpenSky" DOMAIN = "opensky" MANUFACTURER = "OpenSky Network" CONF_ALTITUDE = "altitude" +CONF_CONTRIBUTING_USER = "contributing_user" ATTR_ICAO24 = "icao24" ATTR_CALLSIGN = "callsign" ATTR_ALTITUDE = "altitude" diff --git a/homeassistant/components/opensky/coordinator.py b/homeassistant/components/opensky/coordinator.py index 1c3d10e0c33..d85924737a1 100644 --- a/homeassistant/components/opensky/coordinator.py +++ b/homeassistant/components/opensky/coordinator.py @@ -41,8 +41,10 @@ class OpenSkyDataUpdateCoordinator(DataUpdateCoordinator[int]): 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), + update_interval={ + True: timedelta(seconds=90), + False: timedelta(minutes=15), + }.get(opensky.is_authenticated), ) self._opensky = opensky self._previously_tracked: set[str] | None = None diff --git a/homeassistant/components/opensky/strings.json b/homeassistant/components/opensky/strings.json index c5746ffdb46..4b4dc908b14 100644 --- a/homeassistant/components/opensky/strings.json +++ b/homeassistant/components/opensky/strings.json @@ -11,5 +11,25 @@ } } } + }, + "options": { + "step": { + "init": { + "description": "You can login to your OpenSky account to increase the update frequency.", + "data": { + "radius": "[%key:component::opensky::config::step::user::data::radius%]", + "altitude": "[%key:component::opensky::config::step::user::data::altitude%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "contributing_user": "I'm contributing to OpenSky" + } + } + }, + "error": { + "username_missing": "Username is missing", + "password_missing": "Password is missing", + "no_authentication": "You need to authenticate to be contributing", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + } } } diff --git a/tests/components/opensky/__init__.py b/tests/components/opensky/__init__.py index f985f068ab1..e746521c72c 100644 --- a/tests/components/opensky/__init__.py +++ b/tests/components/opensky/__init__.py @@ -1,9 +1,20 @@ """Opensky tests.""" +import json from unittest.mock import patch +from python_opensky import StatesResponse + +from tests.common import load_fixture + def patch_setup_entry() -> bool: """Patch interface.""" return patch( "homeassistant.components.opensky.async_setup_entry", return_value=True ) + + +def get_states_response_fixture(fixture: str) -> StatesResponse: + """Return the states response from json.""" + json_fixture = load_fixture(fixture) + return StatesResponse.parse_obj(json.loads(json_fixture)) diff --git a/tests/components/opensky/conftest.py b/tests/components/opensky/conftest.py index 7cf3074a2a3..f74c18773f5 100644 --- a/tests/components/opensky/conftest.py +++ b/tests/components/opensky/conftest.py @@ -6,8 +6,18 @@ from unittest.mock import patch import pytest from python_opensky import StatesResponse -from homeassistant.components.opensky.const import CONF_ALTITUDE, DOMAIN -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from homeassistant.components.opensky.const import ( + CONF_ALTITUDE, + CONF_CONTRIBUTING_USER, + DOMAIN, +) +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_PASSWORD, + CONF_RADIUS, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -50,6 +60,26 @@ def mock_config_entry_altitude() -> MockConfigEntry: ) +@pytest.fixture(name="config_entry_authenticated") +def mock_config_entry_authenticated() -> MockConfigEntry: + """Create authenticated Opensky entry in Home Assistant.""" + return MockConfigEntry( + domain=DOMAIN, + title="OpenSky", + data={ + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + }, + options={ + CONF_RADIUS: 10.0, + CONF_ALTITUDE: 12500.0, + CONF_USERNAME: "asd", + CONF_PASSWORD: "secret", + CONF_CONTRIBUTING_USER: True, + }, + ) + + @pytest.fixture(name="setup_integration") async def mock_setup_integration( hass: HomeAssistant, diff --git a/tests/components/opensky/test_config_flow.py b/tests/components/opensky/test_config_flow.py index 5dee2764cff..7fa19762ddf 100644 --- a/tests/components/opensky/test_config_flow.py +++ b/tests/components/opensky/test_config_flow.py @@ -1,15 +1,31 @@ """Test OpenSky config flow.""" from typing import Any +from unittest.mock import patch import pytest +from python_opensky.exceptions import OpenSkyUnauthenticatedError -from homeassistant.components.opensky.const import CONF_ALTITUDE, DEFAULT_NAME, DOMAIN +from homeassistant import data_entry_flow +from homeassistant.components.opensky.const import ( + CONF_ALTITUDE, + CONF_CONTRIBUTING_USER, + 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.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_PASSWORD, + CONF_RADIUS, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from . import patch_setup_entry +from . import get_states_response_fixture, patch_setup_entry +from .conftest import ComponentSetup from tests.common import MockConfigEntry @@ -149,3 +165,109 @@ async def test_importing_already_exists_flow(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("user_input", "error"), + [ + ( + {CONF_USERNAME: "homeassistant", CONF_CONTRIBUTING_USER: False}, + "password_missing", + ), + ({CONF_PASSWORD: "secret", CONF_CONTRIBUTING_USER: False}, "username_missing"), + ({CONF_CONTRIBUTING_USER: True}, "no_authentication"), + ( + { + CONF_USERNAME: "homeassistant", + CONF_PASSWORD: "secret", + CONF_CONTRIBUTING_USER: True, + }, + "invalid_auth", + ), + ], +) +async def test_options_flow_failures( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + user_input: dict[str, Any], + error: str, +) -> None: + """Test load and unload entry.""" + await setup_integration(config_entry) + entry = hass.config_entries.async_entries(DOMAIN)[0] + with patch( + "python_opensky.OpenSky.authenticate", + side_effect=OpenSkyUnauthenticatedError(), + ): + result = await hass.config_entries.options.async_init(entry.entry_id) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_RADIUS: 10000, **user_input}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"]["base"] == error + with patch("python_opensky.OpenSky.authenticate"), patch( + "python_opensky.OpenSky.get_states", + return_value=get_states_response_fixture("opensky/states_1.json"), + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RADIUS: 10000, + CONF_USERNAME: "homeassistant", + CONF_PASSWORD: "secret", + CONF_CONTRIBUTING_USER: True, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RADIUS: 10000, + CONF_USERNAME: "homeassistant", + CONF_PASSWORD: "secret", + CONF_CONTRIBUTING_USER: True, + } + + +async def test_options_flow( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, +) -> None: + """Test options flow.""" + await setup_integration(config_entry) + entry = hass.config_entries.async_entries(DOMAIN)[0] + result = await hass.config_entries.options.async_init(entry.entry_id) + await hass.async_block_till_done() + with patch("python_opensky.OpenSky.authenticate"), patch( + "python_opensky.OpenSky.get_states", + return_value=get_states_response_fixture("opensky/states_1.json"), + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RADIUS: 10000, + CONF_USERNAME: "homeassistant", + CONF_PASSWORD: "secret", + CONF_CONTRIBUTING_USER: True, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_RADIUS: 10000, + CONF_USERNAME: "homeassistant", + CONF_PASSWORD: "secret", + CONF_CONTRIBUTING_USER: True, + } diff --git a/tests/components/opensky/test_init.py b/tests/components/opensky/test_init.py index 961aaab61fc..4c6cb9c3a33 100644 --- a/tests/components/opensky/test_init.py +++ b/tests/components/opensky/test_init.py @@ -4,6 +4,7 @@ from __future__ import annotations from unittest.mock import patch from python_opensky import OpenSkyError +from python_opensky.exceptions import OpenSkyUnauthenticatedError from homeassistant.components.opensky.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -48,3 +49,19 @@ async def test_load_entry_failure( await hass.async_block_till_done() entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_load_entry_authentication_failure( + hass: HomeAssistant, + config_entry_authenticated: MockConfigEntry, +) -> None: + """Test auth failure while loading.""" + config_entry_authenticated.add_to_hass(hass) + with patch( + "python_opensky.OpenSky.authenticate", + side_effect=OpenSkyUnauthenticatedError(), + ): + 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 daade2646633effbe8749c216fa9bf2bb98ff0ff Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 22 Aug 2023 12:35:57 -0400 Subject: [PATCH 0741/1151] Bump aiosomecomfort to 0.0.16 in Honeywell (#98824) bump aiosomecomfort to 0.0.16 --- homeassistant/components/honeywell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index aa07a5248cf..bb72c15cd46 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/honeywell", "iot_class": "cloud_polling", "loggers": ["somecomfort"], - "requirements": ["AIOSomecomfort==0.0.15"] + "requirements": ["AIOSomecomfort==0.0.16"] } diff --git a/requirements_all.txt b/requirements_all.txt index bfefe1385eb..da72addb8ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ AEMET-OpenData==0.2.2 AIOAladdinConnect==0.1.57 # homeassistant.components.honeywell -AIOSomecomfort==0.0.15 +AIOSomecomfort==0.0.16 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac8d36b46b4..ace24eccd59 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ AEMET-OpenData==0.2.2 AIOAladdinConnect==0.1.57 # homeassistant.components.honeywell -AIOSomecomfort==0.0.15 +AIOSomecomfort==0.0.16 # homeassistant.components.adax Adax-local==0.1.5 From 3f2c03fe7769bde12dff9320955d3066fc73c0cc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 22 Aug 2023 19:53:55 +0200 Subject: [PATCH 0742/1151] Add input option to skip coverage [ci] (#98821) --- .github/workflows/ci.yaml | 57 +++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5cb51a30dda..a96a0602473 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,6 +19,10 @@ on: description: "Skip pytest" default: false type: boolean + skip-coverage: + description: "Skip coverage" + default: false + type: boolean pylint-only: description: "Only run pylint" default: false @@ -79,6 +83,7 @@ jobs: test_groups: ${{ steps.info.outputs.test_groups }} tests_glob: ${{ steps.info.outputs.tests_glob }} tests: ${{ steps.info.outputs.tests }} + skip_coverage: ${{ steps.info.outputs.skip_coverage }} runs-on: ubuntu-22.04 steps: - name: Check out code from GitHub @@ -127,6 +132,7 @@ jobs: test_group_count=10 tests="[]" tests_glob="" + skip_coverage="" if [[ "${{ steps.integrations.outputs.changes }}" != "[]" ]]; then @@ -176,6 +182,12 @@ jobs: test_full_suite="true" fi + if [[ "${{ github.event.inputs.skip-coverage }}" == "true" ]] \ + || [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-skip-coverage') }}" == "true" ]]; + then + skip_coverage="true" + fi + # Output & sent to GitHub Actions echo "mariadb_groups: ${mariadb_groups}" echo "mariadb_groups=${mariadb_groups}" >> $GITHUB_OUTPUT @@ -195,6 +207,8 @@ jobs: echo "tests=${tests}" >> $GITHUB_OUTPUT echo "tests_glob: ${tests_glob}" echo "tests_glob=${tests_glob}" >> $GITHUB_OUTPUT + echo "skip_coverage: ${skip_coverage}" + echo "skip_coverage=${skip_coverage}" >> $GITHUB_OUTPUT pre-commit: name: Prepare pre-commit base @@ -741,6 +755,11 @@ jobs: . venv/bin/activate python --version set -o pipefail + cov_params=() + if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then + cov_params+=(--cov="homeassistant") + cov_params+=(--cov-report=xml) + fi python3 -X dev -m pytest \ -qq \ @@ -750,8 +769,7 @@ jobs: --dist=loadfile \ --test-group-count ${{ needs.info.outputs.test_group_count }} \ --test-group=${{ matrix.group }} \ - --cov="homeassistant" \ - --cov-report=xml \ + ${cov_params[@]} \ -o console_output_style=count \ -p no:sugar \ tests \ @@ -773,13 +791,18 @@ jobs: exit 1 fi + cov_params=() + if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then + cov_params+=(--cov="homeassistant.components.${{ matrix.group }}") + cov_params+=(--cov-report=xml) + cov_params+=(--cov-report=term-missing) + fi + python3 -X dev -m pytest \ -qq \ --timeout=9 \ -n auto \ - --cov="homeassistant.components.${{ matrix.group }}" \ - --cov-report=xml \ - --cov-report=term-missing \ + ${cov_params[@]} \ -o console_output_style=count \ --durations=0 \ --durations-min=1 \ @@ -793,6 +816,7 @@ jobs: name: pytest-${{ github.run_number }} path: pytest-*.txt - name: Upload coverage artifact + if: needs.info.outputs.skip_coverage != 'true' uses: actions/upload-artifact@v3.1.2 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} @@ -888,14 +912,18 @@ jobs: python --version set -o pipefail mariadb=$(echo "${{ matrix.mariadb-group }}" | sed "s/:/-/g") + cov_params=() + if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then + cov_params+=(--cov="homeassistant.components.recorder") + cov_params+=(--cov-report=xml) + cov_params+=(--cov-report=term-missing) + fi python3 -X dev -m pytest \ -qq \ --timeout=20 \ -n 1 \ - --cov="homeassistant.components.recorder" \ - --cov-report=xml \ - --cov-report=term-missing \ + ${cov_params[@]} \ -o console_output_style=count \ --durations=10 \ -p no:sugar \ @@ -912,6 +940,7 @@ jobs: name: pytest-${{ github.run_number }} path: pytest-*.txt - name: Upload coverage artifact + if: needs.info.outputs.skip_coverage != 'true' uses: actions/upload-artifact@v3.1.2 with: name: coverage-${{ matrix.python-version }}-mariadb @@ -1007,14 +1036,18 @@ jobs: python --version set -o pipefail postgresql=$(echo "${{ matrix.postgresql-group }}" | sed "s/:/-/g") + cov_params=() + if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then + cov_params+=(--cov="homeassistant.components.recorder") + cov_params+=(--cov-report=xml) + cov_params+=(--cov-report=term-missing) + fi python3 -X dev -m pytest \ -qq \ --timeout=9 \ -n 1 \ - --cov="homeassistant.components.recorder" \ - --cov-report=xml \ - --cov-report=term-missing \ + ${cov_params[@]} \ -o console_output_style=count \ --durations=0 \ --durations-min=10 \ @@ -1032,6 +1065,7 @@ jobs: name: pytest-${{ github.run_number }} path: pytest-*.txt - name: Upload coverage artifact + if: needs.info.outputs.skip_coverage != 'true' uses: actions/upload-artifact@v3.1.0 with: name: coverage-${{ matrix.python-version }}-postgresql @@ -1042,6 +1076,7 @@ jobs: coverage: name: Upload test coverage to Codecov + if: needs.info.outputs.skip_coverage != 'true' runs-on: ubuntu-22.04 needs: - info From 342e55409a65624253304091a0525b91b6cb66c3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 19:55:47 +0200 Subject: [PATCH 0743/1151] Add entity translations to OpenGarage (#98834) --- homeassistant/components/opengarage/binary_sensor.py | 4 +--- homeassistant/components/opengarage/cover.py | 2 +- homeassistant/components/opengarage/entity.py | 2 ++ homeassistant/components/opengarage/sensor.py | 3 --- homeassistant/components/opengarage/strings.json | 7 +++++++ 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/opengarage/binary_sensor.py b/homeassistant/components/opengarage/binary_sensor.py index 64bc7c83d20..22f118ca804 100644 --- a/homeassistant/components/opengarage/binary_sensor.py +++ b/homeassistant/components/opengarage/binary_sensor.py @@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key="vehicle", + translation_key="vehicle", ), ) @@ -66,9 +67,6 @@ class OpenGarageBinarySensor(OpenGarageEntity, BinarySensorEntity): @callback def _update_attr(self) -> None: """Handle updated data from the coordinator.""" - self._attr_name = ( - f'{self.coordinator.data["name"]} {self.entity_description.key}' - ) state = self.coordinator.data.get(self.entity_description.key) if state == 1: self._attr_is_on = True diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 15669a41736..3f3f6b11acf 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -37,6 +37,7 @@ class OpenGarageCover(OpenGarageEntity, CoverEntity): _attr_device_class = CoverDeviceClass.GARAGE _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + _attr_name = None def __init__( self, coordinator: OpenGarageDataUpdateCoordinator, device_id: str @@ -89,7 +90,6 @@ class OpenGarageCover(OpenGarageEntity, CoverEntity): """Update the state and attributes.""" status = self.coordinator.data - self._attr_name = status["name"] state = STATES_MAP.get(status.get("door")) # type: ignore[arg-type] if self._state_before_move is not None: if self._state_before_move != state: diff --git a/homeassistant/components/opengarage/entity.py b/homeassistant/components/opengarage/entity.py index 678f43afb6e..c8380ea9244 100644 --- a/homeassistant/components/opengarage/entity.py +++ b/homeassistant/components/opengarage/entity.py @@ -12,6 +12,8 @@ from . import DOMAIN, OpenGarageDataUpdateCoordinator class OpenGarageEntity(CoordinatorEntity[OpenGarageDataUpdateCoordinator]): """Representation of a OpenGarage entity.""" + _attr_has_entity_name = True + def __init__( self, open_garage_data_coordinator: OpenGarageDataUpdateCoordinator, diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index 796192b406f..b1d6cb921fa 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -83,7 +83,4 @@ class OpenGarageSensor(OpenGarageEntity, SensorEntity): @callback def _update_attr(self) -> None: """Handle updated data from the coordinator.""" - self._attr_name = ( - f'{self.coordinator.data["name"]} {self.entity_description.key}' - ) self._attr_native_value = self.coordinator.data.get(self.entity_description.key) diff --git a/homeassistant/components/opengarage/strings.json b/homeassistant/components/opengarage/strings.json index 26f2f94ff9f..ba4521d4dcf 100644 --- a/homeassistant/components/opengarage/strings.json +++ b/homeassistant/components/opengarage/strings.json @@ -18,5 +18,12 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "binary_sensor": { + "vehicle": { + "name": "Vehicle" + } + } } } From 0d5571811707f88742b8a4bb1cbc10e117e7d010 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 22 Aug 2023 22:42:33 +0300 Subject: [PATCH 0744/1151] Downgrade Debouncer call ignored log message (#98840) --- homeassistant/helpers/debounce.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 4e5d152135a..54b90077cdc 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -64,9 +64,7 @@ class Debouncer(Generic[_R_co]): async def async_call(self) -> None: """Call the function.""" if self._shutdown_requested: - self.logger.warning( - "Debouncer call ignored as shutdown has been requested." - ) + self.logger.debug("Debouncer call ignored as shutdown has been requested.") return assert self._job is not None From 0ab0901f0f3dc8cc118bc56359b23dd7e8bdce0c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 21:58:57 +0200 Subject: [PATCH 0745/1151] Add entity translations to Powerwall (#98843) --- .../components/powerwall/binary_sensor.py | 9 +- homeassistant/components/powerwall/entity.py | 2 + homeassistant/components/powerwall/sensor.py | 20 ++- .../components/powerwall/strings.json | 137 ++++++++++++++++++ homeassistant/components/powerwall/switch.py | 3 +- .../powerwall/test_binary_sensor.py | 33 +++-- tests/components/powerwall/test_sensor.py | 40 ++--- 7 files changed, 191 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index 0bb089898d1..084ec0ea8a6 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -44,7 +44,7 @@ async def async_setup_entry( class PowerWallRunningSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall running sensor.""" - _attr_name = "Powerwall Status" + _attr_translation_key = "status" _attr_device_class = BinarySensorDeviceClass.POWER @property @@ -61,7 +61,7 @@ class PowerWallRunningSensor(PowerWallEntity, BinarySensorEntity): class PowerWallConnectedSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall connected sensor.""" - _attr_name = "Powerwall Connected to Tesla" + _attr_translation_key = "connected_to_tesla" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY @property @@ -78,7 +78,7 @@ class PowerWallConnectedSensor(PowerWallEntity, BinarySensorEntity): class PowerWallGridServicesActiveSensor(PowerWallEntity, BinarySensorEntity): """Representation of a Powerwall grid services active sensor.""" - _attr_name = "Grid Services Active" + _attr_translation_key = "grid_services_active" _attr_device_class = BinarySensorDeviceClass.POWER @property @@ -95,7 +95,7 @@ class PowerWallGridServicesActiveSensor(PowerWallEntity, BinarySensorEntity): class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall grid status sensor.""" - _attr_name = "Grid Status" + _attr_translation_key = "grid_status" _attr_device_class = BinarySensorDeviceClass.POWER @property @@ -112,7 +112,6 @@ class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorEntity): class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall charging status sensor.""" - _attr_name = "Powerwall Charging" _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING @property diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index db1f5997e3e..f0cfec2cbc5 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -20,6 +20,8 @@ from .models import PowerwallData, PowerwallRuntimeData class PowerWallEntity(CoordinatorEntity[DataUpdateCoordinator[PowerwallData]]): """Base class for powerwall entities.""" + _attr_has_entity_name = True + def __init__(self, powerwall_data: PowerwallRuntimeData) -> None: """Initialize the entity.""" base_info = powerwall_data[POWERWALL_BASE_INFO] diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index cf20e51314f..3f02c925f9d 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -69,7 +69,7 @@ def _get_meter_average_voltage(meter: Meter) -> float: POWERWALL_INSTANT_SENSORS = ( PowerwallSensorEntityDescription( key="instant_power", - name="Now", + translation_key="instant_power", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=UnitOfPower.KILO_WATT, @@ -77,7 +77,7 @@ POWERWALL_INSTANT_SENSORS = ( ), PowerwallSensorEntityDescription( key="instant_frequency", - name="Frequency Now", + translation_key="instant_frequency", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.FREQUENCY, native_unit_of_measurement=UnitOfFrequency.HERTZ, @@ -86,7 +86,7 @@ POWERWALL_INSTANT_SENSORS = ( ), PowerwallSensorEntityDescription( key="instant_current", - name="Average Current Now", + translation_key="instant_current", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, @@ -95,7 +95,7 @@ POWERWALL_INSTANT_SENSORS = ( ), PowerwallSensorEntityDescription( key="instant_voltage", - name="Average Voltage Now", + translation_key="instant_voltage", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, @@ -136,7 +136,7 @@ async def async_setup_entry( class PowerWallChargeSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall charge sensor.""" - _attr_name = "Powerwall Charge" + _attr_translation_key = "charge" _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = PERCENTAGE _attr_device_class = SensorDeviceClass.BATTERY @@ -167,10 +167,8 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): self.entity_description = description super().__init__(powerwall_data) self._meter = meter - self._attr_name = f"Powerwall {self._meter.value.title()} {description.name}" - self._attr_unique_id = ( - f"{self.base_unique_id}_{self._meter.value}_{description.key}" - ) + self._attr_translation_key = f"{meter.value}_{description.translation_key}" + self._attr_unique_id = f"{self.base_unique_id}_{meter.value}_{description.key}" @property def native_value(self) -> float: @@ -181,7 +179,7 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity): """Representation of the Powerwall backup reserve setting.""" - _attr_name = "Powerwall Backup Reserve" + _attr_translation_key = "backup_reserve" _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = PERCENTAGE _attr_device_class = SensorDeviceClass.BATTERY @@ -215,7 +213,7 @@ class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): """Initialize the sensor.""" super().__init__(powerwall_data) self._meter = meter - self._attr_name = f"Powerwall {meter.value.title()} {meter_direction.title()}" + self._attr_translation_key = f"{meter.value}_{meter_direction}" self._attr_unique_id = f"{self.base_unique_id}_{meter.value}_{meter_direction}" @property diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json index 6306d52838e..dacf63a68dd 100644 --- a/homeassistant/components/powerwall/strings.json +++ b/homeassistant/components/powerwall/strings.json @@ -33,5 +33,142 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "binary_sensor": { + "status": { + "name": "Status" + }, + "connected_to_tesla": { + "name": "Connected to Tesla" + }, + "grid_status": { + "name": "Grid status" + }, + "grid_services_active": { + "name": "Grid services active" + } + }, + "sensor": { + "charge": { + "name": "Charge" + }, + "solar_instant_power": { + "name": "Solar power" + }, + "solar_instant_frequency": { + "name": "Solar frequency" + }, + "solar_instant_current": { + "name": "Solar current" + }, + "solar_instant_voltage": { + "name": "Solar voltage" + }, + "site_instant_power": { + "name": "Site power" + }, + "site_instant_frequency": { + "name": "Site frequency" + }, + "site_instant_current": { + "name": "Site current" + }, + "site_instant_voltage": { + "name": "Site voltage" + }, + "battery_instant_power": { + "name": "Battery power" + }, + "battery_instant_frequency": { + "name": "Battery frequency" + }, + "battery_instant_current": { + "name": "Battery current" + }, + "battery_instant_voltage": { + "name": "Battery voltage" + }, + "load_instant_power": { + "name": "Load power" + }, + "load_instant_frequency": { + "name": "Load frequency" + }, + "load_instant_current": { + "name": "Load current" + }, + "load_instant_voltage": { + "name": "Load voltage" + }, + "generator_instant_power": { + "name": "Generator power" + }, + "generator_instant_frequency": { + "name": "Generator frequency" + }, + "generator_instant_current": { + "name": "Generator current" + }, + "generator_instant_voltage": { + "name": "Generator voltage" + }, + "busway_instant_power": { + "name": "Busway power" + }, + "busway_instant_frequency": { + "name": "Busway frequency" + }, + "busway_instant_current": { + "name": "Busway current" + }, + "busway_instant_voltage": { + "name": "Busway voltage" + }, + "backup_reserve": { + "name": "Backup reserve" + }, + "solar_import": { + "name": "Solar import" + }, + "solar_export": { + "name": "Solar export" + }, + "site_import": { + "name": "Site import" + }, + "site_export": { + "name": "Site export" + }, + "battery_import": { + "name": "Battery import" + }, + "battery_export": { + "name": "Battery export" + }, + "load_import": { + "name": "Load import" + }, + "load_export": { + "name": "Load export" + }, + "generator_import": { + "name": "Generator import" + }, + "generator_export": { + "name": "Generator export" + }, + "busway_import": { + "name": "Busway import" + }, + "busway_export": { + "name": "Busway export" + } + }, + "switch": { + "off_grid_operation": { + "name": "Off-grid operation" + } + } } } diff --git a/homeassistant/components/powerwall/switch.py b/homeassistant/components/powerwall/switch.py index 48db62df97a..8516890d633 100644 --- a/homeassistant/components/powerwall/switch.py +++ b/homeassistant/components/powerwall/switch.py @@ -34,8 +34,7 @@ async def async_setup_entry( class PowerwallOffGridEnabledEntity(PowerWallEntity, SwitchEntity): """Representation of a Switch entity for Powerwall Off-grid operation.""" - _attr_name = "Off-Grid operation" - _attr_has_entity_name = True + _attr_translation_key = "off_grid_operation" _attr_entity_category = EntityCategory.CONFIG _attr_device_class = SwitchDeviceClass.SWITCH diff --git a/tests/components/powerwall/test_binary_sensor.py b/tests/components/powerwall/test_binary_sensor.py index acea33186a8..b0a62f42368 100644 --- a/tests/components/powerwall/test_binary_sensor.py +++ b/tests/components/powerwall/test_binary_sensor.py @@ -26,47 +26,50 @@ async def test_sensors(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("binary_sensor.grid_services_active") + state = hass.states.get("binary_sensor.mysite_grid_services_active") assert state.state == STATE_ON expected_attributes = { - "friendly_name": "Grid Services Active", + "friendly_name": "MySite Grid services active", "device_class": "power", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all(item in state.attributes.items() for item in expected_attributes.items()) - state = hass.states.get("binary_sensor.grid_status") - assert state.state == STATE_ON - expected_attributes = {"friendly_name": "Grid Status", "device_class": "power"} - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - assert all(item in state.attributes.items() for item in expected_attributes.items()) - - state = hass.states.get("binary_sensor.powerwall_status") + state = hass.states.get("binary_sensor.mysite_grid_status") assert state.state == STATE_ON expected_attributes = { - "friendly_name": "Powerwall Status", + "friendly_name": "MySite Grid status", "device_class": "power", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all(item in state.attributes.items() for item in expected_attributes.items()) - state = hass.states.get("binary_sensor.powerwall_connected_to_tesla") + state = hass.states.get("binary_sensor.mysite_status") assert state.state == STATE_ON expected_attributes = { - "friendly_name": "Powerwall Connected to Tesla", + "friendly_name": "MySite Status", + "device_class": "power", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + + state = hass.states.get("binary_sensor.mysite_connected_to_tesla") + assert state.state == STATE_ON + expected_attributes = { + "friendly_name": "MySite Connected to Tesla", "device_class": "connectivity", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all(item in state.attributes.items() for item in expected_attributes.items()) - state = hass.states.get("binary_sensor.powerwall_charging") + state = hass.states.get("binary_sensor.mysite_charging") assert state.state == STATE_ON expected_attributes = { - "friendly_name": "Powerwall Charging", + "friendly_name": "MySite Charging", "device_class": "battery_charging", } # Only test for a subset of attributes in case diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index a0d4d7f9e96..e7772571c86 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -47,58 +47,58 @@ async def test_sensors( assert reg_device.manufacturer == "Tesla" assert reg_device.name == "MySite" - state = hass.states.get("sensor.powerwall_load_now") + state = hass.states.get("sensor.mysite_load_power") assert state.state == "1.971" attributes = state.attributes assert attributes[ATTR_DEVICE_CLASS] == "power" assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "kW" assert attributes[ATTR_STATE_CLASS] == "measurement" - assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Now" + assert attributes[ATTR_FRIENDLY_NAME] == "MySite Load power" - state = hass.states.get("sensor.powerwall_load_frequency_now") + state = hass.states.get("sensor.mysite_load_frequency") assert state.state == "60" attributes = state.attributes assert attributes[ATTR_DEVICE_CLASS] == "frequency" assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "Hz" assert attributes[ATTR_STATE_CLASS] == "measurement" - assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Frequency Now" + assert attributes[ATTR_FRIENDLY_NAME] == "MySite Load frequency" - state = hass.states.get("sensor.powerwall_load_average_voltage_now") + state = hass.states.get("sensor.mysite_load_voltage") assert state.state == "120.7" attributes = state.attributes assert attributes[ATTR_DEVICE_CLASS] == "voltage" assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "V" assert attributes[ATTR_STATE_CLASS] == "measurement" - assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Average Voltage Now" + assert attributes[ATTR_FRIENDLY_NAME] == "MySite Load voltage" - state = hass.states.get("sensor.powerwall_load_average_current_now") + state = hass.states.get("sensor.mysite_load_current") assert state.state == "0" attributes = state.attributes assert attributes[ATTR_DEVICE_CLASS] == "current" assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "A" assert attributes[ATTR_STATE_CLASS] == "measurement" - assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Average Current Now" + assert attributes[ATTR_FRIENDLY_NAME] == "MySite Load current" - assert float(hass.states.get("sensor.powerwall_load_export").state) == 1056.8 - assert float(hass.states.get("sensor.powerwall_load_import").state) == 4693.0 + assert float(hass.states.get("sensor.mysite_load_export").state) == 1056.8 + assert float(hass.states.get("sensor.mysite_load_import").state) == 4693.0 - state = hass.states.get("sensor.powerwall_battery_now") + state = hass.states.get("sensor.mysite_battery_power") assert state.state == "-8.55" - assert float(hass.states.get("sensor.powerwall_battery_export").state) == 3620.0 - assert float(hass.states.get("sensor.powerwall_battery_import").state) == 4216.2 + assert float(hass.states.get("sensor.mysite_battery_export").state) == 3620.0 + assert float(hass.states.get("sensor.mysite_battery_import").state) == 4216.2 - state = hass.states.get("sensor.powerwall_solar_now") + state = hass.states.get("sensor.mysite_solar_power") assert state.state == "10.49" - assert float(hass.states.get("sensor.powerwall_solar_export").state) == 9864.2 - assert float(hass.states.get("sensor.powerwall_solar_import").state) == 28.2 + assert float(hass.states.get("sensor.mysite_solar_export").state) == 9864.2 + assert float(hass.states.get("sensor.mysite_solar_import").state) == 28.2 - state = hass.states.get("sensor.powerwall_charge") + state = hass.states.get("sensor.mysite_charge") assert state.state == "47" expected_attributes = { "unit_of_measurement": PERCENTAGE, - "friendly_name": "Powerwall Charge", + "friendly_name": "MySite Charge", "device_class": "battery", } # Only test for a subset of attributes in case @@ -106,11 +106,11 @@ async def test_sensors( for key, value in expected_attributes.items(): assert state.attributes[key] == value - state = hass.states.get("sensor.powerwall_backup_reserve") + state = hass.states.get("sensor.mysite_backup_reserve") assert state.state == "15" expected_attributes = { "unit_of_measurement": PERCENTAGE, - "friendly_name": "Powerwall Backup Reserve", + "friendly_name": "MySite Backup reserve", "device_class": "battery", } # Only test for a subset of attributes in case From 35a8385d9d319c72293164c0f732cb8f193dd1ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Aug 2023 15:00:25 -0500 Subject: [PATCH 0746/1151] Bump zeroconf to 0.82.1 (#98839) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 5b605721782..6b04b6c7c4a 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.81.0"] + "requirements": ["zeroconf==0.82.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index da38d4abd50..b09296888fe 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.81.0 +zeroconf==0.82.1 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index da72addb8ee..ece32ca2308 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2755,7 +2755,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.81.0 +zeroconf==0.82.1 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ace24eccd59..40200466222 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2028,7 +2028,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.81.0 +zeroconf==0.82.1 # homeassistant.components.zeversolar zeversolar==0.3.1 From 57bc8ae68e3a8683a557d6b4f54711b486380eb4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Aug 2023 08:00:38 +1200 Subject: [PATCH 0747/1151] Set assist pipeline binary sensor to true only when stt-start is received (#98844) --- homeassistant/components/esphome/manager.py | 1 - homeassistant/components/esphome/voice_assistant.py | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index fb3e0a1e79a..c7433080c84 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -343,7 +343,6 @@ class ESPHomeManager: ), "esphome.voice_assistant_udp_server.run_pipeline", ) - self.entry_data.async_set_assist_pipeline_state(True) return port diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index a9397eda935..c501d756e54 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -71,6 +71,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): self.hass = hass assert entry_data.device_info is not None + self.entry_data = entry_data self.device_info = entry_data.device_info self.queue: asyncio.Queue[bytes] = asyncio.Queue() @@ -158,7 +159,9 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): data_to_send = None error = False - if event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END: + if event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_START: + self.entry_data.async_set_assist_pipeline_state(True) + elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END: assert event.data is not None data_to_send = {"text": event.data["stt_output"]["text"]} elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END: From f10a5b7ee89653b328efa97a8d1d992d51296945 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 22:09:18 +0200 Subject: [PATCH 0748/1151] Add entity translations to Dexcom (#98795) --- homeassistant/components/dexcom/sensor.py | 7 ++- homeassistant/components/dexcom/strings.json | 10 ++++ tests/components/dexcom/test_sensor.py | 48 ++++++-------------- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/dexcom/sensor.py b/homeassistant/components/dexcom/sensor.py index 1a3c2a21011..126d946e57d 100644 --- a/homeassistant/components/dexcom/sensor.py +++ b/homeassistant/components/dexcom/sensor.py @@ -38,6 +38,8 @@ async def async_setup_entry( class DexcomSensorEntity(CoordinatorEntity, SensorEntity): """Base Dexcom sensor entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, username: str, entry_id: str, key: str ) -> None: @@ -54,6 +56,7 @@ class DexcomGlucoseValueSensor(DexcomSensorEntity): """Representation of a Dexcom glucose value sensor.""" _attr_icon = GLUCOSE_VALUE_ICON + _attr_translation_key = "glucose_value" def __init__( self, @@ -66,7 +69,6 @@ class DexcomGlucoseValueSensor(DexcomSensorEntity): super().__init__(coordinator, username, entry_id, "value") self._attr_native_unit_of_measurement = unit_of_measurement self._key = "mg_dl" if unit_of_measurement == MG_DL else "mmol_l" - self._attr_name = f"{DOMAIN}_{username}_glucose_value" @property def native_value(self): @@ -79,12 +81,13 @@ class DexcomGlucoseValueSensor(DexcomSensorEntity): class DexcomGlucoseTrendSensor(DexcomSensorEntity): """Representation of a Dexcom glucose trend sensor.""" + _attr_translation_key = "glucose_trend" + def __init__( self, coordinator: DataUpdateCoordinator, username: str, entry_id: str ) -> None: """Initialize the sensor.""" super().__init__(coordinator, username, entry_id, "trend") - self._attr_name = f"{DOMAIN}_{username}_glucose_trend" @property def icon(self): diff --git a/homeassistant/components/dexcom/strings.json b/homeassistant/components/dexcom/strings.json index 35d80371c12..7efc2708bcc 100644 --- a/homeassistant/components/dexcom/strings.json +++ b/homeassistant/components/dexcom/strings.json @@ -28,5 +28,15 @@ } } } + }, + "entity": { + "sensor": { + "glucose_value": { + "name": "Glucose value" + }, + "glucose_trend": { + "name": "Glucose trend" + } + } } } diff --git a/tests/components/dexcom/test_sensor.py b/tests/components/dexcom/test_sensor.py index 8e1974a3533..a211f0606f3 100644 --- a/tests/components/dexcom/test_sensor.py +++ b/tests/components/dexcom/test_sensor.py @@ -19,13 +19,9 @@ async def test_sensors(hass: HomeAssistant) -> None: """Test we get sensor data.""" await init_integration(hass) - test_username_glucose_value = hass.states.get( - "sensor.dexcom_test_username_glucose_value" - ) + test_username_glucose_value = hass.states.get("sensor.test_username_glucose_value") assert test_username_glucose_value.state == str(GLUCOSE_READING.value) - test_username_glucose_trend = hass.states.get( - "sensor.dexcom_test_username_glucose_trend" - ) + test_username_glucose_trend = hass.states.get("sensor.test_username_glucose_trend") assert test_username_glucose_trend.state == GLUCOSE_READING.trend_description @@ -37,16 +33,12 @@ async def test_sensors_unknown(hass: HomeAssistant) -> None: "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", return_value=None, ): - await async_update_entity(hass, "sensor.dexcom_test_username_glucose_value") - await async_update_entity(hass, "sensor.dexcom_test_username_glucose_trend") + await async_update_entity(hass, "sensor.test_username_glucose_value") + await async_update_entity(hass, "sensor.test_username_glucose_trend") - test_username_glucose_value = hass.states.get( - "sensor.dexcom_test_username_glucose_value" - ) + test_username_glucose_value = hass.states.get("sensor.test_username_glucose_value") assert test_username_glucose_value.state == STATE_UNKNOWN - test_username_glucose_trend = hass.states.get( - "sensor.dexcom_test_username_glucose_trend" - ) + test_username_glucose_trend = hass.states.get("sensor.test_username_glucose_trend") assert test_username_glucose_trend.state == STATE_UNKNOWN @@ -58,16 +50,12 @@ async def test_sensors_update_failed(hass: HomeAssistant) -> None: "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", side_effect=SessionError, ): - await async_update_entity(hass, "sensor.dexcom_test_username_glucose_value") - await async_update_entity(hass, "sensor.dexcom_test_username_glucose_trend") + await async_update_entity(hass, "sensor.test_username_glucose_value") + await async_update_entity(hass, "sensor.test_username_glucose_trend") - test_username_glucose_value = hass.states.get( - "sensor.dexcom_test_username_glucose_value" - ) + test_username_glucose_value = hass.states.get("sensor.test_username_glucose_value") assert test_username_glucose_value.state == STATE_UNAVAILABLE - test_username_glucose_trend = hass.states.get( - "sensor.dexcom_test_username_glucose_trend" - ) + test_username_glucose_trend = hass.states.get("sensor.test_username_glucose_trend") assert test_username_glucose_trend.state == STATE_UNAVAILABLE @@ -75,13 +63,9 @@ async def test_sensors_options_changed(hass: HomeAssistant) -> None: """Test we handle sensor unavailable.""" entry = await init_integration(hass) - test_username_glucose_value = hass.states.get( - "sensor.dexcom_test_username_glucose_value" - ) + test_username_glucose_value = hass.states.get("sensor.test_username_glucose_value") assert test_username_glucose_value.state == str(GLUCOSE_READING.value) - test_username_glucose_trend = hass.states.get( - "sensor.dexcom_test_username_glucose_trend" - ) + test_username_glucose_trend = hass.states.get("sensor.test_username_glucose_trend") assert test_username_glucose_trend.state == GLUCOSE_READING.trend_description with patch( @@ -99,11 +83,7 @@ async def test_sensors_options_changed(hass: HomeAssistant) -> None: assert entry.options == {CONF_UNIT_OF_MEASUREMENT: MMOL_L} - test_username_glucose_value = hass.states.get( - "sensor.dexcom_test_username_glucose_value" - ) + test_username_glucose_value = hass.states.get("sensor.test_username_glucose_value") assert test_username_glucose_value.state == str(GLUCOSE_READING.mmol_l) - test_username_glucose_trend = hass.states.get( - "sensor.dexcom_test_username_glucose_trend" - ) + test_username_glucose_trend = hass.states.get("sensor.test_username_glucose_trend") assert test_username_glucose_trend.state == GLUCOSE_READING.trend_description From afc3899a1b7654a27b35a3993c3148491f9211cd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 22:16:30 +0200 Subject: [PATCH 0749/1151] Add device info to peco (#98836) --- homeassistant/components/peco/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/peco/sensor.py b/homeassistant/components/peco/sensor.py index 5afc300bfa8..e11d8e7ac0b 100644 --- a/homeassistant/components/peco/sensor.py +++ b/homeassistant/components/peco/sensor.py @@ -13,6 +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 DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -104,6 +105,8 @@ class PecoSensor( entity_description: PECOSensorEntityDescription + _attr_has_entity_name = True + def __init__( self, description: PECOSensorEntityDescription, @@ -112,8 +115,10 @@ class PecoSensor( ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._attr_name = f"{county.capitalize()} {description.name}" self._attr_unique_id = f"{county}-{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, county)}, name=county.capitalize() + ) self.entity_description = description @property From b65e3ddc99d8dfee73e5fffd998f981e3e08df0b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 22:21:20 +0200 Subject: [PATCH 0750/1151] Add entity translations to OVO Energy (#98835) --- .../components/ovo_energy/__init__.py | 2 ++ homeassistant/components/ovo_energy/sensor.py | 16 +++++------ .../components/ovo_energy/strings.json | 28 +++++++++++++++++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 99dd02a36a1..f9547fc3493 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -98,6 +98,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class OVOEnergyEntity(CoordinatorEntity[DataUpdateCoordinator[OVODailyUsage]]): """Defines a base OVO Energy entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator[OVODailyUsage], diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 2a4005e748f..b32a17f0323 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -43,7 +43,7 @@ class OVOEnergySensorEntityDescription(SensorEntityDescription): SENSOR_TYPES_ELECTRICITY: tuple[OVOEnergySensorEntityDescription, ...] = ( OVOEnergySensorEntityDescription( key="last_electricity_reading", - name="OVO Last Electricity Reading", + translation_key="last_electricity_reading", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -51,7 +51,7 @@ SENSOR_TYPES_ELECTRICITY: tuple[OVOEnergySensorEntityDescription, ...] = ( ), OVOEnergySensorEntityDescription( key=KEY_LAST_ELECTRICITY_COST, - name="OVO Last Electricity Cost", + translation_key=KEY_LAST_ELECTRICITY_COST, device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL_INCREASING, value=lambda usage: usage.electricity[-1].cost.amount @@ -60,14 +60,14 @@ SENSOR_TYPES_ELECTRICITY: tuple[OVOEnergySensorEntityDescription, ...] = ( ), OVOEnergySensorEntityDescription( key="last_electricity_start_time", - name="OVO Last Electricity Start Time", + translation_key="last_electricity_start_time", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, value=lambda usage: dt_util.as_utc(usage.electricity[-1].interval.start), ), OVOEnergySensorEntityDescription( key="last_electricity_end_time", - name="OVO Last Electricity End Time", + translation_key="last_electricity_end_time", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, value=lambda usage: dt_util.as_utc(usage.electricity[-1].interval.end), @@ -77,7 +77,7 @@ SENSOR_TYPES_ELECTRICITY: tuple[OVOEnergySensorEntityDescription, ...] = ( SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = ( OVOEnergySensorEntityDescription( key="last_gas_reading", - name="OVO Last Gas Reading", + translation_key="last_gas_reading", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, @@ -86,7 +86,7 @@ SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = ( ), OVOEnergySensorEntityDescription( key=KEY_LAST_GAS_COST, - name="OVO Last Gas Cost", + translation_key=KEY_LAST_GAS_COST, device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:cash-multiple", @@ -96,14 +96,14 @@ SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = ( ), OVOEnergySensorEntityDescription( key="last_gas_start_time", - name="OVO Last Gas Start Time", + translation_key="last_gas_start_time", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, value=lambda usage: dt_util.as_utc(usage.gas[-1].interval.start), ), OVOEnergySensorEntityDescription( key="last_gas_end_time", - name="OVO Last Gas End Time", + translation_key="last_gas_end_time", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, value=lambda usage: dt_util.as_utc(usage.gas[-1].interval.end), diff --git a/homeassistant/components/ovo_energy/strings.json b/homeassistant/components/ovo_energy/strings.json index 810602b1412..fda0c2996dc 100644 --- a/homeassistant/components/ovo_energy/strings.json +++ b/homeassistant/components/ovo_energy/strings.json @@ -24,5 +24,33 @@ "title": "Reauthentication" } } + }, + "entity": { + "sensor": { + "last_electricity_reading": { + "name": "Last electricity reading" + }, + "last_electricity_cost": { + "name": "Last electricity cost" + }, + "last_electricity_start_time": { + "name": "Last electricity start time" + }, + "last_electricity_end_time": { + "name": "Last electricity end time" + }, + "last_gas_reading": { + "name": "Last gas reading" + }, + "last_gas_cost": { + "name": "Last gas cost" + }, + "last_gas_start_time": { + "name": "Last gas start time" + }, + "last_gas_end_time": { + "name": "Last gas end time" + } + } } } From d179f8b47d268602deadb94432e463abd75c7ce0 Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Tue, 22 Aug 2023 20:23:34 +0000 Subject: [PATCH 0751/1151] Add filter for affected areas in NINA warnings (#97053) * Add affected areas to warnings * Update config flow * Remove option from config_flow * Add regex check * Remove regex check --- homeassistant/components/nina/__init__.py | 35 +++++++- .../components/nina/binary_sensor.py | 4 +- homeassistant/components/nina/config_flow.py | 5 ++ homeassistant/components/nina/const.py | 3 + homeassistant/components/nina/strings.json | 3 +- tests/components/nina/test_binary_sensor.py | 87 +++++++++++++++++++ tests/components/nina/test_config_flow.py | 4 + tests/components/nina/test_init.py | 1 + 8 files changed, 138 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index dfb556deeb5..4ac2518ffb6 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -16,6 +16,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( _LOGGER, + ALL_MATCH_REGEX, + CONF_AREA_FILTER, CONF_FILTER_CORONA, CONF_HEADLINE_FILTER, CONF_REGIONS, @@ -42,8 +44,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: new_data.pop(CONF_FILTER_CORONA, None) hass.config_entries.async_update_entry(entry, data=new_data) + if CONF_AREA_FILTER not in entry.data: + new_data = {**entry.data, CONF_AREA_FILTER: ALL_MATCH_REGEX} + hass.config_entries.async_update_entry(entry, data=new_data) + coordinator = NINADataUpdateCoordinator( - hass, regions, entry.data[CONF_HEADLINE_FILTER] + hass, + regions, + entry.data[CONF_HEADLINE_FILTER], + entry.data[CONF_AREA_FILTER], ) await coordinator.async_config_entry_first_refresh() @@ -77,6 +86,7 @@ class NinaWarningData: sender: str severity: str recommended_actions: str + affected_areas: str sent: str start: str expires: str @@ -89,12 +99,17 @@ class NINADataUpdateCoordinator( """Class to manage fetching NINA data API.""" def __init__( - self, hass: HomeAssistant, regions: dict[str, str], headline_filter: str + self, + hass: HomeAssistant, + regions: dict[str, str], + headline_filter: str, + area_filter: str, ) -> None: """Initialize.""" self._regions: dict[str, str] = regions self._nina: Nina = Nina(async_get_clientsession(hass)) self.headline_filter: str = headline_filter + self.area_filter: str = area_filter for region in regions: self._nina.addRegion(region) @@ -147,6 +162,21 @@ class NINADataUpdateCoordinator( if re.search( self.headline_filter, raw_warn.headline, flags=re.IGNORECASE ): + _LOGGER.debug( + f"Ignore warning ({raw_warn.id}) by headline filter ({self.headline_filter}) with headline: {raw_warn.headline}" + ) + continue + + affected_areas_string: str = ", ".join( + [str(area) for area in raw_warn.affected_areas] + ) + + if not re.search( + self.area_filter, affected_areas_string, flags=re.IGNORECASE + ): + _LOGGER.debug( + f"Ignore warning ({raw_warn.id}) by area filter ({self.area_filter}) with area: {affected_areas_string}" + ) continue warning_data: NinaWarningData = NinaWarningData( @@ -156,6 +186,7 @@ class NINADataUpdateCoordinator( raw_warn.sender, raw_warn.severity, " ".join([str(action) for action in raw_warn.recommended_actions]), + affected_areas_string, raw_warn.sent or "", raw_warn.start or "", raw_warn.expires or "", diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index 24d6d35d0e8..19f802f1cec 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -14,6 +14,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import NINADataUpdateCoordinator from .const import ( + ATTR_AFFECTED_AREAS, ATTR_DESCRIPTION, ATTR_EXPIRES, ATTR_HEADLINE, @@ -73,7 +74,7 @@ class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEnti @property def is_on(self) -> bool: """Return the state of the sensor.""" - if not len(self.coordinator.data[self._region]) > self._warning_index: + if len(self.coordinator.data[self._region]) <= self._warning_index: return False data = self.coordinator.data[self._region][self._warning_index] @@ -94,6 +95,7 @@ class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEnti ATTR_SENDER: data.sender, ATTR_SEVERITY: data.severity, ATTR_RECOMMENDED_ACTIONS: data.recommended_actions, + ATTR_AFFECTED_AREAS: data.affected_areas, ATTR_ID: data.id, ATTR_SENT: data.sent, ATTR_START: data.start, diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py index d41fa6dee3e..9c6de40ac6b 100644 --- a/homeassistant/components/nina/config_flow.py +++ b/homeassistant/components/nina/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.helpers.entity_registry import ( from .const import ( _LOGGER, + CONF_AREA_FILTER, CONF_HEADLINE_FILTER, CONF_MESSAGE_SLOTS, CONF_REGIONS, @@ -263,6 +264,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_HEADLINE_FILTER, default=self.data[CONF_HEADLINE_FILTER], ): cv.string, + vol.Optional( + CONF_AREA_FILTER, + default=self.data[CONF_AREA_FILTER], + ): cv.string, } ), errors=errors, diff --git a/homeassistant/components/nina/const.py b/homeassistant/components/nina/const.py index 36096d97dc1..198e21c2689 100644 --- a/homeassistant/components/nina/const.py +++ b/homeassistant/components/nina/const.py @@ -12,17 +12,20 @@ SCAN_INTERVAL: timedelta = timedelta(minutes=5) DOMAIN: str = "nina" NO_MATCH_REGEX: str = "/(?!)/" +ALL_MATCH_REGEX: str = ".*" CONF_REGIONS: str = "regions" CONF_MESSAGE_SLOTS: str = "slots" CONF_FILTER_CORONA: str = "corona_filter" # deprecated CONF_HEADLINE_FILTER: str = "headline_filter" +CONF_AREA_FILTER: str = "area_filter" ATTR_HEADLINE: str = "headline" ATTR_DESCRIPTION: str = "description" ATTR_SENDER: str = "sender" ATTR_SEVERITY: str = "severity" ATTR_RECOMMENDED_ACTIONS: str = "recommended_actions" +ATTR_AFFECTED_AREAS: str = "affected_areas" ATTR_ID: str = "id" ATTR_SENT: str = "sent" ATTR_START: str = "start" diff --git a/homeassistant/components/nina/strings.json b/homeassistant/components/nina/strings.json index e145f5ea8ca..5e0393d024f 100644 --- a/homeassistant/components/nina/strings.json +++ b/homeassistant/components/nina/strings.json @@ -36,7 +36,8 @@ "_r_to_u": "[%key:component::nina::config::step::user::data::_r_to_u%]", "_v_to_z": "[%key:component::nina::config::step::user::data::_v_to_z%]", "slots": "[%key:component::nina::config::step::user::data::slots%]", - "headline_filter": "[%key:component::nina::config::step::user::data::headline_filter%]" + "headline_filter": "[%key:component::nina::config::step::user::data::headline_filter%]", + "area_filter": "Whitelist regex to filter warnings based on affected areas" } } }, diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index c6fd5bdd830..8532415c6b1 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -6,6 +6,7 @@ from unittest.mock import patch from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.nina.const import ( + ATTR_AFFECTED_AREAS, ATTR_DESCRIPTION, ATTR_EXPIRES, ATTR_HEADLINE, @@ -38,6 +39,13 @@ ENTRY_DATA_NO_CORONA: dict[str, Any] = { "regions": {"083350000000": "Aach, Stadt"}, } +ENTRY_DATA_NO_AREA: dict[str, Any] = { + "slots": 5, + "corona_filter": False, + "area_filter": ".*nagold.*", + "regions": {"083350000000": "Aach, Stadt"}, +} + async def test_sensors(hass: HomeAssistant) -> None: """Test the creation and values of the NINA sensors.""" @@ -70,6 +78,10 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w1.attributes.get(ATTR_SENDER) == "Deutscher Wetterdienst" assert state_w1.attributes.get(ATTR_SEVERITY) == "Minor" assert state_w1.attributes.get(ATTR_RECOMMENDED_ACTIONS) == "" + assert ( + state_w1.attributes.get(ATTR_AFFECTED_AREAS) + == "Gemeinde Oberreichenbach, Gemeinde Neuweiler, Stadt Nagold, Stadt Neubulach, Gemeinde Schömberg, Gemeinde Simmersfeld, Gemeinde Simmozheim, Gemeinde Rohrdorf, Gemeinde Ostelsheim, Gemeinde Ebhausen, Gemeinde Egenhausen, Gemeinde Dobel, Stadt Bad Liebenzell, Stadt Solingen, Stadt Haiterbach, Stadt Bad Herrenalb, Gemeinde Höfen an der Enz, Gemeinde Gechingen, Gemeinde Enzklösterle, Gemeinde Gutach (Schwarzwaldbahn) und 3392 weitere." + ) assert state_w1.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" assert state_w1.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" @@ -87,6 +99,7 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w2.attributes.get(ATTR_SENDER) is None assert state_w2.attributes.get(ATTR_SEVERITY) is None assert state_w2.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w2.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w2.attributes.get(ATTR_ID) is None assert state_w2.attributes.get(ATTR_SENT) is None assert state_w2.attributes.get(ATTR_START) is None @@ -104,6 +117,7 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w3.attributes.get(ATTR_SENDER) is None assert state_w3.attributes.get(ATTR_SEVERITY) is None assert state_w3.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w3.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w3.attributes.get(ATTR_ID) is None assert state_w3.attributes.get(ATTR_SENT) is None assert state_w3.attributes.get(ATTR_START) is None @@ -121,6 +135,7 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w4.attributes.get(ATTR_SENDER) is None assert state_w4.attributes.get(ATTR_SEVERITY) is None assert state_w4.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w4.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w4.attributes.get(ATTR_ID) is None assert state_w4.attributes.get(ATTR_SENT) is None assert state_w4.attributes.get(ATTR_START) is None @@ -138,6 +153,7 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w5.attributes.get(ATTR_SENDER) is None assert state_w5.attributes.get(ATTR_SEVERITY) is None assert state_w5.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w5.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w5.attributes.get(ATTR_ID) is None assert state_w5.attributes.get(ATTR_SENT) is None assert state_w5.attributes.get(ATTR_START) is None @@ -184,6 +200,10 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: state_w1.attributes.get(ATTR_RECOMMENDED_ACTIONS) == "Waschen sich regelmäßig und gründlich die Hände." ) + assert ( + state_w1.attributes.get(ATTR_AFFECTED_AREAS) + == "Bundesland: Freie Hansestadt Bremen, Land Berlin, Land Hessen, Land Nordrhein-Westfalen, Land Brandenburg, Freistaat Bayern, Land Mecklenburg-Vorpommern, Land Rheinland-Pfalz, Freistaat Sachsen, Land Schleswig-Holstein, Freie und Hansestadt Hamburg, Freistaat Thüringen, Land Niedersachsen, Land Saarland, Land Sachsen-Anhalt, Land Baden-Württemberg" + ) assert state_w1.attributes.get(ATTR_ID) == "mow.DE-BW-S-SE018-20211102-18-001" assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T20:07:16+01:00" assert state_w1.attributes.get(ATTR_START) == "" @@ -201,6 +221,10 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: state_w2.attributes.get(ATTR_DESCRIPTION) == "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden." ) + assert ( + state_w2.attributes.get(ATTR_AFFECTED_AREAS) + == "Gemeinde Oberreichenbach, Gemeinde Neuweiler, Stadt Nagold, Stadt Neubulach, Gemeinde Schömberg, Gemeinde Simmersfeld, Gemeinde Simmozheim, Gemeinde Rohrdorf, Gemeinde Ostelsheim, Gemeinde Ebhausen, Gemeinde Egenhausen, Gemeinde Dobel, Stadt Bad Liebenzell, Stadt Solingen, Stadt Haiterbach, Stadt Bad Herrenalb, Gemeinde Höfen an der Enz, Gemeinde Gechingen, Gemeinde Enzklösterle, Gemeinde Gutach (Schwarzwaldbahn) und 3392 weitere." + ) assert state_w2.attributes.get(ATTR_SENDER) == "Deutscher Wetterdienst" assert state_w2.attributes.get(ATTR_SEVERITY) == "Minor" assert state_w2.attributes.get(ATTR_RECOMMENDED_ACTIONS) == "" @@ -221,6 +245,7 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w3.attributes.get(ATTR_SENDER) is None assert state_w3.attributes.get(ATTR_SEVERITY) is None assert state_w3.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w3.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w3.attributes.get(ATTR_ID) is None assert state_w3.attributes.get(ATTR_SENT) is None assert state_w3.attributes.get(ATTR_START) is None @@ -238,6 +263,7 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w4.attributes.get(ATTR_SENDER) is None assert state_w4.attributes.get(ATTR_SEVERITY) is None assert state_w4.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w4.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w4.attributes.get(ATTR_ID) is None assert state_w4.attributes.get(ATTR_SENT) is None assert state_w4.attributes.get(ATTR_START) is None @@ -255,6 +281,7 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w5.attributes.get(ATTR_SENDER) is None assert state_w5.attributes.get(ATTR_SEVERITY) is None assert state_w5.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None + assert state_w5.attributes.get(ATTR_AFFECTED_AREAS) is None assert state_w5.attributes.get(ATTR_ID) is None assert state_w5.attributes.get(ATTR_SENT) is None assert state_w5.attributes.get(ATTR_START) is None @@ -262,3 +289,63 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert entry_w5.unique_id == "083350000000-5" assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + +async def test_sensors_with_area_filter(hass: HomeAssistant) -> None: + """Test the creation and values of the NINA sensors with an area filter.""" + + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + wraps=mocked_request_function, + ): + conf_entry: MockConfigEntry = MockConfigEntry( + domain=DOMAIN, title="NINA", data=ENTRY_DATA_NO_AREA + ) + + entity_registry: er = er.async_get(hass) + conf_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(conf_entry.entry_id) + await hass.async_block_till_done() + + assert conf_entry.state == ConfigEntryState.LOADED + + state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") + entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") + + assert state_w1.state == STATE_ON + + assert entry_w1.unique_id == "083350000000-1" + assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w2 = hass.states.get("binary_sensor.warning_aach_stadt_2") + entry_w2 = entity_registry.async_get("binary_sensor.warning_aach_stadt_2") + + assert state_w2.state == STATE_OFF + + assert entry_w2.unique_id == "083350000000-2" + assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w3 = hass.states.get("binary_sensor.warning_aach_stadt_3") + entry_w3 = entity_registry.async_get("binary_sensor.warning_aach_stadt_3") + + assert state_w3.state == STATE_OFF + + assert entry_w3.unique_id == "083350000000-3" + assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w4 = hass.states.get("binary_sensor.warning_aach_stadt_4") + entry_w4 = entity_registry.async_get("binary_sensor.warning_aach_stadt_4") + + assert state_w4.state == STATE_OFF + + assert entry_w4.unique_id == "083350000000-4" + assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY + + state_w5 = hass.states.get("binary_sensor.warning_aach_stadt_5") + entry_w5 = entity_registry.async_get("binary_sensor.warning_aach_stadt_5") + + assert state_w5.state == STATE_OFF + + assert entry_w5.unique_id == "083350000000-5" + assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index 194f0298dd5..aad24691f42 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -10,6 +10,7 @@ from pynina import ApiError from homeassistant import data_entry_flow from homeassistant.components.nina.const import ( + CONF_AREA_FILTER, CONF_HEADLINE_FILTER, CONF_MESSAGE_SLOTS, CONF_REGIONS, @@ -38,6 +39,7 @@ DUMMY_DATA: dict[str, Any] = { CONST_REGION_R_TO_U: ["072320000000_0", "072320000000_1"], CONST_REGION_V_TO_Z: ["081270000000_0", "081270000000_1"], CONF_HEADLINE_FILTER: ".*corona.*", + CONF_AREA_FILTER: ".*", } DUMMY_RESPONSE_REGIONS: dict[str, Any] = json.loads( @@ -146,6 +148,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: title="NINA", data={ CONF_HEADLINE_FILTER: deepcopy(DUMMY_DATA[CONF_HEADLINE_FILTER]), + CONF_AREA_FILTER: deepcopy(DUMMY_DATA[CONF_AREA_FILTER]), CONF_MESSAGE_SLOTS: deepcopy(DUMMY_DATA[CONF_MESSAGE_SLOTS]), CONST_REGION_A_TO_D: deepcopy(DUMMY_DATA[CONST_REGION_A_TO_D]), CONF_REGIONS: {"095760000000": "Aach"}, @@ -184,6 +187,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: assert dict(config_entry.data) == { CONF_HEADLINE_FILTER: deepcopy(DUMMY_DATA[CONF_HEADLINE_FILTER]), + CONF_AREA_FILTER: deepcopy(DUMMY_DATA[CONF_AREA_FILTER]), CONF_MESSAGE_SLOTS: deepcopy(DUMMY_DATA[CONF_MESSAGE_SLOTS]), CONST_REGION_A_TO_D: ["072350000000_1"], CONST_REGION_E_TO_H: [], diff --git a/tests/components/nina/test_init.py b/tests/components/nina/test_init.py index 826b8e422ed..da73c8d8711 100644 --- a/tests/components/nina/test_init.py +++ b/tests/components/nina/test_init.py @@ -16,6 +16,7 @@ from tests.common import MockConfigEntry ENTRY_DATA: dict[str, Any] = { "slots": 5, "headline_filter": ".*corona.*", + "area_filter": ".*", "regions": {"083350000000": "Aach, Stadt"}, } From 0f58007e97fc8bb1fd2b6a012ce4ad3441d6ec65 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 22 Aug 2023 22:39:55 +0200 Subject: [PATCH 0752/1151] Deprecate aux heat for mqtt climate (#98666) --- homeassistant/components/mqtt/climate.py | 45 ++++++++++++++++++++++ homeassistant/components/mqtt/strings.json | 4 ++ tests/components/mqtt/test_climate.py | 39 ++++++++++++++++--- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index b95cacc2d08..d5bda57c2b3 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -45,6 +45,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.unit_conversion import TemperatureConverter @@ -77,6 +78,7 @@ from .const import ( CONF_TEMP_STATE_TEMPLATE, CONF_TEMP_STATE_TOPIC, DEFAULT_OPTIMISTIC, + DOMAIN, PAYLOAD_NONE, ) from .debug_info import log_messages @@ -92,8 +94,13 @@ from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) +MQTT_CLIMATE_AUX_DOCS = "https://www.home-assistant.io/integrations/climate.mqtt/" + DEFAULT_NAME = "MQTT HVAC" +# Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC +# and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 +# Support will be removed in HA Core 2024.3 CONF_AUX_COMMAND_TOPIC = "aux_command_topic" CONF_AUX_STATE_TEMPLATE = "aux_state_template" CONF_AUX_STATE_TOPIC = "aux_state_topic" @@ -255,6 +262,9 @@ def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { + # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC + # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 + # Support will be removed in HA Core 2024.3 vol.Optional(CONF_AUX_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, vol.Optional(CONF_AUX_STATE_TOPIC): valid_subscribe_topic, @@ -353,6 +363,12 @@ PLATFORM_SCHEMA_MODERN = vol.All( # was removed in HA Core 2023.8 cv.removed(CONF_POWER_STATE_TEMPLATE), cv.removed(CONF_POWER_STATE_TOPIC), + # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC + # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 + # Support will be removed in HA Core 2024.3 + cv.deprecated(CONF_AUX_COMMAND_TOPIC), + cv.deprecated(CONF_AUX_STATE_TEMPLATE), + cv.deprecated(CONF_AUX_STATE_TOPIC), _PLATFORM_SCHEMA_BASE, valid_preset_mode_configuration, valid_humidity_range_configuration, @@ -667,6 +683,9 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): self._attr_swing_mode = SWING_OFF if self._topic[CONF_MODE_STATE_TOPIC] is None or self._optimistic: self._attr_hvac_mode = HVACMode.OFF + # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC + # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 + # Support will be removed in HA Core 2024.3 if self._topic[CONF_AUX_STATE_TOPIC] is None or self._optimistic: self._attr_is_aux_heat = False self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config @@ -738,12 +757,32 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): if self._feature_preset_mode: support |= ClimateEntityFeature.PRESET_MODE + # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC + # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 + # Support will be removed in HA Core 2024.3 if (self._topic[CONF_AUX_STATE_TOPIC] is not None) or ( self._topic[CONF_AUX_COMMAND_TOPIC] is not None ): support |= ClimateEntityFeature.AUX_HEAT self._attr_supported_features = support + async def mqtt_async_added_to_hass(self) -> None: + """Handle deprecation issues.""" + if self._attr_supported_features & ClimateEntityFeature.AUX_HEAT: + async_create_issue( + self.hass, + DOMAIN, + "deprecated_climate_aux_property_{self.entity_id}", + breaks_in_ha_version="2024.3.0", + is_fixable=False, + translation_key="deprecated_climate_aux_property", + translation_placeholders={ + "entity_id": self.entity_id, + }, + learn_more_url=MQTT_CLIMATE_AUX_DOCS, + severity=IssueSeverity.WARNING, + ) + def _prepare_subscribe_topics(self) -> None: # noqa: C901 """(Re)Subscribe to topics.""" topics: dict[str, dict[str, Any]] = {} @@ -876,6 +915,9 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC + # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 + # Support will be removed in HA Core 2024.3 @callback @log_messages(self.hass, self.entity_id) def handle_aux_mode_received(msg: ReceiveMessage) -> None: @@ -986,6 +1028,9 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): return + # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC + # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 + # Support will be removed in HA Core 2024.3 async def _set_aux_heat(self, state: bool) -> None: await self._publish( CONF_AUX_COMMAND_TOPIC, diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index ae6033de5f9..d1b63b331ed 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -15,6 +15,10 @@ "entity_name_startswith_device_name_yaml": { "title": "Manual configured MQTT entities with a name that starts with the device name", "description": "Some MQTT entities have an entity name that starts with the device name. This is not expected. To avoid a duplicate name the device name prefix is stripped off the entity name as a work-a-round. Please update your configuration and restart Home Assistant to fix this issue. \n\nList of affected entities:\n\n{config}" + }, + "deprecated_climate_aux_property": { + "title": "MQTT entities with auxiliary heat support found", + "description": "Entity `{entity_id}` has auxiliary heat support enabled, which has been deprecated for MQTT climate devices. Please adjust your configuration and remove deperated config options from your configration and restart HA to fix this issue." } }, "config": { diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 18a0a860ad4..9e0363b3611 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -70,7 +70,6 @@ from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient ENTITY_CLIMATE = "climate.test" - DEFAULT_CONFIG = { mqtt.DOMAIN: { climate.DOMAIN: { @@ -82,7 +81,6 @@ DEFAULT_CONFIG = { "temperature_high_command_topic": "temperature-high-topic", "fan_mode_command_topic": "fan-mode-topic", "swing_mode_command_topic": "swing-mode-topic", - "aux_command_topic": "aux-topic", "preset_mode_command_topic": "preset-mode-topic", "preset_modes": [ "eco", @@ -225,7 +223,6 @@ async def test_supported_features( | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE - | ClimateEntityFeature.AUX_HEAT | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE | ClimateEntityFeature.TARGET_HUMIDITY ) @@ -1249,11 +1246,16 @@ async def test_set_preset_mode_pessimistic( assert state.attributes.get("preset_mode") == "home" +# Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC +# and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 +# Support will be removed in HA Core 2024.3 @pytest.mark.parametrize( "hass_config", [ help_custom_config( - climate.DOMAIN, DEFAULT_CONFIG, ({"aux_state_topic": "aux-state"},) + climate.DOMAIN, + DEFAULT_CONFIG, + ({"aux_command_topic": "aux-topic", "aux_state_topic": "aux-state"},), ) ], ) @@ -1283,7 +1285,18 @@ async def test_set_aux_pessimistic( assert state.attributes.get("aux_heat") == "off" -@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +# Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC +# and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 +# Support will be removed in HA Core 2024.3 +# "aux_command_topic": "aux-topic" +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + climate.DOMAIN, DEFAULT_CONFIG, ({"aux_command_topic": "aux-topic"},) + ) + ], +) async def test_set_aux( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: @@ -1303,6 +1316,18 @@ async def test_set_aux( state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("aux_heat") == "off" + support = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.AUX_HEAT + | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.TARGET_HUMIDITY + ) + + assert state.attributes.get("supported_features") == support + @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_availability_when_connection_lost( @@ -1548,6 +1573,10 @@ async def test_get_with_templates( assert state.attributes.get("preset_mode") == "eco" # Aux mode + + # Options CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC + # and CONF_AUX_STATE_TEMPLATE were deprecated in HA Core 2023.9 + # Support will be removed in HA Core 2024.3 assert state.attributes.get("aux_heat") == "off" async_fire_mqtt_message(hass, "aux-state", "switchmeon") state = hass.states.get(ENTITY_CLIMATE) From af915f1425802d01c10c7681ac71d7086c7f1a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 22 Aug 2023 22:41:03 +0200 Subject: [PATCH 0753/1151] Add Airzone Cloud System binary sensors (#95121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * airzone_cloud: add System binary sensors Signed-off-by: Álvaro Fernández Rojas * airzone-cloud: add error example Signed-off-by: Álvaro Fernández Rojas --------- Signed-off-by: Álvaro Fernández Rojas --- .../components/airzone_cloud/binary_sensor.py | 55 ++++++++++++++++++- .../components/airzone_cloud/entity.py | 30 ++++++++++ .../airzone_cloud/test_binary_sensor.py | 12 ++++ tests/components/airzone_cloud/util.py | 7 ++- 4 files changed, 102 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airzone_cloud/binary_sensor.py b/homeassistant/components/airzone_cloud/binary_sensor.py index 765eec2d288..a364ad0d753 100644 --- a/homeassistant/components/airzone_cloud/binary_sensor.py +++ b/homeassistant/components/airzone_cloud/binary_sensor.py @@ -9,6 +9,7 @@ from aioairzone_cloud.const import ( AZD_AIDOOS, AZD_ERRORS, AZD_PROBLEMS, + AZD_SYSTEMS, AZD_WARNINGS, AZD_ZONES, ) @@ -25,7 +26,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import AirzoneUpdateCoordinator -from .entity import AirzoneAidooEntity, AirzoneEntity, AirzoneZoneEntity +from .entity import ( + AirzoneAidooEntity, + AirzoneEntity, + AirzoneSystemEntity, + AirzoneZoneEntity, +) @dataclass @@ -51,6 +57,20 @@ AIDOO_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ... ), ) + +SYSTEM_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( + AirzoneBinarySensorEntityDescription( + attributes={ + "errors": AZD_ERRORS, + "warnings": AZD_WARNINGS, + }, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + key=AZD_PROBLEMS, + ), +) + + ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( AirzoneBinarySensorEntityDescription( device_class=BinarySensorDeviceClass.RUNNING, @@ -87,6 +107,18 @@ async def async_setup_entry( ) ) + for system_id, system_data in coordinator.data.get(AZD_SYSTEMS, {}).items(): + for description in SYSTEM_BINARY_SENSOR_TYPES: + if description.key in system_data: + binary_sensors.append( + AirzoneSystemBinarySensor( + coordinator, + description, + system_id, + system_data, + ) + ) + for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items(): for description in ZONE_BINARY_SENSOR_TYPES: if description.key in zone_data: @@ -145,6 +177,27 @@ class AirzoneAidooBinarySensor(AirzoneAidooEntity, AirzoneBinarySensor): self._async_update_attrs() +class AirzoneSystemBinarySensor(AirzoneSystemEntity, AirzoneBinarySensor): + """Define an Airzone Cloud System binary sensor.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: AirzoneBinarySensorEntityDescription, + system_id: str, + system_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, system_id, system_data) + + self._attr_unique_id = f"{system_id}_{description.key}" + self.entity_description = description + + self._async_update_attrs() + + class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor): """Define an Airzone Cloud Zone binary sensor.""" diff --git a/homeassistant/components/airzone_cloud/entity.py b/homeassistant/components/airzone_cloud/entity.py index 32c41b8f1cd..090e81e4170 100644 --- a/homeassistant/components/airzone_cloud/entity.py +++ b/homeassistant/components/airzone_cloud/entity.py @@ -10,6 +10,7 @@ from aioairzone_cloud.const import ( AZD_FIRMWARE, AZD_NAME, AZD_SYSTEM_ID, + AZD_SYSTEMS, AZD_WEBSERVER, AZD_WEBSERVERS, AZD_ZONES, @@ -65,6 +66,35 @@ class AirzoneAidooEntity(AirzoneEntity): return value +class AirzoneSystemEntity(AirzoneEntity): + """Define an Airzone Cloud System entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + system_id: str, + system_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self.system_id = system_id + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, system_id)}, + manufacturer=MANUFACTURER, + name=system_data[AZD_NAME], + via_device=(DOMAIN, system_data[AZD_WEBSERVER]), + ) + + def get_airzone_value(self, key: str) -> Any: + """Return system value by key.""" + value = None + if system := self.coordinator.data[AZD_SYSTEMS].get(self.system_id): + value = system.get(key) + return value + + class AirzoneWebServerEntity(AirzoneEntity): """Define an Airzone Cloud WebServer entity.""" diff --git a/tests/components/airzone_cloud/test_binary_sensor.py b/tests/components/airzone_cloud/test_binary_sensor.py index 14f7a078156..a1b5d5319c0 100644 --- a/tests/components/airzone_cloud/test_binary_sensor.py +++ b/tests/components/airzone_cloud/test_binary_sensor.py @@ -1,5 +1,7 @@ """The binary sensor tests for the Airzone Cloud platform.""" +from aioairzone_cloud.const import API_OLD_ID + from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -20,6 +22,16 @@ async def test_airzone_create_binary_sensors(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.bron_running") assert state.state == STATE_OFF + # Systems + state = hass.states.get("binary_sensor.system_1_problem") + assert state.state == STATE_ON + assert state.attributes.get("errors") == [ + { + API_OLD_ID: "error-id", + }, + ] + assert state.attributes.get("warnings") is None + # Zones state = hass.states.get("binary_sensor.dormitorio_problem") assert state.state == STATE_OFF diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 21459be60e4..8fd7da06853 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -25,6 +25,7 @@ from aioairzone_cloud.const import ( API_LOCAL_TEMP, API_META, API_NAME, + API_OLD_ID, API_STAT_AP_MAC, API_STAT_CHANNEL, API_STAT_QUALITY, @@ -175,7 +176,11 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: } if device.get_id() == "system1": return { - API_ERRORS: [], + API_ERRORS: [ + { + API_OLD_ID: "error-id", + }, + ], API_IS_CONNECTED: True, API_WS_CONNECTED: True, API_WARNINGS: [], From 99b5c4932fd04344294dcaf213d0d4d673602ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 22 Aug 2023 22:48:05 +0200 Subject: [PATCH 0754/1151] Add hot water sensor support to Airzone (#98500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * airzone: sensors: add hot water support Signed-off-by: Álvaro Fernández Rojas * airzone: sensor: dhw: enable _attr_has_entity_name Signed-off-by: Álvaro Fernández Rojas * Add requested changes Signed-off-by: Álvaro Fernández Rojas --------- Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/entity.py | 26 +++++++++++ homeassistant/components/airzone/sensor.py | 53 +++++++++++++++++++++- tests/components/airzone/test_sensor.py | 4 ++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index 021aaa3535c..267cd210ff0 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -10,6 +10,7 @@ from aioairzone.const import ( AZD_AVAILABLE, AZD_FIRMWARE, AZD_FULL_NAME, + AZD_HOT_WATER, AZD_ID, AZD_MAC, AZD_MODEL, @@ -81,6 +82,31 @@ class AirzoneSystemEntity(AirzoneEntity): return value +class AirzoneHotWaterEntity(AirzoneEntity): + """Define an Airzone Hot Water entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{entry.entry_id}_dhw")}, + manufacturer=MANUFACTURER, + model="DHW", + name=self.get_airzone_value(AZD_NAME), + via_device=(DOMAIN, f"{entry.entry_id}_ws"), + ) + self._attr_unique_id = entry.unique_id or entry.entry_id + + def get_airzone_value(self, key: str) -> Any: + """Return DHW value by key.""" + return self.coordinator.data[AZD_HOT_WATER].get(key) + + class AirzoneWebServerEntity(AirzoneEntity): """Define an Airzone WebServer entity.""" diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py index d90fdf93607..1dd67294aff 100644 --- a/homeassistant/components/airzone/sensor.py +++ b/homeassistant/components/airzone/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any, Final from aioairzone.const import ( + AZD_HOT_WATER, AZD_HUMIDITY, AZD_NAME, AZD_TEMP, @@ -31,7 +32,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS from .coordinator import AirzoneUpdateCoordinator -from .entity import AirzoneEntity, AirzoneWebServerEntity, AirzoneZoneEntity +from .entity import ( + AirzoneEntity, + AirzoneHotWaterEntity, + AirzoneWebServerEntity, + AirzoneZoneEntity, +) + +HOT_WATER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( + SensorEntityDescription( + device_class=SensorDeviceClass.TEMPERATURE, + key=AZD_TEMP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), +) WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( @@ -71,6 +86,18 @@ async def async_setup_entry( sensors: list[AirzoneSensor] = [] + if AZD_HOT_WATER in coordinator.data: + dhw_data = coordinator.data[AZD_HOT_WATER] + for description in HOT_WATER_SENSOR_TYPES: + if description.key in dhw_data: + sensors.append( + AirzoneHotWaterSensor( + coordinator, + description, + entry, + ) + ) + if AZD_WEBSERVER in coordinator.data: ws_data = coordinator.data[AZD_WEBSERVER] for description in WEBSERVER_SENSOR_TYPES: @@ -114,6 +141,30 @@ class AirzoneSensor(AirzoneEntity, SensorEntity): self._attr_native_value = self.get_airzone_value(self.entity_description.key) +class AirzoneHotWaterSensor(AirzoneHotWaterEntity, AirzoneSensor): + """Define an Airzone Hot Water sensor.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: SensorEntityDescription, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = f"{self._attr_unique_id}_dhw_{description.key}" + self.entity_description = description + + self._attr_native_unit_of_measurement = TEMP_UNIT_LIB_TO_HASS.get( + self.get_airzone_value(AZD_TEMP_UNIT) + ) + + self._async_update_attrs() + + class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor): """Define an Airzone WebServer sensor.""" diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py index cce8a452a15..4de1cae7555 100644 --- a/tests/components/airzone/test_sensor.py +++ b/tests/components/airzone/test_sensor.py @@ -28,6 +28,10 @@ async def test_airzone_create_sensors( await async_init_integration(hass) + # Hot Water + state = hass.states.get("sensor.airzone_dhw_temperature") + assert state.state == "43" + # WebServer state = hass.states.get("sensor.webserver_rssi") assert state.state == "-42" From 49d73441bf4098f7f8aef1925797abe05715e839 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Aug 2023 16:02:23 -0500 Subject: [PATCH 0755/1151] Abort ESPHome connection when both name and mac address do not match (#98787) --- homeassistant/components/esphome/manager.py | 112 +++++++--- tests/components/esphome/test_init.py | 25 --- tests/components/esphome/test_manager.py | 223 +++++++++++++++++++- 3 files changed, 298 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index c7433080c84..ee0d2371a56 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -354,51 +354,93 @@ class ESPHomeManager: async def on_connect(self) -> None: """Subscribe to states and list entities on successful API login.""" entry = self.entry + unique_id = entry.unique_id entry_data = self.entry_data reconnect_logic = self.reconnect_logic + assert reconnect_logic is not None, "Reconnect logic must be set" hass = self.hass cli = self.cli + stored_device_name = entry.data.get(CONF_DEVICE_NAME) + unique_id_is_mac_address = unique_id and ":" in unique_id try: device_info = await cli.device_info() + except APIConnectionError as err: + _LOGGER.warning("Error getting device info for %s: %s", self.host, err) + # Re-connection logic will trigger after this + await cli.disconnect() + return - # Migrate config entry to new unique ID if necessary - # This was changed in 2023.1 - if entry.unique_id != format_mac(device_info.mac_address): - hass.config_entries.async_update_entry( - entry, unique_id=format_mac(device_info.mac_address) + device_mac = format_mac(device_info.mac_address) + mac_address_matches = unique_id == device_mac + # + # Migrate config entry to new unique ID if the current + # unique id is not a mac address. + # + # This was changed in 2023.1 + if not mac_address_matches and not unique_id_is_mac_address: + hass.config_entries.async_update_entry(entry, unique_id=device_mac) + + if not mac_address_matches and unique_id_is_mac_address: + # If the unique id is a mac address + # and does not match we have the wrong device and we need + # to abort the connection. This can happen if the DHCP + # server changes the IP address of the device and we end up + # connecting to the wrong device. + _LOGGER.error( + "Unexpected device found at %s; " + "expected `%s` with mac address `%s`, " + "found `%s` with mac address `%s`", + self.host, + stored_device_name, + unique_id, + device_info.name, + device_mac, + ) + await cli.disconnect() + await reconnect_logic.stop() + # We don't want to reconnect to the wrong device + # so we stop the reconnect logic and disconnect + # the client. When discovery finds the new IP address + # for the device, the config entry will be updated + # and we will connect to the correct device when + # the config entry gets reloaded by the discovery + # flow. + return + + # Make sure we have the correct device name stored + # so we can map the device to ESPHome Dashboard config + # If we got here, we know the mac address matches or we + # did a migration to the mac address so we can update + # the device name. + if stored_device_name != device_info.name: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_DEVICE_NAME: device_info.name} + ) + + entry_data.device_info = device_info + assert cli.api_version is not None + entry_data.api_version = cli.api_version + entry_data.available = True + # Reset expected disconnect flag on successful reconnect + # as it will be flipped to False on unexpected disconnect. + # + # We use this to determine if a deep sleep device should + # be marked as unavailable or not. + entry_data.expected_disconnect = True + if device_info.name: + reconnect_logic.name = device_info.name + + if device_info.bluetooth_proxy_feature_flags_compat(cli.api_version): + entry_data.disconnect_callbacks.append( + await async_connect_scanner( + hass, entry, cli, entry_data, self.domain_data.bluetooth_cache ) + ) - # Make sure we have the correct device name stored - # so we can map the device to ESPHome Dashboard config - if entry.data.get(CONF_DEVICE_NAME) != device_info.name: - hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_DEVICE_NAME: device_info.name} - ) - - entry_data.device_info = device_info - assert cli.api_version is not None - entry_data.api_version = cli.api_version - entry_data.available = True - # Reset expected disconnect flag on successful reconnect - # as it will be flipped to False on unexpected disconnect. - # - # We use this to determine if a deep sleep device should - # be marked as unavailable or not. - entry_data.expected_disconnect = True - if entry_data.device_info.name: - assert reconnect_logic is not None, "Reconnect logic must be set" - reconnect_logic.name = entry_data.device_info.name - - if device_info.bluetooth_proxy_feature_flags_compat(cli.api_version): - entry_data.disconnect_callbacks.append( - await async_connect_scanner( - hass, entry, cli, entry_data, self.domain_data.bluetooth_cache - ) - ) - - self.device_id = _async_setup_device_registry(hass, entry, entry_data) - entry_data.async_update_device_state(hass) + self.device_id = _async_setup_device_registry(hass, entry, entry_data) + entry_data.async_update_device_state(hass) + try: entity_infos, services = await cli.list_entities_services() await entry_data.async_update_static_infos(hass, entry, entity_infos) await _setup_services(hass, entry_data, services) diff --git a/tests/components/esphome/test_init.py b/tests/components/esphome/test_init.py index d3d47a40d66..8e7e228e422 100644 --- a/tests/components/esphome/test_init.py +++ b/tests/components/esphome/test_init.py @@ -1,7 +1,5 @@ """ESPHome set up tests.""" -from unittest.mock import AsyncMock -from aioesphomeapi import DeviceInfo from homeassistant.components.esphome import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT @@ -10,29 +8,6 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def test_unique_id_updated_to_mac( - hass: HomeAssistant, mock_client, mock_zeroconf: None -) -> None: - """Test we update config entry unique ID to MAC address.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, - unique_id="mock-config-name", - ) - entry.add_to_hass(hass) - - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - mac_address="1122334455aa", - ) - ) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert entry.unique_id == "11:22:33:44:55:aa" - - async def test_delete_entry( hass: HomeAssistant, mock_client, mock_zeroconf: None ) -> None: diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 3bb298024f9..d297dddee4a 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1,11 +1,20 @@ """Test ESPHome manager.""" from collections.abc import Awaitable, Callable +from unittest.mock import AsyncMock -from aioesphomeapi import APIClient, EntityInfo, EntityState, UserService +from aioesphomeapi import APIClient, DeviceInfo, EntityInfo, EntityState, UserService +import pytest -from homeassistant.components.esphome.const import DOMAIN, STABLE_BLE_VERSION_STR +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.components.esphome.const import ( + CONF_DEVICE_NAME, + DOMAIN, + STABLE_BLE_VERSION_STR, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import issue_registry as ir from .conftest import MockESPHomeDevice @@ -113,3 +122,213 @@ async def test_esphome_device_with_current_bluetooth( ) is None ) + + +async def test_unique_id_updated_to_mac( + hass: HomeAssistant, mock_client, mock_zeroconf: None +) -> None: + """Test we update config entry unique ID to MAC address.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="mock-config-name", + ) + entry.add_to_hass(hass) + + mock_client.device_info = AsyncMock( + return_value=DeviceInfo( + mac_address="1122334455aa", + ) + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.unique_id == "11:22:33:44:55:aa" + + +async def test_unique_id_not_updated_if_name_same_and_already_mac( + hass: HomeAssistant, mock_client: APIClient, mock_zeroconf: None +) -> None: + """Test we never update the entry unique ID event if the name is the same.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "test.local", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(mac_address="1122334455ab", name="test") + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Mac should never update + assert entry.unique_id == "11:22:33:44:55:aa" + + +async def test_unique_id_updated_if_name_unset_and_already_mac( + hass: HomeAssistant, mock_client: APIClient, mock_zeroconf: None +) -> None: + """Test we never update config entry unique ID even if the name is unset.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(mac_address="1122334455ab", name="test") + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Mac should never update + assert entry.unique_id == "11:22:33:44:55:aa" + + +async def test_unique_id_not_updated_if_name_different_and_already_mac( + hass: HomeAssistant, mock_client: APIClient, mock_zeroconf: None +) -> None: + """Test we do not update config entry unique ID if the name is different.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "test.local", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(mac_address="1122334455ab", name="different") + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Mac should not be updated because name is different + assert entry.unique_id == "11:22:33:44:55:aa" + # Name should not be updated either + assert entry.data[CONF_DEVICE_NAME] == "test" + + +async def test_name_updated_only_if_mac_matches( + hass: HomeAssistant, mock_client: APIClient, mock_zeroconf: None +) -> None: + """Test we update config entry name only if the mac matches.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "test.local", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "old", + }, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(mac_address="1122334455aa", name="new") + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.unique_id == "11:22:33:44:55:aa" + assert entry.data[CONF_DEVICE_NAME] == "new" + + +async def test_name_updated_only_if_mac_was_unset( + hass: HomeAssistant, mock_client: APIClient, mock_zeroconf: None +) -> None: + """Test we update config entry name if the old unique id was not a mac.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "test.local", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "old", + }, + unique_id="notamac", + ) + entry.add_to_hass(hass) + + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(mac_address="1122334455aa", name="new") + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.unique_id == "11:22:33:44:55:aa" + assert entry.data[CONF_DEVICE_NAME] == "new" + + +async def test_connection_aborted_wrong_device( + hass: HomeAssistant, + mock_client: APIClient, + mock_zeroconf: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test we abort the connection if the unique id is a mac and neither name or mac match.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.43.183", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + unique_id="11:22:33:44:55:aa", + ) + entry.add_to_hass(hass) + + mock_client.device_info = AsyncMock( + return_value=DeviceInfo(mac_address="1122334455ab", name="different") + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + "Unexpected device found at 192.168.43.183; expected `test` " + "with mac address `11:22:33:44:55:aa`, found `different` " + "with mac address `11:22:33:44:55:ab`" in caplog.text + ) + + caplog.clear() + # Make sure discovery triggers a reconnect to the correct device + service_info = dhcp.DhcpServiceInfo( + ip="192.168.43.184", + hostname="test", + macaddress="1122334455aa", + ) + new_info = AsyncMock( + return_value=DeviceInfo(mac_address="1122334455aa", name="test") + ) + mock_client.device_info = new_info + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == "192.168.43.184" + await hass.async_block_till_done() + assert len(new_info.mock_calls) == 1 + assert "Unexpected device found at" not in caplog.text From ade1d333677d461f778001bed8c1347f5500ac8e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 22 Aug 2023 23:07:31 +0200 Subject: [PATCH 0756/1151] Add entity name translations for Reolink (#98589) Co-authored-by: Joost Lekkerkerker --- .../components/reolink/binary_sensor.py | 17 +- homeassistant/components/reolink/button.py | 20 +- homeassistant/components/reolink/light.py | 6 +- homeassistant/components/reolink/number.py | 38 ++-- homeassistant/components/reolink/select.py | 17 +- homeassistant/components/reolink/siren.py | 2 +- homeassistant/components/reolink/strings.json | 203 ++++++++++++++++++ homeassistant/components/reolink/switch.py | 32 +-- homeassistant/components/reolink/update.py | 1 - tests/components/reolink/test_init.py | 2 +- 10 files changed, 269 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py index 996f2c6b3ab..49e964e2b3f 100644 --- a/homeassistant/components/reolink/binary_sensor.py +++ b/homeassistant/components/reolink/binary_sensor.py @@ -49,26 +49,25 @@ class ReolinkBinarySensorEntityDescription( BINARY_SENSORS = ( ReolinkBinarySensorEntityDescription( key="motion", - name="Motion", device_class=BinarySensorDeviceClass.MOTION, value=lambda api, ch: api.motion_detected(ch), ), ReolinkBinarySensorEntityDescription( key=FACE_DETECTION_TYPE, - name="Face", + translation_key="face", icon="mdi:face-recognition", value=lambda api, ch: api.ai_detected(ch, FACE_DETECTION_TYPE), supported=lambda api, ch: api.ai_supported(ch, FACE_DETECTION_TYPE), ), ReolinkBinarySensorEntityDescription( key=PERSON_DETECTION_TYPE, - name="Person", + translation_key="person", value=lambda api, ch: api.ai_detected(ch, PERSON_DETECTION_TYPE), supported=lambda api, ch: api.ai_supported(ch, PERSON_DETECTION_TYPE), ), ReolinkBinarySensorEntityDescription( key=VEHICLE_DETECTION_TYPE, - name="Vehicle", + translation_key="vehicle", icon="mdi:car", icon_off="mdi:car-off", value=lambda api, ch: api.ai_detected(ch, VEHICLE_DETECTION_TYPE), @@ -76,7 +75,7 @@ BINARY_SENSORS = ( ), ReolinkBinarySensorEntityDescription( key=PET_DETECTION_TYPE, - name="Pet", + translation_key="pet", icon="mdi:dog-side", icon_off="mdi:dog-side-off", value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE), @@ -84,7 +83,7 @@ BINARY_SENSORS = ( ), ReolinkBinarySensorEntityDescription( key="visitor", - name="Visitor", + translation_key="visitor", icon="mdi:bell-ring-outline", icon_off="mdi:doorbell", value=lambda api, ch: api.visitor_detected(ch), @@ -130,7 +129,11 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt self.entity_description = entity_description if self._host.api.model in DUAL_LENS_DUAL_MOTION_MODELS: - self._attr_name = f"{entity_description.name} lens {self._channel}" + if entity_description.translation_key is not None: + key = entity_description.translation_key + else: + key = entity_description.key + self._attr_translation_key = f"{key}_lens_{self._channel}" self._attr_unique_id = ( f"{self._host.unique_id}_{self._channel}_{entity_description.key}" diff --git a/homeassistant/components/reolink/button.py b/homeassistant/components/reolink/button.py index 93ea5810bb6..f1797527914 100644 --- a/homeassistant/components/reolink/button.py +++ b/homeassistant/components/reolink/button.py @@ -58,7 +58,7 @@ class ReolinkHostButtonEntityDescription( BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="ptz_stop", - name="PTZ stop", + translation_key="ptz_stop", icon="mdi:pan", enabled_default=lambda api, ch: api.supported(ch, "pan_tilt"), supported=lambda api, ch: api.supported(ch, "pan_tilt") @@ -67,35 +67,35 @@ BUTTON_ENTITIES = ( ), ReolinkButtonEntityDescription( key="ptz_left", - name="PTZ left", + translation_key="ptz_left", icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "pan"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.left.value), ), ReolinkButtonEntityDescription( key="ptz_right", - name="PTZ right", + translation_key="ptz_right", icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "pan"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.right.value), ), ReolinkButtonEntityDescription( key="ptz_up", - name="PTZ up", + translation_key="ptz_up", icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "tilt"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.up.value), ), ReolinkButtonEntityDescription( key="ptz_down", - name="PTZ down", + translation_key="ptz_down", icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "tilt"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.down.value), ), ReolinkButtonEntityDescription( key="ptz_zoom_in", - name="PTZ zoom in", + translation_key="ptz_zoom_in", icon="mdi:magnify", entity_registry_enabled_default=False, supported=lambda api, ch: api.supported(ch, "zoom_basic"), @@ -103,7 +103,7 @@ BUTTON_ENTITIES = ( ), ReolinkButtonEntityDescription( key="ptz_zoom_out", - name="PTZ zoom out", + translation_key="ptz_zoom_out", icon="mdi:magnify", entity_registry_enabled_default=False, supported=lambda api, ch: api.supported(ch, "zoom_basic"), @@ -111,7 +111,7 @@ BUTTON_ENTITIES = ( ), ReolinkButtonEntityDescription( key="ptz_calibrate", - name="PTZ calibrate", + translation_key="ptz_calibrate", icon="mdi:pan", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "ptz_callibrate"), @@ -119,14 +119,14 @@ BUTTON_ENTITIES = ( ), ReolinkButtonEntityDescription( key="guard_go_to", - name="Guard go to", + translation_key="guard_go_to", icon="mdi:crosshairs-gps", supported=lambda api, ch: api.supported(ch, "ptz_guard"), method=lambda api, ch: api.set_ptz_guard(ch, command=GuardEnum.goto.value), ), ReolinkButtonEntityDescription( key="guard_set", - name="Guard set current position", + translation_key="guard_set", icon="mdi:crosshairs-gps", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "ptz_guard"), diff --git a/homeassistant/components/reolink/light.py b/homeassistant/components/reolink/light.py index 0f80215d506..4ac8166410f 100644 --- a/homeassistant/components/reolink/light.py +++ b/homeassistant/components/reolink/light.py @@ -45,7 +45,7 @@ class ReolinkLightEntityDescription( LIGHT_ENTITIES = ( ReolinkLightEntityDescription( key="floodlight", - name="Floodlight", + translation_key="floodlight", icon="mdi:spotlight-beam", supported_fn=lambda api, ch: api.supported(ch, "floodLight"), is_on_fn=lambda api, ch: api.whiteled_state(ch), @@ -55,7 +55,7 @@ LIGHT_ENTITIES = ( ), ReolinkLightEntityDescription( key="ir_lights", - name="Infra red lights in night mode", + translation_key="ir_lights", icon="mdi:led-off", entity_category=EntityCategory.CONFIG, supported_fn=lambda api, ch: api.supported(ch, "ir_lights"), @@ -64,7 +64,7 @@ LIGHT_ENTITIES = ( ), ReolinkLightEntityDescription( key="status_led", - name="Status LED", + translation_key="status_led", icon="mdi:lightning-bolt-circle", entity_category=EntityCategory.CONFIG, supported_fn=lambda api, ch: api.supported(ch, "power_led"), diff --git a/homeassistant/components/reolink/number.py b/homeassistant/components/reolink/number.py index 1be6cd24027..24e5d1bd72b 100644 --- a/homeassistant/components/reolink/number.py +++ b/homeassistant/components/reolink/number.py @@ -45,7 +45,7 @@ class ReolinkNumberEntityDescription( NUMBER_ENTITIES = ( ReolinkNumberEntityDescription( key="zoom", - name="Zoom", + translation_key="zoom", icon="mdi:magnify", mode=NumberMode.SLIDER, native_step=1, @@ -57,7 +57,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="focus", - name="Focus", + translation_key="focus", icon="mdi:focus-field", mode=NumberMode.SLIDER, native_step=1, @@ -72,7 +72,7 @@ NUMBER_ENTITIES = ( # or when using the "light.floodlight" entity. ReolinkNumberEntityDescription( key="floodlight_brightness", - name="Floodlight turn on brightness", + translation_key="floodlight_brightness", icon="mdi:spotlight-beam", entity_category=EntityCategory.CONFIG, native_step=1, @@ -84,7 +84,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="volume", - name="Volume", + translation_key="volume", icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, native_step=1, @@ -96,7 +96,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="guard_return_time", - name="Guard return time", + translation_key="guard_return_time", icon="mdi:crosshairs-gps", entity_category=EntityCategory.CONFIG, native_step=1, @@ -109,7 +109,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="motion_sensitivity", - name="Motion sensitivity", + translation_key="motion_sensitivity", icon="mdi:motion-sensor", entity_category=EntityCategory.CONFIG, native_step=1, @@ -121,7 +121,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="ai_face_sensititvity", - name="AI face sensitivity", + translation_key="ai_face_sensititvity", icon="mdi:face-recognition", entity_category=EntityCategory.CONFIG, native_step=1, @@ -135,7 +135,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="ai_person_sensititvity", - name="AI person sensitivity", + translation_key="ai_person_sensititvity", icon="mdi:account", entity_category=EntityCategory.CONFIG, native_step=1, @@ -149,7 +149,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="ai_vehicle_sensititvity", - name="AI vehicle sensitivity", + translation_key="ai_vehicle_sensititvity", icon="mdi:car", entity_category=EntityCategory.CONFIG, native_step=1, @@ -163,7 +163,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="ai_pet_sensititvity", - name="AI pet sensitivity", + translation_key="ai_pet_sensititvity", icon="mdi:dog-side", entity_category=EntityCategory.CONFIG, native_step=1, @@ -177,7 +177,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="ai_face_delay", - name="AI face delay", + translation_key="ai_face_delay", icon="mdi:face-recognition", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -193,7 +193,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="ai_person_delay", - name="AI person delay", + translation_key="ai_person_delay", icon="mdi:account", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -209,7 +209,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="ai_vehicle_delay", - name="AI vehicle delay", + translation_key="ai_vehicle_delay", icon="mdi:car", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -225,7 +225,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="ai_pet_delay", - name="AI pet delay", + translation_key="ai_pet_delay", icon="mdi:dog-side", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -241,7 +241,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="auto_quick_reply_time", - name="Auto quick reply time", + translation_key="auto_quick_reply_time", icon="mdi:message-reply-text-outline", entity_category=EntityCategory.CONFIG, native_step=1, @@ -254,7 +254,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="auto_track_limit_left", - name="Auto track limit left", + translation_key="auto_track_limit_left", icon="mdi:angle-acute", mode=NumberMode.SLIDER, entity_category=EntityCategory.CONFIG, @@ -267,7 +267,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="auto_track_limit_right", - name="Auto track limit right", + translation_key="auto_track_limit_right", icon="mdi:angle-acute", mode=NumberMode.SLIDER, entity_category=EntityCategory.CONFIG, @@ -280,7 +280,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="auto_track_disappear_time", - name="Auto track disappear time", + translation_key="auto_track_disappear_time", icon="mdi:target-account", entity_category=EntityCategory.CONFIG, native_step=1, @@ -295,7 +295,7 @@ NUMBER_ENTITIES = ( ), ReolinkNumberEntityDescription( key="auto_track_stop_time", - name="Auto track stop time", + translation_key="auto_track_stop_time", icon="mdi:target-account", entity_category=EntityCategory.CONFIG, native_step=1, diff --git a/homeassistant/components/reolink/select.py b/homeassistant/components/reolink/select.py index 2ae3442278e..e9dc151f33b 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -45,10 +45,9 @@ class ReolinkSelectEntityDescription( SELECT_ENTITIES = ( ReolinkSelectEntityDescription( key="floodlight_mode", - name="Floodlight mode", + translation_key="floodlight_mode", icon="mdi:spotlight-beam", entity_category=EntityCategory.CONFIG, - translation_key="floodlight_mode", get_options=lambda api, ch: api.whiteled_mode_list(ch), supported=lambda api, ch: api.supported(ch, "floodLight"), value=lambda api, ch: SpotlightModeEnum(api.whiteled_mode(ch)).name, @@ -56,10 +55,9 @@ SELECT_ENTITIES = ( ), ReolinkSelectEntityDescription( key="day_night_mode", - name="Day night mode", + translation_key="day_night_mode", icon="mdi:theme-light-dark", entity_category=EntityCategory.CONFIG, - translation_key="day_night_mode", get_options=[mode.name for mode in DayNightEnum], supported=lambda api, ch: api.supported(ch, "dayNight"), value=lambda api, ch: DayNightEnum(api.daynight_state(ch)).name, @@ -67,7 +65,7 @@ SELECT_ENTITIES = ( ), ReolinkSelectEntityDescription( key="ptz_preset", - name="PTZ preset", + translation_key="ptz_preset", icon="mdi:pan", get_options=lambda api, ch: list(api.ptz_presets(ch)), supported=lambda api, ch: api.supported(ch, "ptz_presets"), @@ -75,9 +73,8 @@ SELECT_ENTITIES = ( ), ReolinkSelectEntityDescription( key="auto_quick_reply_message", - name="Auto quick reply message", - icon="mdi:message-reply-text-outline", translation_key="auto_quick_reply_message", + icon="mdi:message-reply-text-outline", get_options=lambda api, ch: list(api.quick_reply_dict(ch).values()), supported=lambda api, ch: api.supported(ch, "quick_reply"), value=lambda api, ch: api.quick_reply_dict(ch)[api.quick_reply_file(ch)], @@ -87,9 +84,8 @@ SELECT_ENTITIES = ( ), ReolinkSelectEntityDescription( key="auto_track_method", - name="Auto track method", - icon="mdi:target-account", translation_key="auto_track_method", + icon="mdi:target-account", entity_category=EntityCategory.CONFIG, get_options=[method.name for method in TrackMethodEnum], supported=lambda api, ch: api.supported(ch, "auto_track_method"), @@ -98,9 +94,8 @@ SELECT_ENTITIES = ( ), ReolinkSelectEntityDescription( key="status_led", - name="Status LED", - icon="mdi:lightning-bolt-circle", translation_key="status_led", + icon="mdi:lightning-bolt-circle", entity_category=EntityCategory.CONFIG, get_options=[state.name for state in StatusLedEnum], supported=lambda api, ch: api.supported(ch, "doorbell_led"), diff --git a/homeassistant/components/reolink/siren.py b/homeassistant/components/reolink/siren.py index 9dba3b840ea..c91f633ecab 100644 --- a/homeassistant/components/reolink/siren.py +++ b/homeassistant/components/reolink/siren.py @@ -33,7 +33,7 @@ class ReolinkSirenEntityDescription(SirenEntityDescription): SIREN_ENTITIES = ( ReolinkSirenEntityDescription( key="siren", - name="Siren", + translation_key="siren", icon="mdi:alarm-light", supported=lambda api, ch: api.supported(ch, "siren_play"), ), diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index cdaeb7d0656..95aa26a1ff5 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -62,8 +62,164 @@ } }, "entity": { + "binary_sensor": { + "face": { + "name": "Face" + }, + "person": { + "name": "Person" + }, + "vehicle": { + "name": "Vehicle" + }, + "pet": { + "name": "Pet" + }, + "visitor": { + "name": "Visitor" + }, + "motion_lens_0": { + "name": "Motion lens 0" + }, + "face_lens_0": { + "name": "Face lens 0" + }, + "person_lens_0": { + "name": "Person lens 0" + }, + "vehicle_lens_0": { + "name": "Vehicle lens 0" + }, + "pet_lens_0": { + "name": "Pet lens 0" + }, + "visitor_lens_0": { + "name": "Visitor lens 0" + }, + "motion_lens_1": { + "name": "Motion lens 1" + }, + "face_lens_1": { + "name": "Face lens 1" + }, + "person_lens_1": { + "name": "Person lens 1" + }, + "vehicle_lens_1": { + "name": "Vehicle lens 1" + }, + "pet_lens_1": { + "name": "Pet lens 1" + }, + "visitor_lens_1": { + "name": "Visitor lens 1" + } + }, + "button": { + "ptz_stop": { + "name": "PTZ stop" + }, + "ptz_left": { + "name": "PTZ left" + }, + "ptz_right": { + "name": "PTZ right" + }, + "ptz_up": { + "name": "PTZ up" + }, + "ptz_down": { + "name": "PTZ down" + }, + "ptz_zoom_in": { + "name": "PTZ zoom in" + }, + "ptz_zoom_out": { + "name": "PTZ zoom out" + }, + "ptz_calibrate": { + "name": "PTZ calibrate" + }, + "guard_go_to": { + "name": "Guard go to" + }, + "guard_set": { + "name": "Guard set current position" + } + }, + "light": { + "floodlight": { + "name": "Floodlight" + }, + "ir_lights": { + "name": "Infra red lights in night mode" + }, + "status_led": { + "name": "Status LED" + } + }, + "number": { + "zoom": { + "name": "Zoom" + }, + "focus": { + "name": "Focus" + }, + "floodlight_brightness": { + "name": "Floodlight turn on brightness" + }, + "volume": { + "name": "Volume" + }, + "guard_return_time": { + "name": "Guard return time" + }, + "motion_sensitivity": { + "name": "Motion sensitivity" + }, + "ai_face_sensititvity": { + "name": "AI face sensitivity" + }, + "ai_person_sensititvity": { + "name": "AI person sensitivity" + }, + "ai_vehicle_sensititvity": { + "name": "AI vehicle sensitivity" + }, + "ai_pet_sensititvity": { + "name": "AI pet sensitivity" + }, + "ai_face_delay": { + "name": "AI face delay" + }, + "ai_person_delay": { + "name": "AI person delay" + }, + "ai_vehicle_delay": { + "name": "AI vehicle delay" + }, + "ai_pet_delay": { + "name": "AI pet delay" + }, + "auto_quick_reply_time": { + "name": "Auto quick reply time" + }, + "auto_track_limit_left": { + "name": "Auto track limit left" + }, + "auto_track_limit_right": { + "name": "Auto track limit right" + }, + "auto_track_disappear_time": { + "name": "Auto track disappear time" + }, + "auto_track_stop_time": { + "name": "Auto track stop time" + } + }, "select": { "floodlight_mode": { + "name": "Floodlight mode", "state": { "off": "[%key:common::state::off%]", "auto": "Auto", @@ -73,18 +229,24 @@ } }, "day_night_mode": { + "name": "Day night mode", "state": { "auto": "Auto", "color": "Color", "blackwhite": "Black&White" } }, + "ptz_preset": { + "name": "PTZ preset" + }, "auto_quick_reply_message": { + "name": "Auto quick reply message", "state": { "off": "[%key:common::state::off%]" } }, "auto_track_method": { + "name": "Auto track method", "state": { "digital": "Digital", "digitalfirst": "Digital first", @@ -92,6 +254,7 @@ } }, "status_led": { + "name": "Status LED", "state": { "stayoff": "Stay off", "auto": "Auto", @@ -106,6 +269,46 @@ "ptz_pan_position": { "name": "PTZ pan position" } + }, + "siren": { + "siren": { + "name": "[%key:component::siren::title%]" + } + }, + "switch": { + "record_audio": { + "name": "Record audio" + }, + "siren_on_event": { + "name": "Siren on event" + }, + "auto_tracking": { + "name": "Auto tracking" + }, + "auto_focus": { + "name": "Auto focus" + }, + "gaurd_return": { + "name": "Guard return" + }, + "email": { + "name": "Email on event" + }, + "ftp_upload": { + "name": "FTP upload" + }, + "push_notifications": { + "name": "Push notifications" + }, + "record": { + "name": "Record" + }, + "buzzer": { + "name": "Buzzer on event" + }, + "doorbell_button_sound": { + "name": "Doorbell button sound" + } } } } diff --git a/homeassistant/components/reolink/switch.py b/homeassistant/components/reolink/switch.py index aa121911758..4a5b415a144 100644 --- a/homeassistant/components/reolink/switch.py +++ b/homeassistant/components/reolink/switch.py @@ -55,7 +55,7 @@ class ReolinkNVRSwitchEntityDescription( SWITCH_ENTITIES = ( ReolinkSwitchEntityDescription( key="record_audio", - name="Record audio", + translation_key="record_audio", icon="mdi:microphone", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "audio"), @@ -64,7 +64,7 @@ SWITCH_ENTITIES = ( ), ReolinkSwitchEntityDescription( key="siren_on_event", - name="Siren on event", + translation_key="siren_on_event", icon="mdi:alarm-light", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "siren"), @@ -73,7 +73,7 @@ SWITCH_ENTITIES = ( ), ReolinkSwitchEntityDescription( key="auto_tracking", - name="Auto tracking", + translation_key="auto_tracking", icon="mdi:target-account", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "auto_track"), @@ -82,7 +82,7 @@ SWITCH_ENTITIES = ( ), ReolinkSwitchEntityDescription( key="auto_focus", - name="Auto focus", + translation_key="auto_focus", icon="mdi:focus-field", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "auto_focus"), @@ -91,7 +91,7 @@ SWITCH_ENTITIES = ( ), ReolinkSwitchEntityDescription( key="gaurd_return", - name="Guard return", + translation_key="gaurd_return", icon="mdi:crosshairs-gps", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "ptz_guard"), @@ -100,7 +100,7 @@ SWITCH_ENTITIES = ( ), ReolinkSwitchEntityDescription( key="email", - name="Email on event", + translation_key="email", icon="mdi:email", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "email") and api.is_nvr, @@ -109,7 +109,7 @@ SWITCH_ENTITIES = ( ), ReolinkSwitchEntityDescription( key="ftp_upload", - name="FTP upload", + translation_key="ftp_upload", icon="mdi:swap-horizontal", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "ftp") and api.is_nvr, @@ -118,7 +118,7 @@ SWITCH_ENTITIES = ( ), ReolinkSwitchEntityDescription( key="push_notifications", - name="Push notifications", + translation_key="push_notifications", icon="mdi:message-badge", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "push") and api.is_nvr, @@ -127,7 +127,7 @@ SWITCH_ENTITIES = ( ), ReolinkSwitchEntityDescription( key="record", - name="Record", + translation_key="record", icon="mdi:record-rec", supported=lambda api, ch: api.supported(ch, "recording") and api.is_nvr, value=lambda api, ch: api.recording_enabled(ch), @@ -135,7 +135,7 @@ SWITCH_ENTITIES = ( ), ReolinkSwitchEntityDescription( key="buzzer", - name="Buzzer on event", + translation_key="buzzer", icon="mdi:room-service", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "buzzer") and api.is_nvr, @@ -144,7 +144,7 @@ SWITCH_ENTITIES = ( ), ReolinkSwitchEntityDescription( key="doorbell_button_sound", - name="Doorbell button sound", + translation_key="doorbell_button_sound", icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "doorbell_button_sound"), @@ -156,7 +156,7 @@ SWITCH_ENTITIES = ( NVR_SWITCH_ENTITIES = ( ReolinkNVRSwitchEntityDescription( key="email", - name="Email on event", + translation_key="email", icon="mdi:email", entity_category=EntityCategory.CONFIG, supported=lambda api: api.supported(None, "email"), @@ -165,7 +165,7 @@ NVR_SWITCH_ENTITIES = ( ), ReolinkNVRSwitchEntityDescription( key="ftp_upload", - name="FTP upload", + translation_key="ftp_upload", icon="mdi:swap-horizontal", entity_category=EntityCategory.CONFIG, supported=lambda api: api.supported(None, "ftp"), @@ -174,7 +174,7 @@ NVR_SWITCH_ENTITIES = ( ), ReolinkNVRSwitchEntityDescription( key="push_notifications", - name="Push notifications", + translation_key="push_notifications", icon="mdi:message-badge", entity_category=EntityCategory.CONFIG, supported=lambda api: api.supported(None, "push"), @@ -183,7 +183,7 @@ NVR_SWITCH_ENTITIES = ( ), ReolinkNVRSwitchEntityDescription( key="record", - name="Record", + translation_key="record", icon="mdi:record-rec", supported=lambda api: api.supported(None, "recording"), value=lambda api: api.recording_enabled(), @@ -191,7 +191,7 @@ NVR_SWITCH_ENTITIES = ( ), ReolinkNVRSwitchEntityDescription( key="buzzer", - name="Buzzer on event", + translation_key="buzzer", icon="mdi:room-service", entity_category=EntityCategory.CONFIG, supported=lambda api: api.supported(None, "buzzer"), diff --git a/homeassistant/components/reolink/update.py b/homeassistant/components/reolink/update.py index fbbb037080b..57efe1d9e92 100644 --- a/homeassistant/components/reolink/update.py +++ b/homeassistant/components/reolink/update.py @@ -41,7 +41,6 @@ class ReolinkUpdateEntity( _attr_device_class = UpdateDeviceClass.FIRMWARE _attr_release_url = "https://reolink.com/download-center/" - _attr_name = "Update" def __init__( self, diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index d649baeb937..e2bd622bb43 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -85,7 +85,7 @@ async def test_firmware_error_twice( assert config_entry.state == ConfigEntryState.LOADED - entity_id = f"{Platform.UPDATE}.{TEST_NVR_NAME}_update" + entity_id = f"{Platform.UPDATE}.{TEST_NVR_NAME}_firmware" assert hass.states.is_state(entity_id, STATE_OFF) async_fire_time_changed( From 2ff5d6290f8b911f9a0de7ef491db8278a2d05a2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 23:09:18 +0200 Subject: [PATCH 0757/1151] Migrate Prosegur to has entity name (#98845) --- .../components/prosegur/alarm_control_panel.py | 9 +++++---- homeassistant/components/prosegur/camera.py | 10 ++++++---- tests/components/prosegur/test_alarm_control_panel.py | 2 +- tests/components/prosegur/test_camera.py | 8 ++++---- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/prosegur/alarm_control_panel.py b/homeassistant/components/prosegur/alarm_control_panel.py index 8d1f087bfff..77cdb5e11a2 100644 --- a/homeassistant/components/prosegur/alarm_control_panel.py +++ b/homeassistant/components/prosegur/alarm_control_panel.py @@ -47,6 +47,8 @@ class ProsegurAlarm(alarm.AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_HOME ) + _attr_has_entity_name = True + _attr_name = None _installation: Installation def __init__(self, contract: str, auth: Auth) -> None: @@ -57,14 +59,13 @@ class ProsegurAlarm(alarm.AlarmControlPanelEntity): self._auth = auth self._attr_code_arm_required = False - self._attr_name = f"contract {self.contract}" - self._attr_unique_id = self.contract + self._attr_unique_id = contract self._attr_device_info = DeviceInfo( - name="Prosegur Alarm", + name=f"Contract {contract}", manufacturer="Prosegur", model="smart", - identifiers={(DOMAIN, self.contract)}, + identifiers={(DOMAIN, contract)}, configuration_url="https://smart.prosegur.com", ) diff --git a/homeassistant/components/prosegur/camera.py b/homeassistant/components/prosegur/camera.py index bdd265d1e42..c711ca2eac6 100644 --- a/homeassistant/components/prosegur/camera.py +++ b/homeassistant/components/prosegur/camera.py @@ -50,6 +50,8 @@ async def async_setup_entry( class ProsegurCamera(Camera): """Representation of a Smart Prosegur Camera.""" + _attr_has_entity_name = True + def __init__( self, installation: Installation, camera: InstallationCamera, auth: Auth ) -> None: @@ -59,14 +61,14 @@ class ProsegurCamera(Camera): self._installation = installation self._camera = camera self._auth = auth + self._attr_unique_id = f"{installation.contract} {camera.id}" self._attr_name = camera.description - self._attr_unique_id = f"{self._installation.contract} {camera.id}" self._attr_device_info = DeviceInfo( - name=self._camera.description, + name=f"Contract {installation.contract}", manufacturer="Prosegur", - model="smart camera", - identifiers={(DOMAIN, self._installation.contract)}, + model="smart", + identifiers={(DOMAIN, installation.contract)}, configuration_url="https://smart.prosegur.com", ) diff --git a/tests/components/prosegur/test_alarm_control_panel.py b/tests/components/prosegur/test_alarm_control_panel.py index 51086e74b00..d5244de1b43 100644 --- a/tests/components/prosegur/test_alarm_control_panel.py +++ b/tests/components/prosegur/test_alarm_control_panel.py @@ -59,7 +59,7 @@ async def test_entity_registry( state = hass.states.get(PROSEGUR_ALARM_ENTITY) - assert state.attributes.get(ATTR_FRIENDLY_NAME) == f"contract {CONTRACT}" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == f"Contract {CONTRACT}" assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 3 diff --git a/tests/components/prosegur/test_camera.py b/tests/components/prosegur/test_camera.py index ba2e478f5cd..58017085aed 100644 --- a/tests/components/prosegur/test_camera.py +++ b/tests/components/prosegur/test_camera.py @@ -16,7 +16,7 @@ from homeassistant.exceptions import HomeAssistantError async def test_camera(hass: HomeAssistant, init_integration) -> None: """Test prosegur get_image.""" - image = await camera.async_get_image(hass, "camera.test_cam") + image = await camera.async_get_image(hass, "camera.contract_1234abcd_test_cam") assert image == Image(content_type="image/jpeg", content=b"ABC") @@ -36,7 +36,7 @@ async def test_camera_fail( with caplog.at_level( logging.ERROR, logger="homeassistant.components.prosegur" ), pytest.raises(HomeAssistantError) as exc: - await camera.async_get_image(hass, "camera.test_cam") + await camera.async_get_image(hass, "camera.contract_1234abcd_test_cam") assert "Unable to get image" in str(exc.value) @@ -51,7 +51,7 @@ async def test_request_image( await hass.services.async_call( DOMAIN, "request_image", - {ATTR_ENTITY_ID: "camera.test_cam"}, + {ATTR_ENTITY_ID: "camera.contract_1234abcd_test_cam"}, ) await hass.async_block_till_done() @@ -72,7 +72,7 @@ async def test_request_image_fail( await hass.services.async_call( DOMAIN, "request_image", - {ATTR_ENTITY_ID: "camera.test_cam"}, + {ATTR_ENTITY_ID: "camera.contract_1234abcd_test_cam"}, ) await hass.async_block_till_done() From e9af99e46910399241a91521f63ed28f5a2c4dce Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 23:10:02 +0200 Subject: [PATCH 0758/1151] Add entity translations to PECO (#98847) --- homeassistant/components/peco/sensor.py | 10 +++++----- homeassistant/components/peco/strings.json | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/peco/sensor.py b/homeassistant/components/peco/sensor.py index e11d8e7ac0b..5be41f7c7e1 100644 --- a/homeassistant/components/peco/sensor.py +++ b/homeassistant/components/peco/sensor.py @@ -43,7 +43,7 @@ PARALLEL_UPDATES: Final = 0 SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( PECOSensorEntityDescription( key="customers_out", - name="Customers Out", + translation_key="customers_out", value_fn=lambda data: int(data.outages.customers_out), attribute_fn=lambda data: {}, icon="mdi:power-plug-off", @@ -51,7 +51,7 @@ SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( ), PECOSensorEntityDescription( key="percent_customers_out", - name="Percent Customers Out", + translation_key="percent_customers_out", native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: int(data.outages.percent_customers_out), attribute_fn=lambda data: {}, @@ -60,7 +60,7 @@ SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( ), PECOSensorEntityDescription( key="outage_count", - name="Outage Count", + translation_key="outage_count", value_fn=lambda data: int(data.outages.outage_count), attribute_fn=lambda data: {}, icon="mdi:power-plug-off", @@ -68,7 +68,7 @@ SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( ), PECOSensorEntityDescription( key="customers_served", - name="Customers Served", + translation_key="customers_served", value_fn=lambda data: int(data.outages.customers_served), attribute_fn=lambda data: {}, icon="mdi:power-plug-off", @@ -76,7 +76,7 @@ SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( ), PECOSensorEntityDescription( key="map_alert", - name="Map Alert", + translation_key="map_alert", value_fn=lambda data: str(data.alerts.alert_title), attribute_fn=lambda data: {ATTR_CONTENT: data.alerts.alert_content}, icon="mdi:alert", diff --git a/homeassistant/components/peco/strings.json b/homeassistant/components/peco/strings.json index 54208b12d93..059b2ba71a7 100644 --- a/homeassistant/components/peco/strings.json +++ b/homeassistant/components/peco/strings.json @@ -10,5 +10,24 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "entity": { + "sensor": { + "customers_out": { + "name": "Customers out" + }, + "percent_customers_out": { + "name": "Percent customers out" + }, + "outage_count": { + "name": "Outage count" + }, + "customers_served": { + "name": "Customers served" + }, + "map_alert": { + "name": "Map alert" + } + } } } From 6399d74c15d312dae33533dae59961fcf34c2a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 23 Aug 2023 00:12:12 +0300 Subject: [PATCH 0759/1151] Remove unnnecessary pylint configs from core (#98704) --- homeassistant/backports/functools.py | 2 +- homeassistant/config.py | 5 ++--- homeassistant/core.py | 5 ++--- homeassistant/helpers/config_validation.py | 5 +---- homeassistant/helpers/json.py | 2 +- homeassistant/helpers/schema_config_entry_flow.py | 4 ++-- homeassistant/helpers/storage.py | 1 - homeassistant/loader.py | 2 +- homeassistant/util/color.py | 7 ++----- homeassistant/util/distance.py | 2 +- homeassistant/util/json.py | 2 +- homeassistant/util/location.py | 1 - homeassistant/util/pressure.py | 2 +- homeassistant/util/speed.py | 4 ++-- homeassistant/util/temperature.py | 2 +- homeassistant/util/timeout.py | 4 ++-- homeassistant/util/volume.py | 2 +- homeassistant/util/yaml/loader.py | 2 +- pyproject.toml | 12 ------------ script/hassfest/manifest.py | 8 ++------ script/lint_and_test.py | 2 +- tests/common.py | 2 -- tests/helpers/test_config_validation.py | 2 +- tests/helpers/test_service.py | 2 +- tests/test_config.py | 5 ----- tests/util/test_color.py | 1 - tests/util/yaml/test_init.py | 2 -- 27 files changed, 27 insertions(+), 63 deletions(-) diff --git a/homeassistant/backports/functools.py b/homeassistant/backports/functools.py index 83d66a39f71..212c8516b48 100644 --- a/homeassistant/backports/functools.py +++ b/homeassistant/backports/functools.py @@ -8,7 +8,7 @@ from typing import Any, Generic, Self, TypeVar, overload _T = TypeVar("_T") -class cached_property(Generic[_T]): # pylint: disable=invalid-name +class cached_property(Generic[_T]): """Backport of Python 3.12's cached_property. Includes https://github.com/python/cpython/pull/101890/files diff --git a/homeassistant/config.py b/homeassistant/config.py index 0d9e1d9034e..7c3bd2e7bfe 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -257,10 +257,10 @@ CORE_CONFIG_SCHEMA = vol.All( vol.Optional(CONF_INTERNAL_URL): cv.url, vol.Optional(CONF_EXTERNAL_URL): cv.url, vol.Optional(CONF_ALLOWLIST_EXTERNAL_DIRS): vol.All( - cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter + cv.ensure_list, [vol.IsDir()] ), vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All( - cv.ensure_list, [vol.IsDir()] # pylint: disable=no-value-for-parameter + cv.ensure_list, [vol.IsDir()] ), vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All( cv.ensure_list, [cv.url] @@ -297,7 +297,6 @@ CORE_CONFIG_SCHEMA = vol.All( ], _no_duplicate_auth_mfa_module, ), - # pylint: disable-next=no-value-for-parameter vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()), vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean, vol.Optional(CONF_CURRENCY): _validate_currency, diff --git a/homeassistant/core.py b/homeassistant/core.py index 49c288188f3..18c5c355ae9 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -108,7 +108,7 @@ _P = ParamSpec("_P") # Internal; not helpers.typing.UNDEFINED due to circular dependency _UNDEF: dict[Any, Any] = {} _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) -CALLBACK_TYPE = Callable[[], None] # pylint: disable=invalid-name +CALLBACK_TYPE = Callable[[], None] CORE_STORAGE_KEY = "core.config" CORE_STORAGE_VERSION = 1 @@ -847,8 +847,7 @@ class HomeAssistant: if ( not handle.cancelled() and (args := handle._args) # pylint: disable=protected-access - # pylint: disable-next=unidiomatic-typecheck - and type(job := args[0]) is HassJob + and type(job := args[0]) is HassJob # noqa: E721 and job.cancel_on_shutdown ): handle.cancel() diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 5e0d66e0a9a..a4018101d0e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -102,8 +102,6 @@ import homeassistant.util.dt as dt_util from . import script_variables as script_variables_helper, template as template_helper -# pylint: disable=invalid-name - TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM', 'HH:MM:SS' or 'HH:MM:SS.F'" @@ -743,7 +741,6 @@ def socket_timeout(value: Any | None) -> object: raise vol.Invalid(f"Invalid socket timeout: {err}") from err -# pylint: disable=no-value-for-parameter def url( value: Any, _schema_list: frozenset[UrlProtocolSchema] = EXTERNAL_URL_PROTOCOL_SCHEMA_LIST, @@ -1360,7 +1357,7 @@ STATE_CONDITION_ATTRIBUTE_SCHEMA = vol.Schema( ) -def STATE_CONDITION_SCHEMA(value: Any) -> dict: # pylint: disable=invalid-name +def STATE_CONDITION_SCHEMA(value: Any) -> dict: """Validate a state condition.""" if not isinstance(value, dict): raise vol.Invalid("Expected a dictionary") diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 33054bcb1b0..e94093cfd2f 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any, Final import orjson from homeassistant.util.file import write_utf8_file, write_utf8_file_atomic -from homeassistant.util.json import ( # pylint: disable=unused-import # noqa: F401 +from homeassistant.util.json import ( # noqa: F401 JSON_DECODE_EXCEPTIONS, JSON_ENCODE_EXCEPTIONS, SerializationError, diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 18d59f4f90d..e9d86f79eec 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -348,7 +348,7 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): """ @callback - def async_create_entry( # pylint: disable=arguments-differ + def async_create_entry( self, data: Mapping[str, Any], **kwargs: Any, @@ -409,7 +409,7 @@ class SchemaOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): return _async_step @callback - def async_create_entry( # pylint: disable=arguments-differ + def async_create_entry( self, data: Mapping[str, Any], **kwargs: Any, diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index c83481365ab..0e92cc6ff01 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -237,7 +237,6 @@ class Store(Generic[_T]): self.minor_version, ) if len(inspect.signature(self._async_migrate_func).parameters) == 2: - # pylint: disable-next=no-value-for-parameter stored = await self._async_migrate_func(data["version"], data["data"]) else: try: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 697e47187ce..40161bd3be9 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -897,7 +897,7 @@ async def async_get_integrations( for domain in domains: int_or_fut = cache.get(domain, _UNDEF) # Integration is never subclassed, so we can check for type - if type(int_or_fut) is Integration: # pylint: disable=unidiomatic-typecheck + if type(int_or_fut) is Integration: # noqa: E721 results[domain] = int_or_fut elif int_or_fut is not _UNDEF: in_progress[domain] = cast(asyncio.Future[None], int_or_fut) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 6ccb7f14ea2..d9f2a4b96ff 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -180,8 +180,8 @@ COLORS = { class XYPoint: """Represents a CIE 1931 XY coordinate pair.""" - x: float = attr.ib() # pylint: disable=invalid-name - y: float = attr.ib() # pylint: disable=invalid-name + x: float = attr.ib() + y: float = attr.ib() @attr.s() @@ -205,9 +205,6 @@ def color_name_to_rgb(color_name: str) -> RGBColor: return hex_value -# pylint: disable=invalid-name - - def color_RGB_to_xy( iR: int, iG: int, iB: int, Gamut: GamutType | None = None ) -> tuple[float, float]: diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index 509760fff19..45b105aea9f 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable -# pylint: disable-next=unused-import,hass-deprecated-import +# pylint: disable-next=hass-deprecated-import from homeassistant.const import ( # noqa: F401 LENGTH, LENGTH_CENTIMETERS, diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 60aa920ed6a..7f81c281340 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -11,7 +11,7 @@ import orjson from homeassistant.exceptions import HomeAssistantError -from .file import WriteError # pylint: disable=unused-import # noqa: F401 +from .file import WriteError # noqa: F401 _SENTINEL = object() _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index a251aec268e..44fcaa07067 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -89,7 +89,6 @@ def vincenty( if point1[0] == point2[0] and point1[1] == point2[1]: return 0.0 - # pylint: disable=invalid-name U1 = math.atan((1 - FLATTENING) * math.tan(math.radians(point1[0]))) U2 = math.atan((1 - FLATTENING) * math.tan(math.radians(point2[0]))) L = math.radians(point2[1] - point1[1]) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index 78a69e15a34..9c5082e95ed 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -1,7 +1,7 @@ """Pressure util functions.""" from __future__ import annotations -# pylint: disable-next=unused-import,hass-deprecated-import +# pylint: disable-next=hass-deprecated-import from homeassistant.const import ( # noqa: F401 PRESSURE, PRESSURE_BAR, diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index a1b6e0a7227..80a3609ab4d 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -1,7 +1,7 @@ """Distance util functions.""" from __future__ import annotations -# pylint: disable-next=unused-import,hass-deprecated-import +# pylint: disable-next=hass-deprecated-import from homeassistant.const import ( # noqa: F401 SPEED, SPEED_FEET_PER_SECOND, @@ -16,7 +16,7 @@ from homeassistant.const import ( # noqa: F401 ) from homeassistant.helpers.frame import report -from .unit_conversion import ( # pylint: disable=unused-import # noqa: F401 +from .unit_conversion import ( # noqa: F401 _FOOT_TO_M as FOOT_TO_M, _HRS_TO_SECS as HRS_TO_SECS, _IN_TO_M as IN_TO_M, diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 409fecd1090..74d56e84d94 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -1,5 +1,5 @@ """Temperature util functions.""" -# pylint: disable-next=unused-import,hass-deprecated-import +# pylint: disable-next=hass-deprecated-import from homeassistant.const import ( # noqa: F401 TEMP_CELSIUS, TEMP_FAHRENHEIT, diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index 6c1de55748f..e2e969d46d2 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -49,7 +49,7 @@ class _GlobalFreezeContext: self._loop.call_soon_threadsafe(self._enter) return self - def __exit__( # pylint: disable=useless-return + def __exit__( self, exc_type: type[BaseException], exc_val: BaseException, @@ -117,7 +117,7 @@ class _ZoneFreezeContext: self._loop.call_soon_threadsafe(self._enter) return self - def __exit__( # pylint: disable=useless-return + def __exit__( self, exc_type: type[BaseException], exc_val: BaseException, diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index 7d70d23c00c..8aae8ff104e 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -1,7 +1,7 @@ """Volume conversion util functions.""" from __future__ import annotations -# pylint: disable-next=unused-import,hass-deprecated-import +# pylint: disable-next=hass-deprecated-import from homeassistant.const import ( # noqa: F401 UNIT_NOT_RECOGNIZED_TEMPLATE, VOLUME, diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index b5840a79e8d..2e31b212f1f 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -28,7 +28,7 @@ from .objects import Input, NodeDictClass, NodeListClass, NodeStrClass # mypy: allow-untyped-calls, no-warn-return-any -JSON_TYPE = list | dict | str # pylint: disable=invalid-name +JSON_TYPE = list | dict | str _DictT = TypeVar("_DictT", bound=dict) _LOGGER = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index cdbcf851a1b..2ae9c96734c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,18 +120,6 @@ fail-on = [ [tool.pylint.BASIC] class-const-naming-style = "any" -good-names = [ - "_", - "ev", - "ex", - "fp", - "i", - "id", - "j", - "k", - "Run", - "ip", -] [tool.pylint."MESSAGES CONTROL"] # Reasons disabled: diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 4a15acb2d1d..65e37aa515d 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -254,12 +254,8 @@ INTEGRATION_MANIFEST_SCHEMA = vol.Schema( } ) ], - vol.Required("documentation"): vol.All( - vol.Url(), documentation_url # pylint: disable=no-value-for-parameter - ), - vol.Optional( - "issue_tracker" - ): vol.Url(), # pylint: disable=no-value-for-parameter + vol.Required("documentation"): vol.All(vol.Url(), documentation_url), + vol.Optional("issue_tracker"): vol.Url(), vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES), vol.Optional("requirements"): [str], vol.Optional("dependencies"): [str], diff --git a/script/lint_and_test.py b/script/lint_and_test.py index 5a3d448c1f4..27963758415 100755 --- a/script/lint_and_test.py +++ b/script/lint_and_test.py @@ -40,7 +40,7 @@ def printc(the_color, *args): def validate_requirements_ok(): """Validate requirements, returns True of ok.""" - # pylint: disable-next=import-error,import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from gen_requirements_all import main as req_main return req_main(True) == 0 diff --git a/tests/common.py b/tests/common.py index df8722a563c..0b63a9a2ef6 100644 --- a/tests/common.py +++ b/tests/common.py @@ -681,7 +681,6 @@ def ensure_auth_manager_loaded(auth_mgr): class MockModule: """Representation of a fake module.""" - # pylint: disable=invalid-name def __init__( self, domain=None, @@ -756,7 +755,6 @@ class MockPlatform: __name__ = "homeassistant.components.light.bla" __file__ = "homeassistant/components/blah/light" - # pylint: disable=invalid-name def __init__( self, setup_platform=None, diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index b5c8cc1716e..80fc1bf2241 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1221,7 +1221,7 @@ def test_enum() -> None: schema("value3") -def test_socket_timeout(): # pylint: disable=invalid-name +def test_socket_timeout(): """Test socket timeout validator.""" schema = vol.Schema(cv.socket_timeout) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 56ee3f74140..803a57e12ed 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -11,7 +11,7 @@ import voluptuous as vol # To prevent circular import when running just this file from homeassistant import exceptions from homeassistant.auth.permissions import PolicyPermissions -import homeassistant.components # noqa: F401, pylint: disable=unused-import +import homeassistant.components # noqa: F401 from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, diff --git a/tests/test_config.py b/tests/test_config.py index 407ca9ef54d..aeb25313302 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -311,7 +311,6 @@ def test_remove_lib_on_upgrade( mock_open = mock.mock_open() with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value - # pylint: disable=no-member opened_file.readline.return_value = ha_version hass.config.path = mock.Mock() config_util.process_ha_config_upgrade(hass) @@ -335,7 +334,6 @@ def test_remove_lib_on_upgrade_94( mock_open = mock.mock_open() with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value - # pylint: disable=no-member opened_file.readline.return_value = ha_version hass.config.path = mock.Mock() config_util.process_ha_config_upgrade(hass) @@ -356,7 +354,6 @@ def test_process_config_upgrade(hass: HomeAssistant) -> None: config_util, "__version__", "0.91.0" ): opened_file = mock_open.return_value - # pylint: disable=no-member opened_file.readline.return_value = ha_version config_util.process_ha_config_upgrade(hass) @@ -372,7 +369,6 @@ def test_config_upgrade_same_version(hass: HomeAssistant) -> None: mock_open = mock.mock_open() with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value - # pylint: disable=no-member opened_file.readline.return_value = ha_version config_util.process_ha_config_upgrade(hass) @@ -386,7 +382,6 @@ def test_config_upgrade_no_file(hass: HomeAssistant) -> None: mock_open.side_effect = [FileNotFoundError(), mock.DEFAULT, mock.DEFAULT] with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value - # pylint: disable=no-member config_util.process_ha_config_upgrade(hass) assert opened_file.write.call_count == 1 assert opened_file.write.call_args == mock.call(__version__) diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 60b1fd547fc..7c5e959aabc 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -31,7 +31,6 @@ GAMUT_INVALID_4 = color_util.GamutType( ) -# pylint: disable=invalid-name def test_color_RGB_to_xy_brightness() -> None: """Test color_RGB_to_xy_brightness.""" assert color_util.color_RGB_to_xy_brightness(0, 0, 0) == (0, 0, 0) diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index bd99889234f..4f60c5836b5 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -354,8 +354,6 @@ def load_yaml(fname, string, secrets=None): class TestSecrets(unittest.TestCase): """Test the secrets parameter in the yaml utility.""" - # pylint: disable=invalid-name - def setUp(self): """Create & load secrets file.""" config_dir = get_test_config_dir() From 30628766aeb476aca394d28c20b93e63cd4a4e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 22 Aug 2023 23:21:42 +0200 Subject: [PATCH 0760/1151] Update AEMET-OpenData to v0.3.0 (#98810) --- homeassistant/components/aemet/__init__.py | 3 +- homeassistant/components/aemet/config_flow.py | 19 ++- homeassistant/components/aemet/manifest.json | 2 +- .../aemet/weather_update_coordinator.py | 24 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../aemet/fixtures/station-3195.json | 6 - .../aemet/fixtures/station-list.json | 6 - .../fixtures/town-28065-forecast-daily.json | 6 - .../fixtures/town-28065-forecast-hourly.json | 6 - tests/components/aemet/test_config_flow.py | 32 ++-- tests/components/aemet/test_init.py | 11 +- tests/components/aemet/test_weather.py | 9 +- tests/components/aemet/util.py | 139 ++++++++---------- 14 files changed, 114 insertions(+), 153 deletions(-) delete mode 100644 tests/components/aemet/fixtures/station-3195.json delete mode 100644 tests/components/aemet/fixtures/station-list.json delete mode 100644 tests/components/aemet/fixtures/town-28065-forecast-daily.json delete mode 100644 tests/components/aemet/fixtures/town-28065-forecast-hourly.json diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 032e0a3a9f6..68e7bb6c5e0 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -6,6 +6,7 @@ from aemet_opendata.interface import AEMET from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client from .const import ( CONF_STATION_UPDATES, @@ -27,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: longitude = entry.data[CONF_LONGITUDE] station_updates = entry.options.get(CONF_STATION_UPDATES, True) - aemet = AEMET(api_key) + aemet = AEMET(aiohttp_client.async_get_clientsession(hass), api_key) weather_coordinator = WeatherUpdateCoordinator( hass, aemet, latitude, longitude, station_updates ) diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py index 9db0c6f7db1..129f513025a 100644 --- a/homeassistant/components/aemet/config_flow.py +++ b/homeassistant/components/aemet/config_flow.py @@ -2,12 +2,13 @@ from __future__ import annotations from aemet_opendata import AEMET +from aemet_opendata.exceptions import AuthError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, SchemaOptionsFlowHandler, @@ -39,8 +40,13 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(f"{latitude}-{longitude}") self._abort_if_unique_id_configured() - api_online = await _is_aemet_api_online(self.hass, user_input[CONF_API_KEY]) - if not api_online: + aemet = AEMET( + aiohttp_client.async_get_clientsession(self.hass), + user_input[CONF_API_KEY], + ) + try: + await aemet.get_conventional_observation_stations(False) + except AuthError: errors["base"] = "invalid_api_key" if not errors: @@ -70,10 +76,3 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) - - -async def _is_aemet_api_online(hass, api_key): - aemet = AEMET(api_key) - return await hass.async_add_executor_job( - aemet.get_conventional_observation_stations, False - ) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index f9f1129f3b0..a460d9e16bc 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.2.2"] + "requirements": ["AEMET-OpenData==0.3.0"] } diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 5e9ce6af677..d44160116f2 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -146,13 +146,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): async def _get_aemet_weather(self): """Poll weather data from AEMET OpenData.""" - weather = await self.hass.async_add_executor_job(self._get_weather_and_forecast) + weather = await self._get_weather_and_forecast() return weather - def _get_weather_station(self): + async def _get_weather_station(self): if not self._station: self._station = ( - self._aemet.get_conventional_observation_station_by_coordinates( + await self._aemet.get_conventional_observation_station_by_coordinates( self._latitude, self._longitude ) ) @@ -171,9 +171,9 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ) return self._station - def _get_weather_town(self): + async def _get_weather_town(self): if not self._town: - self._town = self._aemet.get_town_by_coordinates( + self._town = await self._aemet.get_town_by_coordinates( self._latitude, self._longitude ) if self._town: @@ -192,18 +192,20 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): raise TownNotFound return self._town - def _get_weather_and_forecast(self): + async def _get_weather_and_forecast(self): """Get weather and forecast data from AEMET OpenData.""" - self._get_weather_town() + await self._get_weather_town() - daily = self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID]) + daily = await self._aemet.get_specific_forecast_town_daily( + self._town[AEMET_ATTR_ID] + ) if not daily: _LOGGER.error( 'Error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID] ) - hourly = self._aemet.get_specific_forecast_town_hourly( + hourly = await self._aemet.get_specific_forecast_town_hourly( self._town[AEMET_ATTR_ID] ) if not hourly: @@ -212,8 +214,8 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ) station = None - if self._station_updates and self._get_weather_station(): - station = self._aemet.get_conventional_observation_station_data( + if self._station_updates and await self._get_weather_station(): + station = await self._aemet.get_conventional_observation_station_data( self._station[AEMET_ATTR_IDEMA] ) if not station: diff --git a/requirements_all.txt b/requirements_all.txt index ece32ca2308..55b7de79dfb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.2.2 +AEMET-OpenData==0.3.0 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40200466222..1a6220971c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.2.2 +AEMET-OpenData==0.3.0 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 diff --git a/tests/components/aemet/fixtures/station-3195.json b/tests/components/aemet/fixtures/station-3195.json deleted file mode 100644 index cfd8c59a7ee..00000000000 --- a/tests/components/aemet/fixtures/station-3195.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "descripcion": "exito", - "estado": 200, - "datos": "https://opendata.aemet.es/opendata/sh/208c3ca3", - "metadatos": "https://opendata.aemet.es/opendata/sh/55c2971b" -} diff --git a/tests/components/aemet/fixtures/station-list.json b/tests/components/aemet/fixtures/station-list.json deleted file mode 100644 index 86f79727e7f..00000000000 --- a/tests/components/aemet/fixtures/station-list.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "descripcion": "exito", - "estado": 200, - "datos": "https://opendata.aemet.es/opendata/sh/2c55192f", - "metadatos": "https://opendata.aemet.es/opendata/sh/55c2971b" -} diff --git a/tests/components/aemet/fixtures/town-28065-forecast-daily.json b/tests/components/aemet/fixtures/town-28065-forecast-daily.json deleted file mode 100644 index 41103c1033f..00000000000 --- a/tests/components/aemet/fixtures/town-28065-forecast-daily.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "descripcion": "exito", - "estado": 200, - "datos": "https://opendata.aemet.es/opendata/sh/64e29abb", - "metadatos": "https://opendata.aemet.es/opendata/sh/dfd88b22" -} diff --git a/tests/components/aemet/fixtures/town-28065-forecast-hourly.json b/tests/components/aemet/fixtures/town-28065-forecast-hourly.json deleted file mode 100644 index cdcacfcb6a5..00000000000 --- a/tests/components/aemet/fixtures/town-28065-forecast-hourly.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "descripcion": "exito", - "estado": 200, - "datos": "https://opendata.aemet.es/opendata/sh/18ca1886", - "metadatos": "https://opendata.aemet.es/opendata/sh/93a7c63d" -} diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index 59a6993903f..b311cfd4a54 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -1,8 +1,8 @@ """Define tests for the AEMET OpenData config flow.""" from unittest.mock import AsyncMock, MagicMock, patch +from aemet_opendata.exceptions import AuthError import pytest -import requests_mock from homeassistant import data_entry_flow from homeassistant.components.aemet.const import CONF_STATION_UPDATES, DOMAIN @@ -11,7 +11,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CON from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from .util import aemet_requests_mock +from .util import mock_api_call from tests.common import MockConfigEntry @@ -28,9 +28,10 @@ CONFIG = { async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test that the form is served with valid input.""" - with requests_mock.mock() as _m: - aemet_requests_mock(_m) - + with patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -64,9 +65,10 @@ async def test_form_options(hass: HomeAssistant) -> None: now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( "homeassistant.util.dt.utcnow", return_value=now - ), requests_mock.mock() as _m: - aemet_requests_mock(_m) - + ), patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): entry = MockConfigEntry( domain=DOMAIN, unique_id="40.30403754--3.72935236", data=CONFIG ) @@ -120,9 +122,10 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None: now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( "homeassistant.util.dt.utcnow", return_value=now - ), requests_mock.mock() as _m: - aemet_requests_mock(_m) - + ), patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): entry = MockConfigEntry( domain=DOMAIN, unique_id="40.30403754--3.72935236", data=CONFIG ) @@ -136,11 +139,10 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" -async def test_form_api_offline(hass: HomeAssistant) -> None: - """Test setting up with api call error.""" +async def test_form_auth_error(hass: HomeAssistant) -> None: + """Test setting up with api auth error.""" mocked_aemet = MagicMock() - - mocked_aemet.get_conventional_observation_stations.return_value = None + mocked_aemet.get_conventional_observation_stations.side_effect = AuthError with patch( "homeassistant.components.aemet.config_flow.AEMET", diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index 9db0ffb2bcf..24c16ba3ef3 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -1,15 +1,13 @@ """Define tests for the AEMET OpenData init.""" from unittest.mock import patch -import requests_mock - from homeassistant.components.aemet.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from .util import aemet_requests_mock +from .util import mock_api_call from tests.common import MockConfigEntry @@ -27,9 +25,10 @@ async def test_unload_entry(hass: HomeAssistant) -> None: now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( "homeassistant.util.dt.utcnow", return_value=now - ), requests_mock.mock() as _m: - aemet_requests_mock(_m) - + ), patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): config_entry = MockConfigEntry( domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG ) diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py index c64e824e18d..703ef4348f8 100644 --- a/tests/components/aemet/test_weather.py +++ b/tests/components/aemet/test_weather.py @@ -4,7 +4,6 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest -import requests_mock from syrupy.assertion import SnapshotAssertion from homeassistant.components.aemet.const import ATTRIBUTION, DOMAIN @@ -36,7 +35,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util -from .util import aemet_requests_mock, async_init_integration +from .util import async_init_integration, mock_api_call from tests.typing import WebSocketGenerator @@ -191,8 +190,10 @@ async def test_forecast_subscription( assert forecast1 == snapshot - with requests_mock.mock() as _m: - aemet_requests_mock(_m) + with patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): freezer.tick(WEATHER_UPDATE_INTERVAL + datetime.timedelta(seconds=1)) await hass.async_block_till_done() msg = await client.receive_json() diff --git a/tests/components/aemet/util.py b/tests/components/aemet/util.py index 991e7459bf6..05417563e2f 100644 --- a/tests/components/aemet/util.py +++ b/tests/components/aemet/util.py @@ -1,93 +1,74 @@ """Tests for the AEMET OpenData integration.""" +from typing import Any +from unittest.mock import patch -import requests_mock +from aemet_opendata.const import ATTR_DATA from homeassistant.components.aemet import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, load_json_value_fixture + +FORECAST_DAILY_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/town-28065-forecast-daily-data.json"), +} + +FORECAST_HOURLY_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/town-28065-forecast-hourly-data.json"), +} + +STATION_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/station-3195-data.json"), +} + +STATIONS_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/station-list-data.json"), +} + +TOWN_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/town-id28065.json"), +} + +TOWNS_DATA_MOCK = { + ATTR_DATA: load_json_value_fixture("aemet/town-list.json"), +} -def aemet_requests_mock(mock): - """Mock requests performed to AEMET OpenData API.""" - - station_3195_fixture = "aemet/station-3195.json" - station_3195_data_fixture = "aemet/station-3195-data.json" - station_list_fixture = "aemet/station-list.json" - station_list_data_fixture = "aemet/station-list-data.json" - - town_28065_forecast_daily_fixture = "aemet/town-28065-forecast-daily.json" - town_28065_forecast_daily_data_fixture = "aemet/town-28065-forecast-daily-data.json" - town_28065_forecast_hourly_fixture = "aemet/town-28065-forecast-hourly.json" - town_28065_forecast_hourly_data_fixture = ( - "aemet/town-28065-forecast-hourly-data.json" - ) - town_id28065_fixture = "aemet/town-id28065.json" - town_list_fixture = "aemet/town-list.json" - - mock.get( - "https://opendata.aemet.es/opendata/api/observacion/convencional/datos/estacion/3195", - text=load_fixture(station_3195_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/sh/208c3ca3", - text=load_fixture(station_3195_data_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/api/observacion/convencional/todas", - text=load_fixture(station_list_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/sh/2c55192f", - text=load_fixture(station_list_data_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/diaria/28065", - text=load_fixture(town_28065_forecast_daily_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/sh/64e29abb", - text=load_fixture(town_28065_forecast_daily_data_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/api/prediccion/especifica/municipio/horaria/28065", - text=load_fixture(town_28065_forecast_hourly_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/sh/18ca1886", - text=load_fixture(town_28065_forecast_hourly_data_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/api/maestro/municipio/id28065", - text=load_fixture(town_id28065_fixture), - ) - mock.get( - "https://opendata.aemet.es/opendata/api/maestro/municipios", - text=load_fixture(town_list_fixture), - ) +def mock_api_call(cmd: str, fetch_data: bool = False) -> dict[str, Any]: + """Mock AEMET OpenData API calls.""" + if cmd == "maestro/municipio/id28065": + return TOWN_DATA_MOCK + if cmd == "maestro/municipios": + return TOWNS_DATA_MOCK + if cmd == "observacion/convencional/datos/estacion/3195": + return STATION_DATA_MOCK + if cmd == "observacion/convencional/todas": + return STATIONS_DATA_MOCK + if cmd == "prediccion/especifica/municipio/diaria/28065": + return FORECAST_DAILY_DATA_MOCK + if cmd == "prediccion/especifica/municipio/horaria/28065": + return FORECAST_HOURLY_DATA_MOCK + return {} -async def async_init_integration( - hass: HomeAssistant, - skip_setup: bool = False, -): +async def async_init_integration(hass: HomeAssistant): """Set up the AEMET OpenData integration in Home Assistant.""" - with requests_mock.mock() as _m: - aemet_requests_mock(_m) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "api-key", + CONF_LATITUDE: "40.30403754", + CONF_LONGITUDE: "-3.72935236", + CONF_NAME: "AEMET", + }, + ) + config_entry.add_to_hass(hass) - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "mock", - CONF_LATITUDE: "40.30403754", - CONF_LONGITUDE: "-3.72935236", - CONF_NAME: "AEMET", - }, - ) - entry.add_to_hass(hass) - - if not skip_setup: - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + with patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() From c4ae9ae430f7e8f438dfce4426e3b8455f77509a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 22 Aug 2023 23:23:13 +0200 Subject: [PATCH 0761/1151] Remove data rate converting code from NZBGet (#98806) --- homeassistant/components/nzbget/sensor.py | 31 ++++++++--------------- tests/components/nzbget/test_sensor.py | 6 ++--- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 6d94ef35456..7f4d31c3adf 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -13,6 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, UnitOfDataRate, UnitOfInformation from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utcnow from . import NZBGetEntity @@ -32,7 +33,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="AverageDownloadRate", name="Average Speed", device_class=SensorDeviceClass.DATA_RATE, - native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, ), SensorEntityDescription( key="DownloadPaused", @@ -42,7 +44,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="DownloadRate", name="Speed", device_class=SensorDeviceClass.DATA_RATE, - native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, ), SensorEntityDescription( key="DownloadedSizeMB", @@ -80,7 +83,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="DownloadLimit", name="Speed Limit", device_class=SensorDeviceClass.DATA_RATE, - native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, ), ) @@ -121,30 +125,17 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): self.entity_description = description self._attr_unique_id = f"{entry_id}_{description.key}" - self._native_value: datetime | None = None @property - def native_value(self): + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" sensor_type = self.entity_description.key value = self.coordinator.data["status"].get(sensor_type) - if value is None: - _LOGGER.warning("Unable to locate value for %s", sensor_type) - self._native_value = None - elif "DownloadRate" in sensor_type and value > 0: - # Convert download rate from Bytes/s to MBytes/s - self._native_value = round(value / 2**20, 2) - elif "DownloadLimit" in sensor_type and value > 0: - # Convert download rate from Bytes/s to MBytes/s - self._native_value = round(value / 2**20, 2) - elif "UpTimeSec" in sensor_type and value > 0: + if value is not None and "UpTimeSec" in sensor_type and value > 0: uptime = utcnow().replace(microsecond=0) - timedelta(seconds=value) if not isinstance(self._attr_native_value, datetime) or abs( uptime - self._attr_native_value ) > timedelta(seconds=5): - self._native_value = uptime - else: - self._native_value = value - - return self._native_value + return uptime + return value diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index 980fe14970a..e9365e36b24 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -34,14 +34,14 @@ async def test_sensors(hass: HomeAssistant, nzbget_api) -> None: ), "average_speed": ( "AverageDownloadRate", - "1.19", + "1.250000", UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), "download_paused": ("DownloadPaused", "False", None, None), "speed": ( "DownloadRate", - "2.38", + "2.500000", UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), @@ -68,7 +68,7 @@ async def test_sensors(hass: HomeAssistant, nzbget_api) -> None: "uptime": ("UpTimeSec", uptime.isoformat(), None, SensorDeviceClass.TIMESTAMP), "speed_limit": ( "DownloadLimit", - "0.95", + "1.000000", UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), From 65691fffd6d00c5292ea1cff990662268652fc2e Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 22 Aug 2023 21:28:39 -0400 Subject: [PATCH 0762/1151] Change Enphase dry contact relay binary_sensor to switch (#98467) * Switch relay status from binary_sensor to switch * docstring * Bump pyenphase to 1.7.1 * review comments pt1 * review comments pt2 * Mutate data in lib instead of HA * Bump pyenphase to 1.8.1 --- .../components/enphase_envoy/binary_sensor.py | 43 ---------- .../components/enphase_envoy/manifest.json | 2 +- .../components/enphase_envoy/strings.json | 3 - .../components/enphase_envoy/switch.py | 81 ++++++++++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 83 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index 009b5d18338..7060943deb8 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -5,7 +5,6 @@ from collections.abc import Callable from dataclasses import dataclass from pyenphase import EnvoyEncharge, EnvoyEnpower -from pyenphase.models.dry_contacts import DryContactStatus from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -53,12 +52,6 @@ ENCHARGE_SENSORS = ( ), ) -RELAY_STATUS_SENSOR = BinarySensorEntityDescription( - key="relay_status", - translation_key="relay", - icon="mdi:power-plug", -) - @dataclass class EnvoyEnpowerRequiredKeysMixin: @@ -114,11 +107,6 @@ async def async_setup_entry( 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) @@ -190,34 +178,3 @@ class EnvoyEnpowerBinarySensorEntity(EnvoyBaseBinarySensorEntity): 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_id: str, - ) -> None: - """Init the Enpower base entity.""" - super().__init__(coordinator, description) - enpower = self.data.enpower - assert enpower is not None - self._relay_id = relay_id - self._attr_unique_id = f"{enpower.serial_number}_relay_{relay_id}" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, relay_id)}, - manufacturer="Enphase", - model="Dry contact relay", - name=self.data.dry_contact_settings[relay_id].load_name, - sw_version=str(enpower.firmware_version), - via_device=(DOMAIN, enpower.serial_number), - ) - - @property - def is_on(self) -> bool: - """Return the state of the Enpower binary_sensor.""" - relay = self.data.dry_contact_status[self._relay_id] - return relay.status == DryContactStatus.CLOSED diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 62f7c73ef76..540c121bb17 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.6.0"], + "requirements": ["pyenphase==1.8.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index 477da2b3211..ae0ac31413c 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -31,9 +31,6 @@ }, "grid_status": { "name": "Grid status" - }, - "relay": { - "name": "Relay status" } }, "number": { diff --git a/homeassistant/components/enphase_envoy/switch.py b/homeassistant/components/enphase_envoy/switch.py index e0f211a1019..fb9e14406ac 100644 --- a/homeassistant/components/enphase_envoy/switch.py +++ b/homeassistant/components/enphase_envoy/switch.py @@ -6,7 +6,8 @@ from dataclasses import dataclass import logging from typing import Any -from pyenphase import Envoy, EnvoyEnpower +from pyenphase import Envoy, EnvoyDryContactStatus, EnvoyEnpower +from pyenphase.models.dry_contacts import DryContactStatus from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry @@ -37,6 +38,22 @@ class EnvoyEnpowerSwitchEntityDescription( """Describes an Envoy Enpower switch entity.""" +@dataclass +class EnvoyDryContactRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnvoyDryContactStatus], bool] + turn_on_fn: Callable[[Envoy, str], Coroutine[Any, Any, dict[str, Any]]] + turn_off_fn: Callable[[Envoy, str], Coroutine[Any, Any, dict[str, Any]]] + + +@dataclass +class EnvoyDryContactSwitchEntityDescription( + SwitchEntityDescription, EnvoyDryContactRequiredKeysMixin +): + """Describes an Envoy Enpower dry contact switch entity.""" + + ENPOWER_GRID_SWITCH = EnvoyEnpowerSwitchEntityDescription( key="mains_admin_state", translation_key="grid_enabled", @@ -45,6 +62,13 @@ ENPOWER_GRID_SWITCH = EnvoyEnpowerSwitchEntityDescription( turn_off_fn=lambda envoy: envoy.go_off_grid(), ) +RELAY_STATE_SWITCH = EnvoyDryContactSwitchEntityDescription( + key="relay_status", + value_fn=lambda dry_contact: dry_contact.status == DryContactStatus.CLOSED, + turn_on_fn=lambda envoy, id: envoy.close_dry_contact(id), + turn_off_fn=lambda envoy, id: envoy.open_dry_contact(id), +) + async def async_setup_entry( hass: HomeAssistant, @@ -64,6 +88,13 @@ async def async_setup_entry( ) ] ) + + if envoy_data.dry_contact_status: + entities.extend( + EnvoyDryContactSwitchEntity(coordinator, RELAY_STATE_SWITCH, relay) + for relay in envoy_data.dry_contact_status + ) + async_add_entities(entities) @@ -109,3 +140,51 @@ class EnvoyEnpowerSwitchEntity(EnvoyBaseEntity, SwitchEntity): """Turn off the Enpower switch.""" await self.entity_description.turn_off_fn(self.envoy) await self.coordinator.async_request_refresh() + + +class EnvoyDryContactSwitchEntity(EnvoyBaseEntity, SwitchEntity): + """Representation of an Enphase dry contact switch entity.""" + + entity_description: EnvoyDryContactSwitchEntityDescription + _attr_name = None + + def __init__( + self, + coordinator: EnphaseUpdateCoordinator, + description: EnvoyDryContactSwitchEntityDescription, + relay_id: str, + ) -> None: + """Initialize the Enphase dry contact switch entity.""" + super().__init__(coordinator, description) + self.envoy = coordinator.envoy + enpower = self.data.enpower + assert enpower is not None + self.relay_id = relay_id + serial_number = enpower.serial_number + self._attr_unique_id = f"{serial_number}_relay_{relay_id}_{description.key}" + relay = self.data.dry_contact_settings[relay_id] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, relay_id)}, + manufacturer="Enphase", + model="Dry contact relay", + name=relay.load_name, + sw_version=str(enpower.firmware_version), + via_device=(DOMAIN, enpower.serial_number), + ) + + @property + def is_on(self) -> bool: + """Return the state of the dry contact.""" + relay = self.data.dry_contact_status[self.relay_id] + assert relay is not None + return self.entity_description.value_fn(relay) + + async def async_turn_on(self): + """Turn on (close) the dry contact.""" + if await self.entity_description.turn_on_fn(self.envoy, self.relay_id): + self.async_write_ha_state() + + async def async_turn_off(self): + """Turn off (open) the dry contact.""" + if await self.entity_description.turn_off_fn(self.envoy, self.relay_id): + self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 55b7de79dfb..4a9592a5a1e 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.6.0 +pyenphase==1.8.1 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a6220971c4..bfdca770319 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.6.0 +pyenphase==1.8.1 # homeassistant.components.everlights pyeverlights==0.1.0 From 26d7e9958f4a38ed6e42ddff7a6135518d0e581b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 23 Aug 2023 08:11:28 +0200 Subject: [PATCH 0763/1151] Remove YAML solution from Open Exchange Rates (#98815) --- .../components/openexchangerates/sensor.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index c7806fd90d8..70f2f670de8 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_QUOTE +from homeassistant.const import CONF_QUOTE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -21,14 +21,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Open Exchange Rates sensor.""" - # Only YAML imported configs have name and quote in config entry data. - name: str | None = config_entry.data.get(CONF_NAME) quote: str = config_entry.data.get(CONF_QUOTE, "EUR") coordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( OpenexchangeratesSensor( - config_entry, coordinator, name, rate_quote, rate_quote == quote + config_entry, coordinator, rate_quote, rate_quote == quote ) for rate_quote in coordinator.data.rates ) @@ -39,13 +37,13 @@ class OpenexchangeratesSensor( ): """Representation of an Open Exchange Rates sensor.""" + _attr_has_entity_name = True _attr_attribution = ATTRIBUTION def __init__( self, config_entry: ConfigEntry, coordinator: OpenexchangeratesCoordinator, - name: str | None, quote: str, enabled: bool, ) -> None: @@ -58,14 +56,7 @@ class OpenexchangeratesSensor( name=f"Open Exchange Rates {coordinator.base}", ) self._attr_entity_registry_enabled_default = enabled - if name and enabled: - # name is legacy imported from YAML config - # this block can be removed when removing import from YAML - self._attr_name = name - self._attr_has_entity_name = False - else: - self._attr_name = quote - self._attr_has_entity_name = True + self._attr_name = quote self._attr_native_unit_of_measurement = quote self._attr_unique_id = f"{config_entry.entry_id}_{quote}" self._quote = quote From 6be47b1fbde48d810cc1d5cfd9824f41513805fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 23 Aug 2023 09:20:53 +0200 Subject: [PATCH 0764/1151] Fix Airzone Cloud diagnostics (#98857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- .../airzone_cloud/snapshots/test_diagnostics.ambr | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr index abdcc90978d..94e602ec03b 100644 --- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -139,12 +139,17 @@ 'systems': dict({ 'system1': dict({ 'available': True, + 'errors': list([ + dict({ + '_id': 'error-id', + }), + ]), 'id': 'system1', 'installation': 'installation1', 'is-connected': True, 'mode': None, 'name': 'System 1', - 'problems': False, + 'problems': True, 'system': 1, 'web-server': 'webserver1', 'ws-connected': True, From 5ae366957f1b4f4199fb284c24b4fce839d593e6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 23 Aug 2023 10:52:19 +0200 Subject: [PATCH 0765/1151] Fix imap test RuntimeWarning (#98865) --- tests/components/imap/test_init.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index b9512da0278..b4ee11ba787 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -470,6 +470,8 @@ async def test_reset_last_message( ) -> None: """Test receiving a message successfully.""" event = asyncio.Event() # needed for pushed coordinator to make a new loop + idle_start_future = asyncio.Future() + idle_start_future.set_result(None) async def _sleep_till_event() -> None: """Simulate imap server waiting for pushes message and keep the push loop going. @@ -479,10 +481,10 @@ async def test_reset_last_message( nonlocal event await event.wait() event.clear() - mock_imap_protocol.idle_start.return_value = AsyncMock()() + mock_imap_protocol.idle_start = AsyncMock(return_value=idle_start_future) # Make sure we make another cycle (needed for pushed coordinator) - mock_imap_protocol.idle_start.return_value = AsyncMock()() + mock_imap_protocol.idle_start = AsyncMock(return_value=idle_start_future) # Mock we wait till we push an update (needed for pushed coordinator) mock_imap_protocol.wait_server_push.side_effect = _sleep_till_event From b143fe285ff4ffff2a8c8310d3bef6513b5cea00 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Aug 2023 11:29:03 +0200 Subject: [PATCH 0766/1151] Enable code coverage for metoffice sensor + weather (#98863) --- .coveragerc | 2 -- 1 file changed, 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 2fbfa15997b..7d8147ab648 100644 --- a/.coveragerc +++ b/.coveragerc @@ -725,8 +725,6 @@ omit = homeassistant/components/meteoclimatic/__init__.py homeassistant/components/meteoclimatic/sensor.py homeassistant/components/meteoclimatic/weather.py - homeassistant/components/metoffice/sensor.py - homeassistant/components/metoffice/weather.py homeassistant/components/microsoft/tts.py homeassistant/components/mikrotik/hub.py homeassistant/components/mill/climate.py From 918d822ec792ac23d3564dd6fbfdd870af8bcdb1 Mon Sep 17 00:00:00 2001 From: Barry Williams Date: Wed, 23 Aug 2023 10:31:46 +0100 Subject: [PATCH 0767/1151] Refactor openhome media player getters and attrs (#98690) --- .../components/openhome/media_player.py | 106 +++++------------- 1 file changed, 25 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 1e3654958ab..efc6ab37f21 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -106,18 +106,11 @@ class OpenhomeDevice(MediaPlayerEntity): """Initialise the Openhome device.""" self.hass = hass self._device = device - self._track_information = {} - self._in_standby = None - self._transport_state = None - self._volume_level = None - self._volume_muted = None + self._attr_unique_id = device.uuid() self._attr_supported_features = SUPPORT_OPENHOME - self._source_names = [] self._source_index = {} - self._source = {} - self._name = None self._attr_state = MediaPlayerState.PLAYING - self._available = True + self._attr_available = True @property def device_info(self): @@ -131,47 +124,47 @@ class OpenhomeDevice(MediaPlayerEntity): name=self._device.friendly_name(), ) - @property - def available(self): - """Device is available.""" - return self._available - async def async_update(self) -> None: """Update state of device.""" try: - self._in_standby = await self._device.is_in_standby() - self._transport_state = await self._device.transport_state() - self._track_information = await self._device.track_info() - self._source = await self._device.source() - self._name = await self._device.room() + self._attr_name = await self._device.room() self._attr_supported_features = SUPPORT_OPENHOME source_index = {} source_names = [] + track_information = await self._device.track_info() + self._attr_media_image_url = track_information.get("albumArtwork") + self._attr_media_album_name = track_information.get("albumTitle") + self._attr_media_title = track_information.get("title") + if artists := track_information.get("artist"): + self._attr_media_artist = artists[0] + if self._device.volume_enabled: self._attr_supported_features |= ( MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_SET ) - self._volume_level = await self._device.volume() / 100.0 - self._volume_muted = await self._device.is_muted() + self._attr_volume_level = await self._device.volume() / 100.0 + self._attr_is_volume_muted = await self._device.is_muted() for source in await self._device.sources(): source_names.append(source["name"]) source_index[source["name"]] = source["index"] + source = await self._device.source() + self._attr_source = source.get("name") self._source_index = source_index - self._source_names = source_names + self._attr_source_list = source_names - if self._source["type"] == "Radio": + if source["type"] == "Radio": self._attr_supported_features |= ( MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.BROWSE_MEDIA ) - if self._source["type"] in ("Playlist", "Spotify"): + if source["type"] in ("Playlist", "Spotify"): self._attr_supported_features |= ( MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.NEXT_TRACK @@ -181,21 +174,23 @@ class OpenhomeDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.BROWSE_MEDIA ) - if self._in_standby: + in_standby = await self._device.is_in_standby() + transport_state = await self._device.transport_state() + if in_standby: self._attr_state = MediaPlayerState.OFF - elif self._transport_state == "Paused": + elif transport_state == "Paused": self._attr_state = MediaPlayerState.PAUSED - elif self._transport_state in ("Playing", "Buffering"): + elif transport_state in ("Playing", "Buffering"): self._attr_state = MediaPlayerState.PLAYING - elif self._transport_state == "Stopped": + elif transport_state == "Stopped": self._attr_state = MediaPlayerState.IDLE else: # Device is playing an external source with no transport controls self._attr_state = MediaPlayerState.PLAYING - self._available = True + self._attr_available = True except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): - self._available = False + self._attr_available = False @catch_request_errors() async def async_turn_on(self) -> None: @@ -273,57 +268,6 @@ class OpenhomeDevice(MediaPlayerEntity): except UpnpError: _LOGGER.error("Error invoking pin %s", pin) - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self): - """Return a unique ID.""" - return self._device.uuid() - - @property - def source_list(self): - """List of available input sources.""" - return self._source_names - - @property - def media_image_url(self): - """Image url of current playing media.""" - return self._track_information.get("albumArtwork") - - @property - def media_artist(self): - """Artist of current playing media, music track only.""" - if artists := self._track_information.get("artist"): - return artists[0] - - @property - def media_album_name(self): - """Album name of current playing media, music track only.""" - return self._track_information.get("albumTitle") - - @property - def media_title(self): - """Title of current playing media.""" - return self._track_information.get("title") - - @property - def source(self): - """Name of the current input source.""" - return self._source.get("name") - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume_level - - @property - def is_volume_muted(self): - """Return true if volume is muted.""" - return self._volume_muted - @catch_request_errors() async def async_volume_up(self) -> None: """Volume up media player.""" From a2b01496776d4f9631113b3d65345d6dfd66717e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 23 Aug 2023 11:35:04 +0200 Subject: [PATCH 0768/1151] Remove config name from IPMA config flow (#98576) --- homeassistant/components/ipma/config_flow.py | 73 +++++++++++--------- homeassistant/components/ipma/strings.json | 3 + tests/components/ipma/test_config_flow.py | 65 ++++++++++++++--- 3 files changed, 101 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index d7b8b8cc003..cdea88bdbc0 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -1,53 +1,64 @@ """Config flow to configure IPMA component.""" +import logging +from typing import Any + +from pyipma import IPMAException +from pyipma.api import IPMA_API +from pyipma.location import Location import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, HOME_LOCATION_NAME +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) -class IpmaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class IpmaFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for IPMA component.""" VERSION = 1 - def __init__(self): - """Init IpmaFlowHandler.""" - self._errors = {} - - 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.""" - self._errors = {} + errors = {} if user_input is not None: - self._async_abort_entries_match( - { - CONF_LATITUDE: user_input[CONF_LATITUDE], - CONF_LONGITUDE: user_input[CONF_LONGITUDE], - } - ) + self._async_abort_entries_match(user_input) - return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + api = IPMA_API(async_get_clientsession(self.hass)) - # default location is set hass configuration - return await self._show_config_form( - name=HOME_LOCATION_NAME, - latitude=self.hass.config.latitude, - longitude=self.hass.config.longitude, - ) + try: + location = await Location.get( + api, + user_input[CONF_LATITUDE], + user_input[CONF_LONGITUDE], + ) + except IPMAException as err: + _LOGGER.exception(err) + errors["base"] = "unknown" + else: + return self.async_create_entry(title=location.name, data=user_input) - async def _show_config_form(self, name=None, latitude=None, longitude=None): - """Show the configuration form to edit location data.""" return self.async_show_form( step_id="user", - data_schema=vol.Schema( + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + } + ), { - vol.Required(CONF_NAME, default=name): str, - vol.Required(CONF_LATITUDE, default=latitude): cv.latitude, - vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude, - } + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + }, ), - errors=self._errors, + errors=errors, ) diff --git a/homeassistant/components/ipma/strings.json b/homeassistant/components/ipma/strings.json index 012550d8bd1..b9b672e77d9 100644 --- a/homeassistant/components/ipma/strings.json +++ b/homeassistant/components/ipma/strings.json @@ -12,6 +12,9 @@ } } }, + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]" + }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index 18b68a5a44d..aff8af16bc3 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,12 +1,16 @@ """Tests for IPMA config flow.""" from unittest.mock import patch +from pyipma import IPMAException import pytest -from homeassistant import config_entries, data_entry_flow from homeassistant.components.ipma.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.components.ipma import MockLocation @pytest.fixture(name="ipma_setup", autouse=True) @@ -19,7 +23,7 @@ def ipma_setup_fixture(request): async def test_config_flow(hass: HomeAssistant) -> None: """Test configuration form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == "form" @@ -29,16 +33,59 @@ async def test_config_flow(hass: HomeAssistant) -> None: CONF_LONGITUDE: 0, CONF_LATITUDE: 0, } + with patch( + "pyipma.location.Location.get", + return_value=MockLocation(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + test_data, + ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - test_data, + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "HomeTown" + assert result["data"] == { + CONF_LONGITUDE: 0, + CONF_LATITUDE: 0, + } + + +async def test_config_flow_failures(hass: HomeAssistant) -> None: + """Test config flow with failures.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "Home" + assert result["type"] == "form" + assert result["step_id"] == "user" + + test_data = { + CONF_LONGITUDE: 0, + CONF_LATITUDE: 0, + } + with patch( + "pyipma.location.Location.get", + side_effect=IPMAException(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + test_data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "unknown"} + with patch( + "pyipma.location.Location.get", + return_value=MockLocation(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + test_data, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "HomeTown" assert result["data"] == { - CONF_NAME: "Home", CONF_LONGITUDE: 0, CONF_LATITUDE: 0, } @@ -57,7 +104,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant, config_entry) -> N } result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data + DOMAIN, context={"source": SOURCE_USER}, data=test_data ) await hass.async_block_till_done() From 5b3c60527ab7d7e6facf19a98834452c3d74c528 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 23 Aug 2023 11:52:28 +0200 Subject: [PATCH 0769/1151] Clean up Freebox config flow (#97347) Co-authored-by: Robert Resch --- .../components/freebox/config_flow.py | 68 +++++++++---------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index af641b5430c..c037d3eee1a 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure the Freebox integration.""" import logging +from typing import Any from freebox_api.exceptions import AuthorizationError, HttpRequestError import voluptuous as vol @@ -20,45 +21,35 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self) -> None: - """Initialize Freebox config flow.""" - self._host: str - self._port = None + _data: dict[str, Any] = {} - def _show_setup_form(self, user_input=None, errors=None): - """Show the setup form to the user.""" - - if user_input is None: - user_input = {} - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, - vol.Required(CONF_PORT, default=user_input.get(CONF_PORT, "")): int, - } - ), - errors=errors or {}, - ) - - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" - errors: dict[str, str] = {} - if user_input is None: - return self._show_setup_form(user_input, errors) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT): int, + } + ), + errors={}, + ) - self._host = user_input[CONF_HOST] - self._port = user_input[CONF_PORT] + self._data = user_input # Check if already configured - await self.async_set_unique_id(self._host) + await self.async_set_unique_id(self._data[CONF_HOST]) self._abort_if_unique_id_configured() return await self.async_step_link() - async def async_step_link(self, user_input=None) -> FlowResult: + async def async_step_link( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Attempt to link with the Freebox router. Given a configured host, will ask the user to press the button @@ -69,10 +60,10 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} - fbx = await get_api(self.hass, self._host) + fbx = await get_api(self.hass, self._data[CONF_HOST]) try: # Open connection and check authentication - await fbx.open(self._host, self._port) + await fbx.open(self._data[CONF_HOST], self._data[CONF_PORT]) # Check permissions await fbx.system.get_config() @@ -82,8 +73,8 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await fbx.close() return self.async_create_entry( - title=self._host, - data={CONF_HOST: self._host, CONF_PORT: self._port}, + title=self._data[CONF_HOST], + data=self._data, ) except AuthorizationError as error: @@ -91,18 +82,23 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "register_failed" except HttpRequestError: - _LOGGER.error("Error connecting to the Freebox router at %s", self._host) + _LOGGER.error( + "Error connecting to the Freebox router at %s", self._data[CONF_HOST] + ) errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception( - "Unknown error connecting with Freebox router at %s", self._host + "Unknown error connecting with Freebox router at %s", + self._data[CONF_HOST], ) errors["base"] = "unknown" return self.async_show_form(step_id="link", errors=errors) - async def async_step_import(self, user_input=None) -> FlowResult: + async def async_step_import( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Import a config entry.""" return await self.async_step_user(user_input) From 4f9c6351b0d2dcc3378f8ea1a5632868a629eb52 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 23 Aug 2023 12:08:01 +0200 Subject: [PATCH 0770/1151] Use constructor in Freebox config flow (#98870) Create data object in init --- homeassistant/components/freebox/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index c037d3eee1a..2260e69cc3c 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -21,7 +21,9 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - _data: dict[str, Any] = {} + def __init__(self) -> None: + """Initialize config flow.""" + self._data: dict[str, Any] = {} async def async_step_user( self, user_input: dict[str, Any] | None = None From 480c34180e08e233872e09e382348d19df493103 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 23 Aug 2023 12:17:32 +0200 Subject: [PATCH 0771/1151] Fix forked_daapd test RuntimeWarning (#98864) --- tests/components/forked_daapd/test_browse_media.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index 3d540c1f5af..4d15f083591 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -35,6 +35,7 @@ async def test_async_browse_media( "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", autospec=True, ) as mock_api: + mock_api.return_value.get_request.return_value = {"websocket_port": 2} config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -212,6 +213,7 @@ async def test_async_browse_media_not_found( "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", autospec=True, ) as mock_api: + mock_api.return_value.get_request.return_value = {"websocket_port": 2} config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -366,6 +368,7 @@ async def test_async_browse_image( "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", autospec=True, ) as mock_api: + mock_api.return_value.get_request.return_value = {"websocket_port": 2} config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -421,6 +424,7 @@ async def test_async_browse_image_missing( "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI", autospec=True, ) as mock_api: + mock_api.return_value.get_request.return_value = {"websocket_port": 2} config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 0aeeac5c42aa30d12e6ebd95ea14dbea1d649685 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Wed, 23 Aug 2023 04:19:25 -0600 Subject: [PATCH 0772/1151] Bump pylitterbot to 2023.4.5 (#98854) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 81375dd3a6c..9a3334cbaac 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["pylitterbot"], - "requirements": ["pylitterbot==2023.4.4"] + "requirements": ["pylitterbot==2023.4.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4a9592a5a1e..813e3af0ce7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1812,7 +1812,7 @@ pylibrespot-java==0.1.1 pylitejet==0.5.0 # homeassistant.components.litterrobot -pylitterbot==2023.4.4 +pylitterbot==2023.4.5 # homeassistant.components.lutron_caseta pylutron-caseta==0.18.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfdca770319..681755137b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1340,7 +1340,7 @@ pylibrespot-java==0.1.1 pylitejet==0.5.0 # homeassistant.components.litterrobot -pylitterbot==2023.4.4 +pylitterbot==2023.4.5 # homeassistant.components.lutron_caseta pylutron-caseta==0.18.1 From 3b16a3e1e0fbbec480e9a6a1249aeaab87b5c1ef Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Aug 2023 14:04:06 +0200 Subject: [PATCH 0773/1151] Small typing fix in binary_sensor group (#98874) --- homeassistant/components/group/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 7415ee8c60d..0c4bf89057d 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -100,7 +100,7 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): name: str, device_class: BinarySensorDeviceClass | None, entity_ids: list[str], - mode: str | None, + mode: bool | None, ) -> None: """Initialize a BinarySensorGroup entity.""" super().__init__() From e3b945a8d015f624deb4deb8602df29f4db38c07 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Aug 2023 14:16:40 +0200 Subject: [PATCH 0774/1151] Don't allow numerical sensor state to be NaN or inf (#98110) --- homeassistant/components/sensor/__init__.py | 17 +++++++++++++++-- tests/components/sensor/test_init.py | 19 ++++++++++++------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index c00d20d51ef..b8151256519 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from datetime import UTC, date, datetime, timedelta from decimal import Decimal, InvalidOperation as DecimalInvalidOperation import logging -from math import ceil, floor, log10 +from math import ceil, floor, isfinite, log10 from typing import Any, Final, Self, cast, final from homeassistant.config_entries import ConfigEntry @@ -582,7 +582,11 @@ class SensorEntity(Entity): if not isinstance(value, (int, float, Decimal)): try: if isinstance(value, str) and "." not in value and "e" not in value: - numerical_value = int(value) + try: + numerical_value = int(value) + except ValueError: + # Handle nan, inf + numerical_value = float(value) else: numerical_value = float(value) # type:ignore[arg-type] except (TypeError, ValueError) as err: @@ -596,6 +600,15 @@ class SensorEntity(Entity): else: numerical_value = value + if not isfinite(numerical_value): + raise ValueError( + f"Sensor {self.entity_id} has device class '{device_class}', " + f"state class '{state_class}' unit '{unit_of_measurement}' and " + f"suggested precision '{suggested_precision}' thus indicating it " + f"has a numeric value; however, it has the non-finite value: " + f"'{numerical_value}'" + ) + if native_unit_of_measurement != unit_of_measurement and ( converter := UNIT_CONVERTERS.get(device_class) ): diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 530e8cb4209..1f836ad9095 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1861,13 +1861,17 @@ async def test_device_classes_with_invalid_unit_of_measurement( ], ) @pytest.mark.parametrize( - "native_value", + ("native_value", "problem"), [ - "", - "abc", - "13.7.1", - datetime(2012, 11, 10, 7, 35, 1), - date(2012, 11, 10), + ("", "non-numeric"), + ("abc", "non-numeric"), + ("13.7.1", "non-numeric"), + (datetime(2012, 11, 10, 7, 35, 1), "non-numeric"), + (date(2012, 11, 10), "non-numeric"), + ("inf", "non-finite"), + (float("inf"), "non-finite"), + ("nan", "non-finite"), + (float("nan"), "non-finite"), ], ) async def test_non_numeric_validation_error( @@ -1875,6 +1879,7 @@ async def test_non_numeric_validation_error( caplog: pytest.LogCaptureFixture, enable_custom_integrations: None, native_value: Any, + problem: str, device_class: SensorDeviceClass | None, state_class: SensorStateClass | None, unit: str | None, @@ -1899,7 +1904,7 @@ async def test_non_numeric_validation_error( assert ( "thus indicating it has a numeric value; " - f"however, it has the non-numeric value: '{native_value}'" + f"however, it has the {problem} value: '{native_value}'" ) in caplog.text From 6be20b54087dbaaa8e4b744595ae229c52282083 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Aug 2023 14:24:48 +0200 Subject: [PATCH 0775/1151] Add preview support to binary sensor group (#98872) --- .../components/group/binary_sensor.py | 25 ++- homeassistant/components/group/config_flow.py | 156 +++++++++++++----- tests/components/group/test_config_flow.py | 140 ++++++++++++++-- 3 files changed, 270 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 0c4bf89057d..105b1b95b1d 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -1,6 +1,9 @@ """Platform allowing several binary sensor to be grouped into one binary sensor.""" from __future__ import annotations +from collections.abc import Callable, Mapping +from typing import Any + import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -21,7 +24,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( @@ -113,6 +116,26 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): if mode: self.mode = all + @callback + def async_start_preview( + self, + preview_callback: Callable[[str, Mapping[str, Any]], None], + ) -> CALLBACK_TYPE: + """Render a preview.""" + + @callback + def async_state_changed_listener( + event: EventType[EventStateChangedData] | None, + ) -> None: + """Handle child updates.""" + self.async_update_group_state() + preview_callback(*self._async_generate_attributes()) + + async_state_changed_listener(None) + return async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) + async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index d8c983f83db..869a4d33b5f 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine, Mapping from functools import partial -from typing import Any, cast +from typing import Any, Literal, cast import voluptuous as vol @@ -22,7 +22,7 @@ from homeassistant.helpers.schema_config_entry_flow import ( ) from . import DOMAIN -from .binary_sensor import CONF_ALL +from .binary_sensor import CONF_ALL, BinarySensorGroup from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC from .sensor import SensorGroup @@ -73,7 +73,9 @@ def basic_group_config_schema(domain: str | list[str]) -> vol.Schema: ) -async def binary_sensor_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: +async def binary_sensor_options_schema( + handler: SchemaCommonFlowHandler | None, +) -> vol.Schema: """Generate options schema.""" return (await basic_group_options_schema("binary_sensor", handler)).extend( { @@ -170,6 +172,7 @@ CONFIG_FLOW = { "binary_sensor": SchemaFlowFormStep( BINARY_SENSOR_CONFIG_SCHEMA, validate_user_input=set_group_type("binary_sensor"), + preview="group_binary_sensor", ), "cover": SchemaFlowFormStep( basic_group_config_schema("cover"), @@ -205,7 +208,10 @@ CONFIG_FLOW = { OPTIONS_FLOW = { "init": SchemaFlowFormStep(next_step=choose_options_step), - "binary_sensor": SchemaFlowFormStep(binary_sensor_options_schema), + "binary_sensor": SchemaFlowFormStep( + binary_sensor_options_schema, + preview="group_binary_sensor", + ), "cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")), "fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")), "light": SchemaFlowFormStep(partial(light_switch_options_schema, "light")), @@ -260,6 +266,7 @@ class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): def async_setup_preview(hass: HomeAssistant) -> None: """Set up preview WS API.""" websocket_api.async_register_command(hass, ws_preview_sensor) + websocket_api.async_register_command(hass, ws_preview_binary_sensor) def _async_hide_members( @@ -275,6 +282,86 @@ def _async_hide_members( registry.async_update_entity(entity_id, hidden_by=hidden_by) +@callback +def _async_handle_ws_preview( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], + config_schema: vol.Schema, + options_schema: vol.Schema, + create_preview_entity: Callable[ + [Literal["config_flow", "options_flow"], str, dict[str, Any]], + BinarySensorGroup | SensorGroup, + ], +) -> None: + """Generate a preview.""" + if msg["flow_type"] == "config_flow": + validated = config_schema(msg["user_input"]) + name = validated["name"] + else: + validated = options_schema(msg["user_input"]) + flow_status = hass.config_entries.options.async_get(msg["flow_id"]) + config_entry = hass.config_entries.async_get_entry(flow_status["handler"]) + if not config_entry: + raise HomeAssistantError + name = config_entry.options["name"] + + @callback + def async_preview_updated(state: str, attributes: Mapping[str, Any]) -> None: + """Forward config entry state events to websocket.""" + connection.send_message( + websocket_api.event_message( + msg["id"], {"state": state, "attributes": attributes} + ) + ) + + preview_entity = create_preview_entity(msg["flow_type"], name, validated) + preview_entity.hass = hass + + connection.send_result(msg["id"]) + connection.subscriptions[msg["id"]] = preview_entity.async_start_preview( + async_preview_updated + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "group/binary_sensor/start_preview", + vol.Required("flow_id"): str, + vol.Required("flow_type"): vol.Any("config_flow", "options_flow"), + vol.Required("user_input"): dict, + } +) +@websocket_api.async_response +async def ws_preview_binary_sensor( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Generate a preview.""" + + def create_preview_binary_sensor( + flow_type: Literal["config_flow", "options_flow"], + name: str, + validated_config: dict[str, Any], + ) -> BinarySensorGroup: + """Create a preview sensor.""" + return BinarySensorGroup( + None, + name, + None, + validated_config[CONF_ENTITIES], + validated_config[CONF_ALL], + ) + + _async_handle_ws_preview( + hass, + connection, + msg, + BINARY_SENSOR_CONFIG_SCHEMA, + await binary_sensor_options_schema(None), + create_preview_binary_sensor, + ) + + @websocket_api.websocket_command( { vol.Required("type"): "group/sensor/start_preview", @@ -288,41 +375,34 @@ async def ws_preview_sensor( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] ) -> None: """Generate a preview.""" - if msg["flow_type"] == "config_flow": - validated = SENSOR_CONFIG_SCHEMA(msg["user_input"]) - ignore_non_numeric = False - name = validated["name"] - else: - validated = (await sensor_options_schema("sensor", None))(msg["user_input"]) - flow_status = hass.config_entries.options.async_get(msg["flow_id"]) - config_entry = hass.config_entries.async_get_entry(flow_status["handler"]) - if not config_entry: - raise HomeAssistantError - ignore_non_numeric = validated[CONF_IGNORE_NON_NUMERIC] - name = config_entry.options["name"] - @callback - def async_preview_updated(state: str, attributes: Mapping[str, Any]) -> None: - """Forward config entry state events to websocket.""" - connection.send_message( - websocket_api.event_message( - msg["id"], {"state": state, "attributes": attributes} - ) + def create_preview_sensor( + flow_type: Literal["config_flow", "options_flow"], + name: str, + validated_config: dict[str, Any], + ) -> SensorGroup: + """Create a preview sensor.""" + ignore_non_numeric = ( + False + if flow_type == "config_flow" + else validated_config[CONF_IGNORE_NON_NUMERIC] + ) + return SensorGroup( + None, + name, + validated_config[CONF_ENTITIES], + ignore_non_numeric, + validated_config[CONF_TYPE], + None, + None, + None, ) - sensor = SensorGroup( - None, - name, - validated[CONF_ENTITIES], - ignore_non_numeric, - validated[CONF_TYPE], - None, - None, - None, - ) - sensor.hass = hass - - connection.send_result(msg["id"]) - connection.subscriptions[msg["id"]] = sensor.async_start_preview( - async_preview_updated + _async_handle_ws_preview( + hass, + connection, + msg, + SENSOR_CONFIG_SCHEMA, + await sensor_options_schema("sensor", None), + create_preview_sensor, ) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index a2c5ad64b1d..ce4bad2ac8a 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -449,13 +449,129 @@ async def test_options_flow_hides_members( assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by +async def test_config_flow_binary_sensor_preview( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test the config flow preview.""" + client = await hass_ws_client(hass) + + input_entities = ["binary_sensor.input_one", "binary_sensor.input_two"] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "binary_sensor"}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "binary_sensor" + assert result["errors"] is None + assert result["preview"] == "group_binary_sensor" + + await client.send_json_auto_id( + { + "type": "group/binary_sensor/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": { + "name": "My binary sensor group", + "entities": input_entities, + "all": True, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + msg = await client.receive_json() + assert msg["event"] == { + "attributes": {"friendly_name": "My binary sensor group"}, + "state": "unavailable", + } + + hass.states.async_set("binary_sensor.input_one", "on") + hass.states.async_set("binary_sensor.input_two", "off") + + msg = await client.receive_json() + assert msg["event"] == { + "attributes": { + "entity_id": ["binary_sensor.input_one", "binary_sensor.input_two"], + "friendly_name": "My binary sensor group", + }, + "state": "off", + } + + +async def test_option_flow_binary_sensor_preview( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test the option flow preview.""" + client = await hass_ws_client(hass) + + input_entities = ["binary_sensor.input_one", "binary_sensor.input_two"] + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "all": True, + "entities": input_entities, + "group_type": "binary_sensor", + "hide_members": False, + "name": "My group", + }, + title="My min_max", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + assert result["preview"] == "group_binary_sensor" + + hass.states.async_set("binary_sensor.input_one", "on") + hass.states.async_set("binary_sensor.input_two", "off") + + await client.send_json_auto_id( + { + "type": "group/binary_sensor/start_preview", + "flow_id": result["flow_id"], + "flow_type": "options_flow", + "user_input": { + "entities": input_entities, + "all": False, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + msg = await client.receive_json() + assert msg["event"] == { + "attributes": { + "entity_id": input_entities, + "friendly_name": "My group", + }, + "state": "on", + } + + async def test_config_flow_sensor_preview( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test the config flow preview.""" client = await hass_ws_client(hass) - input_sensors = ["sensor.input_one", "sensor.input_two"] + input_entities = ["sensor.input_one", "sensor.input_two"] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -479,7 +595,7 @@ async def test_config_flow_sensor_preview( "flow_type": "config_flow", "user_input": { "name": "My sensor group", - "entities": input_sensors, + "entities": input_entities, "type": "max", }, } @@ -503,7 +619,7 @@ async def test_config_flow_sensor_preview( msg = await client.receive_json() assert msg["event"] == { "attributes": { - "entity_id": ["sensor.input_one", "sensor.input_two"], + "entity_id": input_entities, "friendly_name": "My sensor group", "icon": "mdi:calculator", "max_entity_id": "sensor.input_two", @@ -518,12 +634,14 @@ async def test_option_flow_sensor_preview( """Test the option flow preview.""" client = await hass_ws_client(hass) + input_entities = ["sensor.input_one", "sensor.input_two"] + # Setup the config entry config_entry = MockConfigEntry( data={}, domain=DOMAIN, options={ - "entities": ["sensor.input_one", "sensor.input_two"], + "entities": input_entities, "group_type": "sensor", "hide_members": False, "name": "My sensor group", @@ -535,8 +653,6 @@ async def test_option_flow_sensor_preview( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - input_sensors = ["sensor.input_one", "sensor.input_two"] - result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == FlowResultType.FORM assert result["errors"] is None @@ -551,7 +667,7 @@ async def test_option_flow_sensor_preview( "flow_id": result["flow_id"], "flow_type": "options_flow", "user_input": { - "entities": input_sensors, + "entities": input_entities, "type": "min", }, } @@ -563,7 +679,7 @@ async def test_option_flow_sensor_preview( msg = await client.receive_json() assert msg["event"] == { "attributes": { - "entity_id": ["sensor.input_one", "sensor.input_two"], + "entity_id": input_entities, "friendly_name": "My sensor group", "icon": "mdi:calculator", "min_entity_id": "sensor.input_one", @@ -578,12 +694,14 @@ async def test_option_flow_sensor_preview_config_entry_removed( """Test the option flow preview where the config entry is removed.""" client = await hass_ws_client(hass) + input_entities = ["sensor.input_one", "sensor.input_two"] + # Setup the config entry config_entry = MockConfigEntry( data={}, domain=DOMAIN, options={ - "entities": ["sensor.input_one", "sensor.input_two"], + "entities": input_entities, "group_type": "sensor", "hide_members": False, "name": "My sensor group", @@ -595,8 +713,6 @@ async def test_option_flow_sensor_preview_config_entry_removed( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - input_sensors = ["sensor.input_one", "sensor.input_two"] - result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == FlowResultType.FORM assert result["errors"] is None @@ -610,7 +726,7 @@ async def test_option_flow_sensor_preview_config_entry_removed( "flow_id": result["flow_id"], "flow_type": "options_flow", "user_input": { - "entities": input_sensors, + "entities": input_entities, "type": "min", }, } From 57990c75971c83f07862dda1ef73f68adedef385 Mon Sep 17 00:00:00 2001 From: Dennis Date: Wed, 23 Aug 2023 16:09:07 +0200 Subject: [PATCH 0776/1151] Add state classes to adguard sensors (#98577) --- homeassistant/components/adguard/sensor.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 9f1c0a5b0fe..56a1a0beb32 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -8,7 +8,11 @@ from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfTime from homeassistant.core import HomeAssistant @@ -43,6 +47,7 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:magnify", native_unit_of_measurement="queries", value_fn=lambda adguard: adguard.stats.dns_queries(), + state_class=SensorStateClass.TOTAL, ), AdGuardHomeEntityDescription( key="blocked_filtering", @@ -50,6 +55,7 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:magnify-close", native_unit_of_measurement="queries", value_fn=lambda adguard: adguard.stats.blocked_filtering(), + state_class=SensorStateClass.TOTAL, ), AdGuardHomeEntityDescription( key="blocked_percentage", @@ -57,6 +63,7 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:magnify-close", native_unit_of_measurement=PERCENTAGE, value_fn=lambda adguard: adguard.stats.blocked_percentage(), + state_class=SensorStateClass.MEASUREMENT, ), AdGuardHomeEntityDescription( key="blocked_parental", @@ -64,6 +71,7 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:human-male-girl", native_unit_of_measurement="requests", value_fn=lambda adguard: adguard.stats.replaced_parental(), + state_class=SensorStateClass.TOTAL, ), AdGuardHomeEntityDescription( key="blocked_safebrowsing", @@ -71,6 +79,7 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:shield-half-full", native_unit_of_measurement="requests", value_fn=lambda adguard: adguard.stats.replaced_safebrowsing(), + state_class=SensorStateClass.TOTAL, ), AdGuardHomeEntityDescription( key="enforced_safesearch", @@ -78,6 +87,7 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:shield-search", native_unit_of_measurement="requests", value_fn=lambda adguard: adguard.stats.replaced_safesearch(), + state_class=SensorStateClass.TOTAL, ), AdGuardHomeEntityDescription( key="average_speed", @@ -85,6 +95,7 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:speedometer", native_unit_of_measurement=UnitOfTime.MILLISECONDS, value_fn=lambda adguard: adguard.stats.avg_processing_time(), + state_class=SensorStateClass.MEASUREMENT, ), AdGuardHomeEntityDescription( key="rules_count", @@ -93,6 +104,7 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( native_unit_of_measurement="rules", value_fn=lambda adguard: adguard.filtering.rules_count(allowlist=False), entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, ), ) From 39c0689fe6910e6e98911dd71d5ef41721eaf8fc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Aug 2023 16:26:20 +0200 Subject: [PATCH 0777/1151] Revert "Add state classes to adguard sensors" (#98880) --- homeassistant/components/adguard/sensor.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 56a1a0beb32..9f1c0a5b0fe 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -8,11 +8,7 @@ from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError -from homeassistant.components.sensor import ( - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfTime from homeassistant.core import HomeAssistant @@ -47,7 +43,6 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:magnify", native_unit_of_measurement="queries", value_fn=lambda adguard: adguard.stats.dns_queries(), - state_class=SensorStateClass.TOTAL, ), AdGuardHomeEntityDescription( key="blocked_filtering", @@ -55,7 +50,6 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:magnify-close", native_unit_of_measurement="queries", value_fn=lambda adguard: adguard.stats.blocked_filtering(), - state_class=SensorStateClass.TOTAL, ), AdGuardHomeEntityDescription( key="blocked_percentage", @@ -63,7 +57,6 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:magnify-close", native_unit_of_measurement=PERCENTAGE, value_fn=lambda adguard: adguard.stats.blocked_percentage(), - state_class=SensorStateClass.MEASUREMENT, ), AdGuardHomeEntityDescription( key="blocked_parental", @@ -71,7 +64,6 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:human-male-girl", native_unit_of_measurement="requests", value_fn=lambda adguard: adguard.stats.replaced_parental(), - state_class=SensorStateClass.TOTAL, ), AdGuardHomeEntityDescription( key="blocked_safebrowsing", @@ -79,7 +71,6 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:shield-half-full", native_unit_of_measurement="requests", value_fn=lambda adguard: adguard.stats.replaced_safebrowsing(), - state_class=SensorStateClass.TOTAL, ), AdGuardHomeEntityDescription( key="enforced_safesearch", @@ -87,7 +78,6 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:shield-search", native_unit_of_measurement="requests", value_fn=lambda adguard: adguard.stats.replaced_safesearch(), - state_class=SensorStateClass.TOTAL, ), AdGuardHomeEntityDescription( key="average_speed", @@ -95,7 +85,6 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( icon="mdi:speedometer", native_unit_of_measurement=UnitOfTime.MILLISECONDS, value_fn=lambda adguard: adguard.stats.avg_processing_time(), - state_class=SensorStateClass.MEASUREMENT, ), AdGuardHomeEntityDescription( key="rules_count", @@ -104,7 +93,6 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( native_unit_of_measurement="rules", value_fn=lambda adguard: adguard.filtering.rules_count(allowlist=False), entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, ), ) From b854551c77cf3a2928018234882fb1950e5de24e Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 23 Aug 2023 09:34:21 -0500 Subject: [PATCH 0778/1151] Use entity descriptions for IPP (#93888) --- homeassistant/components/ipp/__init__.py | 5 + homeassistant/components/ipp/coordinator.py | 2 + homeassistant/components/ipp/entity.py | 26 +- homeassistant/components/ipp/sensor.py | 251 ++++++++------------ homeassistant/components/ipp/strings.json | 3 + tests/components/ipp/test_sensor.py | 9 +- 6 files changed, 127 insertions(+), 169 deletions(-) diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 9df377b939a..98870c44f5a 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -19,6 +19,10 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IPP from a config entry.""" + # config flow sets this to either UUID, serial number or None + if (device_id := entry.unique_id) is None: + device_id = entry.entry_id + coordinator = IPPDataUpdateCoordinator( hass, host=entry.data[CONF_HOST], @@ -26,6 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: base_path=entry.data[CONF_BASE_PATH], tls=entry.data[CONF_SSL], verify_ssl=entry.data[CONF_VERIFY_SSL], + device_id=device_id, ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index abc97dd3dd2..8eb8c972fab 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -29,8 +29,10 @@ class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPPrinter]): base_path: str, tls: bool, verify_ssl: bool, + device_id: str, ) -> None: """Initialize global IPP data updater.""" + self.device_id = device_id self.ipp = IPP( host=host, port=port, diff --git a/homeassistant/components/ipp/entity.py b/homeassistant/components/ipp/entity.py index 2ce6b0f3fa0..05adf711fd9 100644 --- a/homeassistant/components/ipp/entity.py +++ b/homeassistant/components/ipp/entity.py @@ -2,6 +2,7 @@ from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -11,32 +12,21 @@ from .coordinator import IPPDataUpdateCoordinator class IPPEntity(CoordinatorEntity[IPPDataUpdateCoordinator]): """Defines a base IPP entity.""" + _attr_has_entity_name = True + def __init__( self, - *, - entry_id: str, - device_id: str, coordinator: IPPDataUpdateCoordinator, - name: str, - icon: str, - enabled_default: bool = True, + description: EntityDescription, ) -> None: """Initialize the IPP entity.""" super().__init__(coordinator) - self._device_id = device_id - self._entry_id = entry_id - self._attr_name = name - self._attr_icon = icon - self._attr_entity_registry_enabled_default = enabled_default - @property - def device_info(self) -> DeviceInfo | None: - """Return device information about this IPP device.""" - if self._device_id is None: - return None + self.entity_description = description - return DeviceInfo( - identifiers={(DOMAIN, self._device_id)}, + self._attr_unique_id = f"{coordinator.device_id}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.device_id)}, manufacturer=self.coordinator.data.info.manufacturer, model=self.coordinator.data.info.model, name=self.coordinator.data.info.name, diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 5058f6d10a8..3bc7035e26b 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -1,14 +1,23 @@ """Support for IPP sensors.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from pyipp import Marker, Printer + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_LOCATION, PERCENTAGE +from homeassistant.const import ATTR_LOCATION, PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utcnow from .const import ( @@ -27,6 +36,65 @@ from .coordinator import IPPDataUpdateCoordinator from .entity import IPPEntity +@dataclass +class IPPSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[Printer], StateType | datetime] + + +@dataclass +class IPPSensorEntityDescription( + SensorEntityDescription, IPPSensorEntityDescriptionMixin +): + """Describes IPP sensor entity.""" + + attributes_fn: Callable[[Printer], dict[Any, StateType]] = lambda _: {} + + +def _get_marker_attributes_fn( + marker_index: int, attributes_fn: Callable[[Marker], dict[Any, StateType]] +) -> Callable[[Printer], dict[Any, StateType]]: + return lambda printer: attributes_fn(printer.markers[marker_index]) + + +def _get_marker_value_fn( + marker_index: int, value_fn: Callable[[Marker], StateType | datetime] +) -> Callable[[Printer], StateType | datetime]: + return lambda printer: value_fn(printer.markers[marker_index]) + + +PRINTER_SENSORS: tuple[IPPSensorEntityDescription, ...] = ( + IPPSensorEntityDescription( + key="printer", + name=None, + translation_key="printer", + icon="mdi:printer", + device_class=SensorDeviceClass.ENUM, + options=["idle", "printing", "stopped"], + attributes_fn=lambda printer: { + ATTR_INFO: printer.info.printer_info, + ATTR_SERIAL: printer.info.serial, + ATTR_LOCATION: printer.info.location, + ATTR_STATE_MESSAGE: printer.state.message, + ATTR_STATE_REASON: printer.state.reasons, + ATTR_COMMAND_SET: printer.info.command_set, + ATTR_URI_SUPPORTED: ",".join(printer.info.printer_uri_supported), + }, + value_fn=lambda printer: printer.state.printer_state, + ), + IPPSensorEntityDescription( + key="uptime", + translation_key="uptime", + icon="mdi:clock-outline", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda printer: (utcnow() - timedelta(seconds=printer.info.uptime)), + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -34,19 +102,34 @@ async def async_setup_entry( ) -> None: """Set up IPP sensor based on a config entry.""" coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + sensors: list[SensorEntity] = [ + IPPSensor( + coordinator, + description, + ) + for description in PRINTER_SENSORS + ] - # config flow sets this to either UUID, serial number or None - if (unique_id := entry.unique_id) is None: - unique_id = entry.entry_id - - sensors: list[SensorEntity] = [] - - sensors.append(IPPPrinterSensor(entry.entry_id, unique_id, coordinator)) - sensors.append(IPPUptimeSensor(entry.entry_id, unique_id, coordinator)) - - for marker_index in range(len(coordinator.data.markers)): + for index, marker in enumerate(coordinator.data.markers): sensors.append( - IPPMarkerSensor(entry.entry_id, unique_id, coordinator, marker_index) + IPPSensor( + coordinator, + IPPSensorEntityDescription( + key=f"marker_{index}", + name=marker.name, + icon="mdi:water", + native_unit_of_measurement=PERCENTAGE, + attributes_fn=_get_marker_attributes_fn( + index, + lambda marker: { + ATTR_MARKER_HIGH_LEVEL: marker.high_level, + ATTR_MARKER_LOW_LEVEL: marker.low_level, + ATTR_MARKER_TYPE: marker.marker_type, + }, + ), + value_fn=_get_marker_value_fn(index, lambda marker: marker.level), + ), + ) ) async_add_entities(sensors, True) @@ -55,146 +138,14 @@ async def async_setup_entry( class IPPSensor(IPPEntity, SensorEntity): """Defines an IPP sensor.""" - def __init__( - self, - *, - coordinator: IPPDataUpdateCoordinator, - enabled_default: bool = True, - entry_id: str, - unique_id: str, - icon: str, - key: str, - name: str, - unit_of_measurement: str | None = None, - translation_key: str | None = None, - ) -> None: - """Initialize IPP sensor.""" - self._key = key - self._attr_unique_id = f"{unique_id}_{key}" - self._attr_native_unit_of_measurement = unit_of_measurement - self._attr_translation_key = translation_key - - super().__init__( - entry_id=entry_id, - device_id=unique_id, - coordinator=coordinator, - name=name, - icon=icon, - enabled_default=enabled_default, - ) - - -class IPPMarkerSensor(IPPSensor): - """Defines an IPP marker sensor.""" - - def __init__( - self, - entry_id: str, - unique_id: str, - coordinator: IPPDataUpdateCoordinator, - marker_index: int, - ) -> None: - """Initialize IPP marker sensor.""" - self.marker_index = marker_index - - super().__init__( - coordinator=coordinator, - entry_id=entry_id, - unique_id=unique_id, - icon="mdi:water", - key=f"marker_{marker_index}", - name=( - f"{coordinator.data.info.name} {coordinator.data.markers[marker_index].name}" - ), - unit_of_measurement=PERCENTAGE, - ) + entity_description: IPPSensorEntityDescription @property - def extra_state_attributes(self) -> dict[str, Any] | None: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the entity.""" - return { - ATTR_MARKER_HIGH_LEVEL: self.coordinator.data.markers[ - self.marker_index - ].high_level, - ATTR_MARKER_LOW_LEVEL: self.coordinator.data.markers[ - self.marker_index - ].low_level, - ATTR_MARKER_TYPE: self.coordinator.data.markers[ - self.marker_index - ].marker_type, - } + return self.entity_description.attributes_fn(self.coordinator.data) @property - def native_value(self) -> int | None: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" - level = self.coordinator.data.markers[self.marker_index].level - - if level >= 0: - return level - - return None - - -class IPPPrinterSensor(IPPSensor): - """Defines an IPP printer sensor.""" - - _attr_device_class = SensorDeviceClass.ENUM - _attr_options = ["idle", "printing", "stopped"] - - def __init__( - self, entry_id: str, unique_id: str, coordinator: IPPDataUpdateCoordinator - ) -> None: - """Initialize IPP printer sensor.""" - super().__init__( - coordinator=coordinator, - entry_id=entry_id, - unique_id=unique_id, - icon="mdi:printer", - key="printer", - name=coordinator.data.info.name, - unit_of_measurement=None, - translation_key="printer", - ) - - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes of the entity.""" - return { - ATTR_INFO: self.coordinator.data.info.printer_info, - ATTR_SERIAL: self.coordinator.data.info.serial, - ATTR_LOCATION: self.coordinator.data.info.location, - ATTR_STATE_MESSAGE: self.coordinator.data.state.message, - ATTR_STATE_REASON: self.coordinator.data.state.reasons, - ATTR_COMMAND_SET: self.coordinator.data.info.command_set, - ATTR_URI_SUPPORTED: self.coordinator.data.info.printer_uri_supported, - } - - @property - def native_value(self) -> str: - """Return the state of the sensor.""" - return self.coordinator.data.state.printer_state - - -class IPPUptimeSensor(IPPSensor): - """Defines a IPP uptime sensor.""" - - _attr_device_class = SensorDeviceClass.TIMESTAMP - - def __init__( - self, entry_id: str, unique_id: str, coordinator: IPPDataUpdateCoordinator - ) -> None: - """Initialize IPP uptime sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - unique_id=unique_id, - icon="mdi:clock-outline", - key="uptime", - name=f"{coordinator.data.info.name} Uptime", - ) - - @property - def native_value(self) -> datetime: - """Return the state of the sensor.""" - return utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index f3ea929c9ec..ac879ef0ab3 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -40,6 +40,9 @@ "idle": "[%key:common::state::idle%]", "stopped": "Stopped" } + }, + "uptime": { + "name": "Uptime" } } } diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index ebebd18bc72..5992b928f63 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -4,7 +4,12 @@ from unittest.mock import AsyncMock import pytest from homeassistant.components.sensor import ATTR_OPTIONS -from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE +from homeassistant.const import ( + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, + EntityCategory, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -66,8 +71,10 @@ async def test_sensors( assert state.state == "2019-11-11T09:10:02+00:00" entry = entity_registry.async_get("sensor.test_ha_1000_series_uptime") + assert entry assert entry.unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251_uptime" + assert entry.entity_category == EntityCategory.DIAGNOSTIC async def test_disabled_by_default_sensors( From b884dafa819427e0a8b65ae587d8156f0e4742fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Aug 2023 10:08:47 -0500 Subject: [PATCH 0779/1151] Retry enphase_envoy setup later if the wrong device is found (#98882) --- homeassistant/components/enphase_envoy/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 7f06a032128..2473c2d9b2f 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -6,6 +6,7 @@ from pyenphase import Envoy 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 import device_registry as dr from homeassistant.helpers.httpx_client import get_async_client @@ -24,6 +25,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=envoy.serial_number) + if entry.unique_id != envoy.serial_number: + # If the serial number of the device does not match the unique_id + # of the config entry, it likely means the DHCP lease has expired + # and the device has been assigned a new IP address. We need to + # wait for the next discovery to find the device at its new address + # and update the config entry so we do not mix up devices. + raise ConfigEntryNotReady( + f"Unexpected device found at {host}; expected {entry.unique_id}, " + f"found {envoy.serial_number}" + ) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) From ba9c969d91c5f32561b9a07a91a48387fec624d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Aug 2023 10:21:38 -0500 Subject: [PATCH 0780/1151] Retry lookin setup later if the wrong device is found (#98881) --- homeassistant/components/lookin/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index c16d7f34f0f..7656de8d385 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -104,6 +104,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (asyncio.TimeoutError, aiohttp.ClientError, NoUsableService) as ex: raise ConfigEntryNotReady from ex + if entry.unique_id != (found_uuid := lookin_device.id.upper()): + # If the uuid of the device does not match the unique_id + # of the config entry, it likely means the DHCP lease has expired + # and the device has been assigned a new IP address. We need to + # wait for the next discovery to find the device at its new address + # and update the config entry so we do not mix up devices. + raise ConfigEntryNotReady( + f"Unexpected device found at {host}; expected {entry.unique_id}, " + f"found {found_uuid}" + ) + push_coordinator = LookinPushCoordinator(entry.title) if lookin_device.model >= 2: From 4a417c7dcc89a7e1102703afb0a3e60bb43fbba1 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 23 Aug 2023 11:11:14 -0500 Subject: [PATCH 0781/1151] Wake word entity state/category fix (#98886) * Only change wake word entity state on detection * Wake word entity is diagnostic --- .../components/wake_word/__init__.py | 21 +++++++++++------- .../wake_word/snapshots/test_init.ambr | 3 +++ tests/components/wake_word/test_init.py | 22 ++++++++++++++++++- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/wake_word/__init__.py b/homeassistant/components/wake_word/__init__.py index 895dababd54..0a751b7eea2 100644 --- a/homeassistant/components/wake_word/__init__.py +++ b/homeassistant/components/wake_word/__init__.py @@ -7,7 +7,7 @@ import logging from typing import final from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_component import EntityComponent @@ -71,16 +71,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class WakeWordDetectionEntity(RestoreEntity): """Represent a single wake word provider.""" + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_should_poll = False - __last_processed: str | None = None + __last_detected: str | None = None @property @final def state(self) -> str | None: """Return the state of the entity.""" - if self.__last_processed is None: + if self.__last_detected is None: return None - return self.__last_processed + return self.__last_detected @property @abstractmethod @@ -103,9 +104,13 @@ class WakeWordDetectionEntity(RestoreEntity): 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) + result = await self._async_process_audio_stream(stream) + if result is not None: + # Update last detected only when there is a detection + self.__last_detected = dt_util.utcnow().isoformat() + self.async_write_ha_state() + + return result async def async_internal_added_to_hass(self) -> None: """Call when the entity is added to hass.""" @@ -116,4 +121,4 @@ class WakeWordDetectionEntity(RestoreEntity): and state.state is not None and state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) ): - self.__last_processed = state.state + self.__last_detected = state.state diff --git a/tests/components/wake_word/snapshots/test_init.ambr b/tests/components/wake_word/snapshots/test_init.ambr index ca6d5d950f0..cf7c09cd730 100644 --- a/tests/components/wake_word/snapshots/test_init.ambr +++ b/tests/components/wake_word/snapshots/test_init.ambr @@ -1,4 +1,7 @@ # serializer version: 1 +# name: test_detected_entity + None +# --- # name: test_ws_detect dict({ 'event': dict({ diff --git a/tests/components/wake_word/test_init.py b/tests/components/wake_word/test_init.py index 954cbe6dc8c..d37cb3aa540 100644 --- a/tests/components/wake_word/test_init.py +++ b/tests/components/wake_word/test_init.py @@ -3,9 +3,11 @@ from collections.abc import AsyncIterable, Generator from pathlib import Path import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import wake_word from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, State from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.setup import async_setup_component @@ -147,7 +149,10 @@ async def test_config_entry_unload( async def test_detected_entity( - hass: HomeAssistant, tmp_path: Path, setup: MockProviderEntity + hass: HomeAssistant, + tmp_path: Path, + setup: MockProviderEntity, + snapshot: SnapshotAssertion, ) -> None: """Test successful detection through entity.""" @@ -158,9 +163,13 @@ async def test_detected_entity( timestamp += _MS_PER_CHUNK # Need 2 seconds to trigger + state = setup.state result = await setup.async_process_audio_stream(three_second_stream()) assert result == wake_word.DetectionResult("test_ww", 2048) + assert state != setup.state + assert state == snapshot + async def test_not_detected_entity( hass: HomeAssistant, setup: MockProviderEntity @@ -174,9 +183,13 @@ async def test_not_detected_entity( timestamp += _MS_PER_CHUNK # Need 2 seconds to trigger + state = setup.state result = await setup.async_process_audio_stream(one_second_stream()) assert result is None + # State should only change when there's a detection + assert state == setup.state + async def test_default_engine_none(hass: HomeAssistant, tmp_path: Path) -> None: """Test async_default_engine.""" @@ -224,3 +237,10 @@ async def test_restore_state( state = hass.states.get(entity_id) assert state assert state.state == timestamp + + +async def test_entity_attributes( + hass: HomeAssistant, mock_provider_entity: MockProviderEntity +) -> None: + """Test that the provider entity attributes match expectations.""" + assert mock_provider_entity.entity_category == EntityCategory.DIAGNOSTIC From ee1b6a60a048a343cc31797fce01eca90dfe4965 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Aug 2023 19:13:24 +0200 Subject: [PATCH 0782/1151] Deduplicate group preview tests (#98883) --- tests/components/group/test_config_flow.py | 239 +++++++-------------- 1 file changed, 79 insertions(+), 160 deletions(-) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index ce4bad2ac8a..ad084786366 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Switch config flow.""" +from typing import Any from unittest.mock import patch import pytest @@ -449,13 +450,32 @@ async def test_options_flow_hides_members( assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by -async def test_config_flow_binary_sensor_preview( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator +@pytest.mark.parametrize( + ("domain", "extra_user_input", "input_states", "group_state", "extra_attributes"), + [ + ("binary_sensor", {"all": True}, ["on", "off"], "off", [{}, {}]), + ( + "sensor", + {"type": "max"}, + ["10", "20"], + "20.0", + [{"icon": "mdi:calculator"}, {"max_entity_id": "sensor.input_two"}], + ), + ], +) +async def test_config_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + domain: str, + extra_user_input: dict[str, Any], + input_states: list[str], + group_state: str, + extra_attributes: list[dict[str, Any]], ) -> None: """Test the config flow preview.""" client = await hass_ws_client(hass) - input_entities = ["binary_sensor.input_one", "binary_sensor.input_two"] + input_entities = [f"{domain}.input_one", f"{domain}.input_two"] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -464,24 +484,21 @@ async def test_config_flow_binary_sensor_preview( result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"next_step_id": "binary_sensor"}, + {"next_step_id": domain}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "binary_sensor" + assert result["step_id"] == domain assert result["errors"] is None - assert result["preview"] == "group_binary_sensor" + assert result["preview"] == f"group_{domain}" await client.send_json_auto_id( { - "type": "group/binary_sensor/start_preview", + "type": f"group/{domain}/start_preview", "flow_id": result["flow_id"], "flow_type": "config_flow", - "user_input": { - "name": "My binary sensor group", - "entities": input_entities, - "all": True, - }, + "user_input": {"name": "My group", "entities": input_entities} + | extra_user_input, } ) msg = await client.receive_json() @@ -490,151 +507,60 @@ async def test_config_flow_binary_sensor_preview( msg = await client.receive_json() assert msg["event"] == { - "attributes": {"friendly_name": "My binary sensor group"}, + "attributes": {"friendly_name": "My group"} | extra_attributes[0], "state": "unavailable", } - hass.states.async_set("binary_sensor.input_one", "on") - hass.states.async_set("binary_sensor.input_two", "off") - - msg = await client.receive_json() - assert msg["event"] == { - "attributes": { - "entity_id": ["binary_sensor.input_one", "binary_sensor.input_two"], - "friendly_name": "My binary sensor group", - }, - "state": "off", - } - - -async def test_option_flow_binary_sensor_preview( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator -) -> None: - """Test the option flow preview.""" - client = await hass_ws_client(hass) - - input_entities = ["binary_sensor.input_one", "binary_sensor.input_two"] - - # Setup the config entry - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={ - "all": True, - "entities": input_entities, - "group_type": "binary_sensor", - "hide_members": False, - "name": "My group", - }, - title="My min_max", - ) - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["errors"] is None - assert result["preview"] == "group_binary_sensor" - - hass.states.async_set("binary_sensor.input_one", "on") - hass.states.async_set("binary_sensor.input_two", "off") - - await client.send_json_auto_id( - { - "type": "group/binary_sensor/start_preview", - "flow_id": result["flow_id"], - "flow_type": "options_flow", - "user_input": { - "entities": input_entities, - "all": False, - }, - } - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] is None + hass.states.async_set(input_entities[0], input_states[0]) + hass.states.async_set(input_entities[1], input_states[1]) msg = await client.receive_json() assert msg["event"] == { "attributes": { "entity_id": input_entities, "friendly_name": "My group", - }, - "state": "on", - } - - -async def test_config_flow_sensor_preview( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator -) -> None: - """Test the config flow preview.""" - client = await hass_ws_client(hass) - - input_entities = ["sensor.input_one", "sensor.input_two"] - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == FlowResultType.MENU - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"next_step_id": "sensor"}, - ) - await hass.async_block_till_done() - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "sensor" - assert result["errors"] is None - assert result["preview"] == "group_sensor" - - await client.send_json_auto_id( - { - "type": "group/sensor/start_preview", - "flow_id": result["flow_id"], - "flow_type": "config_flow", - "user_input": { - "name": "My sensor group", - "entities": input_entities, - "type": "max", - }, } - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] is None - - msg = await client.receive_json() - assert msg["event"] == { - "attributes": { - "friendly_name": "My sensor group", - "icon": "mdi:calculator", - }, - "state": "unavailable", - } - - hass.states.async_set("sensor.input_one", "10") - hass.states.async_set("sensor.input_two", "20") - - msg = await client.receive_json() - assert msg["event"] == { - "attributes": { - "entity_id": input_entities, - "friendly_name": "My sensor group", - "icon": "mdi:calculator", - "max_entity_id": "sensor.input_two", - }, - "state": "20.0", + | extra_attributes[0] + | extra_attributes[1], + "state": group_state, } -async def test_option_flow_sensor_preview( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator +@pytest.mark.parametrize( + ( + "domain", + "extra_config_flow_data", + "extra_user_input", + "input_states", + "group_state", + "extra_attributes", + ), + [ + ("binary_sensor", {"all": True}, {"all": False}, ["on", "off"], "on", {}), + ( + "sensor", + {"type": "min"}, + {"type": "max"}, + ["10", "20"], + "20.0", + {"icon": "mdi:calculator", "max_entity_id": "sensor.input_two"}, + ), + ], +) +async def test_option_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + domain: str, + extra_config_flow_data: dict[str, Any], + extra_user_input: dict[str, Any], + input_states: list[str], + group_state: str, + extra_attributes: dict[str, Any], ) -> None: """Test the option flow preview.""" client = await hass_ws_client(hass) - input_entities = ["sensor.input_one", "sensor.input_two"] + input_entities = [f"{domain}.input_one", f"{domain}.input_two"] # Setup the config entry config_entry = MockConfigEntry( @@ -642,12 +568,12 @@ async def test_option_flow_sensor_preview( domain=DOMAIN, options={ "entities": input_entities, - "group_type": "sensor", + "group_type": domain, "hide_members": False, - "name": "My sensor group", - "type": "min", - }, - title="My min_max", + "name": "My group", + } + | extra_config_flow_data, + title="My group", ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -656,20 +582,17 @@ async def test_option_flow_sensor_preview( result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == FlowResultType.FORM assert result["errors"] is None - assert result["preview"] == "group_sensor" + assert result["preview"] == f"group_{domain}" - hass.states.async_set("sensor.input_one", "10") - hass.states.async_set("sensor.input_two", "20") + hass.states.async_set(input_entities[0], input_states[0]) + hass.states.async_set(input_entities[1], input_states[1]) await client.send_json_auto_id( { - "type": "group/sensor/start_preview", + "type": f"group/{domain}/start_preview", "flow_id": result["flow_id"], "flow_type": "options_flow", - "user_input": { - "entities": input_entities, - "type": "min", - }, + "user_input": {"entities": input_entities} | extra_user_input, } ) msg = await client.receive_json() @@ -678,13 +601,9 @@ async def test_option_flow_sensor_preview( msg = await client.receive_json() assert msg["event"] == { - "attributes": { - "entity_id": input_entities, - "friendly_name": "My sensor group", - "icon": "mdi:calculator", - "min_entity_id": "sensor.input_one", - }, - "state": "10.0", + "attributes": {"entity_id": input_entities, "friendly_name": "My group"} + | extra_attributes, + "state": group_state, } From 3c10d0e1f7b30cb3b7473bb3f9f665cd195d10db Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Aug 2023 19:20:58 +0200 Subject: [PATCH 0783/1151] Deduplicate entities derived from GroupEntity (#98893) --- homeassistant/components/group/__init__.py | 62 ++++++++++++++++++- .../components/group/binary_sensor.py | 50 +-------------- homeassistant/components/group/cover.py | 45 ++------------ homeassistant/components/group/fan.py | 41 ++---------- homeassistant/components/group/light.py | 25 +------- homeassistant/components/group/lock.py | 25 +------- homeassistant/components/group/sensor.py | 54 +--------------- homeassistant/components/group/switch.py | 25 +------- 8 files changed, 78 insertions(+), 249 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 33df9822ac2..ef011c4308a 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import abstractmethod import asyncio -from collections.abc import Collection, Iterable +from collections.abc import Callable, Collection, Iterable, Mapping from contextvars import ContextVar import logging from typing import Any, Protocol, cast @@ -473,9 +473,60 @@ class GroupEntity(Entity): """Representation of a Group of entities.""" _attr_should_poll = False + _entity_ids: list[str] + + @callback + def async_start_preview( + self, + preview_callback: Callable[[str, Mapping[str, Any]], None], + ) -> CALLBACK_TYPE: + """Render a preview.""" + + for entity_id in self._entity_ids: + if (state := self.hass.states.get(entity_id)) is None: + continue + self.async_update_supported_features(entity_id, state) + + @callback + def async_state_changed_listener( + event: EventType[EventStateChangedData] | None, + ) -> None: + """Handle child updates.""" + self.async_update_group_state() + if event: + self.async_update_supported_features( + event.data["entity_id"], event.data["new_state"] + ) + preview_callback(*self._async_generate_attributes()) + + async_state_changed_listener(None) + return async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) async def async_added_to_hass(self) -> None: """Register listeners.""" + for entity_id in self._entity_ids: + if (state := self.hass.states.get(entity_id)) is None: + continue + self.async_update_supported_features(entity_id, state) + + @callback + def async_state_changed_listener( + event: EventType[EventStateChangedData], + ) -> None: + """Handle child updates.""" + self.async_set_context(event.context) + self.async_update_supported_features( + event.data["entity_id"], event.data["new_state"] + ) + self.async_defer_or_update_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) + ) async def _update_at_start(_: HomeAssistant) -> None: self.async_update_group_state() @@ -493,9 +544,18 @@ class GroupEntity(Entity): self.async_write_ha_state() @abstractmethod + @callback def async_update_group_state(self) -> None: """Abstract method to update the entity.""" + @callback + def async_update_supported_features( + self, + entity_id: str, + new_state: State | None, + ) -> None: + """Update dictionaries with supported features.""" + class Group(Entity): """Track a group of entity ids.""" diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 105b1b95b1d..53bf1affe00 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -1,9 +1,6 @@ """Platform allowing several binary sensor to be grouped into one binary sensor.""" from __future__ import annotations -from collections.abc import Callable, Mapping -from typing import Any - import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -24,14 +21,10 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import GroupEntity @@ -116,45 +109,6 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): if mode: self.mode = all - @callback - def async_start_preview( - self, - preview_callback: Callable[[str, Mapping[str, Any]], None], - ) -> CALLBACK_TYPE: - """Render a preview.""" - - @callback - def async_state_changed_listener( - event: EventType[EventStateChangedData] | None, - ) -> None: - """Handle child updates.""" - self.async_update_group_state() - preview_callback(*self._async_generate_attributes()) - - async_state_changed_listener(None) - return async_track_state_change_event( - self.hass, self._entity_ids, async_state_changed_listener - ) - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - @callback - def async_state_changed_listener( - event: EventType[EventStateChangedData], - ) -> None: - """Handle child updates.""" - self.async_set_context(event.context) - self.async_defer_or_update_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, self._entity_ids, async_state_changed_listener - ) - ) - - await super().async_added_to_hass() - @callback def async_update_group_state(self) -> None: """Query all members and determine the binary sensor group state.""" diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 784ac9a94af..0fe67a9bccd 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -41,11 +41,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import GroupEntity from .util import attribute_equal, reduce_attribute @@ -112,7 +108,7 @@ class CoverGroup(GroupEntity, CoverEntity): def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a CoverGroup entity.""" - self._entities = entities + self._entity_ids = entities self._covers: dict[str, set[str]] = { KEY_OPEN_CLOSE: set(), KEY_STOP: set(), @@ -128,21 +124,11 @@ class CoverGroup(GroupEntity, CoverEntity): self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities} self._attr_unique_id = unique_id - @callback - def _update_supported_features_event( - self, event: EventType[EventStateChangedData] - ) -> None: - self.async_set_context(event.context) - self.async_update_supported_features( - event.data["entity_id"], event.data["new_state"] - ) - @callback def async_update_supported_features( self, entity_id: str, new_state: State | None, - update_state: bool = True, ) -> None: """Update dictionaries with supported features.""" if not new_state: @@ -150,8 +136,6 @@ class CoverGroup(GroupEntity, CoverEntity): values.discard(entity_id) for values in self._tilts.values(): values.discard(entity_id) - if update_state: - self.async_defer_or_update_ha_state() return features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -182,25 +166,6 @@ class CoverGroup(GroupEntity, CoverEntity): else: self._tilts[KEY_POSITION].discard(entity_id) - if update_state: - self.async_defer_or_update_ha_state() - - async def async_added_to_hass(self) -> None: - """Register listeners.""" - for entity_id in self._entities: - if (new_state := self.hass.states.get(entity_id)) is None: - continue - self.async_update_supported_features( - entity_id, new_state, update_state=False - ) - self.async_on_remove( - async_track_state_change_event( - self.hass, self._entities, self._update_supported_features_event - ) - ) - - await super().async_added_to_hass() - async def async_open_cover(self, **kwargs: Any) -> None: """Move the covers up.""" data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} @@ -278,7 +243,7 @@ class CoverGroup(GroupEntity, CoverEntity): states = [ state.state - for entity_id in self._entities + for entity_id in self._entity_ids if (state := self.hass.states.get(entity_id)) is not None ] @@ -292,7 +257,7 @@ class CoverGroup(GroupEntity, CoverEntity): self._attr_is_closed = True self._attr_is_closing = False self._attr_is_opening = False - for entity_id in self._entities: + for entity_id in self._entity_ids: if not (state := self.hass.states.get(entity_id)): continue if state.state == STATE_OPEN: @@ -347,7 +312,7 @@ class CoverGroup(GroupEntity, CoverEntity): self._attr_supported_features = supported_features if not self._attr_assumed_state: - for entity_id in self._entities: + for entity_id in self._entity_ids: if (state := self.hass.states.get(entity_id)) is None: continue if state and state.attributes.get(ATTR_ASSUMED_STATE): diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index 1fcb859f926..79ce6fe0d87 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -38,11 +38,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import GroupEntity from .util import ( @@ -108,7 +104,7 @@ class FanGroup(GroupEntity, FanEntity): def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a FanGroup entity.""" - self._entities = entities + self._entity_ids = entities self._fans: dict[int, set[str]] = {flag: set() for flag in SUPPORTED_FLAGS} self._percentage = None self._oscillating = None @@ -144,21 +140,11 @@ class FanGroup(GroupEntity, FanEntity): """Return whether or not the fan is currently oscillating.""" return self._oscillating - @callback - def _update_supported_features_event( - self, event: EventType[EventStateChangedData] - ) -> None: - self.async_set_context(event.context) - self.async_update_supported_features( - event.data["entity_id"], event.data["new_state"] - ) - @callback def async_update_supported_features( self, entity_id: str, new_state: State | None, - update_state: bool = True, ) -> None: """Update dictionaries with supported features.""" if not new_state: @@ -172,25 +158,6 @@ class FanGroup(GroupEntity, FanEntity): else: self._fans[feature].discard(entity_id) - if update_state: - self.async_defer_or_update_ha_state() - - async def async_added_to_hass(self) -> None: - """Register listeners.""" - for entity_id in self._entities: - if (new_state := self.hass.states.get(entity_id)) is None: - continue - self.async_update_supported_features( - entity_id, new_state, update_state=False - ) - self.async_on_remove( - async_track_state_change_event( - self.hass, self._entities, self._update_supported_features_event - ) - ) - - await super().async_added_to_hass() - async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" if percentage == 0: @@ -250,7 +217,7 @@ class FanGroup(GroupEntity, FanEntity): await self.hass.services.async_call( DOMAIN, service, - {ATTR_ENTITY_ID: self._entities}, + {ATTR_ENTITY_ID: self._entity_ids}, blocking=True, context=self._context, ) @@ -275,7 +242,7 @@ class FanGroup(GroupEntity, FanEntity): states = [ state - for entity_id in self._entities + for entity_id in self._entity_ids if (state := self.hass.states.get(entity_id)) is not None ] self._attr_assumed_state |= not states_equal(states) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index e0f7974631b..c6369d876a4 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -47,11 +47,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import GroupEntity from .util import find_state_attributes, mean_tuple, reduce_attribute @@ -153,25 +149,6 @@ class LightGroup(GroupEntity, LightEntity): if mode: self.mode = all - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - @callback - def async_state_changed_listener( - event: EventType[EventStateChangedData], - ) -> None: - """Handle child updates.""" - self.async_set_context(event.context) - self.async_defer_or_update_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, self._entity_ids, async_state_changed_listener - ) - ) - - await super().async_added_to_hass() - async def async_turn_on(self, **kwargs: Any) -> None: """Forward the turn_on command to all lights in the light group.""" data = { diff --git a/homeassistant/components/group/lock.py b/homeassistant/components/group/lock.py index 233d1155c53..ec0ff13ee15 100644 --- a/homeassistant/components/group/lock.py +++ b/homeassistant/components/group/lock.py @@ -31,11 +31,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import GroupEntity @@ -114,25 +110,6 @@ class LockGroup(GroupEntity, LockEntity): self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} self._attr_unique_id = unique_id - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - @callback - def async_state_changed_listener( - event: EventType[EventStateChangedData], - ) -> None: - """Handle child updates.""" - self.async_set_context(event.context) - self.async_defer_or_update_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, self._entity_ids, async_state_changed_listener - ) - ) - - await super().async_added_to_hass() - async def async_lock(self, **kwargs: Any) -> None: """Forward the lock command to all locks in the group.""" data = {ATTR_ENTITY_ID: self._entity_ids} diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 48175b55358..57ada314707 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -1,7 +1,7 @@ """Platform allowing several sensors to be grouped into one sensor to provide numeric combinations.""" from __future__ import annotations -from collections.abc import Callable, Mapping +from collections.abc import Callable from datetime import datetime import logging import statistics @@ -33,19 +33,10 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - EventType, - StateType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from . import GroupEntity from .const import CONF_IGNORE_NON_NUMERIC @@ -303,45 +294,6 @@ class SensorGroup(GroupEntity, SensorEntity): self._state_incorrect: set[str] = set() self._extra_state_attribute: dict[str, Any] = {} - @callback - def async_start_preview( - self, - preview_callback: Callable[[str, Mapping[str, Any]], None], - ) -> CALLBACK_TYPE: - """Render a preview.""" - - @callback - def async_state_changed_listener( - event: EventType[EventStateChangedData] | None, - ) -> None: - """Handle child updates.""" - self.async_update_group_state() - preview_callback(*self._async_generate_attributes()) - - async_state_changed_listener(None) - return async_track_state_change_event( - self.hass, self._entity_ids, async_state_changed_listener - ) - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - @callback - def async_state_changed_listener( - event: EventType[EventStateChangedData], - ) -> None: - """Handle child updates.""" - self.async_set_context(event.context) - self.async_defer_or_update_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, self._entity_ids, async_state_changed_listener - ) - ) - - await super().async_added_to_hass() - @callback def async_update_group_state(self) -> None: """Query all members and determine the sensor group state.""" diff --git a/homeassistant/components/group/switch.py b/homeassistant/components/group/switch.py index f62c805ba1d..bef42824d86 100644 --- a/homeassistant/components/group/switch.py +++ b/homeassistant/components/group/switch.py @@ -22,11 +22,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import GroupEntity @@ -112,25 +108,6 @@ class SwitchGroup(GroupEntity, SwitchEntity): if mode: self.mode = all - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - @callback - def async_state_changed_listener( - event: EventType[EventStateChangedData], - ) -> None: - """Handle child updates.""" - self.async_set_context(event.context) - self.async_defer_or_update_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, self._entity_ids, async_state_changed_listener - ) - ) - - await super().async_added_to_hass() - async def async_turn_on(self, **kwargs: Any) -> None: """Forward the turn_on command to all switches in the group.""" data = {ATTR_ENTITY_ID: self._entity_ids} From 22c1ddef713754b56d8cfa7ee53bdf0898faa592 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 23 Aug 2023 12:45:49 -0500 Subject: [PATCH 0784/1151] Enable strict typing for ipp (#98792) enable strict typing for ipp --- .strict-typing | 1 + homeassistant/components/ipp/config_flow.py | 4 ++-- mypy.ini | 10 ++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.strict-typing b/.strict-typing index 5ecdc54826b..41138c812ec 100644 --- a/.strict-typing +++ b/.strict-typing @@ -183,6 +183,7 @@ homeassistant.components.imap.* homeassistant.components.input_button.* homeassistant.components.input_select.* homeassistant.components.integration.* +homeassistant.components.ipp.* homeassistant.components.iqvia.* homeassistant.components.isy994.* homeassistant.components.jellyfin.* diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index a00190eebce..8d1da6eca91 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -59,9 +59,9 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Set up the instance.""" - self.discovery_info = {} + self.discovery_info: dict[str, Any] = {} async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/mypy.ini b/mypy.ini index 883a5ec2f26..a4bf83dbf27 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1592,6 +1592,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ipp.*] +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.iqvia.*] check_untyped_defs = true disallow_incomplete_defs = true From 39992c2ccc6cb31f4e971f18cfab5835b3880a52 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 23 Aug 2023 20:20:08 +0200 Subject: [PATCH 0785/1151] Migrate BSB-Lan diagnostics test to snapshot assertion (#98899) Migrate bsblan diagnostics test to snapshot assertion --- .../bsblan/fixtures/diagnostics.json | 75 ------------------ .../bsblan/snapshots/test_diagnostics.ambr | 78 +++++++++++++++++++ tests/components/bsblan/test_diagnostics.py | 10 +-- 3 files changed, 83 insertions(+), 80 deletions(-) delete mode 100644 tests/components/bsblan/fixtures/diagnostics.json create mode 100644 tests/components/bsblan/snapshots/test_diagnostics.ambr diff --git a/tests/components/bsblan/fixtures/diagnostics.json b/tests/components/bsblan/fixtures/diagnostics.json deleted file mode 100644 index bd05aca56d5..00000000000 --- a/tests/components/bsblan/fixtures/diagnostics.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "info": { - "device_identification": { - "name": "Gerte-Identifikation", - "unit": "", - "desc": "", - "value": "RVS21.831F/127", - "dataType": 7 - }, - "controller_family": { - "name": "Device family", - "unit": "", - "desc": "", - "value": "211", - "dataType": 0 - }, - "controller_variant": { - "name": "Device variant", - "unit": "", - "desc": "", - "value": "127", - "dataType": 0 - } - }, - "device": { - "name": "BSB-LAN", - "version": "1.0.38-20200730234859", - "MAC": "00:80:41:19:69:90", - "uptime": 969402857 - }, - "state": { - "hvac_mode": { - "name": "Operating mode", - "unit": "", - "desc": "Komfort", - "value": "heat", - "dataType": 1 - }, - "hvac_mode2": { - "name": "Operating mode", - "unit": "", - "desc": "Reduziert", - "value": "2", - "dataType": 1 - }, - "target_temperature": { - "name": "Room temperature Comfort setpoint", - "unit": "°C", - "desc": "", - "value": "18.5", - "dataType": 0 - }, - "hvac_action": { - "name": "Status heating circuit 1", - "unit": "", - "desc": "Raumtemp\u2019begrenzung", - "value": "122", - "dataType": 1 - }, - "current_temperature": { - "name": "Room temp 1 actual value", - "unit": "°C", - "desc": "", - "value": "18.6", - "dataType": 0 - }, - "room1_thermostat_mode": { - "name": "Raumthermostat 1", - "unit": "", - "desc": "Kein Bedarf", - "value": "0", - "dataType": 1 - } - } -} diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..2fff33de046 --- /dev/null +++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr @@ -0,0 +1,78 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'device': dict({ + 'MAC': '00:80:41:19:69:90', + 'name': 'BSB-LAN', + 'uptime': 969402857, + 'version': '1.0.38-20200730234859', + }), + 'info': dict({ + 'controller_family': dict({ + 'dataType': 0, + 'desc': '', + 'name': 'Device family', + 'unit': '', + 'value': '211', + }), + 'controller_variant': dict({ + 'dataType': 0, + 'desc': '', + 'name': 'Device variant', + 'unit': '', + 'value': '127', + }), + 'device_identification': dict({ + 'dataType': 7, + 'desc': '', + 'name': 'Gerte-Identifikation', + 'unit': '', + 'value': 'RVS21.831F/127', + }), + }), + 'state': dict({ + 'current_temperature': dict({ + 'dataType': 0, + 'desc': '', + 'name': 'Room temp 1 actual value', + 'unit': '°C', + 'value': '18.6', + }), + 'hvac_action': dict({ + 'dataType': 1, + 'desc': 'Raumtemp’begrenzung', + 'name': 'Status heating circuit 1', + 'unit': '', + 'value': '122', + }), + 'hvac_mode': dict({ + 'dataType': 1, + 'desc': 'Komfort', + 'name': 'Operating mode', + 'unit': '', + 'value': 'heat', + }), + 'hvac_mode2': dict({ + 'dataType': 1, + 'desc': 'Reduziert', + 'name': 'Operating mode', + 'unit': '', + 'value': '2', + }), + 'room1_thermostat_mode': dict({ + 'dataType': 1, + 'desc': 'Kein Bedarf', + 'name': 'Raumthermostat 1', + 'unit': '', + 'value': '0', + }), + 'target_temperature': dict({ + 'dataType': 0, + 'desc': '', + 'name': 'Room temperature Comfort setpoint', + 'unit': '°C', + 'value': '18.5', + }), + }), + }) +# --- diff --git a/tests/components/bsblan/test_diagnostics.py b/tests/components/bsblan/test_diagnostics.py index b2b5d201b93..316296df78a 100644 --- a/tests/components/bsblan/test_diagnostics.py +++ b/tests/components/bsblan/test_diagnostics.py @@ -1,9 +1,10 @@ """Tests for the diagnostics data provided by the BSBLan integration.""" -import json + +from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -12,12 +13,11 @@ async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - diagnostics_fixture = json.loads(load_fixture("bsblan/diagnostics.json")) - assert ( await get_diagnostics_for_config_entry(hass, hass_client, init_integration) - == diagnostics_fixture + == snapshot ) From e96ce3f5204246d497f98a4396484c40782dace4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roy?= Date: Wed, 23 Aug 2023 11:34:38 -0700 Subject: [PATCH 0786/1151] baf: Raise ConfigEntryNotReady when the device has a mismatched UUID (#98898) --- homeassistant/components/baf/__init__.py | 5 +++ homeassistant/components/baf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/baf/test_init.py | 43 ++++++++++++++++++++++ 5 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/components/baf/test_init.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index dd784b214f7..fcc648f4001 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -6,6 +6,7 @@ from asyncio import timeout from aiobafi6 import Device, Service from aiobafi6.discovery import PORT +from aiobafi6.exceptions import DeviceUUIDMismatchError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, Platform @@ -37,6 +38,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with timeout(RUN_TIMEOUT): await device.async_wait_available() + except DeviceUUIDMismatchError as ex: + raise ConfigEntryNotReady( + f"Unexpected device found at {ip_address}; expected {entry.unique_id}, found {device.dns_sd_uuid}" + ) from ex except asyncio.TimeoutError as ex: run_future.cancel() raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 37fd5cee7c6..497b3638fce 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", "iot_class": "local_push", - "requirements": ["aiobafi6==0.8.2"], + "requirements": ["aiobafi6==0.9.0"], "zeroconf": [ { "type": "_api._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 813e3af0ce7..f7cb1f9d60a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -203,7 +203,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.8.2 +aiobafi6==0.9.0 # homeassistant.components.aws aiobotocore==2.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 681755137b3..fbae222770c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -184,7 +184,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.8.2 +aiobafi6==0.9.0 # homeassistant.components.aws aiobotocore==2.6.0 diff --git a/tests/components/baf/test_init.py b/tests/components/baf/test_init.py new file mode 100644 index 00000000000..c87237892ad --- /dev/null +++ b/tests/components/baf/test_init.py @@ -0,0 +1,43 @@ +"""Test the baf init flow.""" +from unittest.mock import patch + +from aiobafi6.exceptions import DeviceUUIDMismatchError +import pytest + +from homeassistant.components.baf.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import MOCK_UUID, MockBAFDevice + +from tests.common import MockConfigEntry + + +def _patch_device_init(side_effect=None): + """Mock out the BAF Device object.""" + + def _create_mock_baf(*args, **kwargs): + return MockBAFDevice(side_effect) + + return patch("homeassistant.components.baf.Device", _create_mock_baf) + + +async def test_config_entry_wrong_uuid( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test config entry enters setup retry when uuid mismatches.""" + mismatched_uuid = MOCK_UUID + "0" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_IP_ADDRESS: "127.0.0.1"}, unique_id=mismatched_uuid + ) + already_migrated_config_entry.add_to_hass(hass) + with _patch_device_init(DeviceUUIDMismatchError): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + assert ( + "Unexpected device found at 127.0.0.1; expected 12340, found 1234" + in caplog.text + ) From 4aa7fb0e352fa7986ce952ae56da61a4f5c9f66e Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 23 Aug 2023 21:02:11 +0200 Subject: [PATCH 0787/1151] Use snapshot assertion for Discovergy diagnostics test (#98871) Add snapshot assertion to Discovergy --- .../components/discovergy/diagnostics.py | 4 -- .../snapshots/test_diagnostics.ambr | 47 ++++++++++++++ .../components/discovergy/test_diagnostics.py | 62 ++----------------- 3 files changed, 51 insertions(+), 62 deletions(-) create mode 100644 tests/components/discovergy/snapshots/test_diagnostics.ambr diff --git a/homeassistant/components/discovergy/diagnostics.py b/homeassistant/components/discovergy/diagnostics.py index 5d4a34b07dd..e0a9e47e6fd 100644 --- a/homeassistant/components/discovergy/diagnostics.py +++ b/homeassistant/components/discovergy/diagnostics.py @@ -8,14 +8,11 @@ from pydiscovergy.models import Meter from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant from . import DiscovergyData from .const import DOMAIN -TO_REDACT_CONFIG_ENTRY = {CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID, "title"} - TO_REDACT_METER = { "serial_number", "full_serial_number", @@ -44,7 +41,6 @@ async def async_get_config_entry_diagnostics( last_readings[meter.meter_id] = asdict(coordinator.data) return { - "entry": async_redact_data(entry.as_dict(), TO_REDACT_CONFIG_ENTRY), "meters": flattened_meter, "readings": last_readings, } diff --git a/tests/components/discovergy/snapshots/test_diagnostics.ambr b/tests/components/discovergy/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..d02f57c7540 --- /dev/null +++ b/tests/components/discovergy/snapshots/test_diagnostics.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'meters': list([ + dict({ + 'additional': dict({ + 'administration_number': '**REDACTED**', + 'current_scaling_factor': 1, + 'first_measurement_time': 1517569090926, + 'internal_meters': 1, + 'last_measurement_time': 1678430543742, + 'manufacturer_id': 'TST', + 'printed_full_serial_number': '**REDACTED**', + 'scaling_factor': 1, + 'voltage_scaling_factor': 1, + }), + 'full_serial_number': '**REDACTED**', + 'load_profile_type': 'SLP', + 'location': '**REDACTED**', + 'measurement_type': 'ELECTRICITY', + 'meter_id': 'f8d610b7a8cc4e73939fa33b990ded54', + 'serial_number': '**REDACTED**', + 'type': 'TST', + }), + ]), + 'readings': dict({ + 'f8d610b7a8cc4e73939fa33b990ded54': dict({ + 'time': '2023-03-10T07:32:06.702000', + 'values': dict({ + 'energy': 119348699715000.0, + 'energy1': 2254180000.0, + 'energy2': 119346445534000.0, + 'energyOut': 55048723044000.0, + 'energyOut1': 0.0, + 'energyOut2': 0.0, + 'power': 531750.0, + 'power1': 142680.0, + 'power2': 138010.0, + 'power3': 251060.0, + 'voltage1': 239800.0, + 'voltage2': 239700.0, + 'voltage3': 239000.0, + }), + }), + }), + }) +# --- diff --git a/tests/components/discovergy/test_diagnostics.py b/tests/components/discovergy/test_diagnostics.py index b9da2bb7e6f..d7565e3f0c4 100644 --- a/tests/components/discovergy/test_diagnostics.py +++ b/tests/components/discovergy/test_diagnostics.py @@ -1,7 +1,8 @@ """Test Discovergy diagnostics.""" from unittest.mock import patch -from homeassistant.components.diagnostics import REDACTED +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -14,6 +15,7 @@ async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" with patch("pydiscovergy.Discovergy.meters", return_value=GET_METERS), patch( @@ -26,60 +28,4 @@ async def test_entry_diagnostics( hass, hass_client, mock_config_entry ) - assert result["entry"] == { - "entry_id": mock_config_entry.entry_id, - "version": 1, - "domain": "discovergy", - "title": REDACTED, - "data": {"email": REDACTED, "password": REDACTED}, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - } - - assert result["meters"] == [ - { - "additional": { - "administration_number": REDACTED, - "current_scaling_factor": 1, - "first_measurement_time": 1517569090926, - "internal_meters": 1, - "last_measurement_time": 1678430543742, - "manufacturer_id": "TST", - "printed_full_serial_number": REDACTED, - "scaling_factor": 1, - "voltage_scaling_factor": 1, - }, - "full_serial_number": REDACTED, - "load_profile_type": "SLP", - "location": REDACTED, - "measurement_type": "ELECTRICITY", - "meter_id": "f8d610b7a8cc4e73939fa33b990ded54", - "serial_number": REDACTED, - "type": "TST", - } - ] - - assert result["readings"] == { - "f8d610b7a8cc4e73939fa33b990ded54": { - "time": "2023-03-10T07:32:06.702000", - "values": { - "energy": 119348699715000.0, - "energy1": 2254180000.0, - "energy2": 119346445534000.0, - "energyOut": 55048723044000.0, - "energyOut1": 0.0, - "energyOut2": 0.0, - "power": 531750.0, - "power1": 142680.0, - "power2": 138010.0, - "power3": 251060.0, - "voltage1": 239800.0, - "voltage2": 239700.0, - "voltage3": 239000.0, - }, - } - } + assert result == snapshot From e1db3ecf52ac93ef70ecba0823410c344dd44cff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Aug 2023 14:21:18 -0500 Subject: [PATCH 0788/1151] Retry rainmachine setup later if the wrong device is found (#98888) --- homeassistant/components/rainmachine/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index ef2713cc192..c29154a941c 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -219,10 +219,11 @@ async def async_setup_entry( # noqa: C901 """Set up RainMachine as config entry.""" websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) + ip_address = entry.data[CONF_IP_ADDRESS] try: await client.load_local( - entry.data[CONF_IP_ADDRESS], + ip_address, entry.data[CONF_PASSWORD], port=entry.data[CONF_PORT], use_ssl=entry.data.get(CONF_SSL, DEFAULT_SSL), @@ -238,6 +239,7 @@ async def async_setup_entry( # noqa: C901 if not entry.unique_id or is_ip_address(entry.unique_id): # If the config entry doesn't already have a unique ID, set one: entry_updates["unique_id"] = controller.mac + if CONF_DEFAULT_ZONE_RUN_TIME in entry.data: # If a zone run time exists in the config entry's data, pop it and move it to # options: @@ -252,6 +254,17 @@ async def async_setup_entry( # noqa: C901 if entry_updates: hass.config_entries.async_update_entry(entry, **entry_updates) + if entry.unique_id and controller.mac != entry.unique_id: + # If the mac address of the device does not match the unique_id + # of the config entry, it likely means the DHCP lease has expired + # and the device has been assigned a new IP address. We need to + # wait for the next discovery to find the device at its new address + # and update the config entry so we do not mix up devices. + raise ConfigEntryNotReady( + f"Unexpected device found at {ip_address}; expected {entry.unique_id}, " + f"found {controller.mac}" + ) + async def async_update(api_category: str) -> dict: """Update the appropriate API data based on a category.""" data: dict = {} From 82e92cdf829939820c0d0774d813337626e49054 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 23 Aug 2023 21:36:18 +0200 Subject: [PATCH 0789/1151] Use snapshot assertion for Axis diagnostics test (#98902) --- tests/components/axis/conftest.py | 1 + .../axis/snapshots/test_diagnostics.ambr | 92 ++++++++++++++++++ tests/components/axis/test_diagnostics.py | 96 ++----------------- 3 files changed, 102 insertions(+), 87 deletions(-) create mode 100644 tests/components/axis/snapshots/test_diagnostics.ambr diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index 5c9c4e5a255..3c476705258 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -60,6 +60,7 @@ def config_entry_fixture(hass, config, options, config_entry_version): """Define a config entry fixture.""" entry = MockConfigEntry( domain=AXIS_DOMAIN, + entry_id="676abe5b73621446e6550a2e86ffe3dd", unique_id=FORMATTED_MAC, data=config, options=options, diff --git a/tests/components/axis/snapshots/test_diagnostics.ambr b/tests/components/axis/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..74a1f110c14 --- /dev/null +++ b/tests/components/axis/snapshots/test_diagnostics.ambr @@ -0,0 +1,92 @@ +# serializer version: 1 +# name: test_entry_diagnostics[api_discovery_items0] + dict({ + 'api_discovery': list([ + dict({ + 'id': 'api-discovery', + 'name': 'API Discovery Service', + 'version': '1.0', + }), + dict({ + 'id': 'param-cgi', + 'name': 'Legacy Parameter Handling', + 'version': '1.0', + }), + dict({ + 'id': 'basic-device-info', + 'name': 'Basic Device Information', + 'version': '1.1', + }), + ]), + 'basic_device_info': dict({ + 'ProdNbr': 'M1065-LW', + 'ProdType': 'Network Camera', + 'SerialNumber': '**REDACTED**', + 'Version': '9.80.1', + }), + 'camera_sources': dict({ + 'Image': 'http://1.2.3.4:80/axis-cgi/jpg/image.cgi', + 'MJPEG': 'http://1.2.3.4:80/axis-cgi/mjpg/video.cgi', + 'Stream': 'rtsp://user:pass@1.2.3.4/axis-media/media.amp?videocodec=h264', + }), + 'config': dict({ + 'data': dict({ + 'host': '1.2.3.4', + 'model': 'model', + 'name': 'name', + 'password': '**REDACTED**', + 'port': 80, + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'axis', + 'entry_id': '676abe5b73621446e6550a2e86ffe3dd', + 'options': dict({ + 'events': True, + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': '**REDACTED**', + 'version': 3, + }), + 'params': dict({ + 'root.IOPort': dict({ + 'I0.Configurable': 'no', + 'I0.Direction': 'input', + 'I0.Input.Name': 'PIR sensor', + 'I0.Input.Trig': 'closed', + }), + 'root.Input': dict({ + 'NbrOfInputs': '1', + }), + 'root.Output': dict({ + 'NbrOfOutputs': '0', + }), + 'root.Properties': dict({ + 'API.HTTP.Version': '3', + 'API.Metadata.Metadata': 'yes', + 'API.Metadata.Version': '1.0', + 'EmbeddedDevelopment.Version': '2.16', + 'Firmware.BuildDate': 'Feb 15 2019 09:42', + 'Firmware.BuildNumber': '26', + 'Firmware.Version': '9.10.1', + 'Image.Format': 'jpeg,mjpeg,h264', + 'Image.NbrOfViews': '2', + 'Image.Resolution': '1920x1080,1280x960,1280x720,1024x768,1024x576,800x600,640x480,640x360,352x240,320x240', + 'Image.Rotation': '0,180', + 'System.SerialNumber': '**REDACTED**', + }), + 'root.StreamProfile': dict({ + 'MaxGroups': '26', + 'S0.Description': 'profile_1_description', + 'S0.Name': 'profile_1', + 'S0.Parameters': 'videocodec=h264', + 'S1.Description': 'profile_2_description', + 'S1.Name': 'profile_2', + 'S1.Parameters': 'videocodec=h265', + }), + }), + }) +# --- diff --git a/tests/components/axis/test_diagnostics.py b/tests/components/axis/test_diagnostics.py index a76aa40ebc8..af11fdc388a 100644 --- a/tests/components/axis/test_diagnostics.py +++ b/tests/components/axis/test_diagnostics.py @@ -1,7 +1,7 @@ """Test Axis diagnostics.""" import pytest +from syrupy import SnapshotAssertion -from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant from .const import API_DISCOVERY_BASIC_DEVICE_INFO @@ -12,91 +12,13 @@ from tests.typing import ClientSessionGenerator @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO]) async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator, setup_config_entry + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + setup_config_entry, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert await get_diagnostics_for_config_entry( - hass, hass_client, setup_config_entry - ) == { - "config": { - "entry_id": setup_config_entry.entry_id, - "version": 3, - "domain": "axis", - "title": "Mock Title", - "data": { - "host": "1.2.3.4", - "username": REDACTED, - "password": REDACTED, - "port": 80, - "model": "model", - "name": "name", - }, - "options": {"events": True}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - }, - "camera_sources": { - "Image": "http://1.2.3.4:80/axis-cgi/jpg/image.cgi", - "MJPEG": "http://1.2.3.4:80/axis-cgi/mjpg/video.cgi", - "Stream": "rtsp://user:pass@1.2.3.4/axis-media/media.amp?videocodec=h264", - }, - "api_discovery": [ - { - "id": "api-discovery", - "name": "API Discovery Service", - "version": "1.0", - }, - { - "id": "param-cgi", - "name": "Legacy Parameter Handling", - "version": "1.0", - }, - { - "id": "basic-device-info", - "name": "Basic Device Information", - "version": "1.1", - }, - ], - "basic_device_info": { - "ProdNbr": "M1065-LW", - "ProdType": "Network Camera", - "SerialNumber": REDACTED, - "Version": "9.80.1", - }, - "params": { - "root.IOPort": { - "I0.Configurable": "no", - "I0.Direction": "input", - "I0.Input.Name": "PIR sensor", - "I0.Input.Trig": "closed", - }, - "root.Input": {"NbrOfInputs": "1"}, - "root.Output": {"NbrOfOutputs": "0"}, - "root.Properties": { - "API.HTTP.Version": "3", - "API.Metadata.Metadata": "yes", - "API.Metadata.Version": "1.0", - "EmbeddedDevelopment.Version": "2.16", - "Firmware.BuildDate": "Feb 15 2019 09:42", - "Firmware.BuildNumber": "26", - "Firmware.Version": "9.10.1", - "Image.Format": "jpeg,mjpeg,h264", - "Image.NbrOfViews": "2", - "Image.Resolution": "1920x1080,1280x960,1280x720,1024x768,1024x576,800x600,640x480,640x360,352x240,320x240", - "Image.Rotation": "0,180", - "System.SerialNumber": REDACTED, - }, - "root.StreamProfile": { - "MaxGroups": "26", - "S0.Description": "profile_1_description", - "S0.Name": "profile_1", - "S0.Parameters": "videocodec=h264", - "S1.Description": "profile_2_description", - "S1.Name": "profile_2", - "S1.Parameters": "videocodec=h265", - }, - }, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, setup_config_entry) + == snapshot + ) From 1f0e8f93c577427e24e055dca5c3aa7ccd66eff7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 23 Aug 2023 21:37:03 +0200 Subject: [PATCH 0790/1151] Use snapshot assertion for Deconz diagnostics test (#98908) --- .../deconz/snapshots/test_diagnostics.ambr | 79 +++++++++++++++++++ tests/components/deconz/test_diagnostics.py | 60 ++------------ 2 files changed, 86 insertions(+), 53 deletions(-) create mode 100644 tests/components/deconz/snapshots/test_diagnostics.ambr diff --git a/tests/components/deconz/snapshots/test_diagnostics.ambr b/tests/components/deconz/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..bbd96f1751c --- /dev/null +++ b/tests/components/deconz/snapshots/test_diagnostics.ambr @@ -0,0 +1,79 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'alarm_systems': dict({ + }), + 'config': dict({ + 'data': dict({ + 'api_key': '**REDACTED**', + 'host': '1.2.3.4', + 'port': 80, + }), + 'disabled_by': None, + 'domain': 'deconz', + 'entry_id': '1', + 'options': dict({ + 'master': True, + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + 'deconz_config': dict({ + 'bridgeid': '**REDACTED**', + 'ipaddress': '1.2.3.4', + 'mac': '**REDACTED**', + 'modelid': 'deCONZ', + 'name': 'deCONZ mock gateway', + 'sw_version': '2.05.69', + 'uuid': '1234', + 'websocketport': 1234, + }), + 'deconz_ids': dict({ + }), + 'entities': dict({ + 'alarm_control_panel': list([ + ]), + 'binary_sensor': list([ + ]), + 'button': list([ + ]), + 'climate': list([ + ]), + 'cover': list([ + ]), + 'fan': list([ + ]), + 'light': list([ + ]), + 'lock': list([ + ]), + 'number': list([ + ]), + 'scene': list([ + ]), + 'select': list([ + ]), + 'sensor': list([ + ]), + 'siren': list([ + ]), + 'switch': list([ + ]), + }), + 'events': dict({ + }), + 'groups': dict({ + }), + 'lights': dict({ + }), + 'scenes': dict({ + }), + 'sensors': dict({ + }), + 'websocket_state': 'running', + }) +# --- diff --git a/tests/components/deconz/test_diagnostics.py b/tests/components/deconz/test_diagnostics.py index 44b8bfd50dc..e7e470cdf81 100644 --- a/tests/components/deconz/test_diagnostics.py +++ b/tests/components/deconz/test_diagnostics.py @@ -1,12 +1,10 @@ """Test deCONZ diagnostics.""" from pydeconz.websocket import State +from syrupy import SnapshotAssertion -from homeassistant.components.deconz.const import CONF_MASTER_GATEWAY -from homeassistant.components.diagnostics import REDACTED -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant -from .test_gateway import HOST, PORT, setup_deconz_integration +from .test_gateway import setup_deconz_integration from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.test_util.aiohttp import AiohttpClientMocker @@ -18,6 +16,7 @@ async def test_entry_diagnostics( hass_client: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, mock_deconz_websocket, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) @@ -25,52 +24,7 @@ async def test_entry_diagnostics( await mock_deconz_websocket(state=State.RUNNING) await hass.async_block_till_done() - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "config": { - "data": {CONF_API_KEY: REDACTED, CONF_HOST: HOST, CONF_PORT: PORT}, - "disabled_by": None, - "domain": "deconz", - "entry_id": "1", - "options": {CONF_MASTER_GATEWAY: True}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "title": "Mock Title", - "unique_id": REDACTED, - "version": 1, - }, - "deconz_config": { - "bridgeid": REDACTED, - "ipaddress": HOST, - "mac": REDACTED, - "modelid": "deCONZ", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "uuid": "1234", - "websocketport": 1234, - }, - "websocket_state": State.RUNNING.value, - "deconz_ids": {}, - "entities": { - str(Platform.ALARM_CONTROL_PANEL): [], - str(Platform.BINARY_SENSOR): [], - str(Platform.BUTTON): [], - str(Platform.CLIMATE): [], - str(Platform.COVER): [], - str(Platform.FAN): [], - str(Platform.LIGHT): [], - str(Platform.LOCK): [], - str(Platform.NUMBER): [], - str(Platform.SCENE): [], - str(Platform.SELECT): [], - str(Platform.SENSOR): [], - str(Platform.SIREN): [], - str(Platform.SWITCH): [], - }, - "events": {}, - "alarm_systems": {}, - "groups": {}, - "lights": {}, - "scenes": {}, - "sensors": {}, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From f83c33540924e8dea809621fee60b9c07b85bafc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 23 Aug 2023 22:21:24 +0200 Subject: [PATCH 0791/1151] Use snapshot assertion for Environment Canada diagnostics test (#98912) --- .../fixtures/config_entry_data.json | 110 ----------------- .../snapshots/test_diagnostics.ambr | 113 ++++++++++++++++++ .../environment_canada/test_diagnostics.py | 11 +- 3 files changed, 119 insertions(+), 115 deletions(-) delete mode 100644 tests/components/environment_canada/fixtures/config_entry_data.json create mode 100644 tests/components/environment_canada/snapshots/test_diagnostics.ambr diff --git a/tests/components/environment_canada/fixtures/config_entry_data.json b/tests/components/environment_canada/fixtures/config_entry_data.json deleted file mode 100644 index 085a3394dce..00000000000 --- a/tests/components/environment_canada/fixtures/config_entry_data.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "config_entry_data": { - "latitude": "**REDACTED**", - "longitude": "**REDACTED**", - "station": "XX/1234567", - "language": "Gibberish" - }, - "weather_data": { - "temperature": { - "label": "Temperature", - "value": 14.9, - "unit": "C" - }, - "dewpoint": { - "label": "Dew Point", - "value": 1.4, - "unit": "C" - }, - "wind_chill": { - "label": "Wind Chill", - "value": null - }, - "humidex": { - "label": "Humidex", - "value": null - }, - "pressure": { - "label": "Pressure", - "value": 102.7, - "unit": "kPa" - }, - "tendency": { - "label": "Tendency", - "value": "falling" - }, - "humidity": { - "label": "Humidity", - "value": 40, - "unit": "%" - }, - "visibility": { - "label": "Visibility", - "value": 24.1, - "unit": "km" - }, - "condition": { - "label": "Condition", - "value": "Mainly Sunny" - }, - "wind_speed": { - "label": "Wind Speed", - "value": 1, - "unit": "km/h" - }, - "wind_gust": { - "label": "Wind Gust", - "value": null - }, - "wind_dir": { - "label": "Wind Direction", - "value": "N" - }, - "wind_bearing": { - "label": "Wind Bearing", - "value": 0, - "unit": "degrees" - }, - "high_temp": { - "label": "High Temperature", - "value": 18, - "unit": "C" - }, - "low_temp": { - "label": "Low Temperature", - "value": -1, - "unit": "C" - }, - "uv_index": { - "label": "UV Index", - "value": 5 - }, - "pop": { - "label": "Chance of Precip.", - "value": null - }, - "icon_code": { - "label": "Icon Code", - "value": "01" - }, - "precip_yesterday": { - "label": "Precipitation Yesterday", - "value": 0.0, - "unit": "mm" - }, - "normal_high": { - "label": "Normal High Temperature", - "value": 15, - "unit": "C" - }, - "normal_low": { - "label": "Normal Low Temperature", - "value": 6, - "unit": "C" - }, - "text_summary": { - "label": "Forecast", - "value": "Tonight. Clear. Fog patches developing after midnight. Low minus 1 with frost." - } - } -} diff --git a/tests/components/environment_canada/snapshots/test_diagnostics.ambr b/tests/components/environment_canada/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..94ed1d88201 --- /dev/null +++ b/tests/components/environment_canada/snapshots/test_diagnostics.ambr @@ -0,0 +1,113 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config_entry_data': dict({ + 'language': 'Gibberish', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'station': 'XX/1234567', + }), + 'weather_data': dict({ + 'condition': dict({ + 'label': 'Condition', + 'value': 'Mainly Sunny', + }), + 'dewpoint': dict({ + 'label': 'Dew Point', + 'unit': 'C', + 'value': 1.4, + }), + 'high_temp': dict({ + 'label': 'High Temperature', + 'unit': 'C', + 'value': 18, + }), + 'humidex': dict({ + 'label': 'Humidex', + 'value': None, + }), + 'humidity': dict({ + 'label': 'Humidity', + 'unit': '%', + 'value': 40, + }), + 'icon_code': dict({ + 'label': 'Icon Code', + 'value': '01', + }), + 'low_temp': dict({ + 'label': 'Low Temperature', + 'unit': 'C', + 'value': -1, + }), + 'normal_high': dict({ + 'label': 'Normal High Temperature', + 'unit': 'C', + 'value': 15, + }), + 'normal_low': dict({ + 'label': 'Normal Low Temperature', + 'unit': 'C', + 'value': 6, + }), + 'pop': dict({ + 'label': 'Chance of Precip.', + 'value': None, + }), + 'precip_yesterday': dict({ + 'label': 'Precipitation Yesterday', + 'unit': 'mm', + 'value': 0.0, + }), + 'pressure': dict({ + 'label': 'Pressure', + 'unit': 'kPa', + 'value': 102.7, + }), + 'temperature': dict({ + 'label': 'Temperature', + 'unit': 'C', + 'value': 14.9, + }), + 'tendency': dict({ + 'label': 'Tendency', + 'value': 'falling', + }), + 'text_summary': dict({ + 'label': 'Forecast', + 'value': 'Tonight. Clear. Fog patches developing after midnight. Low minus 1 with frost.', + }), + 'uv_index': dict({ + 'label': 'UV Index', + 'value': 5, + }), + 'visibility': dict({ + 'label': 'Visibility', + 'unit': 'km', + 'value': 24.1, + }), + 'wind_bearing': dict({ + 'label': 'Wind Bearing', + 'unit': 'degrees', + 'value': 0, + }), + 'wind_chill': dict({ + 'label': 'Wind Chill', + 'value': None, + }), + 'wind_dir': dict({ + 'label': 'Wind Direction', + 'value': 'N', + }), + 'wind_gust': dict({ + 'label': 'Wind Gust', + 'value': None, + }), + 'wind_speed': dict({ + 'label': 'Wind Speed', + 'unit': 'km/h', + 'value': 1, + }), + }), + }) +# --- diff --git a/tests/components/environment_canada/test_diagnostics.py b/tests/components/environment_canada/test_diagnostics.py index 6044c9e778b..3eedb7a0ddb 100644 --- a/tests/components/environment_canada/test_diagnostics.py +++ b/tests/components/environment_canada/test_diagnostics.py @@ -3,6 +3,8 @@ from datetime import UTC, datetime import json from unittest.mock import AsyncMock, MagicMock, patch +from syrupy import SnapshotAssertion + from homeassistant.components.environment_canada.const import ( CONF_LANGUAGE, CONF_STATION, @@ -72,7 +74,9 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" @@ -80,8 +84,5 @@ async def test_entry_diagnostics( diagnostics = await get_diagnostics_for_config_entry( hass, hass_client, config_entry ) - redacted_entry = json.loads( - load_fixture("environment_canada/config_entry_data.json") - ) - assert diagnostics == redacted_entry + assert diagnostics == snapshot From 364d872a47d253b95cad179ae579c5c254ff329e Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Wed, 23 Aug 2023 22:43:08 +0200 Subject: [PATCH 0792/1151] Bump energyzero to v0.5.0 (#98914) --- homeassistant/components/energyzero/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/energyzero/manifest.json b/homeassistant/components/energyzero/manifest.json index 05d23ca4464..8e2b8aba894 100644 --- a/homeassistant/components/energyzero/manifest.json +++ b/homeassistant/components/energyzero/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/energyzero", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["energyzero==0.4.1"] + "requirements": ["energyzero==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index f7cb1f9d60a..d6572d05f3b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -730,7 +730,7 @@ emulated-roku==0.2.1 energyflip-client==0.2.2 # homeassistant.components.energyzero -energyzero==0.4.1 +energyzero==0.5.0 # homeassistant.components.enocean enocean==0.50 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fbae222770c..ea5e8680ec7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -586,7 +586,7 @@ emulated-roku==0.2.1 energyflip-client==0.2.2 # homeassistant.components.energyzero -energyzero==0.4.1 +energyzero==0.5.0 # homeassistant.components.enocean enocean==0.50 From 816f834807341aa0036dbed140f4a734de34f6bc Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 23 Aug 2023 22:46:34 +0200 Subject: [PATCH 0793/1151] Add moisture sensors entities for gardena (#98282) Add support for soil moisture sensors for gardena --- .../gardena_bluetooth/binary_sensor.py | 16 +++++- .../components/gardena_bluetooth/button.py | 7 ++- .../gardena_bluetooth/coordinator.py | 15 ++++-- .../components/gardena_bluetooth/number.py | 34 ++++++++++-- .../components/gardena_bluetooth/sensor.py | 53 ++++++++++++++++++- .../components/gardena_bluetooth/strings.json | 15 ++++++ .../snapshots/test_number.ambr | 34 ++++++++++++ .../snapshots/test_sensor.ambr | 30 +++++++++++ .../gardena_bluetooth/test_number.py | 27 +++++++++- .../gardena_bluetooth/test_sensor.py | 27 +++++++++- 10 files changed, 244 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/binary_sensor.py b/homeassistant/components/gardena_bluetooth/binary_sensor.py index 0285f7bdf82..b66cb8cd00d 100644 --- a/homeassistant/components/gardena_bluetooth/binary_sensor.py +++ b/homeassistant/components/gardena_bluetooth/binary_sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from gardena_bluetooth.const import Valve +from gardena_bluetooth.const import Sensor, Valve from gardena_bluetooth.parse import CharacteristicBool from homeassistant.components.binary_sensor import ( @@ -26,6 +26,11 @@ class GardenaBluetoothBinarySensorEntityDescription(BinarySensorEntityDescriptio char: CharacteristicBool = field(default_factory=lambda: CharacteristicBool("")) + @property + def context(self) -> set[str]: + """Context needed for update coordinator.""" + return {self.char.uuid} + DESCRIPTIONS = ( GardenaBluetoothBinarySensorEntityDescription( @@ -35,6 +40,13 @@ DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, char=Valve.connected_state, ), + GardenaBluetoothBinarySensorEntityDescription( + key=Sensor.connected_state.uuid, + translation_key="sensor_connected_state", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + char=Sensor.connected_state, + ), ) @@ -44,7 +56,7 @@ async def async_setup_entry( """Set up binary sensor based on a config entry.""" coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] entities = [ - GardenaBluetoothBinarySensor(coordinator, description) + GardenaBluetoothBinarySensor(coordinator, description, description.context) for description in DESCRIPTIONS if description.key in coordinator.characteristics ] diff --git a/homeassistant/components/gardena_bluetooth/button.py b/homeassistant/components/gardena_bluetooth/button.py index a9dac9902f8..1ed738a9690 100644 --- a/homeassistant/components/gardena_bluetooth/button.py +++ b/homeassistant/components/gardena_bluetooth/button.py @@ -22,6 +22,11 @@ class GardenaBluetoothButtonEntityDescription(ButtonEntityDescription): char: CharacteristicBool = field(default_factory=lambda: CharacteristicBool("")) + @property + def context(self) -> set[str]: + """Context needed for update coordinator.""" + return {self.char.uuid} + DESCRIPTIONS = ( GardenaBluetoothButtonEntityDescription( @@ -40,7 +45,7 @@ async def async_setup_entry( """Set up button based on a config entry.""" coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] entities = [ - GardenaBluetoothButton(coordinator, description) + GardenaBluetoothButton(coordinator, description, description.context) for description in DESCRIPTIONS if description.key in coordinator.characteristics ] diff --git a/homeassistant/components/gardena_bluetooth/coordinator.py b/homeassistant/components/gardena_bluetooth/coordinator.py index 67ed056f7b1..73552e25c03 100644 --- a/homeassistant/components/gardena_bluetooth/coordinator.py +++ b/homeassistant/components/gardena_bluetooth/coordinator.py @@ -117,8 +117,12 @@ class GardenaBluetoothEntity(CoordinatorEntity[Coordinator]): @property def available(self) -> bool: """Return if entity is available.""" - return super().available and bluetooth.async_address_present( - self.hass, self.coordinator.address, True + return ( + self.coordinator.last_update_success + and bluetooth.async_address_present( + self.hass, self.coordinator.address, True + ) + and self._attr_available ) @@ -126,9 +130,12 @@ class GardenaBluetoothDescriptorEntity(GardenaBluetoothEntity): """Coordinator entity for entities with entity description.""" def __init__( - self, coordinator: Coordinator, description: EntityDescription + self, + coordinator: Coordinator, + description: EntityDescription, + context: set[str], ) -> None: """Initialize description entity.""" - super().__init__(coordinator, {description.key}) + super().__init__(coordinator, context) self._attr_unique_id = f"{coordinator.address}-{description.key}" self.entity_description = description diff --git a/homeassistant/components/gardena_bluetooth/number.py b/homeassistant/components/gardena_bluetooth/number.py index f53a7720577..f0ba5dbd2fe 100644 --- a/homeassistant/components/gardena_bluetooth/number.py +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -3,8 +3,9 @@ from __future__ import annotations from dataclasses import dataclass, field -from gardena_bluetooth.const import DeviceConfiguration, Valve +from gardena_bluetooth.const import DeviceConfiguration, Sensor, Valve from gardena_bluetooth.parse import ( + Characteristic, CharacteristicInt, CharacteristicLong, CharacteristicUInt16, @@ -16,7 +17,7 @@ from homeassistant.components.number import ( NumberMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory, UnitOfTime +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -35,6 +36,15 @@ class GardenaBluetoothNumberEntityDescription(NumberEntityDescription): char: CharacteristicInt | CharacteristicUInt16 | CharacteristicLong = field( default_factory=lambda: CharacteristicInt("") ) + connected_state: Characteristic | None = None + + @property + def context(self) -> set[str]: + """Context needed for update coordinator.""" + data = {self.char.uuid} + if self.connected_state: + data.add(self.connected_state.uuid) + return data DESCRIPTIONS = ( @@ -81,6 +91,18 @@ DESCRIPTIONS = ( entity_category=EntityCategory.CONFIG, char=DeviceConfiguration.seasonal_adjust, ), + GardenaBluetoothNumberEntityDescription( + key=Sensor.threshold.uuid, + translation_key="sensor_threshold", + native_unit_of_measurement=PERCENTAGE, + mode=NumberMode.BOX, + native_min_value=0.0, + native_max_value=100.0, + native_step=1.0, + entity_category=EntityCategory.CONFIG, + char=Sensor.threshold, + connected_state=Sensor.connected_state, + ), ) @@ -90,7 +112,7 @@ async def async_setup_entry( """Set up entity based on a config entry.""" coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] entities: list[NumberEntity] = [ - GardenaBluetoothNumber(coordinator, description) + GardenaBluetoothNumber(coordinator, description, description.context) for description in DESCRIPTIONS if description.key in coordinator.characteristics ] @@ -110,6 +132,12 @@ class GardenaBluetoothNumber(GardenaBluetoothDescriptorEntity, NumberEntity): self._attr_native_value = None else: self._attr_native_value = float(data) + + if char := self.entity_description.connected_state: + self._attr_available = bool(self.coordinator.get_cached(char)) + else: + self._attr_available = True + super()._handle_coordinator_update() async def async_set_native_value(self, value: float) -> None: diff --git a/homeassistant/components/gardena_bluetooth/sensor.py b/homeassistant/components/gardena_bluetooth/sensor.py index dd2bde43cc4..396d8469ffc 100644 --- a/homeassistant/components/gardena_bluetooth/sensor.py +++ b/homeassistant/components/gardena_bluetooth/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass, field from datetime import UTC, datetime, timedelta -from gardena_bluetooth.const import Battery, Valve +from gardena_bluetooth.const import Battery, Sensor, Valve from gardena_bluetooth.parse import Characteristic from homeassistant.components.sensor import ( @@ -32,6 +32,15 @@ class GardenaBluetoothSensorEntityDescription(SensorEntityDescription): """Description of entity.""" char: Characteristic = field(default_factory=lambda: Characteristic("")) + connected_state: Characteristic | None = None + + @property + def context(self) -> set[str]: + """Context needed for update coordinator.""" + data = {self.char.uuid} + if self.connected_state: + data.add(self.connected_state.uuid) + return data DESCRIPTIONS = ( @@ -51,6 +60,40 @@ DESCRIPTIONS = ( native_unit_of_measurement=PERCENTAGE, char=Battery.battery_level, ), + GardenaBluetoothSensorEntityDescription( + key=Sensor.battery_level.uuid, + translation_key="sensor_battery_level", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + char=Sensor.battery_level, + connected_state=Sensor.connected_state, + ), + GardenaBluetoothSensorEntityDescription( + key=Sensor.value.uuid, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.MOISTURE, + native_unit_of_measurement=PERCENTAGE, + char=Sensor.value, + connected_state=Sensor.connected_state, + ), + GardenaBluetoothSensorEntityDescription( + key=Sensor.type.uuid, + translation_key="sensor_type", + entity_category=EntityCategory.DIAGNOSTIC, + char=Sensor.type, + connected_state=Sensor.connected_state, + ), + GardenaBluetoothSensorEntityDescription( + key=Sensor.measurement_timestamp.uuid, + translation_key="sensor_measurement_timestamp", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + char=Sensor.measurement_timestamp, + connected_state=Sensor.connected_state, + ), ) @@ -60,7 +103,7 @@ async def async_setup_entry( """Set up Gardena Bluetooth sensor based on a config entry.""" coordinator: Coordinator = hass.data[DOMAIN][entry.entry_id] entities: list[GardenaBluetoothEntity] = [ - GardenaBluetoothSensor(coordinator, description) + GardenaBluetoothSensor(coordinator, description, description.context) for description in DESCRIPTIONS if description.key in coordinator.characteristics ] @@ -81,6 +124,12 @@ class GardenaBluetoothSensor(GardenaBluetoothDescriptorEntity, SensorEntity): tzinfo=dt_util.get_time_zone(self.hass.config.time_zone) ) self._attr_native_value = value + + if char := self.entity_description.connected_state: + self._attr_available = bool(self.coordinator.get_cached(char)) + else: + self._attr_available = True + super()._handle_coordinator_update() diff --git a/homeassistant/components/gardena_bluetooth/strings.json b/homeassistant/components/gardena_bluetooth/strings.json index 538f97ffdb3..01eac80d1e0 100644 --- a/homeassistant/components/gardena_bluetooth/strings.json +++ b/homeassistant/components/gardena_bluetooth/strings.json @@ -23,6 +23,9 @@ "binary_sensor": { "valve_connected_state": { "name": "Valve connection" + }, + "sensor_connected_state": { + "name": "Sensor connection" } }, "button": { @@ -45,12 +48,24 @@ }, "seasonal_adjust": { "name": "Seasonal adjust" + }, + "sensor_threshold": { + "name": "Sensor threshold" } }, "sensor": { "activation_reason": { "name": "Activation reason" }, + "sensor_battery_level": { + "name": "Sensor battery" + }, + "sensor_type": { + "name": "Sensor type" + }, + "sensor_measurement_timestamp": { + "name": "Sensor timestamp" + }, "remaining_open_timestamp": { "name": "Valve closing" } diff --git a/tests/components/gardena_bluetooth/snapshots/test_number.ambr b/tests/components/gardena_bluetooth/snapshots/test_number.ambr index 0c464f7cbc1..0b39525dc82 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_number.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_number.ambr @@ -67,6 +67,40 @@ 'state': 'unavailable', }) # --- +# name: test_connected_state + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Sensor threshold', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.mock_title_sensor_threshold', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_connected_state.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Title Sensor threshold', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.mock_title_sensor_threshold', + 'last_changed': , + 'last_updated': , + 'state': '45.0', + }) +# --- # name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-number.mock_title_remaining_open_time] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr index 8df37b40abc..1c33e8ebab9 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr @@ -1,4 +1,34 @@ # serializer version: 1 +# name: test_connected_state + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Mock Title Sensor battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.mock_title_sensor_battery', + 'last_changed': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_connected_state.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Mock Title Sensor battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.mock_title_sensor_battery', + 'last_changed': , + 'last_updated': , + 'state': '45', + }) +# --- # name: test_setup[98bd0f13-0b0e-421a-84e5-ddbf75dc6de4-raw1-sensor.mock_title_valve_closing] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/gardena_bluetooth/test_number.py b/tests/components/gardena_bluetooth/test_number.py index 0003532fb60..ce2d19b8c63 100644 --- a/tests/components/gardena_bluetooth/test_number.py +++ b/tests/components/gardena_bluetooth/test_number.py @@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable from typing import Any from unittest.mock import Mock, call -from gardena_bluetooth.const import Valve +from gardena_bluetooth.const import Sensor, Valve from gardena_bluetooth.exceptions import ( CharacteristicNoAccess, GardenaBluetoothException, @@ -149,3 +149,28 @@ async def test_bluetooth_error_unavailable( await scan_step() assert hass.states.get("number.mock_title_remaining_open_time") == snapshot assert hass.states.get("number.mock_title_manual_watering_time") == snapshot + + +async def test_connected_state( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], + scan_step: Callable[[], Awaitable[None]], +) -> None: + """Verify that a connectivity error makes all entities unavailable.""" + + mock_read_char_raw[Sensor.connected_state.uuid] = Sensor.connected_state.encode( + False + ) + mock_read_char_raw[Sensor.threshold.uuid] = Sensor.threshold.encode(45) + + await setup_entry(hass, mock_entry, [Platform.NUMBER]) + assert hass.states.get("number.mock_title_sensor_threshold") == snapshot + + mock_read_char_raw[Sensor.connected_state.uuid] = Sensor.connected_state.encode( + True + ) + + await scan_step() + assert hass.states.get("number.mock_title_sensor_threshold") == snapshot diff --git a/tests/components/gardena_bluetooth/test_sensor.py b/tests/components/gardena_bluetooth/test_sensor.py index 307a9467f00..dc0d0cb4809 100644 --- a/tests/components/gardena_bluetooth/test_sensor.py +++ b/tests/components/gardena_bluetooth/test_sensor.py @@ -1,7 +1,7 @@ """Test Gardena Bluetooth sensor.""" from collections.abc import Awaitable, Callable -from gardena_bluetooth.const import Battery, Valve +from gardena_bluetooth.const import Battery, Sensor, Valve import pytest from syrupy.assertion import SnapshotAssertion @@ -52,3 +52,28 @@ async def test_setup( mock_read_char_raw[uuid] = char_raw await scan_step() assert hass.states.get(entity_id) == snapshot + + +async def test_connected_state( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_read_char_raw: dict[str, bytes], + scan_step: Callable[[], Awaitable[None]], +) -> None: + """Verify that a connectivity error makes all entities unavailable.""" + + mock_read_char_raw[Sensor.connected_state.uuid] = Sensor.connected_state.encode( + False + ) + mock_read_char_raw[Sensor.battery_level.uuid] = Sensor.battery_level.encode(45) + + await setup_entry(hass, mock_entry, [Platform.SENSOR]) + assert hass.states.get("sensor.mock_title_sensor_battery") == snapshot + + mock_read_char_raw[Sensor.connected_state.uuid] = Sensor.connected_state.encode( + True + ) + + await scan_step() + assert hass.states.get("sensor.mock_title_sensor_battery") == snapshot From d8f0c090cf6ce10c061311885b2e116058342c00 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Wed, 23 Aug 2023 23:02:19 +0200 Subject: [PATCH 0794/1151] Energyzero - Add sensor entity to pick best hours (#98916) * Add entity to pick best hours * Add entity also to diagnostics * Remove string translation that doesn't exists --------- Co-authored-by: Joost Lekkerkerker --- .../components/energyzero/diagnostics.py | 1 + homeassistant/components/energyzero/sensor.py | 16 ++++- .../components/energyzero/strings.json | 3 - .../snapshots/test_diagnostics.ambr | 2 + .../energyzero/snapshots/test_sensor.ambr | 65 +++++++++++++++++++ tests/components/energyzero/test_sensor.py | 5 ++ 6 files changed, 88 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/energyzero/diagnostics.py b/homeassistant/components/energyzero/diagnostics.py index 5e3e402efbf..3b0c05b7368 100644 --- a/homeassistant/components/energyzero/diagnostics.py +++ b/homeassistant/components/energyzero/diagnostics.py @@ -50,6 +50,7 @@ async def async_get_config_entry_diagnostics( "highest_price_time": coordinator.data.energy_today.highest_price_time, "lowest_price_time": coordinator.data.energy_today.lowest_price_time, "percentage_of_max": coordinator.data.energy_today.pct_of_max_price, + "hours_priced_equal_or_lower": coordinator.data.energy_today.hours_priced_equal_or_lower, }, "gas": { "current_hour_price": get_gas_price(coordinator.data, 0), diff --git a/homeassistant/components/energyzero/sensor.py b/homeassistant/components/energyzero/sensor.py index 2d3a8954220..2468e5e68bf 100644 --- a/homeassistant/components/energyzero/sensor.py +++ b/homeassistant/components/energyzero/sensor.py @@ -13,7 +13,13 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CURRENCY_EURO, PERCENTAGE, UnitOfEnergy, UnitOfVolume +from homeassistant.const import ( + CURRENCY_EURO, + PERCENTAGE, + UnitOfEnergy, + UnitOfTime, + UnitOfVolume, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -114,6 +120,14 @@ SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = ( icon="mdi:percent", value_fn=lambda data: data.energy_today.pct_of_max_price, ), + EnergyZeroSensorEntityDescription( + key="hours_priced_equal_or_lower", + translation_key="hours_priced_equal_or_lower", + service_type="today_energy", + native_unit_of_measurement=UnitOfTime.HOURS, + icon="mdi:clock", + value_fn=lambda data: data.energy_today.hours_priced_equal_or_lower, + ), ) diff --git a/homeassistant/components/energyzero/strings.json b/homeassistant/components/energyzero/strings.json index 93fb264b01d..a27ce236c28 100644 --- a/homeassistant/components/energyzero/strings.json +++ b/homeassistant/components/energyzero/strings.json @@ -37,9 +37,6 @@ }, "hours_priced_equal_or_lower": { "name": "Hours priced equal or lower than current - today" - }, - "hours_priced_equal_or_higher": { - "name": "Hours priced equal or higher than current - today" } } } diff --git a/tests/components/energyzero/snapshots/test_diagnostics.ambr b/tests/components/energyzero/snapshots/test_diagnostics.ambr index 488e01e8d18..90c11ecfc6f 100644 --- a/tests/components/energyzero/snapshots/test_diagnostics.ambr +++ b/tests/components/energyzero/snapshots/test_diagnostics.ambr @@ -5,6 +5,7 @@ 'average_price': 0.37, 'current_hour_price': 0.49, 'highest_price_time': '2022-12-07T16:00:00+00:00', + 'hours_priced_equal_or_lower': 23, 'lowest_price_time': '2022-12-07T02:00:00+00:00', 'max_price': 0.55, 'min_price': 0.26, @@ -26,6 +27,7 @@ 'average_price': 0.37, 'current_hour_price': 0.49, 'highest_price_time': '2022-12-07T16:00:00+00:00', + 'hours_priced_equal_or_lower': 23, 'lowest_price_time': '2022-12-07T02:00:00+00:00', 'max_price': 0.55, 'min_price': 0.26, diff --git a/tests/components/energyzero/snapshots/test_sensor.ambr b/tests/components/energyzero/snapshots/test_sensor.ambr index 619813c52c1..e51aef980d1 100644 --- a/tests/components/energyzero/snapshots/test_sensor.ambr +++ b/tests/components/energyzero/snapshots/test_sensor.ambr @@ -651,6 +651,71 @@ 'via_device_id': None, }) # --- +# name: test_sensor[sensor.energyzero_today_energy_hours_priced_equal_or_lower-today_energy_hours_priced_equal_or_lower-today_energy] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by EnergyZero', + 'friendly_name': 'Energy market price Hours priced equal or lower than current - today', + 'icon': 'mdi:clock', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energyzero_today_energy_hours_priced_equal_or_lower', + 'last_changed': , + 'last_updated': , + 'state': '23', + }) +# --- +# name: test_sensor[sensor.energyzero_today_energy_hours_priced_equal_or_lower-today_energy_hours_priced_equal_or_lower-today_energy].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energyzero_today_energy_hours_priced_equal_or_lower', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:clock', + 'original_name': 'Hours priced equal or lower than current - today', + 'platform': 'energyzero', + 'supported_features': 0, + 'translation_key': 'hours_priced_equal_or_lower', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.energyzero_today_energy_hours_priced_equal_or_lower-today_energy_hours_priced_equal_or_lower-today_energy].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'is_new': False, + 'manufacturer': 'EnergyZero', + 'model': None, + 'name': 'Energy market price', + 'name_by_user': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_sensor[sensor.energyzero_today_energy_max_price-today_energy_max_price-today_energy] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/energyzero/test_sensor.py b/tests/components/energyzero/test_sensor.py index 466e754df27..6c7eec9d5d8 100644 --- a/tests/components/energyzero/test_sensor.py +++ b/tests/components/energyzero/test_sensor.py @@ -41,6 +41,11 @@ pytestmark = [pytest.mark.freeze_time("2022-12-07 15:00:00")] "today_energy_highest_price_time", "today_energy", ), + ( + "sensor.energyzero_today_energy_hours_priced_equal_or_lower", + "today_energy_hours_priced_equal_or_lower", + "today_energy", + ), ( "sensor.energyzero_today_gas_current_hour_price", "today_gas_current_hour_price", From e471110288d597dac63ceef3dc1e1fa716da2a73 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 00:52:04 +0200 Subject: [PATCH 0795/1151] Use snapshot assertion for August diagnostics test (#98901) --- .../august/snapshots/test_diagnostics.ambr | 125 +++++++++++++++++ tests/components/august/test_diagnostics.py | 127 +----------------- 2 files changed, 131 insertions(+), 121 deletions(-) create mode 100644 tests/components/august/snapshots/test_diagnostics.ambr diff --git a/tests/components/august/snapshots/test_diagnostics.ambr b/tests/components/august/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..b394255c555 --- /dev/null +++ b/tests/components/august/snapshots/test_diagnostics.ambr @@ -0,0 +1,125 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'brand': 'august', + 'doorbells': dict({ + 'K98GiDT45GUL': dict({ + 'HouseID': '**REDACTED**', + 'LockID': 'BBBB1F5F11114C24CCCC97571DD6AAAA', + 'appID': 'august-iphone', + 'caps': list([ + 'reconnect', + ]), + 'createdAt': '2016-11-26T22:27:11.176Z', + 'doorbellID': 'K98GiDT45GUL', + 'doorbellServerURL': 'https://doorbells.august.com', + 'dvrSubscriptionSetupDone': True, + 'firmwareVersion': '2.3.0-RC153+201711151527', + 'installDate': '2016-11-26T22:27:11.176Z', + 'installUserID': '**REDACTED**', + 'name': 'Front Door', + 'pubsubChannel': '**REDACTED**', + 'recentImage': '**REDACTED**', + 'serialNumber': 'tBXZR0Z35E', + 'settings': dict({ + 'ABREnabled': True, + 'IREnabled': True, + 'IVAEnabled': False, + 'JPGQuality': 70, + 'batteryLowThreshold': 3.1, + 'batteryRun': False, + 'batteryUseThreshold': 3.4, + 'bitrateCeiling': 512000, + 'buttonpush_notifications': True, + 'debug': False, + 'directLink': True, + 'initialBitrate': 384000, + 'irConfiguration': 8448272, + 'keepEncoderRunning': True, + 'micVolume': 100, + 'minACNoScaling': 40, + 'motion_notifications': True, + 'notify_when_offline': True, + 'overlayEnabled': True, + 'ringSoundEnabled': True, + 'speakerVolume': 92, + 'turnOffCamera': False, + 'videoResolution': '640x480', + }), + 'status': 'doorbell_call_status_online', + 'status_timestamp': 1512811834532, + 'telemetry': dict({ + 'BSSID': '88:ee:00:dd:aa:11', + 'SSID': 'foo_ssid', + 'ac_in': 23.856874, + 'battery': 4.061763, + 'battery_soc': 96, + 'battery_soh': 95, + 'date': '2017-12-10 08:05:12', + 'doorbell_low_battery': False, + 'ip_addr': '10.0.1.11', + 'link_quality': 54, + 'load_average': '0.50 0.47 0.35 1/154 9345', + 'signal_level': -56, + 'steady_ac_in': 22.196405, + 'temperature': 28.25, + 'updated_at': '2017-12-10T08:05:13.650Z', + 'uptime': '16168.75 13830.49', + 'wifi_freq': 5745, + }), + 'updatedAt': '2017-12-10T08:05:13.650Z', + }), + }), + 'locks': dict({ + 'online_with_doorsense': dict({ + 'Bridge': dict({ + '_id': 'bridgeid', + 'deviceModel': 'august-connect', + 'firmwareVersion': '2.2.1', + 'hyperBridge': True, + 'mfgBridgeID': 'C5WY200WSH', + 'operative': True, + 'status': dict({ + 'current': 'online', + 'lastOffline': '2000-00-00T00:00:00.447Z', + 'lastOnline': '2000-00-00T00:00:00.447Z', + 'updated': '2000-00-00T00:00:00.447Z', + }), + }), + 'Calibrated': False, + 'Created': '2000-00-00T00:00:00.447Z', + 'HouseID': '**REDACTED**', + 'HouseName': 'Test', + 'LockID': 'online_with_doorsense', + 'LockName': 'Online door with doorsense', + 'LockStatus': dict({ + 'dateTime': '2017-12-10T04:48:30.272Z', + 'doorState': 'open', + 'isLockStatusChanged': False, + 'status': 'locked', + 'valid': True, + }), + 'SerialNumber': 'XY', + 'Type': 1001, + 'Updated': '2000-00-00T00:00:00.447Z', + 'battery': 0.922, + 'currentFirmwareVersion': 'undefined-4.3.0-1.8.14', + 'homeKitEnabled': True, + 'hostLockInfo': dict({ + 'manufacturer': 'yale', + 'productID': 1536, + 'productTypeID': 32770, + 'serialNumber': 'ABC', + }), + 'isGalileo': False, + 'macAddress': '12:22', + 'pins': '**REDACTED**', + 'pubsubChannel': '**REDACTED**', + 'skuNumber': 'AUG-MD01', + 'supportsEntryCodes': True, + 'timeZone': 'Pacific/Hawaii', + 'zWaveEnabled': False, + }), + }), + }) +# --- diff --git a/tests/components/august/test_diagnostics.py b/tests/components/august/test_diagnostics.py index c15ccfd0119..72008f02d03 100644 --- a/tests/components/august/test_diagnostics.py +++ b/tests/components/august/test_diagnostics.py @@ -1,4 +1,6 @@ """Test august diagnostics.""" +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from .mocks import ( @@ -12,7 +14,9 @@ from tests.typing import ClientSessionGenerator async def test_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test generating diagnostics for a config entry.""" lock_one = await _mock_lock_from_fixture( @@ -23,123 +27,4 @@ async def test_diagnostics( entry, _ = await _create_august_api_with_devices(hass, [lock_one, doorbell_one]) diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert diag == { - "doorbells": { - "K98GiDT45GUL": { - "HouseID": "**REDACTED**", - "LockID": "BBBB1F5F11114C24CCCC97571DD6AAAA", - "appID": "august-iphone", - "caps": ["reconnect"], - "createdAt": "2016-11-26T22:27:11.176Z", - "doorbellID": "K98GiDT45GUL", - "doorbellServerURL": "https://doorbells.august.com", - "dvrSubscriptionSetupDone": True, - "firmwareVersion": "2.3.0-RC153+201711151527", - "installDate": "2016-11-26T22:27:11.176Z", - "installUserID": "**REDACTED**", - "name": "Front Door", - "pubsubChannel": "**REDACTED**", - "recentImage": "**REDACTED**", - "serialNumber": "tBXZR0Z35E", - "settings": { - "ABREnabled": True, - "IREnabled": True, - "IVAEnabled": False, - "JPGQuality": 70, - "batteryLowThreshold": 3.1, - "batteryRun": False, - "batteryUseThreshold": 3.4, - "bitrateCeiling": 512000, - "buttonpush_notifications": True, - "debug": False, - "directLink": True, - "initialBitrate": 384000, - "irConfiguration": 8448272, - "keepEncoderRunning": True, - "micVolume": 100, - "minACNoScaling": 40, - "motion_notifications": True, - "notify_when_offline": True, - "overlayEnabled": True, - "ringSoundEnabled": True, - "speakerVolume": 92, - "turnOffCamera": False, - "videoResolution": "640x480", - }, - "status": "doorbell_call_status_online", - "status_timestamp": 1512811834532, - "telemetry": { - "BSSID": "88:ee:00:dd:aa:11", - "SSID": "foo_ssid", - "ac_in": 23.856874, - "battery": 4.061763, - "battery_soc": 96, - "battery_soh": 95, - "date": "2017-12-10 08:05:12", - "doorbell_low_battery": False, - "ip_addr": "10.0.1.11", - "link_quality": 54, - "load_average": "0.50 0.47 0.35 1/154 9345", - "signal_level": -56, - "steady_ac_in": 22.196405, - "temperature": 28.25, - "updated_at": "2017-12-10T08:05:13.650Z", - "uptime": "16168.75 13830.49", - "wifi_freq": 5745, - }, - "updatedAt": "2017-12-10T08:05:13.650Z", - } - }, - "locks": { - "online_with_doorsense": { - "Bridge": { - "_id": "bridgeid", - "deviceModel": "august-connect", - "firmwareVersion": "2.2.1", - "hyperBridge": True, - "mfgBridgeID": "C5WY200WSH", - "operative": True, - "status": { - "current": "online", - "lastOffline": "2000-00-00T00:00:00.447Z", - "lastOnline": "2000-00-00T00:00:00.447Z", - "updated": "2000-00-00T00:00:00.447Z", - }, - }, - "Calibrated": False, - "Created": "2000-00-00T00:00:00.447Z", - "HouseID": "**REDACTED**", - "HouseName": "Test", - "LockID": "online_with_doorsense", - "LockName": "Online door with doorsense", - "LockStatus": { - "dateTime": "2017-12-10T04:48:30.272Z", - "doorState": "open", - "isLockStatusChanged": False, - "status": "locked", - "valid": True, - }, - "SerialNumber": "XY", - "Type": 1001, - "Updated": "2000-00-00T00:00:00.447Z", - "battery": 0.922, - "currentFirmwareVersion": "undefined-4.3.0-1.8.14", - "homeKitEnabled": True, - "hostLockInfo": { - "manufacturer": "yale", - "productID": 1536, - "productTypeID": 32770, - "serialNumber": "ABC", - }, - "isGalileo": False, - "macAddress": "12:22", - "pins": "**REDACTED**", - "pubsubChannel": "**REDACTED**", - "skuNumber": "AUG-MD01", - "supportsEntryCodes": True, - "timeZone": "Pacific/Hawaii", - "zWaveEnabled": False, - } - }, - "brand": "august", - } + assert diag == snapshot From 3b4774d9edc81eaae30447e2a8846f85467dd8ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 24 Aug 2023 01:54:02 +0300 Subject: [PATCH 0796/1151] Remove unnnecessary pylint configs from components/[a-d]* (#98911) --- homeassistant/components/abode/camera.py | 2 +- homeassistant/components/aten_pe/switch.py | 2 +- homeassistant/components/avea/light.py | 2 +- homeassistant/components/azure_service_bus/notify.py | 6 +++--- homeassistant/components/beewi_smartclim/sensor.py | 2 +- homeassistant/components/bloomsky/binary_sensor.py | 2 +- homeassistant/components/bloomsky/sensor.py | 2 +- homeassistant/components/bluetooth/wrappers.py | 2 +- .../components/bluetooth_tracker/device_tracker.py | 2 +- homeassistant/components/browser/__init__.py | 3 +-- homeassistant/components/caldav/calendar.py | 1 - homeassistant/components/color_extractor/__init__.py | 2 +- homeassistant/components/config/config_entries.py | 4 ---- homeassistant/components/conversation/default_agent.py | 4 +--- homeassistant/components/decora/light.py | 4 ++-- homeassistant/components/decora_wifi/light.py | 1 - homeassistant/components/demo/light.py | 2 +- homeassistant/components/dhcp/__init__.py | 4 +--- homeassistant/components/digital_ocean/binary_sensor.py | 2 +- homeassistant/components/digital_ocean/switch.py | 2 +- .../components/dlib_face_detect/image_processing.py | 3 +-- .../components/dlib_face_identify/image_processing.py | 1 - homeassistant/components/dsmr/sensor.py | 2 +- homeassistant/components/duckdns/__init__.py | 2 +- tests/components/alexa/test_flash_briefings.py | 1 - tests/components/alexa/test_intent.py | 1 - tests/components/api/test_init.py | 7 ------- tests/components/arcam_fmj/test_media_player.py | 4 ++-- tests/components/assist_pipeline/test_select.py | 1 - tests/components/balboa/conftest.py | 2 +- tests/components/cast/test_media_player.py | 1 - tests/components/dhcp/test_init.py | 2 +- 32 files changed, 27 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index afe017bfcc7..326e845b16b 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -30,7 +30,7 @@ async def async_setup_entry( data: AbodeSystem = hass.data[DOMAIN] async_add_entities( - AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) # pylint: disable=no-member + AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA) ) diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 13214b04628..3293a3e7a09 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from atenpdu import AtenPE, AtenPEError # pylint: disable=import-error +from atenpdu import AtenPE, AtenPEError import voluptuous as vol from homeassistant.components.switch import ( diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py index 5b306b058d3..a33fbfeab79 100644 --- a/homeassistant/components/avea/light.py +++ b/homeassistant/components/avea/light.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -import avea # pylint: disable=import-error +import avea from homeassistant.components.light import ( ATTR_BRIGHTNESS, diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index b318c5224df..23235a23dff 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -4,13 +4,13 @@ from __future__ import annotations import json import logging -# pylint: disable-next=import-error, no-name-in-module +# pylint: disable-next=no-name-in-module from azure.servicebus import ServiceBusMessage -# pylint: disable-next=import-error, no-name-in-module +# pylint: disable-next=no-name-in-module from azure.servicebus.aio import ServiceBusClient, ServiceBusSender -# pylint: disable-next=import-error, no-name-in-module +# pylint: disable-next=no-name-in-module from azure.servicebus.exceptions import ( MessagingEntityNotFoundError, ServiceBusConnectionError, diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 0bb3a5bbb69..08f2410ee06 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -1,7 +1,7 @@ """Platform for beewi_smartclim integration.""" from __future__ import annotations -from beewi_smartclim import BeewiSmartClimPoller # pylint: disable=import-error +from beewi_smartclim import BeewiSmartClimPoller import voluptuous as vol from homeassistant.components.sensor import ( diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 7b59039a89e..b99fdfe0c78 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -49,7 +49,7 @@ def setup_platform( class BloomSkySensor(BinarySensorEntity): """Representation of a single binary sensor in a BloomSky device.""" - def __init__(self, bs, device, sensor_name): # pylint: disable=invalid-name + def __init__(self, bs, device, sensor_name): """Initialize a BloomSky binary sensor.""" self._bloomsky = bs self._device_id = device["DeviceID"] diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 6cefcdb3346..35c9a40a46a 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -93,7 +93,7 @@ def setup_platform( class BloomSkySensor(SensorEntity): """Representation of a single sensor in a BloomSky device.""" - def __init__(self, bs, device, sensor_name): # pylint: disable=invalid-name + def __init__(self, bs, device, sensor_name): """Initialize a BloomSky sensor.""" self._bloomsky = bs self._device_id = device["DeviceID"] diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index 2ae036080f8..3a0abc855b5 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -199,7 +199,7 @@ class HaBleakClientWrapper(BleakClient): when an integration does this. """ - def __init__( # pylint: disable=super-init-not-called, keyword-arg-before-vararg + def __init__( # pylint: disable=super-init-not-called self, address_or_ble_device: str | BLEDevice, disconnected_callback: Callable[[BleakClient], None] | None = None, diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index f4fc6a8df08..4bfbe72d8b5 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -7,7 +7,7 @@ from datetime import datetime, timedelta import logging from typing import Final -import bluetooth # pylint: disable=import-error +import bluetooth from bt_proximity import BluetoothRSSI import voluptuous as vol diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py index b01f04fa140..9dc3e1fe66a 100644 --- a/homeassistant/components/browser/__init__.py +++ b/homeassistant/components/browser/__init__.py @@ -16,8 +16,7 @@ SERVICE_BROWSE_URL = "browse_url" SERVICE_BROWSE_URL_SCHEMA = vol.Schema( { - # pylint: disable-next=no-value-for-parameter - vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url() + vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url(), } ) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 57bf8e81e03..f30f79f7275 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -43,7 +43,6 @@ OFFSET = "!!" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - # pylint: disable=no-value-for-parameter vol.Required(CONF_URL): vol.Url(), vol.Optional(CONF_CALENDARS, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index 0e27f396c6d..fb04ebb76a4 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -81,7 +81,7 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: except UnidentifiedImageError as ex: _LOGGER.error( "Bad image from %s '%s' provided, are you sure it's an image? %s", - image_type, # pylint: disable=used-before-assignment + image_type, image_reference, ex, ) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 9691994512c..77e2548d424 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -143,7 +143,6 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): ) async def post(self, request): """Handle a POST request.""" - # pylint: disable=no-value-for-parameter try: return await super().post(request) except DependencyError as exc: @@ -175,7 +174,6 @@ class ConfigManagerFlowResourceView(FlowManagerResourceView): ) async def post(self, request, flow_id): """Handle a POST request.""" - # pylint: disable=no-value-for-parameter return await super().post(request, flow_id) def _prepare_result_json(self, result): @@ -212,7 +210,6 @@ class OptionManagerFlowIndexView(FlowManagerIndexView): handler in request is entry_id. """ - # pylint: disable=no-value-for-parameter return await super().post(request) @@ -234,7 +231,6 @@ class OptionManagerFlowResourceView(FlowManagerResourceView): ) async def post(self, request, flow_id): """Handle a POST request.""" - # pylint: disable=no-value-for-parameter return await super().post(request, flow_id) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 04aafc8a99d..09245fde8dc 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -54,9 +54,7 @@ _DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" _ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"] REGEX_TYPE = type(re.compile("")) -TRIGGER_CALLBACK_TYPE = Callable[ # pylint: disable=invalid-name - [str, RecognizeResult], Awaitable[str | None] -] +TRIGGER_CALLBACK_TYPE = Callable[[str, RecognizeResult], Awaitable[str | None]] def json_load(fp: IO[str]) -> JsonObjectType: diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index b46732178b8..d060b69c3f6 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -8,8 +8,8 @@ import logging import time from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar -from bluepy.btle import BTLEException # pylint: disable=import-error -import decora # pylint: disable=import-error +from bluepy.btle import BTLEException +import decora import voluptuous as vol from homeassistant import util diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index c103636563c..a9d43736743 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -4,7 +4,6 @@ from __future__ import annotations import logging from typing import Any -# pylint: disable=import-error from decora_wifi import DecoraWiFiSession from decora_wifi.models.person import Person from decora_wifi.models.residence import Residence diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 7009df75caa..d8451bdd683 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -106,7 +106,7 @@ class DemoLight(LightEntity): state: bool, available: bool = False, brightness: int = 180, - ct: int | None = None, # pylint: disable=invalid-name + ct: int | None = None, effect_list: list[str] | None = None, effect: str | None = None, hs_color: tuple[int, int] | None = None, diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index b3cfd1b65f2..29b25d0781b 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -415,9 +415,7 @@ class DHCPWatcher(WatcherBase): """Start watching for dhcp packets.""" # Local import because importing from scapy has side effects such as opening # sockets - from scapy import ( # pylint: disable=import-outside-toplevel,unused-import # noqa: F401 - arch, - ) + from scapy import arch # pylint: disable=import-outside-toplevel # noqa: F401 from scapy.layers.dhcp import DHCP # pylint: disable=import-outside-toplevel from scapy.layers.inet import IP # pylint: disable=import-outside-toplevel from scapy.layers.l2 import Ether # pylint: disable=import-outside-toplevel diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index 59c6f7961c2..e2bd09ba15e 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -65,7 +65,7 @@ class DigitalOceanBinarySensor(BinarySensorEntity): _attr_attribution = ATTRIBUTION - def __init__(self, do, droplet_id): # pylint: disable=invalid-name + def __init__(self, do, droplet_id): """Initialize a new Digital Ocean sensor.""" self._digital_ocean = do self._droplet_id = droplet_id diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index 2791d83d6bc..b226dbab0a9 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -63,7 +63,7 @@ class DigitalOceanSwitch(SwitchEntity): _attr_attribution = ATTRIBUTION - def __init__(self, do, droplet_id): # pylint: disable=invalid-name + def __init__(self, do, droplet_id): """Initialize a new Digital Ocean sensor.""" self._digital_ocean = do self._droplet_id = droplet_id diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index dcae1c1eb40..42031b28844 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -3,7 +3,7 @@ from __future__ import annotations import io -import face_recognition # pylint: disable=import-error +import face_recognition from homeassistant.components.image_processing import ImageProcessingFaceEntity from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE @@ -11,7 +11,6 @@ from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -# pylint: disable=unused-import from homeassistant.components.image_processing import ( # noqa: F401, isort:skip PLATFORM_SCHEMA, ) diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index 373f2c2b928..e6aaa6848d0 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -4,7 +4,6 @@ from __future__ import annotations import io import logging -# pylint: disable=import-error import face_recognition import voluptuous as vol diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 3d198e38f36..e4f9d0e9ab9 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -509,7 +509,7 @@ async def async_setup_entry( if stop_listener and ( hass.state == CoreState.not_running or hass.is_running ): - stop_listener() # pylint: disable=not-callable + stop_listener() if transport: transport.close() diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index 278c3c989db..d477bd41a26 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -138,6 +138,6 @@ def async_track_time_interval_backoff( def remove_listener() -> None: """Remove interval listener.""" if remove: - remove() # pylint: disable=not-callable + remove() return remove_listener diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index 0a4acda79f5..c6c2b3cc421 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -14,7 +14,6 @@ SESSION_ID = "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000" APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" REQUEST_ID = "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000" -# pylint: disable=invalid-name calls = [] NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 03546c0ed22..c63825b3c12 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -19,7 +19,6 @@ REQUEST_ID = "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000" AUTHORITY_ID = "amzn1.er-authority.000000-d0ed-0000-ad00-000000d00ebe.ZODIAC" BUILTIN_AUTH_ID = "amzn1.er-authority.000000-d0ed-0000-ad00-000000d00ebe.TEST" -# pylint: disable=invalid-name calls = [] NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index 5ba9d60996b..116529b02a4 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -81,7 +81,6 @@ async def test_api_state_change( assert hass.states.get("test.test").state == "debug_state_change2" -# pylint: disable=invalid-name async def test_api_state_change_of_non_existing_entity( hass: HomeAssistant, mock_api_client: TestClient ) -> None: @@ -97,7 +96,6 @@ async def test_api_state_change_of_non_existing_entity( assert hass.states.get("test_entity.that_does_not_exist").state == new_state -# pylint: disable=invalid-name async def test_api_state_change_with_bad_data( hass: HomeAssistant, mock_api_client: TestClient ) -> None: @@ -109,7 +107,6 @@ async def test_api_state_change_with_bad_data( assert resp.status == HTTPStatus.BAD_REQUEST -# pylint: disable=invalid-name async def test_api_state_change_to_zero_value( hass: HomeAssistant, mock_api_client: TestClient ) -> None: @@ -127,7 +124,6 @@ async def test_api_state_change_to_zero_value( assert resp.status == HTTPStatus.OK -# pylint: disable=invalid-name async def test_api_state_change_push( hass: HomeAssistant, mock_api_client: TestClient ) -> None: @@ -154,7 +150,6 @@ async def test_api_state_change_push( assert len(events) == 1 -# pylint: disable=invalid-name async def test_api_fire_event_with_no_data( hass: HomeAssistant, mock_api_client: TestClient ) -> None: @@ -174,7 +169,6 @@ async def test_api_fire_event_with_no_data( assert len(test_value) == 1 -# pylint: disable=invalid-name async def test_api_fire_event_with_data( hass: HomeAssistant, mock_api_client: TestClient ) -> None: @@ -199,7 +193,6 @@ async def test_api_fire_event_with_data( assert len(test_value) == 1 -# pylint: disable=invalid-name async def test_api_fire_event_with_invalid_json( hass: HomeAssistant, mock_api_client: TestClient ) -> None: diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index b9c86140cb9..9287e8dbc18 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -228,9 +228,9 @@ async def test_sound_mode_list(player, state, modes, modes_enum) -> None: async def test_is_volume_muted(player, state) -> None: """Test muted.""" state.get_mute.return_value = True - assert player.is_volume_muted is True # pylint: disable=singleton-comparison + assert player.is_volume_muted is True state.get_mute.return_value = False - assert player.is_volume_muted is False # pylint: disable=singleton-comparison + assert player.is_volume_muted is False state.get_mute.return_value = None assert player.is_volume_muted is None diff --git a/tests/components/assist_pipeline/test_select.py b/tests/components/assist_pipeline/test_select.py index 1868d9b005e..1419eb58750 100644 --- a/tests/components/assist_pipeline/test_select.py +++ b/tests/components/assist_pipeline/test_select.py @@ -26,7 +26,6 @@ from tests.common import MockConfigEntry, MockPlatform, mock_entity_platform class SelectPlatform(MockPlatform): """Fake select platform.""" - # pylint: disable=method-hidden async def async_setup_entry( self, hass: HomeAssistant, diff --git a/tests/components/balboa/conftest.py b/tests/components/balboa/conftest.py index 04447d0b3cc..e5da4582454 100644 --- a/tests/components/balboa/conftest.py +++ b/tests/components/balboa/conftest.py @@ -29,7 +29,7 @@ def client_fixture() -> Generator[MagicMock, None, None]: client = mock_balboa.return_value callback: list[Callable] = [] - def on(_, _callback: Callable): # pylint: disable=invalid-name + def on(_, _callback: Callable): callback.append(_callback) return lambda: None diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 6f7a13b47af..3d9feb3e43c 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -42,7 +42,6 @@ from tests.components.media_player import common from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import WebSocketGenerator -# pylint: disable=invalid-name FakeUUID = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e2") FakeUUID2 = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e4") FakeGroupUUID = UUID("57355bce-9364-4aa6-ac1e-eb849dccf9e3") diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 0754febfc76..076138080cc 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -4,7 +4,7 @@ import threading from unittest.mock import MagicMock, patch import pytest -from scapy import arch # pylint: disable=unused-import # noqa: F401 +from scapy import arch # noqa: F401 from scapy.error import Scapy_Exception from scapy.layers.dhcp import DHCP from scapy.layers.l2 import Ether From 34b47a2597d27da584ee3a5f4aa92857a3606817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 24 Aug 2023 01:56:50 +0300 Subject: [PATCH 0797/1151] Remove unnnecessary pylint configs from components [m-r]* (#98924) --- homeassistant/components/mailgun/notify.py | 1 - .../components/mobile_app/helpers.py | 2 +- homeassistant/components/mobile_app/notify.py | 1 - homeassistant/components/mpd/media_player.py | 1 - homeassistant/components/mqtt/config_flow.py | 4 +-- .../components/mqtt/light/schema_json.py | 28 +++++++++---------- homeassistant/components/mycroft/notify.py | 2 +- .../components/opencv/image_processing.py | 1 - .../components/owntracks/__init__.py | 1 - .../components/pandora/media_player.py | 2 -- .../components/panel_iframe/__init__.py | 1 - .../auto_repairs/statistics/duplicates.py | 4 --- homeassistant/components/recorder/const.py | 4 +-- .../components/recorder/db_schema.py | 1 - homeassistant/components/recorder/executor.py | 1 - .../components/recorder/history/legacy.py | 4 --- .../components/recorder/history/modern.py | 4 --- .../components/recorder/migration.py | 8 ++---- .../components/recorder/models/time.py | 2 -- homeassistant/components/recorder/queries.py | 8 ------ .../components/recorder/statistics.py | 16 ----------- homeassistant/components/recorder/util.py | 2 +- homeassistant/components/rocketchat/notify.py | 1 - tests/components/mobile_app/conftest.py | 1 - .../owntracks/test_device_tracker.py | 6 ++-- .../auto_repairs/events/test_schema.py | 1 - .../auto_repairs/states/test_schema.py | 1 - .../statistics/test_duplicates.py | 2 -- .../auto_repairs/statistics/test_schema.py | 1 - .../recorder/auto_repairs/test_schema.py | 1 - tests/components/recorder/db_schema_0.py | 1 - tests/components/recorder/db_schema_16.py | 1 - tests/components/recorder/db_schema_18.py | 1 - tests/components/recorder/db_schema_22.py | 1 - tests/components/recorder/db_schema_23.py | 1 - .../db_schema_23_with_newer_columns.py | 1 - tests/components/recorder/db_schema_25.py | 1 - tests/components/recorder/db_schema_28.py | 1 - tests/components/recorder/db_schema_30.py | 1 - tests/components/recorder/db_schema_32.py | 1 - ...est_filters_with_entityfilter_schema_37.py | 1 - tests/components/recorder/test_history.py | 2 -- .../recorder/test_history_db_schema_30.py | 2 -- .../recorder/test_history_db_schema_32.py | 2 -- tests/components/recorder/test_init.py | 2 -- .../recorder/test_migration_from_schema_32.py | 1 - .../recorder/test_purge_v32_schema.py | 1 - tests/components/recorder/test_statistics.py | 2 -- .../recorder/test_statistics_v23_migration.py | 2 -- .../components/recorder/test_v32_migration.py | 1 - .../components/recorder/test_websocket_api.py | 1 - 51 files changed, 23 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index 7bea67b596d..b7104d4a0f1 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -31,7 +31,6 @@ ATTR_IMAGES = "images" DEFAULT_SANDBOX = False -# pylint: disable=no-value-for-parameter PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Required(CONF_RECIPIENT): vol.Email(), vol.Optional(CONF_SENDER): vol.Email()} ) diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 741b0a400cc..e8460b721a2 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -144,7 +144,7 @@ def error_response( def supports_encryption() -> bool: """Test if we support encryption.""" try: - import nacl # noqa: F401 pylint: disable=unused-import, import-outside-toplevel + import nacl # noqa: F401 pylint: disable=import-outside-toplevel return True except OSError: diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 47b997e410c..164f21af15a 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -59,7 +59,6 @@ def push_registrations(hass): return targets -# pylint: disable=invalid-name def log_rate_limits(hass, device_name, resp, level=logging.INFO): """Output rate limit log line at given level.""" if ATTR_PUSH_RATE_LIMITS not in resp: diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 457f9058242..8eab83b5d41 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -85,7 +85,6 @@ class MpdDevice(MediaPlayerEntity): _attr_media_content_type = MediaType.MUSIC - # pylint: disable=no-member def __init__(self, server, port, password, name): """Initialize the MPD device.""" self.server = server diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index bea8a900a83..9f960b0d909 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -563,9 +563,7 @@ async def async_get_broker_settings( ) schema = vol.Schema({cv.string: cv.template}) schema(validated_user_input[CONF_WS_HEADERS]) - except JSON_DECODE_EXCEPTIONS + ( # pylint: disable=wrong-exception-operation - vol.MultipleInvalid, - ): + except JSON_DECODE_EXCEPTIONS + (vol.MultipleInvalid,): errors["base"] = "bad_ws_headers" return False return True diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 8f710eb5ea6..b7787912161 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -307,31 +307,31 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._attr_color_mode = ColorMode.HS self._attr_hs_color = (hue, saturation) elif color_mode == ColorMode.RGB: - r = int(values["color"]["r"]) # pylint: disable=invalid-name - g = int(values["color"]["g"]) # pylint: disable=invalid-name - b = int(values["color"]["b"]) # pylint: disable=invalid-name + r = int(values["color"]["r"]) + g = int(values["color"]["g"]) + b = int(values["color"]["b"]) self._attr_color_mode = ColorMode.RGB self._attr_rgb_color = (r, g, b) elif color_mode == ColorMode.RGBW: - r = int(values["color"]["r"]) # pylint: disable=invalid-name - g = int(values["color"]["g"]) # pylint: disable=invalid-name - b = int(values["color"]["b"]) # pylint: disable=invalid-name - w = int(values["color"]["w"]) # pylint: disable=invalid-name + r = int(values["color"]["r"]) + g = int(values["color"]["g"]) + b = int(values["color"]["b"]) + w = int(values["color"]["w"]) self._attr_color_mode = ColorMode.RGBW self._attr_rgbw_color = (r, g, b, w) elif color_mode == ColorMode.RGBWW: - r = int(values["color"]["r"]) # pylint: disable=invalid-name - g = int(values["color"]["g"]) # pylint: disable=invalid-name - b = int(values["color"]["b"]) # pylint: disable=invalid-name - c = int(values["color"]["c"]) # pylint: disable=invalid-name - w = int(values["color"]["w"]) # pylint: disable=invalid-name + r = int(values["color"]["r"]) + g = int(values["color"]["g"]) + b = int(values["color"]["b"]) + c = int(values["color"]["c"]) + w = int(values["color"]["w"]) self._attr_color_mode = ColorMode.RGBWW self._attr_rgbww_color = (r, g, b, c, w) elif color_mode == ColorMode.WHITE: self._attr_color_mode = ColorMode.WHITE elif color_mode == ColorMode.XY: - x = float(values["color"]["x"]) # pylint: disable=invalid-name - y = float(values["color"]["y"]) # pylint: disable=invalid-name + x = float(values["color"]["x"]) + y = float(values["color"]["y"]) self._attr_color_mode = ColorMode.XY self._attr_xy_color = (x, y) except (KeyError, ValueError): diff --git a/homeassistant/components/mycroft/notify.py b/homeassistant/components/mycroft/notify.py index 172a01017c4..a9dd82caef1 100644 --- a/homeassistant/components/mycroft/notify.py +++ b/homeassistant/components/mycroft/notify.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from mycroftapi import MycroftAPI # pylint: disable=import-error +from mycroftapi import MycroftAPI from homeassistant.components.notify import BaseNotificationService from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py index 41738100cab..89c1a16aa59 100644 --- a/homeassistant/components/opencv/image_processing.py +++ b/homeassistant/components/opencv/image_processing.py @@ -188,7 +188,6 @@ class OpenCVImageProcessor(ImageProcessingEntity): cv_image, scaleFactor=scale, minNeighbors=neighbors, minSize=min_size ) regions = [] - # pylint: disable=invalid-name for x, y, w, h in detections: regions.append((int(x), int(y), int(w), int(h))) total_matches += 1 diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 560493888d4..1b3d67ce7b4 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -278,7 +278,6 @@ class OwnTracksContext: func(**msg) self._pending_msg.clear() - # pylint: disable=method-hidden @callback def async_see(self, **data): """Send a see message to the device tracker.""" diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 43410895010..7b09f40c3f1 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -223,11 +223,9 @@ class PandoraMediaPlayer(MediaPlayerEntity): _LOGGER.warning("On unexpected station list page") self._pianobar.sendcontrol("m") # press enter self._pianobar.sendcontrol("m") # do it again b/c an 'i' got in - # pylint: disable=assignment-from-none response = self.update_playing_status() elif match_idx == 3: _LOGGER.debug("Received new playlist list") - # pylint: disable=assignment-from-none response = self.update_playing_status() else: response = self._pianobar.before.decode("utf-8") diff --git a/homeassistant/components/panel_iframe/__init__.py b/homeassistant/components/panel_iframe/__init__.py index 8313bc4ba25..e33e5078288 100644 --- a/homeassistant/components/panel_iframe/__init__.py +++ b/homeassistant/components/panel_iframe/__init__.py @@ -20,7 +20,6 @@ CONFIG_SCHEMA = vol.Schema( DOMAIN: cv.schema_with_slug_keys( vol.Schema( { - # pylint: disable=no-value-for-parameter vol.Optional(CONF_TITLE): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean, diff --git a/homeassistant/components/recorder/auto_repairs/statistics/duplicates.py b/homeassistant/components/recorder/auto_repairs/statistics/duplicates.py index 8a24dcbf92b..5b7a141bd70 100644 --- a/homeassistant/components/recorder/auto_repairs/statistics/duplicates.py +++ b/homeassistant/components/recorder/auto_repairs/statistics/duplicates.py @@ -37,8 +37,6 @@ def _find_duplicates( literal_column("1").label("is_duplicate"), ) .group_by(table.metadata_id, table.start) - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable .having(func.count() > 1) .subquery() ) @@ -195,8 +193,6 @@ def _find_statistics_meta_duplicates(session: Session) -> list[int]: literal_column("1").label("is_duplicate"), ) .group_by(StatisticsMeta.statistic_id) - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable .having(func.count() > 1) .subquery() ) diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index fc7683db901..724a9589680 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -3,9 +3,7 @@ from enum import StrEnum from homeassistant.const import ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES -from homeassistant.helpers.json import ( # noqa: F401 pylint: disable=unused-import - JSON_DUMP, -) +from homeassistant.helpers.json import JSON_DUMP # noqa: F401 DATA_INSTANCE = "recorder_instance" SQLITE_URL_PREFIX = "sqlite://" diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index c99aadb8caa..508874c54e5 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -63,7 +63,6 @@ from .models import ( # SQLAlchemy Schema -# pylint: disable=invalid-name class Base(DeclarativeBase): """Base class for tables.""" diff --git a/homeassistant/components/recorder/executor.py b/homeassistant/components/recorder/executor.py index 6eea2f651c3..3f677e72fdf 100644 --- a/homeassistant/components/recorder/executor.py +++ b/homeassistant/components/recorder/executor.py @@ -39,7 +39,6 @@ class DBInterruptibleThreadPoolExecutor(InterruptibleThreadPoolExecutor): # When the executor gets lost, the weakref callback will wake up # the worker threads. - # pylint: disable=invalid-name def weakref_cb( # type: ignore[no-untyped-def] _: Any, q=self._work_queue, diff --git a/homeassistant/components/recorder/history/legacy.py b/homeassistant/components/recorder/history/legacy.py index 64ce1aa7d55..191c74ac0d4 100644 --- a/homeassistant/components/recorder/history/legacy.py +++ b/homeassistant/components/recorder/history/legacy.py @@ -565,8 +565,6 @@ def _get_states_for_entities_stmt( most_recent_states_for_entities_by_date := ( select( States.entity_id.label("max_entity_id"), - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable func.max(States.last_updated_ts).label("max_last_updated"), ) .filter( @@ -590,8 +588,6 @@ def _get_states_for_entities_stmt( ( most_recent_states_for_entities_by_date := select( States.entity_id.label("max_entity_id"), - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable func.max(States.last_updated).label("max_last_updated"), ) .filter( diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index 393bcfa3676..68c357c0ed4 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -432,8 +432,6 @@ def _get_last_state_changes_single_stmt(metadata_id: int) -> Select: lastest_state_for_metadata_id := ( select( States.metadata_id.label("max_metadata_id"), - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable func.max(States.last_updated_ts).label("max_last_updated"), ) .filter(States.metadata_id == metadata_id) @@ -537,8 +535,6 @@ def _get_start_time_state_for_entities_stmt( most_recent_states_for_entities_by_date := ( select( States.metadata_id.label("max_metadata_id"), - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable func.max(States.last_updated_ts).label("max_last_updated"), ) .filter( diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 8fe1d0482e9..f07e91ddaea 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -524,7 +524,7 @@ def _update_states_table_with_foreign_key_options( return states_key_constraints = Base.metadata.tables[TABLE_STATES].foreign_key_constraints - old_states_table = Table( # noqa: F841 pylint: disable=unused-variable + old_states_table = Table( # noqa: F841 TABLE_STATES, MetaData(), *(alter["old_fk"] for alter in alters) # type: ignore[arg-type] ) @@ -553,9 +553,7 @@ def _drop_foreign_key_constraints( drops.append(ForeignKeyConstraint((), (), name=foreign_key["name"])) # Bind the ForeignKeyConstraints to the table - old_table = Table( # noqa: F841 pylint: disable=unused-variable - table, MetaData(), *drops - ) + old_table = Table(table, MetaData(), *drops) # noqa: F841 for drop in drops: with session_scope(session=session_maker()) as session: @@ -772,8 +770,6 @@ def _apply_update( # noqa: C901 with session_scope(session=session_maker()) as session: if session.query(Statistics.id).count() and ( last_run_string := session.query( - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable func.max(StatisticsRuns.start) ).scalar() ): diff --git a/homeassistant/components/recorder/models/time.py b/homeassistant/components/recorder/models/time.py index 078a982d5ad..40e4afd18a7 100644 --- a/homeassistant/components/recorder/models/time.py +++ b/homeassistant/components/recorder/models/time.py @@ -7,8 +7,6 @@ from typing import overload import homeassistant.util.dt as dt_util -# pylint: disable=invalid-name - _LOGGER = logging.getLogger(__name__) DB_TIMEZONE = "+00:00" diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index 49f66fdcd68..71a996f0381 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -76,8 +76,6 @@ def find_states_metadata_ids(entity_ids: Iterable[str]) -> StatementLambdaElemen def _state_attrs_exist(attr: int | None) -> Select: """Check if a state attributes id exists in the states table.""" - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable return select(func.min(States.attributes_id)).where(States.attributes_id == attr) @@ -315,8 +313,6 @@ def data_ids_exist_in_events_with_fast_in_distinct( def _event_data_id_exist(data_id: int | None) -> Select: """Check if a event data id exists in the events table.""" - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable return select(func.min(Events.data_id)).where(Events.data_id == data_id) @@ -659,8 +655,6 @@ def find_statistics_runs_to_purge( def find_latest_statistics_runs_run_id() -> StatementLambdaElement: """Find the latest statistics_runs run_id.""" - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable return lambda_stmt(lambda: select(func.max(StatisticsRuns.run_id))) @@ -696,8 +690,6 @@ def find_legacy_detached_states_and_attributes_to_purge( def find_legacy_row() -> StatementLambdaElement: """Check if there are still states in the table with an event_id.""" - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable return lambda_stmt(lambda: select(func.max(States.event_id))) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 9bbf35bb40a..005859b865b 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -103,11 +103,7 @@ QUERY_STATISTICS_SHORT_TERM = ( QUERY_STATISTICS_SUMMARY_MEAN = ( StatisticsShortTerm.metadata_id, func.avg(StatisticsShortTerm.mean), - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable func.min(StatisticsShortTerm.min), - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable func.max(StatisticsShortTerm.max), ) @@ -417,8 +413,6 @@ def compile_missing_statistics(instance: Recorder) -> bool: exception_filter=_filter_unique_constraint_integrity_error(instance), ) as session: # Find the newest statistics run, if any - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable if last_run := session.query(func.max(StatisticsRuns.start)).scalar(): start = max(start, process_timestamp(last_run) + timedelta(minutes=5)) @@ -1078,17 +1072,11 @@ def _get_max_mean_min_statistic_in_sub_period( # Calculate max, mean, min columns = select() if "max" in types: - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable columns = columns.add_columns(func.max(table.max)) if "mean" in types: columns = columns.add_columns(func.avg(table.mean)) - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable columns = columns.add_columns(func.count(table.mean)) if "min" in types: - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable columns = columns.add_columns(func.min(table.min)) stmt = _generate_max_mean_min_statistic_in_sub_period_stmt( columns, start_time, end_time, table, metadata_id @@ -1831,8 +1819,6 @@ def _latest_short_term_statistics_stmt( most_recent_statistic_row := ( select( StatisticsShortTerm.metadata_id, - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable func.max(StatisticsShortTerm.start_ts).label("start_max"), ) .where(StatisticsShortTerm.metadata_id.in_(metadata_ids)) @@ -1895,8 +1881,6 @@ def _generate_statistics_at_time_stmt( ( most_recent_statistic_ids := ( select( - # https://github.com/sqlalchemy/sqlalchemy/issues/9189 - # pylint: disable-next=not-callable func.max(table.start_ts).label("max_start_ts"), table.metadata_id.label("max_metadata_id"), ) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 1c3e07f40fd..d438cbede9f 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -426,7 +426,7 @@ def _datetime_or_none(value: str) -> datetime | None: def build_mysqldb_conv() -> dict: """Build a MySQLDB conv dict that uses cisco8601 to parse datetimes.""" # Late imports since we only call this if they are using mysqldb - # pylint: disable=import-outside-toplevel,import-error + # pylint: disable=import-outside-toplevel from MySQLdb.constants import FIELD_TYPE from MySQLdb.converters import conversions diff --git a/homeassistant/components/rocketchat/notify.py b/homeassistant/components/rocketchat/notify.py index 317364b262a..30bcc6c4515 100644 --- a/homeassistant/components/rocketchat/notify.py +++ b/homeassistant/components/rocketchat/notify.py @@ -23,7 +23,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -# pylint: disable=no-value-for-parameter PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): vol.Url(), diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index 875fccea294..e7c9ad4995a 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -1,7 +1,6 @@ """Tests for mobile_app component.""" from http import HTTPStatus -# pylint: disable=unused-import import pytest from homeassistant.components.mobile_app.const import DOMAIN diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 865c5e52770..41c3b7f058d 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -279,7 +279,7 @@ BAD_MESSAGE = {"_type": "unsupported", "tst": 1} BAD_JSON_PREFIX = "--$this is bad json#--" BAD_JSON_SUFFIX = "** and it ends here ^^" -# pylint: disable=invalid-name, len-as-condition +# pylint: disable=len-as-condition @pytest.fixture @@ -311,8 +311,6 @@ def context(hass, setup_comp): orig_context = owntracks.OwnTracksContext context = None - # pylint: disable=no-value-for-parameter - def store_context(*args): """Store the context.""" nonlocal context @@ -1503,7 +1501,7 @@ async def test_encrypted_payload_no_topic_key(hass: HomeAssistant, setup_comp) - async def test_encrypted_payload_libsodium(hass: HomeAssistant, setup_comp) -> None: """Test sending encrypted message payload.""" try: - import nacl # noqa: F401 pylint: disable=unused-import + import nacl # noqa: F401 except (ImportError, OSError): pytest.skip("PyNaCl/libsodium is not installed") return diff --git a/tests/components/recorder/auto_repairs/events/test_schema.py b/tests/components/recorder/auto_repairs/events/test_schema.py index 1fd5d769c7c..1dc9fb1f560 100644 --- a/tests/components/recorder/auto_repairs/events/test_schema.py +++ b/tests/components/recorder/auto_repairs/events/test_schema.py @@ -1,6 +1,5 @@ """The test repairing events schema.""" -# pylint: disable=invalid-name from unittest.mock import ANY, patch import pytest diff --git a/tests/components/recorder/auto_repairs/states/test_schema.py b/tests/components/recorder/auto_repairs/states/test_schema.py index 9b90489d7c0..f3d733c7c45 100644 --- a/tests/components/recorder/auto_repairs/states/test_schema.py +++ b/tests/components/recorder/auto_repairs/states/test_schema.py @@ -1,6 +1,5 @@ """The test repairing states schema.""" -# pylint: disable=invalid-name from unittest.mock import ANY, patch import pytest diff --git a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py index 98f46cadf03..0d0d9847c5d 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py +++ b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py @@ -1,7 +1,5 @@ """Test removing statistics duplicates.""" from collections.abc import Callable - -# pylint: disable=invalid-name import importlib from pathlib import Path import sys diff --git a/tests/components/recorder/auto_repairs/statistics/test_schema.py b/tests/components/recorder/auto_repairs/statistics/test_schema.py index 10d1ed00b5b..032cd57ce49 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_schema.py +++ b/tests/components/recorder/auto_repairs/statistics/test_schema.py @@ -1,6 +1,5 @@ """The test repairing statistics schema.""" -# pylint: disable=invalid-name from unittest.mock import ANY, patch import pytest diff --git a/tests/components/recorder/auto_repairs/test_schema.py b/tests/components/recorder/auto_repairs/test_schema.py index ad2c33bfb88..83fb64dca6c 100644 --- a/tests/components/recorder/auto_repairs/test_schema.py +++ b/tests/components/recorder/auto_repairs/test_schema.py @@ -1,6 +1,5 @@ """The test validating and repairing schema.""" -# pylint: disable=invalid-name from unittest.mock import patch import pytest diff --git a/tests/components/recorder/db_schema_0.py b/tests/components/recorder/db_schema_0.py index 5f64fbda736..3fbf9cce5fc 100644 --- a/tests/components/recorder/db_schema_0.py +++ b/tests/components/recorder/db_schema_0.py @@ -26,7 +26,6 @@ from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util # SQLAlchemy Schema -# pylint: disable=invalid-name Base = declarative_base() _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/recorder/db_schema_16.py b/tests/components/recorder/db_schema_16.py index 36641ada625..8c491b82c39 100644 --- a/tests/components/recorder/db_schema_16.py +++ b/tests/components/recorder/db_schema_16.py @@ -39,7 +39,6 @@ from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util # SQLAlchemy Schema -# pylint: disable=invalid-name Base = declarative_base() SCHEMA_VERSION = 16 diff --git a/tests/components/recorder/db_schema_18.py b/tests/components/recorder/db_schema_18.py index ba1cfa09cd4..2ce0dfae5f5 100644 --- a/tests/components/recorder/db_schema_18.py +++ b/tests/components/recorder/db_schema_18.py @@ -39,7 +39,6 @@ from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util # SQLAlchemy Schema -# pylint: disable=invalid-name Base = declarative_base() SCHEMA_VERSION = 18 diff --git a/tests/components/recorder/db_schema_22.py b/tests/components/recorder/db_schema_22.py index 3bcef248e0f..329e5d262bc 100644 --- a/tests/components/recorder/db_schema_22.py +++ b/tests/components/recorder/db_schema_22.py @@ -45,7 +45,6 @@ from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util # SQLAlchemy Schema -# pylint: disable=invalid-name Base = declarative_base() SCHEMA_VERSION = 22 diff --git a/tests/components/recorder/db_schema_23.py b/tests/components/recorder/db_schema_23.py index 50839f41906..a89599520c0 100644 --- a/tests/components/recorder/db_schema_23.py +++ b/tests/components/recorder/db_schema_23.py @@ -43,7 +43,6 @@ from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util # SQLAlchemy Schema -# pylint: disable=invalid-name Base = declarative_base() SCHEMA_VERSION = 23 diff --git a/tests/components/recorder/db_schema_23_with_newer_columns.py b/tests/components/recorder/db_schema_23_with_newer_columns.py index 9f73e304e9b..160ddc5761c 100644 --- a/tests/components/recorder/db_schema_23_with_newer_columns.py +++ b/tests/components/recorder/db_schema_23_with_newer_columns.py @@ -51,7 +51,6 @@ from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util # SQLAlchemy Schema -# pylint: disable=invalid-name Base = declarative_base() SCHEMA_VERSION = 23 diff --git a/tests/components/recorder/db_schema_25.py b/tests/components/recorder/db_schema_25.py index 291fdb1231d..24b5b764c65 100644 --- a/tests/components/recorder/db_schema_25.py +++ b/tests/components/recorder/db_schema_25.py @@ -39,7 +39,6 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType import homeassistant.util.dt as dt_util # SQLAlchemy Schema -# pylint: disable=invalid-name Base = declarative_base() SCHEMA_VERSION = 25 diff --git a/tests/components/recorder/db_schema_28.py b/tests/components/recorder/db_schema_28.py index 7e88d6a5548..9df32f1b6c1 100644 --- a/tests/components/recorder/db_schema_28.py +++ b/tests/components/recorder/db_schema_28.py @@ -45,7 +45,6 @@ from homeassistant.core import Context, Event, EventOrigin, State, split_entity_ import homeassistant.util.dt as dt_util # SQLAlchemy Schema -# pylint: disable=invalid-name Base = declarative_base() SCHEMA_VERSION = 28 diff --git a/tests/components/recorder/db_schema_30.py b/tests/components/recorder/db_schema_30.py index 55bee20df56..c1a61159c98 100644 --- a/tests/components/recorder/db_schema_30.py +++ b/tests/components/recorder/db_schema_30.py @@ -55,7 +55,6 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES} # SQLAlchemy Schema -# pylint: disable=invalid-name Base = declarative_base() SCHEMA_VERSION = 30 diff --git a/tests/components/recorder/db_schema_32.py b/tests/components/recorder/db_schema_32.py index 660a2a54d4b..e092de28eca 100644 --- a/tests/components/recorder/db_schema_32.py +++ b/tests/components/recorder/db_schema_32.py @@ -55,7 +55,6 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES} # SQLAlchemy Schema -# pylint: disable=invalid-name Base = declarative_base() SCHEMA_VERSION = 32 diff --git a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py index 9829996818f..9a03c024a83 100644 --- a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py +++ b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py @@ -1,5 +1,4 @@ """The tests for the recorder filter matching the EntityFilter component.""" -# pylint: disable=invalid-name import json from unittest.mock import patch diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index be77f2907d6..21016a65cc2 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -2,8 +2,6 @@ from __future__ import annotations from collections.abc import Callable - -# pylint: disable=invalid-name from copy import copy from datetime import datetime, timedelta import json diff --git a/tests/components/recorder/test_history_db_schema_30.py b/tests/components/recorder/test_history_db_schema_30.py index 30d8de654d7..0ed6061de98 100644 --- a/tests/components/recorder/test_history_db_schema_30.py +++ b/tests/components/recorder/test_history_db_schema_30.py @@ -2,8 +2,6 @@ from __future__ import annotations from collections.abc import Callable - -# pylint: disable=invalid-name from copy import copy from datetime import datetime, timedelta import json diff --git a/tests/components/recorder/test_history_db_schema_32.py b/tests/components/recorder/test_history_db_schema_32.py index 51e4bfdc402..5b721cd4c87 100644 --- a/tests/components/recorder/test_history_db_schema_32.py +++ b/tests/components/recorder/test_history_db_schema_32.py @@ -2,8 +2,6 @@ from __future__ import annotations from collections.abc import Callable - -# pylint: disable=invalid-name from copy import copy from datetime import datetime, timedelta import json diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index bdeecf14c57..e4e5e49eab5 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -594,7 +594,6 @@ def test_setup_without_migration(hass_recorder: Callable[..., HomeAssistant]) -> assert recorder.get_instance(hass).schema_version == SCHEMA_VERSION -# pylint: disable=invalid-name def test_saving_state_include_domains( hass_recorder: Callable[..., HomeAssistant] ) -> None: @@ -955,7 +954,6 @@ async def test_defaults_set(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "history", {}) assert recorder_config is not None - # pylint: disable=unsubscriptable-object assert recorder_config["auto_purge"] assert recorder_config["auto_repack"] assert recorder_config["purge_keep_days"] == 10 diff --git a/tests/components/recorder/test_migration_from_schema_32.py b/tests/components/recorder/test_migration_from_schema_32.py index 2ae32018213..cdf930fde26 100644 --- a/tests/components/recorder/test_migration_from_schema_32.py +++ b/tests/components/recorder/test_migration_from_schema_32.py @@ -1,5 +1,4 @@ """The tests for the recorder filter matching the EntityFilter component.""" -# pylint: disable=invalid-name import importlib import sys from unittest.mock import patch diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index 18c35e8eb81..3b315481f4e 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -1,6 +1,5 @@ """Test data purging.""" -# pylint: disable=invalid-name from datetime import datetime, timedelta import json import sqlite3 diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index de10d9f569b..ab89b82d713 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -1,7 +1,5 @@ """The tests for sensor recorder platform.""" from collections.abc import Callable - -# pylint: disable=invalid-name from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index c3d65e7290f..75a9fed4ad1 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -4,8 +4,6 @@ The v23 schema used for these tests has been slightly modified to add the EventData table to allow the recorder to startup successfully. """ from functools import partial - -# pylint: disable=invalid-name import importlib import json from pathlib import Path diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index dae4fb39c59..98f401e45d8 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -1,5 +1,4 @@ """The tests for recorder platform migrating data from v30.""" -# pylint: disable=invalid-name import asyncio from datetime import timedelta import importlib diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 32d4fabb02b..a9dc23ef5b3 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -1,5 +1,4 @@ """The tests for sensor recorder platform.""" -# pylint: disable=invalid-name import datetime from datetime import timedelta from statistics import fmean From 360d2de1e1591ddf61ebedc357b64738316f3c0f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 00:57:27 +0200 Subject: [PATCH 0798/1151] Use snapshot assertion for Cpuspeed diagnostics test (#98907) --- .../cpuspeed/snapshots/test_diagnostics.ambr | 15 +++++++++++++++ tests/components/cpuspeed/test_diagnostics.py | 15 +++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 tests/components/cpuspeed/snapshots/test_diagnostics.ambr diff --git a/tests/components/cpuspeed/snapshots/test_diagnostics.ambr b/tests/components/cpuspeed/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..8efe36def6d --- /dev/null +++ b/tests/components/cpuspeed/snapshots/test_diagnostics.ambr @@ -0,0 +1,15 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'arch_string_raw': 'aargh', + 'brand_raw': 'Intel Ryzen 7', + 'hz_actual': list([ + 3200000001, + 0, + ]), + 'hz_advertised': list([ + 3600000001, + 0, + ]), + }) +# --- diff --git a/tests/components/cpuspeed/test_diagnostics.py b/tests/components/cpuspeed/test_diagnostics.py index 154f79f2f3e..2c91566216d 100644 --- a/tests/components/cpuspeed/test_diagnostics.py +++ b/tests/components/cpuspeed/test_diagnostics.py @@ -1,6 +1,8 @@ """Tests for the diagnostics data provided by the CPU Speed integration.""" from unittest.mock import patch +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -12,6 +14,7 @@ async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" info = { @@ -25,11 +28,7 @@ async def test_diagnostics( "homeassistant.components.cpuspeed.diagnostics.cpuinfo.get_cpu_info", return_value=info, ): - assert await get_diagnostics_for_config_entry( - hass, hass_client, init_integration - ) == { - "hz_actual": [3200000001, 0], - "arch_string_raw": "aargh", - "brand_raw": "Intel Ryzen 7", - "hz_advertised": [3600000001, 0], - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) From a539d851ccc5ac49e7df5bd6a586893f7ec60156 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 00:57:55 +0200 Subject: [PATCH 0799/1151] Use snapshot assertion for Enphase Envoy diagnostics test (#98910) --- tests/components/enphase_envoy/conftest.py | 1 + .../snapshots/test_diagnostics.ambr | 28 +++++++++++++++++ .../enphase_envoy/test_diagnostics.py | 30 +++++-------------- 3 files changed, 36 insertions(+), 23 deletions(-) create mode 100644 tests/components/enphase_envoy/snapshots/test_diagnostics.ambr diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index 355c247b182..41cbb239129 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -24,6 +24,7 @@ def config_entry_fixture(hass: HomeAssistant, config, serial_number): """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, + entry_id="45a36e55aaddb2007c5f6602e0c38e72", title=f"Envoy {serial_number}" if serial_number else "Envoy", unique_id=serial_number, data=config, diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..098fc4ee37e --- /dev/null +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -0,0 +1,28 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'varies_by': 'firmware_version', + }), + 'entry': dict({ + 'data': dict({ + 'host': '1.1.1.1', + 'name': '**REDACTED**', + 'password': '**REDACTED**', + 'token': '**REDACTED**', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'enphase_envoy', + 'entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/enphase_envoy/test_diagnostics.py b/tests/components/enphase_envoy/test_diagnostics.py index fb1a54dc522..c3659b2a9bb 100644 --- a/tests/components/enphase_envoy/test_diagnostics.py +++ b/tests/components/enphase_envoy/test_diagnostics.py @@ -1,5 +1,6 @@ """Test Enphase Envoy diagnostics.""" -from homeassistant.components.diagnostics import REDACTED +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -11,27 +12,10 @@ async def test_entry_diagnostics( config_entry, hass_client: ClientSessionGenerator, setup_enphase_envoy, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "entry_id": config_entry.entry_id, - "version": 1, - "domain": "enphase_envoy", - "title": REDACTED, - "data": { - "host": "1.1.1.1", - "name": REDACTED, - "username": REDACTED, - "password": REDACTED, - "token": REDACTED, - }, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - }, - "data": {"varies_by": "firmware_version"}, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From f1fb28aad55eebf1a0774cee47bb5f634435f2b5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 01:01:58 +0200 Subject: [PATCH 0800/1151] Use snapshot assertion for ESPHome diagnostics test (#98913) --- tests/components/esphome/conftest.py | 1 + .../esphome/snapshots/test_diagnostics.ambr | 26 +++++++++++++++++++ tests/components/esphome/test_diagnostics.py | 18 +++---------- 3 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 tests/components/esphome/snapshots/test_diagnostics.ambr diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index f0fe2d9ccb0..6b06545a06b 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -62,6 +62,7 @@ def mock_config_entry(hass) -> MockConfigEntry: """Return the default mocked config entry.""" config_entry = MockConfigEntry( title="ESPHome Device", + entry_id="08d821dc059cf4f645cb024d32c8e708", domain=DOMAIN, data={ CONF_HOST: "192.168.1.2", diff --git a/tests/components/esphome/snapshots/test_diagnostics.ambr b/tests/components/esphome/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..d8de8f06bc6 --- /dev/null +++ b/tests/components/esphome/snapshots/test_diagnostics.ambr @@ -0,0 +1,26 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'config': dict({ + 'data': dict({ + 'device_name': 'test', + 'host': '192.168.1.2', + 'noise_psk': '**REDACTED**', + 'password': '**REDACTED**', + 'port': 6053, + }), + 'disabled_by': None, + 'domain': 'esphome', + 'entry_id': '08d821dc059cf4f645cb024d32c8e708', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'ESPHome Device', + 'unique_id': '11:22:33:44:55:aa', + 'version': 1, + }), + 'dashboard': 'mock-slug', + }) +# --- diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 025c5bcaae8..6000b270d87 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -1,12 +1,8 @@ """Tests for the diagnostics data provided by the ESPHome integration.""" +from syrupy import SnapshotAssertion - -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 -from . import DASHBOARD_SLUG - from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -18,17 +14,9 @@ async def test_diagnostics( init_integration: MockConfigEntry, enable_bluetooth: None, mock_dashboard, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics for config entry.""" result = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) - assert isinstance(result, dict) - assert result["config"]["data"] == { - CONF_DEVICE_NAME: "test", - CONF_HOST: "192.168.1.2", - CONF_PORT: 6053, - CONF_PASSWORD: "**REDACTED**", - CONF_NOISE_PSK: "**REDACTED**", - } - assert result["config"]["unique_id"] == "11:22:33:44:55:aa" - assert result["dashboard"] == DASHBOARD_SLUG + assert result == snapshot From a1307e117dd4ff466ed1e19068021db29bf3d0bc Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 24 Aug 2023 01:02:52 +0200 Subject: [PATCH 0801/1151] Add additional debug logging for imap (#98877) --- homeassistant/components/imap/coordinator.py | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index b9b541997a3..72be5e9bcf0 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -65,14 +65,28 @@ async def connect_to_server(data: Mapping[str, Any]) -> IMAP4_SSL: else: ssl_context = create_no_verify_ssl_context() client = IMAP4_SSL(data[CONF_SERVER], data[CONF_PORT], ssl_context=ssl_context) - + _LOGGER.debug( + "Wait for hello message from server %s on port %s, verify_ssl: %s", + data[CONF_SERVER], + data[CONF_PORT], + data.get(CONF_VERIFY_SSL, True), + ) await client.wait_hello_from_server() - if client.protocol.state == NONAUTH: + _LOGGER.debug( + "Authenticating with %s on server %s", + data[CONF_USERNAME], + data[CONF_SERVER], + ) await client.login(data[CONF_USERNAME], data[CONF_PASSWORD]) if client.protocol.state not in {AUTH, SELECTED}: raise InvalidAuth("Invalid username or password") if client.protocol.state == AUTH: + _LOGGER.debug( + "Selecting mail folder %s on server %s", + data[CONF_FOLDER], + data[CONF_SERVER], + ) await client.select(data[CONF_FOLDER]) if client.protocol.state != SELECTED: raise InvalidFolder(f"Folder {data[CONF_FOLDER]} is invalid") @@ -312,6 +326,9 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): self, hass: HomeAssistant, imap_client: IMAP4_SSL, entry: ConfigEntry ) -> None: """Initiate imap client.""" + _LOGGER.debug( + "Connected to server %s using IMAP polling", entry.data[CONF_SERVER] + ) super().__init__(hass, imap_client, entry, timedelta(seconds=10)) async def _async_update_data(self) -> int | None: @@ -354,6 +371,7 @@ class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): self, hass: HomeAssistant, imap_client: IMAP4_SSL, entry: ConfigEntry ) -> None: """Initiate imap client.""" + _LOGGER.debug("Connected to server %s using IMAP push", entry.data[CONF_SERVER]) super().__init__(hass, imap_client, entry, None) self._push_wait_task: asyncio.Task[None] | None = None From faa4489f4c817d64cbbe51405b905f36a6e85288 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 01:18:49 +0200 Subject: [PATCH 0802/1151] Use snapshot assertion for Co2signal diagnostics test (#98905) --- .../co2signal/snapshots/test_diagnostics.ambr | 33 +++++++++++++++++++ .../components/co2signal/test_diagnostics.py | 19 +++++------ 2 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 tests/components/co2signal/snapshots/test_diagnostics.ambr diff --git a/tests/components/co2signal/snapshots/test_diagnostics.ambr b/tests/components/co2signal/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..ffb35edfbbb --- /dev/null +++ b/tests/components/co2signal/snapshots/test_diagnostics.ambr @@ -0,0 +1,33 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config_entry': dict({ + 'data': dict({ + 'api_key': '**REDACTED**', + 'location': '', + }), + 'disabled_by': None, + 'domain': 'co2signal', + 'entry_id': '904a74160aa6f335526706bee85dfb83', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': None, + 'version': 1, + }), + 'data': dict({ + 'countryCode': 'FR', + 'data': dict({ + 'carbonIntensity': 45.98623190095805, + 'fossilFuelPercentage': 5.461182741937103, + }), + 'status': 'ok', + 'units': dict({ + 'carbonIntensity': 'gCO2eq/kWh', + }), + }), + }) +# --- diff --git a/tests/components/co2signal/test_diagnostics.py b/tests/components/co2signal/test_diagnostics.py index c73409fa59b..ed73cb960b5 100644 --- a/tests/components/co2signal/test_diagnostics.py +++ b/tests/components/co2signal/test_diagnostics.py @@ -1,8 +1,9 @@ """Test the CO2Signal diagnostics.""" from unittest.mock import patch +from syrupy import SnapshotAssertion + from homeassistant.components.co2signal import DOMAIN -from homeassistant.components.diagnostics import REDACTED from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -15,11 +16,15 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_API_KEY: "api_key", "location": ""} + domain=DOMAIN, + data={CONF_API_KEY: "api_key", "location": ""}, + entry_id="904a74160aa6f335526706bee85dfb83", ) config_entry.add_to_hass(hass) with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD): @@ -27,10 +32,4 @@ async def test_entry_diagnostics( result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - config_entry_dict = config_entry.as_dict() - config_entry_dict["data"][CONF_API_KEY] = REDACTED - - assert result == { - "config_entry": config_entry_dict, - "data": VALID_PAYLOAD, - } + assert result == snapshot From c39f6b3bea37139b842b47b8750b0cde80cbc64e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 01:23:31 +0200 Subject: [PATCH 0803/1151] Use snapshot assertion for Coinbase diagnostics test (#98906) --- tests/components/coinbase/common.py | 1 + tests/components/coinbase/const.py | 41 ----------- .../coinbase/snapshots/test_diagnostics.ambr | 70 +++++++++++++++++++ tests/components/coinbase/test_diagnostics.py | 15 ++-- 4 files changed, 77 insertions(+), 50 deletions(-) create mode 100644 tests/components/coinbase/snapshots/test_diagnostics.ambr diff --git a/tests/components/coinbase/common.py b/tests/components/coinbase/common.py index 4866039f310..6ab33f3bc7c 100644 --- a/tests/components/coinbase/common.py +++ b/tests/components/coinbase/common.py @@ -68,6 +68,7 @@ async def init_mock_coinbase(hass, currencies=None, rates=None): """Init Coinbase integration for testing.""" config_entry = MockConfigEntry( domain=DOMAIN, + entry_id="080272b77a4f80c41b94d7cdc86fd826", unique_id=None, title="Test User", data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}, diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py index 4db6abca37d..2b437e15478 100644 --- a/tests/components/coinbase/const.py +++ b/tests/components/coinbase/const.py @@ -1,6 +1,5 @@ """Constants for testing the Coinbase integration.""" -from homeassistant.components.diagnostics import REDACTED GOOD_CURRENCY = "BTC" GOOD_CURRENCY_2 = "USD" @@ -36,43 +35,3 @@ MOCK_ACCOUNTS_RESPONSE = [ "type": "fiat", }, ] - -MOCK_ACCOUNTS_RESPONSE_REDACTED = [ - { - "balance": {"amount": REDACTED, "currency": GOOD_CURRENCY}, - "currency": GOOD_CURRENCY, - "id": REDACTED, - "name": "BTC Wallet", - "native_balance": {"amount": REDACTED, "currency": GOOD_CURRENCY_2}, - "type": "wallet", - }, - { - "balance": {"amount": REDACTED, "currency": GOOD_CURRENCY}, - "currency": GOOD_CURRENCY, - "id": REDACTED, - "name": "BTC Vault", - "native_balance": {"amount": REDACTED, "currency": GOOD_CURRENCY_2}, - "type": "vault", - }, - { - "balance": {"amount": REDACTED, "currency": GOOD_CURRENCY_2}, - "currency": "USD", - "id": REDACTED, - "name": "USD Wallet", - "native_balance": {"amount": REDACTED, "currency": GOOD_CURRENCY_2}, - "type": "fiat", - }, -] - -MOCK_ENTRY_REDACTED = { - "version": 1, - "domain": "coinbase", - "title": REDACTED, - "data": {"api_token": REDACTED, "api_key": REDACTED}, - "options": {"account_balance_currencies": [], "exchange_rate_currencies": []}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": None, - "disabled_by": None, -} diff --git a/tests/components/coinbase/snapshots/test_diagnostics.ambr b/tests/components/coinbase/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..c214330d5f9 --- /dev/null +++ b/tests/components/coinbase/snapshots/test_diagnostics.ambr @@ -0,0 +1,70 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'accounts': list([ + dict({ + 'balance': dict({ + 'amount': '**REDACTED**', + 'currency': 'BTC', + }), + 'currency': 'BTC', + 'id': '**REDACTED**', + 'name': 'BTC Wallet', + 'native_balance': dict({ + 'amount': '**REDACTED**', + 'currency': 'USD', + }), + 'type': 'wallet', + }), + dict({ + 'balance': dict({ + 'amount': '**REDACTED**', + 'currency': 'BTC', + }), + 'currency': 'BTC', + 'id': '**REDACTED**', + 'name': 'BTC Vault', + 'native_balance': dict({ + 'amount': '**REDACTED**', + 'currency': 'USD', + }), + 'type': 'vault', + }), + dict({ + 'balance': dict({ + 'amount': '**REDACTED**', + 'currency': 'USD', + }), + 'currency': 'USD', + 'id': '**REDACTED**', + 'name': 'USD Wallet', + 'native_balance': dict({ + 'amount': '**REDACTED**', + 'currency': 'USD', + }), + 'type': 'fiat', + }), + ]), + 'entry': dict({ + 'data': dict({ + 'api_key': '**REDACTED**', + 'api_token': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'coinbase', + 'entry_id': '080272b77a4f80c41b94d7cdc86fd826', + 'options': dict({ + 'account_balance_currencies': list([ + ]), + 'exchange_rate_currencies': list([ + ]), + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': None, + 'version': 1, + }), + }) +# --- diff --git a/tests/components/coinbase/test_diagnostics.py b/tests/components/coinbase/test_diagnostics.py index 73978790441..897722b32b4 100644 --- a/tests/components/coinbase/test_diagnostics.py +++ b/tests/components/coinbase/test_diagnostics.py @@ -1,6 +1,8 @@ """Test the Coinbase diagnostics.""" from unittest.mock import patch +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from .common import ( @@ -9,14 +11,15 @@ from .common import ( mock_get_exchange_rates, mocked_get_accounts, ) -from .const import MOCK_ACCOUNTS_RESPONSE_REDACTED, MOCK_ENTRY_REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test we handle a and redact a diagnostics request.""" @@ -34,10 +37,4 @@ async def test_entry_diagnostics( result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - # Remove the ID to match the constant - result["entry"].pop("entry_id") - - assert result == { - "entry": MOCK_ENTRY_REDACTED, - "accounts": MOCK_ACCOUNTS_RESPONSE_REDACTED, - } + assert result == snapshot From b51c0f6ddc0b2c939620e8a95714d4d4f3877754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 24 Aug 2023 02:25:32 +0300 Subject: [PATCH 0804/1151] Remove unnnecessary pylint configs from components [s-z]* (#98925) --- homeassistant/components/saj/sensor.py | 4 ++-- homeassistant/components/sendgrid/notify.py | 1 - homeassistant/components/sia/hub.py | 14 ++++++-------- homeassistant/components/smarty/__init__.py | 2 +- homeassistant/components/smarty/binary_sensor.py | 2 +- homeassistant/components/smarty/fan.py | 2 +- homeassistant/components/smarty/sensor.py | 2 +- homeassistant/components/smhi/weather.py | 4 +--- homeassistant/components/sms/__init__.py | 2 +- homeassistant/components/sms/config_flow.py | 2 +- homeassistant/components/sms/gateway.py | 4 ++-- homeassistant/components/sms/notify.py | 2 +- homeassistant/components/smtp/notify.py | 1 - homeassistant/components/ssdp/__init__.py | 10 ++++------ homeassistant/components/system_log/__init__.py | 2 +- homeassistant/components/tank_utility/sensor.py | 9 +++------ .../components/template/template_entity.py | 2 +- homeassistant/components/template/trigger.py | 2 -- .../components/tensorflow/image_processing.py | 4 ++-- homeassistant/components/tuya/__init__.py | 4 ---- homeassistant/components/upnp/__init__.py | 2 +- homeassistant/components/watson_tts/tts.py | 6 ++---- homeassistant/components/xiaomi_aqara/__init__.py | 6 ++---- homeassistant/components/xiaomi_miio/light.py | 15 ++++++--------- homeassistant/components/xmpp/notify.py | 4 +--- .../zha/core/cluster_handlers/general.py | 6 ++---- .../core/cluster_handlers/manufacturerspecific.py | 4 ++-- .../zha/core/cluster_handlers/measurement.py | 4 +--- homeassistant/components/zha/core/const.py | 2 +- homeassistant/components/zha/core/discovery.py | 4 ++-- homeassistant/components/zha/core/endpoint.py | 2 +- homeassistant/components/zha/device_tracker.py | 2 +- homeassistant/components/zha/light.py | 4 ++-- homeassistant/components/zha/sensor.py | 2 +- tests/components/sensor/test_recorder.py | 2 -- tests/components/smartthings/test_init.py | 2 +- tests/components/time_date/test_sensor.py | 1 - tests/components/water_heater/test_init.py | 4 ---- tests/components/websocket_api/test_auth.py | 1 - 39 files changed, 55 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 58dd4436861..12a5ae99570 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -141,7 +141,7 @@ async def async_setup_platform( @callback def stop_update_interval(event): """Properly cancel the scheduled update.""" - remove_interval_update() # pylint: disable=not-callable + remove_interval_update() hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_update_interval) async_at_start(hass, start_update_interval) @@ -171,7 +171,7 @@ def async_track_time_interval_backoff( def remove_listener() -> None: """Remove interval listener.""" if remove: - remove() # pylint: disable=not-callable + remove() return remove_listener diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index 4c47f497b36..25d00fdd3b8 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -29,7 +29,6 @@ CONF_SENDER_NAME = "sender_name" DEFAULT_SENDER_NAME = "Home Assistant" -# pylint: disable=no-value-for-parameter PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index 64ca3832ce0..859841d3bea 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -110,14 +110,12 @@ class SIAHub: self.sia_client.accounts = self.sia_accounts return # the new client class method creates a subclass based on protocol, hence the type ignore - self.sia_client = ( - SIAClient( # pylint: disable=abstract-class-instantiated # type: ignore - host="", - port=self._port, - accounts=self.sia_accounts, - function=self.async_create_and_fire_event, - protocol=CommunicationsProtocol(self._protocol), - ) + self.sia_client = SIAClient( + host="", + port=self._port, + accounts=self.sia_accounts, + function=self.async_create_and_fire_event, + protocol=CommunicationsProtocol(self._protocol), ) def _load_options(self) -> None: diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py index 036fb6e1e90..e3cf1dcf287 100644 --- a/homeassistant/components/smarty/__init__.py +++ b/homeassistant/components/smarty/__init__.py @@ -3,7 +3,7 @@ from datetime import timedelta import ipaddress import logging -from pysmarty import Smarty # pylint: disable=import-error +from pysmarty import Smarty import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_NAME, Platform diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index baa25115186..d9d757a71b5 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from pysmarty import Smarty # pylint: disable=import-error +from pysmarty import Smarty from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index cf7db560c15..cf4b49e6105 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -5,7 +5,7 @@ import logging import math from typing import Any -from pysmarty import Smarty # pylint: disable=import-error +from pysmarty import Smarty from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index df99529b1f4..57d681594cf 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations import datetime as dt import logging -from pysmarty import Smarty # pylint: disable=import-error +from pysmarty import Smarty from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import UnitOfTemperature diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index c8ff9127ba8..05683f19b11 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -192,9 +192,7 @@ class SmhiWeather(WeatherEntity): async def retry_update(self, _: datetime) -> None: """Retry refresh weather forecast.""" - await self.async_update( # pylint: disable=unexpected-keyword-arg - no_throttle=True - ) + await self.async_update(no_throttle=True) @property def forecast(self) -> list[Forecast] | None: diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 5b4ecc3a141..824a95e36b1 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -3,7 +3,7 @@ import asyncio from datetime import timedelta import logging -import gammu # pylint: disable=import-error +import gammu import voluptuous as vol from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/sms/config_flow.py b/homeassistant/components/sms/config_flow.py index 9128b6187c1..df3530764cb 100644 --- a/homeassistant/components/sms/config_flow.py +++ b/homeassistant/components/sms/config_flow.py @@ -1,7 +1,7 @@ """Config flow for SMS integration.""" import logging -import gammu # pylint: disable=import-error +import gammu import voluptuous as vol from homeassistant import config_entries, core, exceptions diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 36ada5421e0..578b2191bd2 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -1,8 +1,8 @@ """The sms gateway to interact with a GSM modem.""" import logging -import gammu # pylint: disable=import-error -from gammu.asyncworker import GammuAsyncWorker # pylint: disable=import-error +import gammu +from gammu.asyncworker import GammuAsyncWorker from homeassistant.core import callback diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index 9d94472b1b8..21d3ab2beb5 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -import gammu # pylint: disable=import-error +import gammu from homeassistant.components.notify import ATTR_DATA, BaseNotificationService from homeassistant.const import CONF_TARGET diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 28c3121a172..7037c239db3 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -56,7 +56,6 @@ DEFAULT_ENCRYPTION = "starttls" ENCRYPTION_OPTIONS = ["tls", "starttls", "none"] -# pylint: disable=no-value-for-parameter PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [vol.Email()]), diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 4bc9bb24835..3be5475a71a 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -153,7 +153,7 @@ async def async_register_callback( @bind_hass -async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name +async def async_get_discovery_info_by_udn_st( hass: HomeAssistant, udn: str, st: str ) -> SsdpServiceInfo | None: """Fetch the discovery info cache.""" @@ -162,7 +162,7 @@ async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name @bind_hass -async def async_get_discovery_info_by_st( # pylint: disable=invalid-name +async def async_get_discovery_info_by_st( hass: HomeAssistant, st: str ) -> list[SsdpServiceInfo]: """Fetch all the entries matching the st.""" @@ -575,7 +575,7 @@ class Scanner: info_desc = await self._async_get_description_dict(location) return discovery_info_from_headers_and_description(headers, info_desc) - async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name + async def async_get_discovery_info_by_udn_st( self, udn: str, st: str ) -> SsdpServiceInfo | None: """Return discovery_info for a udn and st.""" @@ -583,9 +583,7 @@ class Scanner: return await self._async_headers_to_discovery_info(headers) return None - async def async_get_discovery_info_by_st( # pylint: disable=invalid-name - self, st: str - ) -> list[SsdpServiceInfo]: + async def async_get_discovery_info_by_st(self, st: str) -> list[SsdpServiceInfo]: """Return matching discovery_infos for a st.""" return [ await self._async_headers_to_discovery_info(headers) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index cba8082d23c..ab271ec676c 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -141,7 +141,7 @@ class LogEntry: self.root_cause = None if record.exc_info: self.exception = "".join(traceback.format_exception(*record.exc_info)) - _, _, tb = record.exc_info # pylint: disable=invalid-name + _, _, tb = record.exc_info # Last line of traceback contains the root cause of the exception if traceback.extract_tb(tb): self.root_cause = str(traceback.extract_tb(tb)[-1]) diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index f902abc22e0..0aecbb0f405 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -56,10 +56,7 @@ def setup_platform( try: token = auth.get_token(email, password) except requests.exceptions.HTTPError as http_error: - if ( - http_error.response.status_code - == requests.codes.unauthorized # pylint: disable=no-member - ): + if http_error.response.status_code == requests.codes.unauthorized: _LOGGER.error("Invalid credentials") return @@ -121,8 +118,8 @@ class TankUtilitySensor(SensorEntity): data = tank_monitor.get_device_data(self._token, self.device) except requests.exceptions.HTTPError as http_error: if http_error.response.status_code in ( - requests.codes.unauthorized, # pylint: disable=no-member - requests.codes.bad_request, # pylint: disable=no-member + requests.codes.unauthorized, + requests.codes.bad_request, ): _LOGGER.info("Getting new token") self._token = auth.get_token(self._email, self._password, force=True) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 0d6d5a99748..fe1a53e6510 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -15,7 +15,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import Template -from homeassistant.helpers.template_entity import ( # noqa: F401 pylint: disable=unused-import +from homeassistant.helpers.template_entity import ( # noqa: F401 TEMPLATE_ENTITY_BASE_SCHEMA, TemplateEntity, make_template_entity_base_schema, diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 113da3aa3ee..327c988106e 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -80,7 +80,6 @@ async def async_attach_trigger( return if delay_cancel: - # pylint: disable-next=not-callable delay_cancel() delay_cancel = None @@ -156,7 +155,6 @@ async def async_attach_trigger( """Remove state listeners async.""" unsub() if delay_cancel: - # pylint: disable-next=not-callable delay_cancel() return async_remove diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index a149ea92371..e2fce4b94c2 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -9,7 +9,7 @@ import time import numpy as np from PIL import Image, ImageDraw, UnidentifiedImageError -import tensorflow as tf # pylint: disable=import-error +import tensorflow as tf import voluptuous as vol from homeassistant.components.image_processing import ( @@ -148,7 +148,7 @@ def setup_platform( try: # Display warning that PIL will be used if no OpenCV is found. - import cv2 # noqa: F401 pylint: disable=unused-import, import-outside-toplevel + import cv2 # noqa: F401 pylint: disable=import-outside-toplevel except ImportError: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 2b28a7e4e5e..509e7e17013 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -234,10 +234,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class DeviceListener(TuyaDeviceListener): """Device Update Listener.""" - # pylint: disable=arguments-differ - # Library incorrectly defines methods as 'classmethod' - # https://github.com/tuya/tuya-iot-python-sdk/pull/48 - def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index bb505c08ad0..326ff5d7651 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) udn = entry.data[CONFIG_ENTRY_UDN] - st = entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name + st = entry.data[CONFIG_ENTRY_ST] usn = f"{udn}::{st}" # Register device discovered-callback. diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 7af6c1ce97b..7adb1b1582f 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -1,10 +1,8 @@ """Support for IBM Watson TTS integration.""" import logging -from ibm_cloud_sdk_core.authenticators import ( # pylint: disable=import-error - IAMAuthenticator, -) -from ibm_watson import TextToSpeechV1 # pylint: disable=import-error +from ibm_cloud_sdk_core.authenticators import IAMAuthenticator +from ibm_watson import TextToSpeechV1 import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 8f5ac19ee68..f7bc1910521 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -267,10 +267,8 @@ class XiaomiDevice(Entity): self.parse_data(device["data"], device["raw_data"]) self.parse_voltage(device["data"]) - if hasattr(self, "_data_key") and self._data_key: # pylint: disable=no-member - self._unique_id = ( - f"{self._data_key}{self._sid}" # pylint: disable=no-member - ) + if hasattr(self, "_data_key") and self._data_key: + self._unique_id = f"{self._data_key}{self._sid}" else: self._unique_id = f"{self._type}{self._sid}" diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 0a4ed1527c0..1fc032b5c36 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -449,14 +449,11 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): if ATTR_BRIGHTNESS in kwargs and ATTR_COLOR_TEMP in kwargs: _LOGGER.debug( - ( - "Setting brightness and color temperature: " - "%s %s%%, %s mireds, %s%% cct" - ), + "Setting brightness and color temperature: %s %s%%, %s mireds, %s%% cct", brightness, - percent_brightness, # pylint: disable=used-before-assignment + percent_brightness, color_temp, - percent_color_temp, # pylint: disable=used-before-assignment + percent_color_temp, ) result = await self._try_command( @@ -832,8 +829,8 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): _LOGGER.debug( "Setting brightness and color: %s %s%%, %s", brightness, - percent_brightness, # pylint: disable=used-before-assignment - rgb, # pylint: disable=used-before-assignment + percent_brightness, + rgb, ) result = await self._try_command( @@ -856,7 +853,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): brightness, percent_brightness, color_temp, - percent_color_temp, # pylint: disable=used-before-assignment + percent_color_temp, ) result = await self._try_command( diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 2f5bad116c4..0150e761838 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -198,9 +198,7 @@ async def async_send_message( # noqa: C901 _LOGGER.info("Sending file to %s", recipient) message = self.Message(sto=recipient, stype="chat") message["body"] = url - message["oob"][ # pylint: disable=invalid-sequence-index - "url" - ] = url + message["oob"]["url"] = url try: message.send() except (IqError, IqTimeout, XMPPError) as ex: diff --git a/homeassistant/components/zha/core/cluster_handlers/general.py b/homeassistant/components/zha/core/cluster_handlers/general.py index 622c9e4340e..bd66b0f6c63 100644 --- a/homeassistant/components/zha/core/cluster_handlers/general.py +++ b/homeassistant/components/zha/core/cluster_handlers/general.py @@ -164,9 +164,7 @@ class BasicClusterHandler(ClusterHandler): """Initialize Basic cluster handler.""" super().__init__(cluster, endpoint) if is_hue_motion_sensor(self) and self.cluster.endpoint.endpoint_id == 2: - self.ZCL_INIT_ATTRS = ( # pylint: disable=invalid-name - self.ZCL_INIT_ATTRS.copy() - ) + self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() self.ZCL_INIT_ATTRS["trigger_indicator"] = True elif ( self.cluster.endpoint.manufacturer == "TexasInstruments" @@ -373,7 +371,7 @@ class OnOffClusterHandler(ClusterHandler): except KeyError: return - self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() # pylint: disable=invalid-name + self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() self.ZCL_INIT_ATTRS["backlight_mode"] = True self.ZCL_INIT_ATTRS["power_on_state"] = True diff --git a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py b/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py index e46031cce14..450a1aeec97 100644 --- a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py +++ b/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py @@ -92,7 +92,7 @@ class TuyaClusterHandler(ClusterHandler): "_TZE200_k6jhsr0q", "_TZE200_9mahtqtg", ): - self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name + self.ZCL_INIT_ATTRS = { "backlight_mode": True, "power_on_state": True, } @@ -109,7 +109,7 @@ class OppleRemote(ClusterHandler): """Initialize Opple cluster handler.""" super().__init__(cluster, endpoint) if self.cluster.endpoint.model == "lumi.motion.ac02": - self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name + self.ZCL_INIT_ATTRS = { "detection_interval": True, "motion_sensitivity": True, "trigger_indicator": True, diff --git a/homeassistant/components/zha/core/cluster_handlers/measurement.py b/homeassistant/components/zha/core/cluster_handlers/measurement.py index beeb6296e32..bd483920842 100644 --- a/homeassistant/components/zha/core/cluster_handlers/measurement.py +++ b/homeassistant/components/zha/core/cluster_handlers/measurement.py @@ -67,9 +67,7 @@ class OccupancySensing(ClusterHandler): """Initialize Occupancy cluster handler.""" super().__init__(cluster, endpoint) if is_hue_motion_sensor(self): - self.ZCL_INIT_ATTRS = ( # pylint: disable=invalid-name - self.ZCL_INIT_ATTRS.copy() - ) + self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() self.ZCL_INIT_ATTRS["sensitivity"] = True diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c90c78243d1..7aab6112ab0 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -7,7 +7,7 @@ import logging import bellows.zigbee.application import voluptuous as vol import zigpy.application -from zigpy.config import CONF_DEVICE_PATH # noqa: F401 # pylint: disable=unused-import +from zigpy.config import CONF_DEVICE_PATH # noqa: F401 import zigpy.types as t import zigpy_deconz.zigbee.application import zigpy_xbee.zigbee.application diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 0ca1c136271..92b68bdb159 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType -from .. import ( # noqa: F401 pylint: disable=unused-import, +from .. import ( # noqa: F401 alarm_control_panel, binary_sensor, button, @@ -35,7 +35,7 @@ from .. import ( # noqa: F401 pylint: disable=unused-import, from . import const as zha_const, registries as zha_regs # importing cluster handlers updates registries -from .cluster_handlers import ( # noqa: F401 pylint: disable=unused-import, +from .cluster_handlers import ( # noqa: F401 ClusterHandler, closures, general, diff --git a/homeassistant/components/zha/core/endpoint.py b/homeassistant/components/zha/core/endpoint.py index 53a3fb883ef..bdef5ac46af 100644 --- a/homeassistant/components/zha/core/endpoint.py +++ b/homeassistant/components/zha/core/endpoint.py @@ -27,7 +27,7 @@ ATTR_IN_CLUSTERS: Final[str] = "input_clusters" ATTR_OUT_CLUSTERS: Final[str] = "output_clusters" _LOGGER = logging.getLogger(__name__) -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) class Endpoint: diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 04c74a44dbe..bda346624dd 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -111,7 +111,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): return self._battery_level @property # type: ignore[misc] - def device_info( # pylint: disable=overridden-final-method + def device_info( self, ) -> DeviceInfo: """Return device info.""" diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 73955614c07..6331b192859 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1112,13 +1112,13 @@ class LightGroup(BaseLight, ZhaGroupEntity): super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) group = self.zha_device.gateway.get_group(self._group_id) - self._GROUP_SUPPORTS_EXECUTE_IF_OFF = True # pylint: disable=invalid-name + self._GROUP_SUPPORTS_EXECUTE_IF_OFF = True for member in group.members: # Ensure we do not send group commands that violate the minimum transition # time of any members. if member.device.manufacturer in DEFAULT_MIN_TRANSITION_MANUFACTURERS: - self._DEFAULT_MIN_TRANSITION_TIME = ( # pylint: disable=invalid-name + self._DEFAULT_MIN_TRANSITION_TIME = ( MinTransitionLight._DEFAULT_MIN_TRANSITION_TIME ) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index c514e02ec57..535733230b9 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -228,7 +228,7 @@ class Battery(Sensor): return cls(unique_id, zha_device, cluster_handlers, **kwargs) @staticmethod - def formatter(value: int) -> int | None: # pylint: disable=arguments-differ + def formatter(value: int) -> int | None: """Return the state of the entity.""" # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ if not isinstance(value, numbers.Number) or value == -1: diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 65b0a0b9485..1c0200e1b53 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1,7 +1,5 @@ """The tests for sensor recorder platform.""" from collections.abc import Callable - -# pylint: disable=invalid-name from datetime import datetime, timedelta import math from statistics import mean diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index f08d1b54985..0630ffd8392 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -422,7 +422,7 @@ async def test_broker_regenerates_token(hass: HomeAssistant, config_entry) -> No broker.connect() assert stored_action - await stored_action(None) # pylint:disable=not-callable + await stored_action(None) assert token.refresh.call_count == 1 assert config_entry.data[CONF_REFRESH_TOKEN] == token.refresh_token diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index 42f1e260280..96c7edf422b 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -102,7 +102,6 @@ async def test_states_non_default_timezone(hass: HomeAssistant) -> None: assert device.state == "2017-05-17T20:54:00" -# pylint: disable=no-member async def test_timezone_intervals(hass: HomeAssistant) -> None: """Test date sensor behavior in a timezone besides UTC.""" hass.config.set_time_zone("America/New_York") diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py index 66276f0bc88..bc996ab6fa4 100644 --- a/tests/components/water_heater/test_init.py +++ b/tests/components/water_heater/test_init.py @@ -71,14 +71,12 @@ async def test_sync_turn_on(hass: HomeAssistant) -> None: setattr(water_heater, "turn_on", MagicMock()) await water_heater.async_turn_on() - # pylint: disable-next=no-member assert water_heater.turn_on.call_count == 1 # Test with async_turn_on method defined setattr(water_heater, "async_turn_on", AsyncMock()) await water_heater.async_turn_on() - # pylint: disable-next=no-member assert water_heater.async_turn_on.call_count == 1 @@ -91,12 +89,10 @@ async def test_sync_turn_off(hass: HomeAssistant) -> None: setattr(water_heater, "turn_off", MagicMock()) await water_heater.async_turn_off() - # pylint: disable-next=no-member assert water_heater.turn_off.call_count == 1 # Test with async_turn_off method defined setattr(water_heater, "async_turn_off", AsyncMock()) await water_heater.async_turn_off() - # pylint: disable-next=no-member assert water_heater.async_turn_off.call_count == 1 diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 51bff1af0d7..aba34aeb44b 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -296,7 +296,6 @@ async def test_auth_sending_unknown_type_disconnects( auth_msg = await ws.receive_json() assert auth_msg["type"] == TYPE_AUTH_REQUIRED - # pylint: disable-next=protected-access await ws._writer._send_frame(b"1" * 130, 0x30) auth_msg = await ws.receive() assert auth_msg.type == WSMsgType.close From 46a0f8410173de30d45ef9182022a0da28eb247a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Aug 2023 20:18:21 -0500 Subject: [PATCH 0805/1151] Bump bluetooth-data-tools to 1.9.0 (#98927) --- homeassistant/components/bluetooth/manifest.json | 2 +- 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 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 84453344c3c..db28e550d23 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.8.0", + "bluetooth-data-tools==1.9.0", "dbus-fast==1.93.0" ] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 313ba5355bb..0bcae814b75 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "requirements": [ "async_interrupt==1.1.1", "aioesphomeapi==16.0.1", - "bluetooth-data-tools==1.8.0", + "bluetooth-data-tools==1.9.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 1115a0efc54..6065009760e 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.8.0", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.9.0", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index ffaf2bf87db..b9bdf31f066 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.8.0", "led-ble==1.0.0"] + "requirements": ["bluetooth-data-tools==1.9.0", "led-ble==1.0.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b09296888fe..878e1dabb47 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.8.0 +bluetooth-data-tools==1.9.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index d6572d05f3b..2c8f3b9f8f1 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.8.0 +bluetooth-data-tools==1.9.0 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea5e8680ec7..7f7865d5b40 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.8.0 +bluetooth-data-tools==1.9.0 # homeassistant.components.bond bond-async==0.2.1 From 14f80560c005a8bb479ca78dbff7cd2d562f5d90 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 08:14:46 +0200 Subject: [PATCH 0806/1151] Use snapshot assertion for Ridwell diagnostics test (#98919) --- tests/components/ridwell/conftest.py | 7 ++- .../ridwell/snapshots/test_diagnostics.ambr | 49 ++++++++++++++++++ tests/components/ridwell/test_diagnostics.py | 50 +++---------------- 3 files changed, 62 insertions(+), 44 deletions(-) create mode 100644 tests/components/ridwell/snapshots/test_diagnostics.ambr diff --git a/tests/components/ridwell/conftest.py b/tests/components/ridwell/conftest.py index 87ca00c37c3..651c2a96388 100644 --- a/tests/components/ridwell/conftest.py +++ b/tests/components/ridwell/conftest.py @@ -56,7 +56,12 @@ def client_fixture(account): @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=config[CONF_USERNAME], data=config) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=config[CONF_USERNAME], + data=config, + entry_id="11554ec901379b9cc8f5a6c1d11ce978", + ) entry.add_to_hass(hass) return entry diff --git a/tests/components/ridwell/snapshots/test_diagnostics.ambr b/tests/components/ridwell/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..a98374d2941 --- /dev/null +++ b/tests/components/ridwell/snapshots/test_diagnostics.ambr @@ -0,0 +1,49 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': list([ + dict({ + '_async_request': None, + 'event_id': 'event_123', + 'pickup_date': dict({ + '__type': "", + 'isoformat': '2022-01-24', + }), + 'pickups': list([ + dict({ + 'category': dict({ + '__type': "", + 'repr': "", + }), + 'name': 'Plastic Film', + 'offer_id': 'offer_123', + 'priority': 1, + 'product_id': 'product_123', + 'quantity': 1, + }), + ]), + 'state': dict({ + '__type': "", + 'repr': "", + }), + }), + ]), + 'entry': dict({ + 'data': dict({ + 'password': '**REDACTED**', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'ridwell', + 'entry_id': '11554ec901379b9cc8f5a6c1d11ce978', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': '**REDACTED**', + 'version': 2, + }), + }) +# --- diff --git a/tests/components/ridwell/test_diagnostics.py b/tests/components/ridwell/test_diagnostics.py index e73b352f3d9..c87004a8e76 100644 --- a/tests/components/ridwell/test_diagnostics.py +++ b/tests/components/ridwell/test_diagnostics.py @@ -1,5 +1,6 @@ """Test Ridwell diagnostics.""" -from homeassistant.components.diagnostics import REDACTED +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -11,47 +12,10 @@ async def test_entry_diagnostics( config_entry, hass_client: ClientSessionGenerator, setup_config_entry, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "entry_id": config_entry.entry_id, - "version": 2, - "domain": "ridwell", - "title": REDACTED, - "data": {"username": REDACTED, "password": REDACTED}, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - }, - "data": [ - { - "_async_request": None, - "event_id": "event_123", - "pickup_date": { - "__type": "", - "isoformat": "2022-01-24", - }, - "pickups": [ - { - "name": "Plastic Film", - "offer_id": "offer_123", - "priority": 1, - "product_id": "product_123", - "quantity": 1, - "category": { - "__type": "", - "repr": "", - }, - } - ], - "state": { - "__type": "", - "repr": "", - }, - } - ], - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From 8d576c900dd50c20b5559a0ea24298fde82d408c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 24 Aug 2023 09:18:25 +0200 Subject: [PATCH 0807/1151] Bump hass-nabucasa from 0.69.0 to 0.70.0 (#98935) --- homeassistant/components/cloud/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/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index d8fd2148b4d..a8e28d66291 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -8,5 +8,5 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["hass_nabucasa"], - "requirements": ["hass-nabucasa==0.69.0"] + "requirements": ["hass-nabucasa==0.70.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 878e1dabb47..9d9e98e0c26 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ cryptography==41.0.3 dbus-fast==1.93.0 fnv-hash-fast==0.4.0 ha-av==10.1.1 -hass-nabucasa==0.69.0 +hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.2 home-assistant-frontend==20230802.1 diff --git a/requirements_all.txt b/requirements_all.txt index 2c8f3b9f8f1..3310ca991be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -952,7 +952,7 @@ ha-philipsjs==3.1.0 habitipy==0.2.0 # homeassistant.components.cloud -hass-nabucasa==0.69.0 +hass-nabucasa==0.70.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f7865d5b40..45aca4548f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -747,7 +747,7 @@ ha-philipsjs==3.1.0 habitipy==0.2.0 # homeassistant.components.cloud -hass-nabucasa==0.69.0 +hass-nabucasa==0.70.0 # homeassistant.components.conversation hassil==1.2.5 From 602a80c35c1fcaee7435758475a694f1c19b157d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 09:19:36 +0200 Subject: [PATCH 0808/1151] Use snapshot assertion for EasyEnergy diagnostics test (#98909) --- .../snapshots/test_diagnostics.ambr | 63 ++++++++++++++++ .../components/easyenergy/test_diagnostics.py | 73 +++---------------- 2 files changed, 74 insertions(+), 62 deletions(-) create mode 100644 tests/components/easyenergy/snapshots/test_diagnostics.ambr diff --git a/tests/components/easyenergy/snapshots/test_diagnostics.ambr b/tests/components/easyenergy/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..805846832aa --- /dev/null +++ b/tests/components/easyenergy/snapshots/test_diagnostics.ambr @@ -0,0 +1,63 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'energy_return': dict({ + 'average_price': 0.14599, + 'current_hour_price': 0.18629, + 'highest_price_time': '2023-01-19T16:00:00+00:00', + 'lowest_price_time': '2023-01-19T02:00:00+00:00', + 'max_price': 0.20394, + 'min_price': 0.10172, + 'next_hour_price': 0.20394, + 'percentage_of_max': 91.35, + }), + 'energy_usage': dict({ + 'average_price': 0.17665, + 'current_hour_price': 0.22541, + 'highest_price_time': '2023-01-19T16:00:00+00:00', + 'lowest_price_time': '2023-01-19T02:00:00+00:00', + 'max_price': 0.24677, + 'min_price': 0.12308, + 'next_hour_price': 0.24677, + 'percentage_of_max': 91.34, + }), + 'entry': dict({ + 'title': 'energy', + }), + 'gas': dict({ + 'current_hour_price': 0.7253, + 'next_hour_price': 0.7253, + }), + }) +# --- +# name: test_diagnostics_no_gas_today + dict({ + 'energy_return': dict({ + 'average_price': 0.14599, + 'current_hour_price': 0.18629, + 'highest_price_time': '2023-01-19T16:00:00+00:00', + 'lowest_price_time': '2023-01-19T02:00:00+00:00', + 'max_price': 0.20394, + 'min_price': 0.10172, + 'next_hour_price': 0.20394, + 'percentage_of_max': 91.35, + }), + 'energy_usage': dict({ + 'average_price': 0.17665, + 'current_hour_price': 0.22541, + 'highest_price_time': '2023-01-19T16:00:00+00:00', + 'lowest_price_time': '2023-01-19T02:00:00+00:00', + 'max_price': 0.24677, + 'min_price': 0.12308, + 'next_hour_price': 0.24677, + 'percentage_of_max': 91.34, + }), + 'entry': dict({ + 'title': 'energy', + }), + 'gas': dict({ + 'current_hour_price': None, + 'next_hour_price': None, + }), + }) +# --- diff --git a/tests/components/easyenergy/test_diagnostics.py b/tests/components/easyenergy/test_diagnostics.py index 336f363e6a1..f76821cf265 100644 --- a/tests/components/easyenergy/test_diagnostics.py +++ b/tests/components/easyenergy/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock from easyenergy import EasyEnergyNoDataError import pytest +from syrupy import SnapshotAssertion from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY from homeassistant.const import ATTR_ENTITY_ID @@ -19,39 +20,13 @@ async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - assert await get_diagnostics_for_config_entry( - hass, hass_client, init_integration - ) == { - "entry": { - "title": "energy", - }, - "energy_usage": { - "current_hour_price": 0.22541, - "next_hour_price": 0.24677, - "average_price": 0.17665, - "max_price": 0.24677, - "min_price": 0.12308, - "highest_price_time": "2023-01-19T16:00:00+00:00", - "lowest_price_time": "2023-01-19T02:00:00+00:00", - "percentage_of_max": 91.34, - }, - "energy_return": { - "current_hour_price": 0.18629, - "next_hour_price": 0.20394, - "average_price": 0.14599, - "max_price": 0.20394, - "min_price": 0.10172, - "highest_price_time": "2023-01-19T16:00:00+00:00", - "lowest_price_time": "2023-01-19T02:00:00+00:00", - "percentage_of_max": 91.35, - }, - "gas": { - "current_hour_price": 0.7253, - "next_hour_price": 0.7253, - }, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) @pytest.mark.freeze_time("2023-01-19 15:00:00") @@ -60,6 +35,7 @@ async def test_diagnostics_no_gas_today( hass_client: ClientSessionGenerator, mock_easyenergy: MagicMock, init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics, no gas sensors available.""" await async_setup_component(hass, "homeassistant", {}) @@ -73,34 +49,7 @@ async def test_diagnostics_no_gas_today( ) await hass.async_block_till_done() - assert await get_diagnostics_for_config_entry( - hass, hass_client, init_integration - ) == { - "entry": { - "title": "energy", - }, - "energy_usage": { - "current_hour_price": 0.22541, - "next_hour_price": 0.24677, - "average_price": 0.17665, - "max_price": 0.24677, - "min_price": 0.12308, - "highest_price_time": "2023-01-19T16:00:00+00:00", - "lowest_price_time": "2023-01-19T02:00:00+00:00", - "percentage_of_max": 91.34, - }, - "energy_return": { - "current_hour_price": 0.18629, - "next_hour_price": 0.20394, - "average_price": 0.14599, - "max_price": 0.20394, - "min_price": 0.10172, - "highest_price_time": "2023-01-19T16:00:00+00:00", - "lowest_price_time": "2023-01-19T02:00:00+00:00", - "percentage_of_max": 91.35, - }, - "gas": { - "current_hour_price": None, - "next_hour_price": None, - }, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) From fe164d06a7e6a4344581d27aad8b653a2108735b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 09:23:48 +0200 Subject: [PATCH 0809/1151] Add entity translations to Sabnzbd (#98923) --- homeassistant/components/sabnzbd/sensor.py | 33 +++++++---------- homeassistant/components/sabnzbd/strings.json | 37 +++++++++++++++++++ 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 8edc579b7ab..d4920ef77f3 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -17,7 +17,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, SIGNAL_SABNZBD_UPDATED -from .const import DEFAULT_NAME, KEY_API_DATA, KEY_NAME +from .const import DEFAULT_NAME, KEY_API_DATA @dataclass @@ -37,51 +37,51 @@ SPEED_KEY = "kbpersec" SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( SabnzbdSensorEntityDescription( key="status", - name="Status", + translation_key="status", ), SabnzbdSensorEntityDescription( key=SPEED_KEY, - name="Speed", + translation_key="speed", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, ), SabnzbdSensorEntityDescription( key="mb", - name="Queue", + translation_key="queue", native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, state_class=SensorStateClass.MEASUREMENT, ), SabnzbdSensorEntityDescription( key="mbleft", - name="Left", + translation_key="left", native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, state_class=SensorStateClass.MEASUREMENT, ), SabnzbdSensorEntityDescription( key="diskspacetotal1", - name="Disk", + translation_key="total_disk_space", native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, state_class=SensorStateClass.MEASUREMENT, ), SabnzbdSensorEntityDescription( key="diskspace1", - name="Disk Free", + translation_key="free_disk_space", native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, state_class=SensorStateClass.MEASUREMENT, ), SabnzbdSensorEntityDescription( key="noofslots_total", - name="Queue Count", + translation_key="queue_count", state_class=SensorStateClass.TOTAL, ), SabnzbdSensorEntityDescription( key="day_size", - name="Daily Total", + translation_key="daily_total", native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, @@ -89,7 +89,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( ), SabnzbdSensorEntityDescription( key="week_size", - name="Weekly Total", + translation_key="weekly_total", native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, @@ -97,7 +97,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( ), SabnzbdSensorEntityDescription( key="month_size", - name="Monthly Total", + translation_key="monthly_total", native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, @@ -105,7 +105,7 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( ), SabnzbdSensorEntityDescription( key="total_size", - name="Total", + translation_key="overall_total", native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, state_class=SensorStateClass.TOTAL_INCREASING, @@ -137,13 +137,9 @@ async def async_setup_entry( entry_id = config_entry.entry_id sab_api_data = hass.data[DOMAIN][entry_id][KEY_API_DATA] - client_name = hass.data[DOMAIN][entry_id][KEY_NAME] async_add_entities( - [ - SabnzbdSensor(sab_api_data, client_name, sensor, entry_id) - for sensor in SENSOR_TYPES - ] + [SabnzbdSensor(sab_api_data, sensor, entry_id) for sensor in SENSOR_TYPES] ) @@ -152,11 +148,11 @@ class SabnzbdSensor(SensorEntity): entity_description: SabnzbdSensorEntityDescription _attr_should_poll = False + _attr_has_entity_name = True def __init__( self, sabnzbd_api_data, - client_name, description: SabnzbdSensorEntityDescription, entry_id, ) -> None: @@ -165,7 +161,6 @@ class SabnzbdSensor(SensorEntity): self._attr_unique_id = f"{entry_id}_{description.key}" self.entity_description = description self._sabnzbd_api = sabnzbd_api_data - self._attr_name = f"{client_name} {description.name}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, diff --git a/homeassistant/components/sabnzbd/strings.json b/homeassistant/components/sabnzbd/strings.json index a8e146eeb27..8d3fb84fb9f 100644 --- a/homeassistant/components/sabnzbd/strings.json +++ b/homeassistant/components/sabnzbd/strings.json @@ -14,6 +14,43 @@ "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" } }, + "entity": { + "sensor": { + "status": { + "name": "Status" + }, + "speed": { + "name": "Speed" + }, + "queue": { + "name": "Queue" + }, + "left": { + "name": "Left" + }, + "total_disk_space": { + "name": "Total disk space" + }, + "free_disk_space": { + "name": "Free disk space" + }, + "queue_count": { + "name": "Queue count" + }, + "daily_total": { + "name": "Daily total" + }, + "weekly_total": { + "name": "Weekly total" + }, + "monthly_total": { + "name": "Monthly total" + }, + "overall_total": { + "name": "Overall total" + } + } + }, "services": { "pause": { "name": "[%key:common::action::pause%]", From 8b232047c4298aeb213acf0847bb9ed49aa03abc Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 24 Aug 2023 09:50:39 +0200 Subject: [PATCH 0810/1151] Add origin info support for MQTT discovered items (#98782) * Add integration info support for MQTT discovery. * Moving logs to discovery * Revert adding class property * Rename to origin * Follow up comments --- .../components/mqtt/abbreviations.py | 7 ++ homeassistant/components/mqtt/const.py | 14 ++++ homeassistant/components/mqtt/discovery.py | 71 ++++++++++++++--- homeassistant/components/mqtt/mixins.py | 24 +++--- homeassistant/components/mqtt/models.py | 10 +++ tests/components/mqtt/test_discovery.py | 79 +++++++++++++++++++ 6 files changed, 185 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index cc0f37ea145..43f14eba1c5 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -111,6 +111,7 @@ ABBREVIATIONS = { "mode_stat_tpl": "mode_state_template", "modes": "modes", "name": "name", + "o": "origin", "obj_id": "object_id", "off_dly": "off_delay", "on_cmd_type": "on_command_type", @@ -275,3 +276,9 @@ DEVICE_ABBREVIATIONS = { "sw": "sw_version", "sa": "suggested_area", } + +ORIGIN_ABBREVIATIONS = { + "name": "name", + "sw": "sw_version", + "url": "support_url", +} diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 97d2e1473f5..c0589f60cbe 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -17,6 +17,7 @@ CONF_COMMAND_TOPIC = "command_topic" CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_ENCODING = "encoding" CONF_KEEPALIVE = "keepalive" +CONF_ORIGIN = "origin" CONF_QOS = ATTR_QOS CONF_RETAIN = ATTR_RETAIN CONF_SCHEMA = "schema" @@ -57,6 +58,19 @@ CONF_CLIENT_KEY = "client_key" CONF_CLIENT_CERT = "client_cert" CONF_TLS_INSECURE = "tls_insecure" +# Device and integration info options +CONF_IDENTIFIERS = "identifiers" +CONF_CONNECTIONS = "connections" +CONF_MANUFACTURER = "manufacturer" +CONF_HW_VERSION = "hw_version" +CONF_SW_VERSION = "sw_version" +CONF_VIA_DEVICE = "via_device" +CONF_DEPRECATED_VIA_HUB = "via_hub" +CONF_SUGGESTED_AREA = "suggested_area" +CONF_CONFIGURATION_URL = "configuration_url" +CONF_OBJECT_ID = "object_id" +CONF_SUPPORT_URL = "support_url" + DATA_MQTT = "mqtt" DATA_MQTT_AVAILABLE = "mqtt_client_available" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 8e563a48cdd..e701937a048 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -9,8 +9,10 @@ import re import time from typing import Any +import voluptuous as vol + from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE, CONF_PLATFORM +from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_PLATFORM from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv @@ -24,16 +26,19 @@ from homeassistant.loader import async_get_mqtt from homeassistant.util.json import json_loads_object from .. import mqtt -from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS +from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS, ORIGIN_ABBREVIATIONS from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, + CONF_ORIGIN, + CONF_SUPPORT_URL, + CONF_SW_VERSION, CONF_TOPIC, DOMAIN, ) -from .models import ReceiveMessage +from .models import MqttOriginInfo, ReceiveMessage from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -77,6 +82,16 @@ MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}" TOPIC_BASE = "~" +MQTT_ORIGIN_INFO_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_SW_VERSION): cv.string, + vol.Optional(CONF_SUPPORT_URL): cv.configuration_url, + } + ), +) + class MQTTDiscoveryPayload(dict[str, Any]): """Class to hold and MQTT discovery payload and discovery data.""" @@ -94,6 +109,30 @@ def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> get_mqtt_data(hass).discovery_already_discovered.add(discovery_hash) +@callback +def async_log_discovery_origin_info( + message: str, discovery_payload: MQTTDiscoveryPayload +) -> None: + """Log information about the discovery and origin.""" + if CONF_ORIGIN not in discovery_payload: + _LOGGER.info(message) + return + origin_info: MqttOriginInfo = discovery_payload[CONF_ORIGIN] + sw_version_log = "" + if sw_version := origin_info.get("sw_version"): + sw_version_log = f", version: {sw_version}" + support_url_log = "" + if support_url := origin_info.get("support_url"): + support_url_log = f", support URL: {support_url}" + _LOGGER.info( + "%s from external application %s%s%s", + message, + origin_info["name"], + sw_version_log, + support_url_log, + ) + + async def async_start( # noqa: C901 hass: HomeAssistant, discovery_topic: str, config_entry: ConfigEntry ) -> None: @@ -149,6 +188,22 @@ async def async_start( # noqa: C901 key = DEVICE_ABBREVIATIONS.get(key, key) device[key] = device.pop(abbreviated_key) + if CONF_ORIGIN in discovery_payload: + origin_info: dict[str, Any] = discovery_payload[CONF_ORIGIN] + try: + for key in list(origin_info): + abbreviated_key = key + key = ORIGIN_ABBREVIATIONS.get(key, key) + origin_info[key] = origin_info.pop(abbreviated_key) + MQTT_ORIGIN_INFO_SCHEMA(discovery_payload[CONF_ORIGIN]) + except Exception: # pylint: disable=broad-except + _LOGGER.warning( + "Unable to parse origin information " + "from discovery message, got %s", + discovery_payload[CONF_ORIGIN], + ) + return + if CONF_AVAILABILITY in discovery_payload: for availability_conf in cv.ensure_list( discovery_payload[CONF_AVAILABILITY] @@ -246,17 +301,15 @@ async def async_start( # noqa: C901 if discovery_hash in mqtt_data.discovery_already_discovered: # Dispatch update - _LOGGER.info( - "Component has already been discovered: %s %s, sending update", - component, - discovery_id, - ) + message = f"Component has already been discovered: {component} {discovery_id}, sending update" + async_log_discovery_origin_info(message, payload) async_dispatcher_send( hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), payload ) elif payload: # Add component - _LOGGER.info("Found new component: %s %s", component, discovery_id) + message = f"Found new component: {component} {discovery_id}" + async_log_discovery_origin_info(message, payload) mqtt_data.discovery_already_discovered.add(discovery_hash) async_dispatcher_send( hass, MQTT_DISCOVERY_NEW.format(component, "mqtt"), payload diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 97ba96f0207..3b28bc8804f 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -69,9 +69,20 @@ from .const import ( ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, + CONF_CONFIGURATION_URL, + CONF_CONNECTIONS, + CONF_DEPRECATED_VIA_HUB, CONF_ENCODING, + CONF_HW_VERSION, + CONF_IDENTIFIERS, + CONF_MANUFACTURER, + CONF_OBJECT_ID, + CONF_ORIGIN, CONF_QOS, + CONF_SUGGESTED_AREA, + CONF_SW_VERSION, CONF_TOPIC, + CONF_VIA_DEVICE, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE, @@ -84,6 +95,7 @@ from .discovery import ( MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, MQTT_DISCOVERY_UPDATED, + MQTT_ORIGIN_INFO_SCHEMA, MQTTDiscoveryPayload, clear_discovery_hash, set_discovery_hash, @@ -119,17 +131,6 @@ CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" CONF_JSON_ATTRS_TOPIC = "json_attributes_topic" CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template" -CONF_IDENTIFIERS = "identifiers" -CONF_CONNECTIONS = "connections" -CONF_MANUFACTURER = "manufacturer" -CONF_HW_VERSION = "hw_version" -CONF_SW_VERSION = "sw_version" -CONF_VIA_DEVICE = "via_device" -CONF_DEPRECATED_VIA_HUB = "via_hub" -CONF_SUGGESTED_AREA = "suggested_area" -CONF_CONFIGURATION_URL = "configuration_url" -CONF_OBJECT_ID = "object_id" - MQTT_ATTRIBUTES_BLOCKED = { "assumed_state", "available", @@ -228,6 +229,7 @@ MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend( { vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_ORIGIN): MQTT_ORIGIN_INFO_SCHEMA, vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean, vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, vol.Optional(CONF_ICON): cv.icon, diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 99267d9572a..d553274ab3e 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -99,6 +99,16 @@ class PendingDiscovered(TypedDict): unsub: CALLBACK_TYPE +class MqttOriginInfo(TypedDict, total=False): + """Integration info of discovered entity.""" + + name: str + manufacturer: str + sw_version: str + hw_version: str + support_url: str + + class MqttCommandTemplate: """Class for rendering MQTT payload with command templates.""" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index f51d469bde7..c528687623b 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -168,6 +168,83 @@ async def test_correct_config_discovery( assert ("binary_sensor", "bla") in hass.data["mqtt"].discovery_already_discovered +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) +async def test_discovery_integration_info( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test logging discovery of new and updated items.""" + await mqtt_mock_entry() + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic", "o": {"name": "bla2mqtt", "sw": "1.0" } }', + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.beer") + + assert state is not None + assert state.name == "Beer" + + assert ( + "Found new component: binary_sensor bla from external application bla2mqtt, version: 1.0" + in caplog.text + ) + caplog.clear() + + # Send an update and add support url + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Milk", "state_topic": "test-topic", "o": {"name": "bla2mqtt", "sw": "1.1", "url": "https://bla2mqtt.example.com/support" } }', + ) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.beer") + + assert state is not None + assert state.name == "Milk" + + assert ( + "Component has already been discovered: binary_sensor bla, sending update from external application bla2mqtt, version: 1.1, support URL: https://bla2mqtt.example.com/support" + in caplog.text + ) + + +@pytest.mark.parametrize( + "config_message", + [ + '{ "name": "Beer", "state_topic": "test-topic", "o": "bla2mqtt" }', + '{ "name": "Beer", "state_topic": "test-topic", "o": 2.0 }', + '{ "name": "Beer", "state_topic": "test-topic", "o": null }', + '{ "name": "Beer", "state_topic": "test-topic", "o": {"sw": "bla2mqtt"} }', + ], +) +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) +async def test_discovery_with_invalid_integration_info( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + config_message: str, +) -> None: + """Test sending in correct JSON.""" + await mqtt_mock_entry() + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + config_message, + ) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.beer") + + assert state is None + assert ( + "Unable to parse origin information from discovery message, got" in caplog.text + ) + + @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.FAN]) async def test_discover_fan( hass: HomeAssistant, @@ -1266,6 +1343,8 @@ ABBREVIATIONS_WHITE_LIST = [ "CONF_WILL_MESSAGE", "CONF_WS_PATH", "CONF_WS_HEADERS", + # Integration info + "CONF_SUPPORT_URL", # Undocumented device configuration "CONF_DEPRECATED_VIA_HUB", "CONF_VIA_DEVICE", From 7926c5cea92a99cce18dc56a794fd912c5a603e4 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 24 Aug 2023 10:33:06 +0200 Subject: [PATCH 0811/1151] Add repair issue about the deprecation of home plus control (#98828) Co-authored-by: Joost Lekkerkerker Co-authored-by: Franck Nijhof --- .../components/home_plus_control/__init__.py | 37 +++++++++++++++++++ .../components/home_plus_control/strings.json | 6 +++ 2 files changed, 43 insertions(+) diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index b6a1fc68a17..0accf53970d 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -15,6 +15,11 @@ from homeassistant.helpers import ( dispatcher, ) from homeassistant.helpers.device_registry import async_get as async_get_device_registry +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -49,6 +54,8 @@ PLATFORMS = [Platform.SWITCH] _LOGGER = logging.getLogger(__name__) +_ISSUE_MOTE_TO_NETAMO = "move_to_netamo" + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Legrand Home+ Control component from configuration.yaml.""" @@ -57,6 +64,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True + async_create_issue( + hass, + DOMAIN, + _ISSUE_MOTE_TO_NETAMO, + is_fixable=False, + is_persistent=False, + breaks_in_ha_version="2023.12.0", # Netamo decided to shutdown the api in december + severity=IssueSeverity.WARNING, + translation_key=_ISSUE_MOTE_TO_NETAMO, + translation_placeholders={ + "url": "https://www.home-assistant.io/integrations/netatmo/" + }, + ) + # Register the implementation from the config information config_flow.HomePlusControlFlowHandler.async_register_implementation( hass, @@ -70,6 +91,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Legrand Home+ Control from a config entry.""" hass_entry_data = hass.data[DOMAIN].setdefault(entry.entry_id, {}) + async_create_issue( + hass, + DOMAIN, + _ISSUE_MOTE_TO_NETAMO, + is_fixable=False, + is_persistent=False, + breaks_in_ha_version="2023.12.0", # Netamo decided to shutdown the api in december + severity=IssueSeverity.WARNING, + translation_key=_ISSUE_MOTE_TO_NETAMO, + translation_placeholders={ + "url": "https://www.home-assistant.io/integrations/netatmo/" + }, + ) + # Retrieve the registered implementation implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( @@ -168,4 +203,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # And finally unload the domain config entry data hass.data[DOMAIN].pop(config_entry.entry_id) + async_delete_issue(hass, DOMAIN, _ISSUE_MOTE_TO_NETAMO) + return unload_ok diff --git a/homeassistant/components/home_plus_control/strings.json b/homeassistant/components/home_plus_control/strings.json index 9e860b397fb..d795323586d 100644 --- a/homeassistant/components/home_plus_control/strings.json +++ b/homeassistant/components/home_plus_control/strings.json @@ -16,5 +16,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "move_to_netamo": { + "title": "Legrand Home+ Control deprecation", + "description": "Home Assistant has been informed that the platform the Legrand Home+ Control integration is using, will be shutting down upcoming December.\n\nOnce that happens, it means this integration is no longer functional. We advise you to remove this integration and switch to the [Netatmo]({url}) integration, which provides a replacement for controlling your Legrand Home+ Control devices." + } } } From 147351be6e3c2890af28fa8bf87db4c914d38681 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 24 Aug 2023 10:39:22 +0200 Subject: [PATCH 0812/1151] Add Trafikverket Camera integration (#79873) --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/brands/trafikverket.json | 1 + .../trafikverket_camera/__init__.py | 29 +++ .../components/trafikverket_camera/camera.py | 84 +++++++ .../trafikverket_camera/config_flow.py | 122 +++++++++ .../components/trafikverket_camera/const.py | 10 + .../trafikverket_camera/coordinator.py | 76 ++++++ .../trafikverket_camera/manifest.json | 10 + .../trafikverket_camera/recorder.py | 13 + .../trafikverket_camera/strings.json | 51 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 1 + requirements_test_all.txt | 1 + .../trafikverket_camera/__init__.py | 10 + .../trafikverket_camera/conftest.py | 69 ++++++ .../trafikverket_camera/test_camera.py | 75 ++++++ .../trafikverket_camera/test_config_flow.py | 234 ++++++++++++++++++ .../trafikverket_camera/test_coordinator.py | 151 +++++++++++ .../trafikverket_camera/test_init.py | 80 ++++++ .../trafikverket_camera/test_recorder.py | 46 ++++ 23 files changed, 1083 insertions(+) create mode 100644 homeassistant/components/trafikverket_camera/__init__.py create mode 100644 homeassistant/components/trafikverket_camera/camera.py create mode 100644 homeassistant/components/trafikverket_camera/config_flow.py create mode 100644 homeassistant/components/trafikverket_camera/const.py create mode 100644 homeassistant/components/trafikverket_camera/coordinator.py create mode 100644 homeassistant/components/trafikverket_camera/manifest.json create mode 100644 homeassistant/components/trafikverket_camera/recorder.py create mode 100644 homeassistant/components/trafikverket_camera/strings.json create mode 100644 tests/components/trafikverket_camera/__init__.py create mode 100644 tests/components/trafikverket_camera/conftest.py create mode 100644 tests/components/trafikverket_camera/test_camera.py create mode 100644 tests/components/trafikverket_camera/test_config_flow.py create mode 100644 tests/components/trafikverket_camera/test_coordinator.py create mode 100644 tests/components/trafikverket_camera/test_init.py create mode 100644 tests/components/trafikverket_camera/test_recorder.py diff --git a/.strict-typing b/.strict-typing index 41138c812ec..19cee069b42 100644 --- a/.strict-typing +++ b/.strict-typing @@ -326,6 +326,7 @@ homeassistant.components.tplink.* homeassistant.components.tplink_omada.* homeassistant.components.tractive.* homeassistant.components.tradfri.* +homeassistant.components.trafikverket_camera.* homeassistant.components.trafikverket_ferry.* homeassistant.components.trafikverket_train.* homeassistant.components.trafikverket_weatherstation.* diff --git a/CODEOWNERS b/CODEOWNERS index 427c8290b60..e3e42b75280 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1300,6 +1300,8 @@ build.json @home-assistant/supervisor /tests/components/trace/ @home-assistant/core /homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu /tests/components/tractive/ @Danielhiversen @zhulik @bieniu +/homeassistant/components/trafikverket_camera/ @gjohansson-ST +/tests/components/trafikverket_camera/ @gjohansson-ST /homeassistant/components/trafikverket_ferry/ @gjohansson-ST /tests/components/trafikverket_ferry/ @gjohansson-ST /homeassistant/components/trafikverket_train/ @endor-force @gjohansson-ST diff --git a/homeassistant/brands/trafikverket.json b/homeassistant/brands/trafikverket.json index df444cbeb60..4b925d5c633 100644 --- a/homeassistant/brands/trafikverket.json +++ b/homeassistant/brands/trafikverket.json @@ -2,6 +2,7 @@ "domain": "trafikverket", "name": "Trafikverket", "integrations": [ + "trafikverket_camera", "trafikverket_ferry", "trafikverket_train", "trafikverket_weatherstation" diff --git a/homeassistant/components/trafikverket_camera/__init__.py b/homeassistant/components/trafikverket_camera/__init__.py new file mode 100644 index 00000000000..0ee4fd5010e --- /dev/null +++ b/homeassistant/components/trafikverket_camera/__init__.py @@ -0,0 +1,29 @@ +"""The trafikverket_camera component.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, PLATFORMS +from .coordinator import TVDataUpdateCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Trafikverket Camera from a config entry.""" + + coordinator = TVDataUpdateCoordinator(hass, entry) + 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) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Trafikverket Camera config entry.""" + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/trafikverket_camera/camera.py b/homeassistant/components/trafikverket_camera/camera.py new file mode 100644 index 00000000000..936e460638f --- /dev/null +++ b/homeassistant/components/trafikverket_camera/camera.py @@ -0,0 +1,84 @@ +"""Camera for the Trafikverket Camera integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from homeassistant.components.camera import Camera +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_LOCATION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTR_DESCRIPTION, ATTR_TYPE, DOMAIN +from .coordinator import TVDataUpdateCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Trafikverket Camera.""" + + coordinator: TVDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + [ + TVCamera( + coordinator, + entry.title, + entry.entry_id, + ) + ], + ) + + +class TVCamera(CoordinatorEntity[TVDataUpdateCoordinator], Camera): + """Implement Trafikverket camera.""" + + _attr_has_entity_name = True + _attr_name = None + _attr_translation_key = "tv_camera" + coordinator: TVDataUpdateCoordinator + + def __init__( + self, + coordinator: TVDataUpdateCoordinator, + name: str, + entry_id: str, + ) -> None: + """Initialize the camera.""" + super().__init__(coordinator) + Camera.__init__(self) + self._attr_unique_id = entry_id + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry_id)}, + manufacturer="Trafikverket", + model="v1.0", + name=name, + configuration_url="https://api.trafikinfo.trafikverket.se/", + ) + + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: + """Return camera picture.""" + return self.coordinator.data.image + + @property + def is_on(self) -> bool: + """Return camera on.""" + return self.coordinator.data.data.active is True + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return additional attributes.""" + return { + ATTR_DESCRIPTION: self.coordinator.data.data.description, + ATTR_LOCATION: self.coordinator.data.data.location, + ATTR_TYPE: self.coordinator.data.data.camera_type, + } diff --git a/homeassistant/components/trafikverket_camera/config_flow.py b/homeassistant/components/trafikverket_camera/config_flow.py new file mode 100644 index 00000000000..b8a14a5424e --- /dev/null +++ b/homeassistant/components/trafikverket_camera/config_flow.py @@ -0,0 +1,122 @@ +"""Adds config flow for Trafikverket Camera integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from pytrafikverket.exceptions import ( + InvalidAuthentication, + MultipleCamerasFound, + NoCameraFound, + UnknownError, +) +from pytrafikverket.trafikverket_camera import TrafikverketCamera +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import CONF_LOCATION, DOMAIN + + +class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Trafikverket Camera integration.""" + + VERSION = 1 + + entry: config_entries.ConfigEntry | None + + async def validate_input(self, sensor_api: str, location: str) -> dict[str, str]: + """Validate input from user input.""" + errors: dict[str, str] = {} + + web_session = async_get_clientsession(self.hass) + camera_api = TrafikverketCamera(web_session, sensor_api) + try: + await camera_api.async_get_camera(location) + except NoCameraFound: + errors["location"] = "invalid_location" + except MultipleCamerasFound: + errors["location"] = "more_locations" + except InvalidAuthentication: + errors["base"] = "invalid_auth" + except UnknownError: + errors["base"] = "cannot_connect" + + return errors + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle re-authentication with Trafikverket.""" + + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm re-authentication with Trafikverket.""" + errors = {} + + if user_input: + api_key = user_input[CONF_API_KEY] + + assert self.entry is not None + errors = await self.validate_input(api_key, self.entry.data[CONF_LOCATION]) + + if not errors: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_API_KEY: api_key, + }, + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + } + ), + errors=errors, + ) + + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + + if user_input: + api_key = user_input[CONF_API_KEY] + location = user_input[CONF_LOCATION] + + errors = await self.validate_input(api_key, location) + + if not errors: + await self.async_set_unique_id(f"{DOMAIN}-{location}") + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_LOCATION], + data={ + CONF_API_KEY: api_key, + CONF_LOCATION: location, + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_LOCATION): cv.string, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/trafikverket_camera/const.py b/homeassistant/components/trafikverket_camera/const.py new file mode 100644 index 00000000000..6657ab1a853 --- /dev/null +++ b/homeassistant/components/trafikverket_camera/const.py @@ -0,0 +1,10 @@ +"""Adds constants for Trafikverket Camera integration.""" +from homeassistant.const import Platform + +DOMAIN = "trafikverket_camera" +CONF_LOCATION = "location" +PLATFORMS = [Platform.CAMERA] +ATTRIBUTION = "Data provided by Trafikverket" + +ATTR_DESCRIPTION = "description" +ATTR_TYPE = "type" diff --git a/homeassistant/components/trafikverket_camera/coordinator.py b/homeassistant/components/trafikverket_camera/coordinator.py new file mode 100644 index 00000000000..eb5a047ca73 --- /dev/null +++ b/homeassistant/components/trafikverket_camera/coordinator.py @@ -0,0 +1,76 @@ +"""DataUpdateCoordinator for the Trafikverket Camera integration.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +from io import BytesIO +import logging + +from pytrafikverket.exceptions import ( + InvalidAuthentication, + MultipleCamerasFound, + NoCameraFound, + UnknownError, +) +from pytrafikverket.trafikverket_camera import CameraInfo, TrafikverketCamera + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_LOCATION, DOMAIN + +_LOGGER = logging.getLogger(__name__) +TIME_BETWEEN_UPDATES = timedelta(minutes=5) + + +@dataclass +class CameraData: + """Dataclass for Camera data.""" + + data: CameraInfo + image: bytes | None + + +class TVDataUpdateCoordinator(DataUpdateCoordinator[CameraData]): + """A Trafikverket Data Update Coordinator.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the Trafikverket coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=TIME_BETWEEN_UPDATES, + ) + self.session = async_get_clientsession(hass) + self._camera_api = TrafikverketCamera(self.session, entry.data[CONF_API_KEY]) + self._location = entry.data[CONF_LOCATION] + + async def _async_update_data(self) -> CameraData: + """Fetch data from Trafikverket.""" + camera_data: CameraInfo + image: bytes | None = None + try: + camera_data = await self._camera_api.async_get_camera(self._location) + except (NoCameraFound, MultipleCamerasFound, UnknownError) as error: + raise UpdateFailed from error + except InvalidAuthentication as error: + raise ConfigEntryAuthFailed from error + + if camera_data.photourl is None: + return CameraData(data=camera_data, image=None) + + image_url = camera_data.photourl + if camera_data.fullsizephoto: + image_url = f"{camera_data.photourl}?type=fullsize" + + async with self.session.get(image_url, timeout=10) as get_image: + if get_image.status not in range(200, 299): + raise UpdateFailed("Could not retrieve image") + image = BytesIO(await get_image.read()).getvalue() + + return CameraData(data=camera_data, image=image) diff --git a/homeassistant/components/trafikverket_camera/manifest.json b/homeassistant/components/trafikverket_camera/manifest.json new file mode 100644 index 00000000000..440d7237171 --- /dev/null +++ b/homeassistant/components/trafikverket_camera/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "trafikverket_camera", + "name": "Trafikverket Camera", + "codeowners": ["@gjohansson-ST"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/trafikverket_camera", + "iot_class": "cloud_polling", + "loggers": ["pytrafikverket"], + "requirements": ["pytrafikverket==0.3.5"] +} diff --git a/homeassistant/components/trafikverket_camera/recorder.py b/homeassistant/components/trafikverket_camera/recorder.py new file mode 100644 index 00000000000..b6b608749ad --- /dev/null +++ b/homeassistant/components/trafikverket_camera/recorder.py @@ -0,0 +1,13 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.const import ATTR_LOCATION +from homeassistant.core import HomeAssistant, callback + +from .const import ATTR_DESCRIPTION + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude description and location from being recorded in the database.""" + return {ATTR_DESCRIPTION, ATTR_LOCATION} diff --git a/homeassistant/components/trafikverket_camera/strings.json b/homeassistant/components/trafikverket_camera/strings.json new file mode 100644 index 00000000000..c128f7729bc --- /dev/null +++ b/homeassistant/components/trafikverket_camera/strings.json @@ -0,0 +1,51 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_location": "Could not find a camera location with the specified name", + "more_locations": "Found multiple camera locations with the specified name" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "location": "[%key:common::config_flow::data::location%]" + } + } + } + }, + "entity": { + "camera": { + "tv_camera": { + "state_attributes": { + "description": { + "name": "Description" + }, + "direction": { + "name": "Direction" + }, + "full_size_photo": { + "name": "Full size photo" + }, + "location": { + "name": "[%key:common::config_flow::data::location%]" + }, + "photo_url": { + "name": "Photo url" + }, + "status": { + "name": "Status" + }, + "type": { + "name": "Camera type" + } + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0bfbf362eb3..82c2d82f423 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -480,6 +480,7 @@ FLOWS = { "traccar", "tractive", "tradfri", + "trafikverket_camera", "trafikverket_ferry", "trafikverket_train", "trafikverket_weatherstation", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 40883ef3d7c..75540a3af83 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5878,6 +5878,12 @@ "trafikverket": { "name": "Trafikverket", "integrations": { + "trafikverket_camera": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Trafikverket Camera" + }, "trafikverket_ferry": { "integration_type": "hub", "config_flow": true, diff --git a/mypy.ini b/mypy.ini index a4bf83dbf27..644fba0df89 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3023,6 +3023,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.trafikverket_camera.*] +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.trafikverket_ferry.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 3310ca991be..601be9507ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2191,6 +2191,7 @@ pytraccar==1.0.0 # homeassistant.components.tradfri pytradfri[async]==9.0.1 +# homeassistant.components.trafikverket_camera # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45aca4548f9..43ccd45c09c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1608,6 +1608,7 @@ pytraccar==1.0.0 # homeassistant.components.tradfri pytradfri[async]==9.0.1 +# homeassistant.components.trafikverket_camera # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/tests/components/trafikverket_camera/__init__.py b/tests/components/trafikverket_camera/__init__.py new file mode 100644 index 00000000000..026c122fb57 --- /dev/null +++ b/tests/components/trafikverket_camera/__init__.py @@ -0,0 +1,10 @@ +"""Tests for the Trafikverket Camera integration.""" +from __future__ import annotations + +from homeassistant.components.trafikverket_camera.const import CONF_LOCATION +from homeassistant.const import CONF_API_KEY + +ENTRY_CONFIG = { + CONF_API_KEY: "1234567890", + CONF_LOCATION: "Test location", +} diff --git a/tests/components/trafikverket_camera/conftest.py b/tests/components/trafikverket_camera/conftest.py new file mode 100644 index 00000000000..2bbc888b31d --- /dev/null +++ b/tests/components/trafikverket_camera/conftest.py @@ -0,0 +1,69 @@ +"""Fixtures for Trafikverket Camera integration tests.""" +from __future__ import annotations + +from datetime import datetime +from unittest.mock import patch + +import pytest +from pytrafikverket.trafikverket_camera import CameraInfo + +from homeassistant.components.trafikverket_camera.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from . import ENTRY_CONFIG + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + + +@pytest.fixture(name="load_int") +async def load_integration_from_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, get_camera: CameraInfo +) -> MockConfigEntry: + """Set up the Trafikverket Ferry integration in Home Assistant.""" + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", content=b"0123456789" + ) + + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="123", + title="Test location", + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + return_value=get_camera, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +@pytest.fixture(name="get_camera") +def fixture_get_camera() -> CameraInfo: + """Construct Camera Mock.""" + + return CameraInfo( + camera_name="Test_camera", + camera_id="1234", + active=True, + deleted=False, + description="Test Camera for testing", + direction="180", + fullsizephoto=True, + location="Test location", + modified=datetime(2022, 4, 4, 4, 4, 4, tzinfo=dt_util.UTC), + phototime=datetime(2022, 4, 4, 4, 4, 4, tzinfo=dt_util.UTC), + photourl="https://www.testurl.com/test_photo.jpg", + status="Running", + camera_type="Road", + ) diff --git a/tests/components/trafikverket_camera/test_camera.py b/tests/components/trafikverket_camera/test_camera.py new file mode 100644 index 00000000000..57451ae93a9 --- /dev/null +++ b/tests/components/trafikverket_camera/test_camera.py @@ -0,0 +1,75 @@ +"""The test for the Trafikverket camera platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +import pytest +from pytrafikverket.trafikverket_camera import CameraInfo + +from homeassistant.components.camera import async_get_image +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_camera( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + aioclient_mock: AiohttpClientMocker, + get_camera: CameraInfo, +) -> None: + """Test the Trafikverket Camera sensor.""" + state1 = hass.states.get("camera.test_location") + assert state1.state == "idle" + assert state1.attributes["description"] == "Test Camera for testing" + assert state1.attributes["location"] == "Test location" + assert state1.attributes["type"] == "Road" + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + return_value=get_camera, + ): + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", + content=b"9876543210", + ) + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(minutes=6), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("camera.test_location") + assert state1.state == "idle" + assert state1.attributes != {} + + assert await async_get_image(hass, "camera.test_location") + + monkeypatch.setattr( + get_camera, + "photourl", + None, + ) + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", + status=404, + ) + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + return_value=get_camera, + ): + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(minutes=6), + ) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError): + await async_get_image(hass, "camera.test_location") diff --git a/tests/components/trafikverket_camera/test_config_flow.py b/tests/components/trafikverket_camera/test_config_flow.py new file mode 100644 index 00000000000..38c49d54208 --- /dev/null +++ b/tests/components/trafikverket_camera/test_config_flow.py @@ -0,0 +1,234 @@ +"""Test the Trafikverket Camera config flow.""" +from __future__ import annotations + +from unittest.mock import patch + +import pytest +from pytrafikverket.exceptions import ( + InvalidAuthentication, + MultipleCamerasFound, + NoCameraFound, + UnknownError, +) + +from homeassistant import config_entries +from homeassistant.components.trafikverket_camera.const import CONF_LOCATION, DOMAIN +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_camera", + ), patch( + "homeassistant.components.trafikverket_camera.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "1234567890", + CONF_LOCATION: "Test location", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Test location" + assert result2["data"] == { + "api_key": "1234567890", + "location": "Test location", + } + assert len(mock_setup_entry.mock_calls) == 1 + assert result2["result"].unique_id == "trafikverket_camera-Test location" + + +@pytest.mark.parametrize( + ("side_effect", "error_key", "base_error"), + [ + ( + InvalidAuthentication, + "base", + "invalid_auth", + ), + ( + NoCameraFound, + "location", + "invalid_location", + ), + ( + MultipleCamerasFound, + "location", + "more_locations", + ), + ( + UnknownError, + "base", + "cannot_connect", + ), + ], +) +async def test_flow_fails( + hass: HomeAssistant, side_effect: Exception, error_key: str, base_error: str +) -> None: + """Test config flow errors.""" + result4 = 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 + + with patch( + "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_camera", + side_effect=side_effect, + ): + result4 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + CONF_LOCATION: "incorrect", + }, + ) + + assert result4["errors"] == {error_key: base_error} + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a reauthentication flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "1234567890", + CONF_LOCATION: "Test location", + }, + unique_id="1234", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_camera", + ), patch( + "homeassistant.components.trafikverket_camera.async_setup_entry", + return_value=True, + ): + result2 = 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 entry.data == { + "api_key": "1234567891", + "location": "Test location", + } + + +@pytest.mark.parametrize( + ("side_effect", "error_key", "p_error"), + [ + ( + InvalidAuthentication, + "base", + "invalid_auth", + ), + ( + NoCameraFound, + "location", + "invalid_location", + ), + ( + MultipleCamerasFound, + "location", + "more_locations", + ), + ( + UnknownError, + "base", + "cannot_connect", + ), + ], +) +async def test_reauth_flow_error( + hass: HomeAssistant, side_effect: Exception, error_key: str, p_error: str +) -> None: + """Test a reauthentication flow with error.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "1234567890", + CONF_LOCATION: "Test location", + }, + unique_id="1234", + ) + entry.add_to_hass(hass) + await hass.async_block_till_done() + + 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_camera.config_flow.TrafikverketCamera.async_get_camera", + side_effect=side_effect, + ): + result2 = 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"] == {error_key: p_error} + + with patch( + "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_camera", + ), patch( + "homeassistant.components.trafikverket_camera.async_setup_entry", + return_value=True, + ): + result2 = 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 entry.data == { + "api_key": "1234567891", + "location": "Test location", + } diff --git a/tests/components/trafikverket_camera/test_coordinator.py b/tests/components/trafikverket_camera/test_coordinator.py new file mode 100644 index 00000000000..2b21ce935b2 --- /dev/null +++ b/tests/components/trafikverket_camera/test_coordinator.py @@ -0,0 +1,151 @@ +"""The test for the Trafikverket Camera coordinator.""" +from __future__ import annotations + +from unittest.mock import patch + +import pytest +from pytrafikverket.exceptions import ( + InvalidAuthentication, + MultipleCamerasFound, + NoCameraFound, + UnknownError, +) + +from homeassistant import config_entries +from homeassistant.components.trafikverket_camera.const import DOMAIN +from homeassistant.components.trafikverket_camera.coordinator import CameraData +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import UpdateFailed + +from . import ENTRY_CONFIG + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_coordinator( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + get_camera: CameraData, +) -> None: + """Test the Trafikverket Camera coordinator.""" + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", content=b"0123456789" + ) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="123", + title="Test location", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + return_value=get_camera, + ) as mock_data: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_data.assert_called_once() + state1 = hass.states.get("camera.test_location") + assert state1.state == "idle" + + +@pytest.mark.parametrize( + ("sideeffect", "p_error", "entry_state"), + [ + ( + InvalidAuthentication, + ConfigEntryAuthFailed, + config_entries.ConfigEntryState.SETUP_ERROR, + ), + ( + NoCameraFound, + UpdateFailed, + config_entries.ConfigEntryState.SETUP_RETRY, + ), + ( + MultipleCamerasFound, + UpdateFailed, + config_entries.ConfigEntryState.SETUP_RETRY, + ), + ( + UnknownError, + UpdateFailed, + config_entries.ConfigEntryState.SETUP_RETRY, + ), + ], +) +async def test_coordinator_failed_update( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + get_camera: CameraData, + sideeffect: str, + p_error: Exception, + entry_state: str, +) -> None: + """Test the Trafikverket Camera coordinator.""" + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", content=b"0123456789" + ) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="123", + title="Test location", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + side_effect=sideeffect, + ) as mock_data: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_data.assert_called_once() + state = hass.states.get("camera.test_location") + assert state is None + assert entry.state == entry_state + + +async def test_coordinator_failed_get_image( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + get_camera: CameraData, +) -> None: + """Test the Trafikverket Camera coordinator.""" + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", status=404 + ) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="123", + title="Test location", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + return_value=get_camera, + ) as mock_data: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_data.assert_called_once() + state = hass.states.get("camera.test_location") + assert state is None + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY diff --git a/tests/components/trafikverket_camera/test_init.py b/tests/components/trafikverket_camera/test_init.py new file mode 100644 index 00000000000..d9de0a830a6 --- /dev/null +++ b/tests/components/trafikverket_camera/test_init.py @@ -0,0 +1,80 @@ +"""Test for Trafikverket Ferry component Init.""" +from __future__ import annotations + +from unittest.mock import patch + +from pytrafikverket.trafikverket_camera import CameraInfo + +from homeassistant import config_entries +from homeassistant.components.trafikverket_camera.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from . import ENTRY_CONFIG + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_setup_entry( + hass: HomeAssistant, + get_camera: CameraInfo, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test setup entry.""" + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", content=b"0123456789" + ) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="123", + title="Test location", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + return_value=get_camera, + ) as mock_tvt_camera: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is config_entries.ConfigEntryState.LOADED + assert len(mock_tvt_camera.mock_calls) == 1 + + +async def test_unload_entry( + hass: HomeAssistant, + get_camera: CameraInfo, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test unload an entry.""" + aioclient_mock.get( + "https://www.testurl.com/test_photo.jpg?type=fullsize", content=b"0123456789" + ) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="321", + title="Test location", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", + return_value=get_camera, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is config_entries.ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/trafikverket_camera/test_recorder.py b/tests/components/trafikverket_camera/test_recorder.py new file mode 100644 index 00000000000..021433b33e7 --- /dev/null +++ b/tests/components/trafikverket_camera/test_recorder.py @@ -0,0 +1,46 @@ +"""The tests for Trafikcerket Camera recorder.""" +from __future__ import annotations + +import pytest +from pytrafikverket.trafikverket_camera import CameraInfo + +from homeassistant.components.recorder import Recorder +from homeassistant.components.recorder.history import get_significant_states +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from tests.components.recorder.common import async_wait_recording_done +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_exclude_attributes( + recorder_mock: Recorder, + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + aioclient_mock: AiohttpClientMocker, + get_camera: CameraInfo, +) -> None: + """Test camera has description and location excluded from recording.""" + state1 = hass.states.get("camera.test_location") + assert state1.state == "idle" + assert state1.attributes["description"] == "Test Camera for testing" + assert state1.attributes["location"] == "Test location" + assert state1.attributes["type"] == "Road" + await async_wait_recording_done(hass) + + states = await hass.async_add_executor_job( + get_significant_states, + hass, + dt_util.now(), + None, + hass.states.async_entity_ids(), + ) + assert len(states) == 1 + assert states.get("camera.test_location") + for entity_states in states.values(): + for state in entity_states: + assert "location" not in state.attributes + assert "description" not in state.attributes + assert "type" in state.attributes From 0a1ad8a1199fa5da465a8b748fbd983dc96d9f6a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 10:42:05 +0200 Subject: [PATCH 0813/1151] Add entity translations to Ridwell (#98918) --- homeassistant/components/ridwell/sensor.py | 2 +- homeassistant/components/ridwell/strings.json | 12 ++++++++++++ homeassistant/components/ridwell/switch.py | 6 ++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index 1eba555e955..e4626831d7d 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -27,7 +27,7 @@ ATTR_QUANTITY = "quantity" SENSOR_DESCRIPTION = SensorEntityDescription( key=SENSOR_TYPE_NEXT_PICKUP, - name="Next Ridwell pickup", + translation_key="next_pickup", device_class=SensorDeviceClass.DATE, ) diff --git a/homeassistant/components/ridwell/strings.json b/homeassistant/components/ridwell/strings.json index 3f4cc1806a4..c3cf6365860 100644 --- a/homeassistant/components/ridwell/strings.json +++ b/homeassistant/components/ridwell/strings.json @@ -24,5 +24,17 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "sensor": { + "next_pickup": { + "name": "Next pickup" + } + }, + "switch": { + "opt_in": { + "name": "Opt-in to next pickup" + } + } } } diff --git a/homeassistant/components/ridwell/switch.py b/homeassistant/components/ridwell/switch.py index 7a948f8b883..f47fc1ca0af 100644 --- a/homeassistant/components/ridwell/switch.py +++ b/homeassistant/components/ridwell/switch.py @@ -16,11 +16,9 @@ from .const import DOMAIN from .coordinator import RidwellDataUpdateCoordinator from .entity import RidwellEntity -SWITCH_TYPE_OPT_IN = "opt_in" - SWITCH_DESCRIPTION = SwitchEntityDescription( - key=SWITCH_TYPE_OPT_IN, - name="Opt-in to next pickup", + key="opt_in", + translation_key="opt_in", icon="mdi:calendar-check", ) From f44215d28605144b415fb0f0e7121fed53713fd8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 11:19:16 +0200 Subject: [PATCH 0814/1151] Use snapshot assertion for Brother diagnostics test (#98904) --- .../brother/fixtures/diagnostics_data.json | 66 ---------------- .../brother/snapshots/test_diagnostics.ambr | 75 +++++++++++++++++++ tests/components/brother/test_diagnostics.py | 10 ++- 3 files changed, 81 insertions(+), 70 deletions(-) delete mode 100644 tests/components/brother/fixtures/diagnostics_data.json create mode 100644 tests/components/brother/snapshots/test_diagnostics.ambr diff --git a/tests/components/brother/fixtures/diagnostics_data.json b/tests/components/brother/fixtures/diagnostics_data.json deleted file mode 100644 index fd22f861e8d..00000000000 --- a/tests/components/brother/fixtures/diagnostics_data.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "black_counter": null, - "black_ink": null, - "black_ink_remaining": null, - "black_ink_status": null, - "cyan_counter": null, - "bw_counter": 709, - "belt_unit_remaining_life": 97, - "belt_unit_remaining_pages": 48436, - "black_drum_counter": 1611, - "black_drum_remaining_life": 92, - "black_drum_remaining_pages": 16389, - "black_toner": 80, - "black_toner_remaining": 75, - "black_toner_status": 1, - "color_counter": 902, - "cyan_drum_counter": 1611, - "cyan_drum_remaining_life": 92, - "cyan_drum_remaining_pages": 16389, - "cyan_ink": null, - "cyan_ink_remaining": null, - "cyan_ink_status": null, - "cyan_toner": 10, - "cyan_toner_remaining": 10, - "cyan_toner_status": 1, - "drum_counter": 986, - "drum_remaining_life": 92, - "drum_remaining_pages": 11014, - "drum_status": 1, - "duplex_unit_pages_counter": 538, - "firmware": "1.17", - "fuser_remaining_life": 97, - "fuser_unit_remaining_pages": null, - "image_counter": null, - "laser_remaining_life": null, - "laser_unit_remaining_pages": 48389, - "magenta_counter": null, - "magenta_drum_counter": 1611, - "magenta_drum_remaining_life": 92, - "magenta_drum_remaining_pages": 16389, - "magenta_ink": null, - "magenta_ink_remaining": null, - "magenta_ink_status": null, - "magenta_toner": 10, - "magenta_toner_remaining": 8, - "magenta_toner_status": 2, - "model": "HL-L2340DW", - "page_counter": 986, - "pf_kit_1_remaining_life": 98, - "pf_kit_1_remaining_pages": 48741, - "pf_kit_mp_remaining_life": null, - "pf_kit_mp_remaining_pages": null, - "serial": "0123456789", - "status": "waiting", - "uptime": "2019-09-24T12:14:56+00:00", - "yellow_counter": null, - "yellow_drum_counter": 1611, - "yellow_drum_remaining_life": 92, - "yellow_drum_remaining_pages": 16389, - "yellow_ink": null, - "yellow_ink_remaining": null, - "yellow_ink_status": null, - "yellow_toner": 10, - "yellow_toner_remaining": 2, - "yellow_toner_status": 2 -} diff --git a/tests/components/brother/snapshots/test_diagnostics.ambr b/tests/components/brother/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..1bff613e557 --- /dev/null +++ b/tests/components/brother/snapshots/test_diagnostics.ambr @@ -0,0 +1,75 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'belt_unit_remaining_life': 97, + 'belt_unit_remaining_pages': 48436, + 'black_counter': None, + 'black_drum_counter': 1611, + 'black_drum_remaining_life': 92, + 'black_drum_remaining_pages': 16389, + 'black_ink': None, + 'black_ink_remaining': None, + 'black_ink_status': None, + 'black_toner': 80, + 'black_toner_remaining': 75, + 'black_toner_status': 1, + 'bw_counter': 709, + 'color_counter': 902, + 'cyan_counter': None, + 'cyan_drum_counter': 1611, + 'cyan_drum_remaining_life': 92, + 'cyan_drum_remaining_pages': 16389, + 'cyan_ink': None, + 'cyan_ink_remaining': None, + 'cyan_ink_status': None, + 'cyan_toner': 10, + 'cyan_toner_remaining': 10, + 'cyan_toner_status': 1, + 'drum_counter': 986, + 'drum_remaining_life': 92, + 'drum_remaining_pages': 11014, + 'drum_status': 1, + 'duplex_unit_pages_counter': 538, + 'firmware': '1.17', + 'fuser_remaining_life': 97, + 'fuser_unit_remaining_pages': None, + 'image_counter': None, + 'laser_remaining_life': None, + 'laser_unit_remaining_pages': 48389, + 'magenta_counter': None, + 'magenta_drum_counter': 1611, + 'magenta_drum_remaining_life': 92, + 'magenta_drum_remaining_pages': 16389, + 'magenta_ink': None, + 'magenta_ink_remaining': None, + 'magenta_ink_status': None, + 'magenta_toner': 10, + 'magenta_toner_remaining': 8, + 'magenta_toner_status': 2, + 'model': 'HL-L2340DW', + 'page_counter': 986, + 'pf_kit_1_remaining_life': 98, + 'pf_kit_1_remaining_pages': 48741, + 'pf_kit_mp_remaining_life': None, + 'pf_kit_mp_remaining_pages': None, + 'serial': '0123456789', + 'status': 'waiting', + 'uptime': '2019-09-24T12:14:56+00:00', + 'yellow_counter': None, + 'yellow_drum_counter': 1611, + 'yellow_drum_remaining_life': 92, + 'yellow_drum_remaining_pages': 16389, + 'yellow_ink': None, + 'yellow_ink_remaining': None, + 'yellow_ink_status': None, + 'yellow_toner': 10, + 'yellow_toner_remaining': 2, + 'yellow_toner_status': 2, + }), + 'info': dict({ + 'host': 'localhost', + 'type': 'laser', + }), + }) +# --- diff --git a/tests/components/brother/test_diagnostics.py b/tests/components/brother/test_diagnostics.py index ce09fe13d1a..26ed77931b4 100644 --- a/tests/components/brother/test_diagnostics.py +++ b/tests/components/brother/test_diagnostics.py @@ -3,6 +3,8 @@ from datetime import datetime import json from unittest.mock import Mock, patch +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from homeassistant.util.dt import UTC @@ -14,12 +16,13 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" entry = await init_integration(hass, skip_setup=True) - diagnostics_data = json.loads(load_fixture("diagnostics_data.json", "brother")) test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC) with patch("brother.Brother.initialize"), patch( "brother.datetime", now=Mock(return_value=test_time) @@ -32,5 +35,4 @@ async def test_entry_diagnostics( result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert result["info"] == {"host": "localhost", "type": "laser"} - assert result["data"] == diagnostics_data + assert result == snapshot From f395147f7c8db37f1f85524241689eb3b53e1581 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 11:27:24 +0200 Subject: [PATCH 0815/1151] Move platform specifics out of Solaredge const (#98941) --- homeassistant/components/solaredge/const.py | 172 ---------------- homeassistant/components/solaredge/models.py | 20 -- homeassistant/components/solaredge/sensor.py | 192 +++++++++++++++++- .../components/solaredge/test_coordinator.py | 2 +- 4 files changed, 190 insertions(+), 196 deletions(-) delete mode 100644 homeassistant/components/solaredge/models.py diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 6d95f8b6aec..aa6251ff433 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -2,11 +2,6 @@ from datetime import timedelta import logging -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfPower - -from .models import SolarEdgeSensorEntityDescription - DOMAIN = "solaredge" LOGGER = logging.getLogger(__package__) @@ -24,170 +19,3 @@ POWER_FLOW_UPDATE_DELAY = timedelta(minutes=15) ENERGY_DETAILS_DELAY = timedelta(minutes=15) SCAN_INTERVAL = timedelta(minutes=15) - - -# Supported overview sensors -SENSOR_TYPES = [ - SolarEdgeSensorEntityDescription( - key="lifetime_energy", - json_key="lifeTimeData", - name="Lifetime energy", - icon="mdi:solar-power", - state_class=SensorStateClass.TOTAL, - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - SolarEdgeSensorEntityDescription( - key="energy_this_year", - json_key="lastYearData", - name="Energy this year", - entity_registry_enabled_default=False, - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - SolarEdgeSensorEntityDescription( - key="energy_this_month", - json_key="lastMonthData", - name="Energy this month", - entity_registry_enabled_default=False, - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - SolarEdgeSensorEntityDescription( - key="energy_today", - json_key="lastDayData", - name="Energy today", - entity_registry_enabled_default=False, - icon="mdi:solar-power", - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - SolarEdgeSensorEntityDescription( - key="current_power", - json_key="currentPower", - name="Current Power", - icon="mdi:solar-power", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - ), - SolarEdgeSensorEntityDescription( - key="site_details", - json_key="status", - name="Site details", - entity_registry_enabled_default=False, - ), - SolarEdgeSensorEntityDescription( - key="meters", - json_key="meters", - name="Meters", - entity_registry_enabled_default=False, - ), - SolarEdgeSensorEntityDescription( - key="sensors", - json_key="sensors", - name="Sensors", - entity_registry_enabled_default=False, - ), - SolarEdgeSensorEntityDescription( - key="gateways", - json_key="gateways", - name="Gateways", - entity_registry_enabled_default=False, - ), - SolarEdgeSensorEntityDescription( - key="batteries", - json_key="batteries", - name="Batteries", - entity_registry_enabled_default=False, - ), - SolarEdgeSensorEntityDescription( - key="inverters", - json_key="inverters", - name="Inverters", - entity_registry_enabled_default=False, - ), - SolarEdgeSensorEntityDescription( - key="power_consumption", - json_key="LOAD", - name="Power Consumption", - entity_registry_enabled_default=False, - icon="mdi:flash", - ), - SolarEdgeSensorEntityDescription( - key="solar_power", - json_key="PV", - name="Solar Power", - entity_registry_enabled_default=False, - icon="mdi:solar-power", - ), - SolarEdgeSensorEntityDescription( - key="grid_power", - json_key="GRID", - name="Grid Power", - entity_registry_enabled_default=False, - icon="mdi:power-plug", - ), - SolarEdgeSensorEntityDescription( - key="storage_power", - json_key="STORAGE", - name="Storage Power", - entity_registry_enabled_default=False, - icon="mdi:car-battery", - ), - SolarEdgeSensorEntityDescription( - key="purchased_energy", - json_key="Purchased", - name="Imported Energy", - entity_registry_enabled_default=False, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - SolarEdgeSensorEntityDescription( - key="production_energy", - json_key="Production", - name="Production Energy", - entity_registry_enabled_default=False, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - SolarEdgeSensorEntityDescription( - key="consumption_energy", - json_key="Consumption", - name="Consumption Energy", - entity_registry_enabled_default=False, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - SolarEdgeSensorEntityDescription( - key="selfconsumption_energy", - json_key="SelfConsumption", - name="SelfConsumption Energy", - entity_registry_enabled_default=False, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - SolarEdgeSensorEntityDescription( - key="feedin_energy", - json_key="FeedIn", - name="Exported Energy", - entity_registry_enabled_default=False, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - SolarEdgeSensorEntityDescription( - key="storage_level", - json_key="STORAGE", - name="Storage Level", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - ), -] diff --git a/homeassistant/components/solaredge/models.py b/homeassistant/components/solaredge/models.py deleted file mode 100644 index 57efb88023c..00000000000 --- a/homeassistant/components/solaredge/models.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Models for the SolarEdge integration.""" -from __future__ import annotations - -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription - - -@dataclass -class SolarEdgeSensorEntityRequiredKeyMixin: - """Sensor entity description with json_key for SolarEdge.""" - - json_key: str - - -@dataclass -class SolarEdgeSensorEntityDescription( - SensorEntityDescription, SolarEdgeSensorEntityRequiredKeyMixin -): - """Sensor entity description for SolarEdge.""" diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 3a4b5ad90c2..e1ea7960086 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -1,12 +1,19 @@ """Support for SolarEdge Monitoring API.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any from solaredge import Solaredge -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -14,7 +21,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, SENSOR_TYPES +from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN from .coordinator import ( SolarEdgeDataService, SolarEdgeDetailsDataService, @@ -23,7 +30,186 @@ from .coordinator import ( SolarEdgeOverviewDataService, SolarEdgePowerFlowDataService, ) -from .models import SolarEdgeSensorEntityDescription + + +@dataclass +class SolarEdgeSensorEntityRequiredKeyMixin: + """Sensor entity description with json_key for SolarEdge.""" + + json_key: str + + +@dataclass +class SolarEdgeSensorEntityDescription( + SensorEntityDescription, SolarEdgeSensorEntityRequiredKeyMixin +): + """Sensor entity description for SolarEdge.""" + + +SENSOR_TYPES = [ + SolarEdgeSensorEntityDescription( + key="lifetime_energy", + json_key="lifeTimeData", + name="Lifetime energy", + icon="mdi:solar-power", + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ), + SolarEdgeSensorEntityDescription( + key="energy_this_year", + json_key="lastYearData", + name="Energy this year", + entity_registry_enabled_default=False, + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ), + SolarEdgeSensorEntityDescription( + key="energy_this_month", + json_key="lastMonthData", + name="Energy this month", + entity_registry_enabled_default=False, + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ), + SolarEdgeSensorEntityDescription( + key="energy_today", + json_key="lastDayData", + name="Energy today", + entity_registry_enabled_default=False, + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ), + SolarEdgeSensorEntityDescription( + key="current_power", + json_key="currentPower", + name="Current Power", + icon="mdi:solar-power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + ), + SolarEdgeSensorEntityDescription( + key="site_details", + json_key="status", + name="Site details", + entity_registry_enabled_default=False, + ), + SolarEdgeSensorEntityDescription( + key="meters", + json_key="meters", + name="Meters", + entity_registry_enabled_default=False, + ), + SolarEdgeSensorEntityDescription( + key="sensors", + json_key="sensors", + name="Sensors", + entity_registry_enabled_default=False, + ), + SolarEdgeSensorEntityDescription( + key="gateways", + json_key="gateways", + name="Gateways", + entity_registry_enabled_default=False, + ), + SolarEdgeSensorEntityDescription( + key="batteries", + json_key="batteries", + name="Batteries", + entity_registry_enabled_default=False, + ), + SolarEdgeSensorEntityDescription( + key="inverters", + json_key="inverters", + name="Inverters", + entity_registry_enabled_default=False, + ), + SolarEdgeSensorEntityDescription( + key="power_consumption", + json_key="LOAD", + name="Power Consumption", + entity_registry_enabled_default=False, + icon="mdi:flash", + ), + SolarEdgeSensorEntityDescription( + key="solar_power", + json_key="PV", + name="Solar Power", + entity_registry_enabled_default=False, + icon="mdi:solar-power", + ), + SolarEdgeSensorEntityDescription( + key="grid_power", + json_key="GRID", + name="Grid Power", + entity_registry_enabled_default=False, + icon="mdi:power-plug", + ), + SolarEdgeSensorEntityDescription( + key="storage_power", + json_key="STORAGE", + name="Storage Power", + entity_registry_enabled_default=False, + icon="mdi:car-battery", + ), + SolarEdgeSensorEntityDescription( + key="purchased_energy", + json_key="Purchased", + name="Imported Energy", + entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ), + SolarEdgeSensorEntityDescription( + key="production_energy", + json_key="Production", + name="Production Energy", + entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ), + SolarEdgeSensorEntityDescription( + key="consumption_energy", + json_key="Consumption", + name="Consumption Energy", + entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ), + SolarEdgeSensorEntityDescription( + key="selfconsumption_energy", + json_key="SelfConsumption", + name="SelfConsumption Energy", + entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ), + SolarEdgeSensorEntityDescription( + key="feedin_energy", + json_key="FeedIn", + name="Exported Energy", + entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + ), + SolarEdgeSensorEntityDescription( + key="storage_level", + json_key="STORAGE", + name="Storage Level", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + ), +] async def async_setup_entry( diff --git a/tests/components/solaredge/test_coordinator.py b/tests/components/solaredge/test_coordinator.py index 5d9656b05d8..7b746a2ae05 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -6,8 +6,8 @@ from homeassistant.components.solaredge.const import ( DEFAULT_NAME, DOMAIN, OVERVIEW_UPDATE_DELAY, - SENSOR_TYPES, ) +from homeassistant.components.solaredge.sensor import SENSOR_TYPES from homeassistant.const import CONF_API_KEY, CONF_NAME, STATE_UNKNOWN from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util From c47983621c4c28dcd9418ae3f83653ed2edc774d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Aug 2023 11:28:20 +0200 Subject: [PATCH 0816/1151] Teach CoordinatorWeatherEntity about multiple coordinators (#98830) --- homeassistant/components/aemet/weather.py | 12 +- .../components/environment_canada/weather.py | 12 +- homeassistant/components/met/weather.py | 12 +- .../components/met_eireann/weather.py | 12 +- homeassistant/components/nws/__init__.py | 22 +- homeassistant/components/nws/weather.py | 148 ++++-------- .../components/open_meteo/weather.py | 9 +- .../components/tomorrowio/weather.py | 12 +- homeassistant/components/weather/__init__.py | 228 +++++++++++++++++- homeassistant/helpers/update_coordinator.py | 23 ++ 10 files changed, 332 insertions(+), 158 deletions(-) diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index 6affc39c7a8..60289f4723a 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -11,8 +11,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, DOMAIN as WEATHER_DOMAIN, - CoordinatorWeatherEntity, Forecast, + SingleCoordinatorWeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -22,7 +22,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -110,7 +110,7 @@ async def async_setup_entry( async_add_entities(entities, False) -class AemetWeather(CoordinatorWeatherEntity[WeatherUpdateCoordinator]): +class AemetWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION @@ -160,11 +160,13 @@ class AemetWeather(CoordinatorWeatherEntity[WeatherUpdateCoordinator]): """Return the forecast array.""" return self._forecast(self._forecast_mode) - async def async_forecast_daily(self) -> list[Forecast]: + @callback + def _async_forecast_daily(self) -> list[Forecast]: """Return the daily forecast in native units.""" return self._forecast(FORECAST_MODE_DAILY) - async def async_forecast_hourly(self) -> list[Forecast]: + @callback + def _async_forecast_hourly(self) -> list[Forecast]: """Return the hourly forecast in native units.""" return self._forecast(FORECAST_MODE_HOURLY) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 67cb2df5473..b4b5d27f45f 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -22,8 +22,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, DOMAIN as WEATHER_DOMAIN, - CoordinatorWeatherEntity, Forecast, + SingleCoordinatorWeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -33,7 +33,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util @@ -86,7 +86,7 @@ def _calculate_unique_id(config_entry_unique_id: str | None, hourly: bool) -> st return f"{config_entry_unique_id}{'-hourly' if hourly else '-daily'}" -class ECWeather(CoordinatorWeatherEntity): +class ECWeather(SingleCoordinatorWeatherEntity): """Representation of a weather condition.""" _attr_has_entity_name = True @@ -182,11 +182,13 @@ class ECWeather(CoordinatorWeatherEntity): """Return the forecast array.""" return get_forecast(self.ec_data, self._hourly) - async def async_forecast_daily(self) -> list[Forecast] | None: + @callback + def _async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" return get_forecast(self.ec_data, False) - async def async_forecast_hourly(self) -> list[Forecast] | None: + @callback + def _async_forecast_hourly(self) -> list[Forecast] | None: """Return the hourly forecast in native units.""" return get_forecast(self.ec_data, True) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index c697befd01f..a5a0d34d4eb 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -16,8 +16,8 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_GUST_SPEED, ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, - CoordinatorWeatherEntity, Forecast, + SingleCoordinatorWeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -30,7 +30,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -91,7 +91,7 @@ def format_condition(condition: str) -> str: return condition -class MetWeather(CoordinatorWeatherEntity[MetDataUpdateCoordinator]): +class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]): """Implementation of a Met.no weather condition.""" _attr_attribution = ( @@ -239,11 +239,13 @@ class MetWeather(CoordinatorWeatherEntity[MetDataUpdateCoordinator]): """Return the forecast array.""" return self._forecast(self._hourly) - async def async_forecast_daily(self) -> list[Forecast] | None: + @callback + 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: + @callback + def _async_forecast_hourly(self) -> list[Forecast] | None: """Return the hourly forecast in native units.""" return self._forecast(True) diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index a69c1f24c08..3a45a74c36b 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -7,8 +7,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TIME, DOMAIN as WEATHER_DOMAIN, - CoordinatorWeatherEntity, Forecast, + SingleCoordinatorWeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -21,7 +21,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -75,7 +75,7 @@ def _calculate_unique_id(config: MappingProxyType[str, Any], hourly: bool) -> st class MetEireannWeather( - CoordinatorWeatherEntity[DataUpdateCoordinator[MetEireannWeatherData]] + SingleCoordinatorWeatherEntity[DataUpdateCoordinator[MetEireannWeatherData]] ): """Implementation of a Met Éireann weather condition.""" @@ -182,11 +182,13 @@ class MetEireannWeather( """Return the forecast array.""" return self._forecast(self._hourly) - async def async_forecast_daily(self) -> list[Forecast]: + @callback + 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]: + @callback + def _async_forecast_hourly(self) -> list[Forecast]: """Return the hourly forecast in native units.""" return self._forecast(True) diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index a6af045776f..063ecdabab2 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers import debounce from homeassistant.helpers.aiohttp_client import async_get_clientsession 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.helpers.update_coordinator import TimestampDataUpdateCoordinator from homeassistant.util.dt import utcnow from .const import CONF_STATION, DOMAIN, UPDATE_TIME_PERIOD @@ -45,7 +45,7 @@ class NWSData: coordinator_forecast_hourly: NwsDataUpdateCoordinator -class NwsDataUpdateCoordinator(DataUpdateCoordinator[None]): +class NwsDataUpdateCoordinator(TimestampDataUpdateCoordinator[None]): """NWS data update coordinator. Implements faster data update intervals for failed updates and exposes a last successful update time. @@ -72,7 +72,6 @@ class NwsDataUpdateCoordinator(DataUpdateCoordinator[None]): request_refresh_debouncer=request_refresh_debouncer, ) self.failed_update_interval = failed_update_interval - self.last_update_success_time: datetime.datetime | None = None @callback def _schedule_refresh(self) -> None: @@ -98,23 +97,6 @@ class NwsDataUpdateCoordinator(DataUpdateCoordinator[None]): utcnow().replace(microsecond=0) + update_interval, ) - async def _async_refresh( - self, - log_failures: bool = True, - raise_on_auth_failed: bool = False, - scheduled: bool = False, - raise_on_entry_error: bool = False, - ) -> None: - """Refresh data.""" - await super()._async_refresh( - log_failures, - raise_on_auth_failed, - scheduled, - raise_on_entry_error, - ) - if self.last_update_success: - self.last_update_success_time = utcnow() - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a National Weather Service entry.""" diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index dec7e9bf3b3..0f594133f69 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -1,9 +1,8 @@ """Support for NWS weather service.""" from __future__ import annotations -from collections.abc import Callable from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import TYPE_CHECKING, Any, cast from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, @@ -18,8 +17,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, DOMAIN as WEATHER_DOMAIN, + CoordinatorWeatherEntity, Forecast, - WeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -38,13 +37,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter -from . import ( - DEFAULT_SCAN_INTERVAL, - NWSData, - NwsDataUpdateCoordinator, - base_unique_id, - device_info, -) +from . import NWSData, base_unique_id, device_info from .const import ( ATTR_FORECAST_DETAILED_DESCRIPTION, ATTRIBUTION, @@ -120,7 +113,7 @@ def _calculate_unique_id(entry_data: MappingProxyType[str, Any], mode: str) -> s return f"{base_unique_id(latitude, longitude)}_{mode}" -class NWSWeather(WeatherEntity): +class NWSWeather(CoordinatorWeatherEntity): """Representation of a weather condition.""" _attr_attribution = ATTRIBUTION @@ -136,19 +129,21 @@ class NWSWeather(WeatherEntity): mode: str, ) -> None: """Initialise the platform with a data instance and station name.""" + super().__init__( + observation_coordinator=nws_data.coordinator_observation, + hourly_coordinator=nws_data.coordinator_forecast_hourly, + twice_daily_coordinator=nws_data.coordinator_forecast, + hourly_forecast_valid=FORECAST_VALID_TIME, + twice_daily_forecast_valid=FORECAST_VALID_TIME, + ) self.nws = nws_data.api self.latitude = entry_data[CONF_LATITUDE] self.longitude = entry_data[CONF_LONGITUDE] - self.coordinator_forecast_hourly = nws_data.coordinator_forecast_hourly - self.coordinator_forecast_twice_daily = nws_data.coordinator_forecast - self.coordinator_observation = nws_data.coordinator_observation if mode == DAYNIGHT: self.coordinator_forecast_legacy = nws_data.coordinator_forecast else: self.coordinator_forecast_legacy = nws_data.coordinator_forecast_hourly self.station = self.nws.station - self._unsub_hourly_forecast: Callable[[], None] | None = None - self._unsub_twice_daily_forecast: Callable[[], None] | None = None self.mode = mode @@ -161,76 +156,42 @@ class NWSWeather(WeatherEntity): async def async_added_to_hass(self) -> None: """Set up a listener and load data.""" + await super().async_added_to_hass() self.async_on_remove( - self.coordinator_observation.async_add_listener(self._update_callback) - ) - self.async_on_remove( - self.coordinator_forecast_legacy.async_add_listener(self._update_callback) - ) - self.async_on_remove(self._remove_hourly_forecast_listener) - self.async_on_remove(self._remove_twice_daily_forecast_listener) - self._update_callback() - - def _remove_hourly_forecast_listener(self) -> None: - """Remove hourly forecast listener.""" - if self._unsub_hourly_forecast: - self._unsub_hourly_forecast() - self._unsub_hourly_forecast = None - - def _remove_twice_daily_forecast_listener(self) -> None: - """Remove hourly forecast listener.""" - if self._unsub_twice_daily_forecast: - self._unsub_twice_daily_forecast() - self._unsub_twice_daily_forecast = None - - @callback - def _async_subscription_started( - self, - forecast_type: Literal["daily", "hourly", "twice_daily"], - ) -> None: - """Start subscription to forecast_type.""" - if forecast_type == "hourly" and self.mode == DAYNIGHT: - self._unsub_hourly_forecast = ( - self.coordinator_forecast_hourly.async_add_listener( - self._update_callback - ) + self.coordinator_forecast_legacy.async_add_listener( + self._handle_legacy_forecast_coordinator_update ) - return - if forecast_type == "twice_daily" and self.mode == HOURLY: - self._unsub_twice_daily_forecast = ( - self.coordinator_forecast_twice_daily.async_add_listener( - self._update_callback - ) - ) - return + ) + # Load initial data from coordinators + self._handle_coordinator_update() + self._handle_hourly_forecast_coordinator_update() + self._handle_twice_daily_forecast_coordinator_update() + self._handle_legacy_forecast_coordinator_update() @callback - def _async_subscription_ended( - self, - forecast_type: Literal["daily", "hourly", "twice_daily"], - ) -> None: - """End subscription to forecast_type.""" - if forecast_type == "hourly" and self.mode == DAYNIGHT: - self._remove_hourly_forecast_listener() - if forecast_type == "twice_daily" and self.mode == HOURLY: - self._remove_twice_daily_forecast_listener() - - @callback - def _update_callback(self) -> None: + def _handle_coordinator_update(self) -> None: """Load data from integration.""" self.observation = self.nws.observation + self.async_write_ha_state() + + @callback + def _handle_hourly_forecast_coordinator_update(self) -> None: + """Handle updated data from the hourly forecast coordinator.""" self._forecast_hourly = self.nws.forecast_hourly + + @callback + def _handle_twice_daily_forecast_coordinator_update(self) -> None: + """Handle updated data from the twice daily forecast coordinator.""" self._forecast_twice_daily = self.nws.forecast + + @callback + def _handle_legacy_forecast_coordinator_update(self) -> None: + """Handle updated data from the legacy forecast coordinator.""" if self.mode == DAYNIGHT: self._forecast_legacy = self.nws.forecast else: self._forecast_legacy = self.nws.forecast_hourly - self.async_write_ha_state() - assert self.platform.config_entry - self.platform.config_entry.async_create_task( - self.hass, self.async_update_listeners(("hourly", "twice_daily")) - ) @property def name(self) -> str: @@ -373,50 +334,29 @@ class NWSWeather(WeatherEntity): """Return forecast.""" return self._forecast(self._forecast_legacy, self.mode) - async def _async_forecast( - self, - coordinator: NwsDataUpdateCoordinator, - nws_forecast: list[dict[str, Any]] | None, - mode: str, - ) -> list[Forecast] | None: - """Refresh stale forecast and return it in native units.""" - if ( - not (last_success_time := coordinator.last_update_success_time) - or utcnow() - last_success_time >= DEFAULT_SCAN_INTERVAL - ): - await coordinator.async_refresh() - if ( - not (last_success_time := coordinator.last_update_success_time) - or utcnow() - last_success_time >= FORECAST_VALID_TIME - ): - return None - return self._forecast(nws_forecast, mode) - - async def async_forecast_hourly(self) -> list[Forecast] | None: + @callback + def _async_forecast_hourly(self) -> list[Forecast] | None: """Return the hourly forecast in native units.""" - coordinator = self.coordinator_forecast_hourly - return await self._async_forecast(coordinator, self._forecast_hourly, HOURLY) + return self._forecast(self._forecast_hourly, HOURLY) - async def async_forecast_twice_daily(self) -> list[Forecast] | None: + @callback + def _async_forecast_twice_daily(self) -> list[Forecast] | None: """Return the twice daily forecast in native units.""" - coordinator = self.coordinator_forecast_twice_daily - return await self._async_forecast( - coordinator, self._forecast_twice_daily, DAYNIGHT - ) + return self._forecast(self._forecast_twice_daily, DAYNIGHT) @property def available(self) -> bool: """Return if state is available.""" last_success = ( - self.coordinator_observation.last_update_success + self.coordinator.last_update_success and self.coordinator_forecast_legacy.last_update_success ) if ( - self.coordinator_observation.last_update_success_time + self.coordinator.last_update_success_time and self.coordinator_forecast_legacy.last_update_success_time ): last_success_time = ( - utcnow() - self.coordinator_observation.last_update_success_time + utcnow() - self.coordinator.last_update_success_time < OBSERVATION_VALID_TIME and utcnow() - self.coordinator_forecast_legacy.last_update_success_time < FORECAST_VALID_TIME @@ -430,7 +370,7 @@ class NWSWeather(WeatherEntity): Only used by the generic entity update service. """ - await self.coordinator_observation.async_request_refresh() + await self.coordinator.async_request_refresh() await self.coordinator_forecast_legacy.async_request_refresh() @property diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index b874e066031..3d66422fd60 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -4,13 +4,13 @@ from __future__ import annotations from open_meteo import Forecast as OpenMeteoForecast from homeassistant.components.weather import ( - CoordinatorWeatherEntity, Forecast, + SingleCoordinatorWeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfPrecipitationDepth, UnitOfSpeed, UnitOfTemperature -from homeassistant.core import HomeAssistant +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 DataUpdateCoordinator @@ -29,7 +29,7 @@ async def async_setup_entry( class OpenMeteoWeatherEntity( - CoordinatorWeatherEntity[DataUpdateCoordinator[OpenMeteoForecast]] + SingleCoordinatorWeatherEntity[DataUpdateCoordinator[OpenMeteoForecast]] ): """Defines an Open-Meteo weather entity.""" @@ -124,6 +124,7 @@ class OpenMeteoWeatherEntity( return forecasts - async def async_forecast_daily(self) -> list[Forecast] | None: + @callback + def _async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" return self.forecast diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index f88887e64dd..b0b82d81463 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -17,8 +17,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, DOMAIN as WEATHER_DOMAIN, - CoordinatorWeatherEntity, Forecast, + SingleCoordinatorWeatherEntity, WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -31,7 +31,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import is_up @@ -93,7 +93,7 @@ def _calculate_unique_id(config_entry_unique_id: str | None, forecast_type: str) return f"{config_entry_unique_id}_{forecast_type}" -class TomorrowioWeatherEntity(TomorrowioEntity, CoordinatorWeatherEntity): +class TomorrowioWeatherEntity(TomorrowioEntity, SingleCoordinatorWeatherEntity): """Entity that talks to Tomorrow.io v4 API to retrieve weather data.""" _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS @@ -303,10 +303,12 @@ class TomorrowioWeatherEntity(TomorrowioEntity, CoordinatorWeatherEntity): """Return the forecast array.""" return self._forecast(self.forecast_type) - async def async_forecast_daily(self) -> list[Forecast] | None: + @callback + def _async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" return self._forecast(DAILY) - async def async_forecast_hourly(self) -> list[Forecast] | None: + @callback + def _async_forecast_hourly(self) -> list[Forecast] | None: """Return the hourly forecast in native units.""" return self._forecast(HOURLY) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index eb137f06d7b..d73d00ec9df 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -6,9 +6,20 @@ from collections.abc import Callable, Iterable from contextlib import suppress from dataclasses import dataclass from datetime import timedelta +from functools import partial import inspect import logging -from typing import Any, Final, Literal, Required, TypedDict, TypeVar, final +from typing import ( + Any, + Final, + Generic, + Literal, + Required, + TypedDict, + TypeVar, + cast, + final, +) import voluptuous as vol @@ -40,7 +51,9 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + TimestampDataUpdateCoordinator, ) +from homeassistant.util.dt import utcnow from homeassistant.util.json import JsonValueType from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM @@ -121,8 +134,22 @@ ROUNDING_PRECISION = 2 SERVICE_GET_FORECAST: Final = "get_forecast" -_DataUpdateCoordinatorT = TypeVar( - "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" +_ObservationUpdateCoordinatorT = TypeVar( + "_ObservationUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" +) + +# Note: +# Mypy bug https://github.com/python/mypy/issues/9424 prevents us from making the +# forecast cooordinators optional, bound=TimestampDataUpdateCoordinator[Any] | None + +_DailyForecastUpdateCoordinatorT = TypeVar( + "_DailyForecastUpdateCoordinatorT", bound="TimestampDataUpdateCoordinator[Any]" +) +_HourlyForecastUpdateCoordinatorT = TypeVar( + "_HourlyForecastUpdateCoordinatorT", bound="TimestampDataUpdateCoordinator[Any]" +) +_TwiceDailyForecastUpdateCoordinatorT = TypeVar( + "_TwiceDailyForecastUpdateCoordinatorT", bound="TimestampDataUpdateCoordinator[Any]" ) # mypy: disallow-any-generics @@ -1187,9 +1214,200 @@ async def async_get_forecast_service( class CoordinatorWeatherEntity( - CoordinatorEntity[_DataUpdateCoordinatorT], WeatherEntity + CoordinatorEntity[_ObservationUpdateCoordinatorT], + WeatherEntity, + Generic[ + _ObservationUpdateCoordinatorT, + _DailyForecastUpdateCoordinatorT, + _HourlyForecastUpdateCoordinatorT, + _TwiceDailyForecastUpdateCoordinatorT, + ], ): - """A class for weather entities using a single DataUpdateCoordinator.""" + """A class for weather entities using DataUpdateCoordinators.""" + + def __init__( + self, + observation_coordinator: _ObservationUpdateCoordinatorT, + *, + context: Any = None, + daily_coordinator: _DailyForecastUpdateCoordinatorT | None = None, + hourly_coordinator: _DailyForecastUpdateCoordinatorT | None = None, + twice_daily_coordinator: _DailyForecastUpdateCoordinatorT | None = None, + daily_forecast_valid: timedelta | None = None, + hourly_forecast_valid: timedelta | None = None, + twice_daily_forecast_valid: timedelta | None = None, + ) -> None: + """Initialize.""" + super().__init__(observation_coordinator, context) + self.forecast_coordinators = { + "daily": daily_coordinator, + "hourly": hourly_coordinator, + "twice_daily": twice_daily_coordinator, + } + self.forecast_valid = { + "daily": daily_forecast_valid, + "hourly": hourly_forecast_valid, + "twice_daily": twice_daily_forecast_valid, + } + self.unsub_forecast: dict[str, Callable[[], None] | None] = { + "daily": None, + "hourly": None, + "twice_daily": None, + } + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove(partial(self._remove_forecast_listener, "daily")) + self.async_on_remove(partial(self._remove_forecast_listener, "hourly")) + self.async_on_remove(partial(self._remove_forecast_listener, "twice_daily")) + + def _remove_forecast_listener( + self, forecast_type: Literal["daily", "hourly", "twice_daily"] + ) -> None: + """Remove weather forecast listener.""" + if unsub_fn := self.unsub_forecast[forecast_type]: + unsub_fn() + self.unsub_forecast[forecast_type] = None + + @callback + def _async_subscription_started( + self, + forecast_type: Literal["daily", "hourly", "twice_daily"], + ) -> None: + """Start subscription to forecast_type.""" + if not (coordinator := self.forecast_coordinators[forecast_type]): + return + self.unsub_forecast[forecast_type] = coordinator.async_add_listener( + partial(self._handle_forecast_update, forecast_type) + ) + + @callback + def _handle_daily_forecast_coordinator_update(self) -> None: + """Handle updated data from the daily forecast coordinator.""" + + @callback + def _handle_hourly_forecast_coordinator_update(self) -> None: + """Handle updated data from the hourly forecast coordinator.""" + + @callback + def _handle_twice_daily_forecast_coordinator_update(self) -> None: + """Handle updated data from the twice daily forecast coordinator.""" + + @final + @callback + def _handle_forecast_update( + self, forecast_type: Literal["daily", "hourly", "twice_daily"] + ) -> None: + """Update forecast data.""" + coordinator = self.forecast_coordinators[forecast_type] + assert coordinator + assert coordinator.config_entry is not None + getattr(self, f"_handle_{forecast_type}_forecast_coordinator_update")() + coordinator.config_entry.async_create_task( + self.hass, self.async_update_listeners((forecast_type,)) + ) + + @callback + def _async_subscription_ended( + self, + forecast_type: Literal["daily", "hourly", "twice_daily"], + ) -> None: + """End subscription to forecast_type.""" + self._remove_forecast_listener(forecast_type) + + @final + async def _async_refresh_forecast( + self, + coordinator: TimestampDataUpdateCoordinator[Any], + forecast_valid_time: timedelta | None, + ) -> bool: + """Refresh stale forecast if needed.""" + if coordinator.update_interval is None: + return True + if forecast_valid_time is None: + forecast_valid_time = coordinator.update_interval + if ( + not (last_success_time := coordinator.last_update_success_time) + or utcnow() - last_success_time >= coordinator.update_interval + ): + await coordinator.async_refresh() + if ( + not (last_success_time := coordinator.last_update_success_time) + or utcnow() - last_success_time >= forecast_valid_time + ): + return False + return True + + @callback + def _async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + raise NotImplementedError + + @callback + def _async_forecast_hourly(self) -> list[Forecast] | None: + """Return the hourly forecast in native units.""" + raise NotImplementedError + + @callback + def _async_forecast_twice_daily(self) -> list[Forecast] | None: + """Return the twice daily forecast in native units.""" + raise NotImplementedError + + @final + async def _async_forecast( + self, forecast_type: Literal["daily", "hourly", "twice_daily"] + ) -> list[Forecast] | None: + """Return the forecast in native units.""" + coordinator = self.forecast_coordinators[forecast_type] + if coordinator and not await self._async_refresh_forecast( + coordinator, self.forecast_valid[forecast_type] + ): + return None + return cast( + list[Forecast] | None, getattr(self, f"_async_forecast_{forecast_type}")() + ) + + @final + async def async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + return await self._async_forecast("daily") + + @final + async def async_forecast_hourly(self) -> list[Forecast] | None: + """Return the hourly forecast in native units.""" + return await self._async_forecast("hourly") + + @final + async def async_forecast_twice_daily(self) -> list[Forecast] | None: + """Return the twice daily forecast in native units.""" + return await self._async_forecast("twice_daily") + + +class SingleCoordinatorWeatherEntity( + CoordinatorWeatherEntity[ + _ObservationUpdateCoordinatorT, + TimestampDataUpdateCoordinator[None], + TimestampDataUpdateCoordinator[None], + TimestampDataUpdateCoordinator[None], + ], +): + """A class for weather entities using a single DataUpdateCoordinators. + + This class is added as a convenience because: + - Deriving from CoordinatorWeatherEntity requires specifying all type parameters + until we upgrade to Python 3.12 which supports defaults + - Mypy bug https://github.com/python/mypy/issues/9424 prevents us from making the + forecast cooordinator type vars optional + """ + + def __init__( + self, + coordinator: _ObservationUpdateCoordinatorT, + context: Any = None, + ) -> None: + """Initialize.""" + super().__init__(coordinator, context=context) @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 8057e77de4f..a050c0da9e4 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -419,6 +419,29 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): self.async_update_listeners() +class TimestampDataUpdateCoordinator(DataUpdateCoordinator[_DataT]): + """DataUpdateCoordinator which keeps track of the last successful update.""" + + last_update_success_time: datetime | None = None + + async def _async_refresh( + self, + log_failures: bool = True, + raise_on_auth_failed: bool = False, + scheduled: bool = False, + raise_on_entry_error: bool = False, + ) -> None: + """Refresh data.""" + await super()._async_refresh( + log_failures, + raise_on_auth_failed, + scheduled, + raise_on_entry_error, + ) + if self.last_update_success: + self.last_update_success_time = utcnow() + + class BaseCoordinatorEntity(entity.Entity, Generic[_BaseDataUpdateCoordinatorT]): """Base class for all Coordinator entities.""" From 577f545113659f77b5d96d942e1c04ec87512b2a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 11:43:10 +0200 Subject: [PATCH 0817/1151] Add entity translations to Rachio (#98917) --- .../components/rachio/binary_sensor.py | 34 +++---------------- homeassistant/components/rachio/strings.json | 15 ++++++++ homeassistant/components/rachio/switch.py | 33 ++++-------------- 3 files changed, 27 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index f1c515d37f7..029b1bac6e3 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -43,7 +43,6 @@ async def async_setup_entry( """Set up the Rachio binary sensors.""" entities = await hass.async_add_executor_job(_create_entities, hass, config_entry) async_add_entities(entities) - _LOGGER.debug("%d Rachio binary sensor(s) added", len(entities)) def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]: @@ -58,6 +57,8 @@ def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Ent class RachioControllerBinarySensor(RachioDevice, BinarySensorEntity): """Represent a binary sensor that reflects a Rachio state.""" + _attr_has_entity_name = True + def __init__(self, controller): """Set up a new Rachio controller binary sensor.""" super().__init__(controller) @@ -86,26 +87,13 @@ class RachioControllerBinarySensor(RachioDevice, BinarySensorEntity): class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): """Represent a binary sensor that reflects if the controller is online.""" - @property - def name(self) -> str: - """Return the name of this sensor including the controller name.""" - return self._controller.name + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY @property def unique_id(self) -> str: """Return a unique id for this entity.""" return f"{self._controller.controller_id}-online" - @property - def device_class(self) -> BinarySensorDeviceClass: - """Return the class of this device, from BinarySensorDeviceClass.""" - return BinarySensorDeviceClass.CONNECTIVITY - - @property - def icon(self) -> str: - """Return the name of an icon for this sensor.""" - return "mdi:wifi-strength-4" if self.is_on else "mdi:wifi-strength-off-outline" - @callback def _async_handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" @@ -132,26 +120,14 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): class RachioRainSensor(RachioControllerBinarySensor): """Represent a binary sensor that reflects the status of the rain sensor.""" - @property - def name(self) -> str: - """Return the name of this sensor including the controller name.""" - return f"{self._controller.name} rain sensor" + _attr_device_class = BinarySensorDeviceClass.MOISTURE + _attr_translation_key = "rain" @property def unique_id(self) -> str: """Return a unique id for this entity.""" return f"{self._controller.controller_id}-rain_sensor" - @property - def device_class(self) -> BinarySensorDeviceClass: - """Return the class of this device.""" - return BinarySensorDeviceClass.MOISTURE - - @property - def icon(self) -> str: - """Return the icon for this sensor.""" - return "mdi:water" if self.is_on else "mdi:water-off" - @callback def _async_handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json index 2132cab8682..560c300db17 100644 --- a/homeassistant/components/rachio/strings.json +++ b/homeassistant/components/rachio/strings.json @@ -27,6 +27,21 @@ } } }, + "entity": { + "binary_sensor": { + "rain": { + "name": "Rain" + } + }, + "switch": { + "standby": { + "name": "Standby" + }, + "rain_delay": { + "name": "Rain delay" + } + } + }, "services": { "set_zone_moisture_percent": { "name": "Set zone moisture percent", diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index c04a1a09f81..0557a2bdb19 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -109,7 +109,6 @@ async def async_setup_entry( has_flex_sched = True async_add_entities(entities) - _LOGGER.debug("%d Rachio switch(es) added", len(entities)) def start_multiple(service: ServiceCall) -> None: """Service to start multiple zones in sequence.""" @@ -173,7 +172,6 @@ def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Ent entities.append(RachioZone(person, controller, zone, current_schedule)) for sched in schedules + flex_schedules: entities.append(RachioSchedule(person, controller, sched, current_schedule)) - _LOGGER.debug("Added %s", entities) return entities @@ -185,11 +183,6 @@ class RachioSwitch(RachioDevice, SwitchEntity): super().__init__(controller) self._state = None - @property - def name(self) -> str: - """Get a name for this switch.""" - return f"Switch on {self._controller.name}" - @property def is_on(self) -> bool: """Return whether the switch is currently on.""" @@ -213,21 +206,15 @@ class RachioSwitch(RachioDevice, SwitchEntity): class RachioStandbySwitch(RachioSwitch): """Representation of a standby status/button.""" - @property - def name(self) -> str: - """Return the name of the standby switch.""" - return f"{self._controller.name} in standby mode" + _attr_has_entity_name = True + _attr_translation_key = "standby" + _attr_icon = "mdi:power" @property def unique_id(self) -> str: """Return a unique id by combining controller id and purpose.""" return f"{self._controller.controller_id}-standby" - @property - def icon(self) -> str: - """Return an icon for the standby switch.""" - return "mdi:power" - @callback def _async_handle_update(self, *args, **kwargs) -> None: """Update the state using webhook data.""" @@ -263,26 +250,20 @@ class RachioStandbySwitch(RachioSwitch): class RachioRainDelay(RachioSwitch): """Representation of a rain delay status/switch.""" + _attr_has_entity_name = True + _attr_translation_key = "rain_delay" + _attr_icon = "mdi:camera-timer" + def __init__(self, controller): """Set up a Rachio rain delay switch.""" self._cancel_update = None super().__init__(controller) - @property - def name(self) -> str: - """Return the name of the switch.""" - return f"{self._controller.name} rain delay" - @property def unique_id(self) -> str: """Return a unique id by combining controller id and purpose.""" return f"{self._controller.controller_id}-delay" - @property - def icon(self) -> str: - """Return an icon for rain delay.""" - return "mdi:camera-timer" - @callback def _async_handle_update(self, *args, **kwargs) -> None: """Update the state using webhook data.""" From 3b31c58ebae037d1c257be0ebc4bdc65e86fd8c8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 24 Aug 2023 11:44:04 +0200 Subject: [PATCH 0818/1151] Add coordinator test for Yale Smart Living (#98638) --- .coveragerc | 1 - tests/components/yale_smart_alarm/conftest.py | 6 +- .../yale_smart_alarm/fixtures/get_all.json | 908 +++++++++++++++- .../snapshots/test_diagnostics.ambr | 968 ++++++++++++++++++ .../yale_smart_alarm/test_coordinator.py | 123 +++ .../yale_smart_alarm/test_diagnostics.py | 8 +- 6 files changed, 2001 insertions(+), 13 deletions(-) create mode 100644 tests/components/yale_smart_alarm/test_coordinator.py diff --git a/.coveragerc b/.coveragerc index 7d8147ab648..5155cac79f1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1507,7 +1507,6 @@ omit = homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/binary_sensor.py homeassistant/components/yale_smart_alarm/button.py - homeassistant/components/yale_smart_alarm/coordinator.py homeassistant/components/yale_smart_alarm/entity.py homeassistant/components/yale_smart_alarm/lock.py homeassistant/components/yalexs_ble/__init__.py diff --git a/tests/components/yale_smart_alarm/conftest.py b/tests/components/yale_smart_alarm/conftest.py index 144a24a4897..c3f5fcf74b8 100644 --- a/tests/components/yale_smart_alarm/conftest.py +++ b/tests/components/yale_smart_alarm/conftest.py @@ -3,7 +3,7 @@ from __future__ import annotations import json from typing import Any -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from yalesmartalarmclient.const import YALE_STATE_ARM_FULL @@ -26,7 +26,7 @@ OPTIONS_CONFIG = {"lock_code_digits": 6} @pytest.fixture async def load_config_entry( hass: HomeAssistant, load_json: dict[str, Any] -) -> MockConfigEntry: +) -> tuple[MockConfigEntry, Mock]: """Set up the Yale Smart Living integration in Home Assistant.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -52,7 +52,7 @@ async def load_config_entry( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - return config_entry + return (config_entry, client) @pytest.fixture(name="load_json", scope="session") diff --git a/tests/components/yale_smart_alarm/fixtures/get_all.json b/tests/components/yale_smart_alarm/fixtures/get_all.json index 08f60fafd3f..0878cbf9c6a 100644 --- a/tests/components/yale_smart_alarm/fixtures/get_all.json +++ b/tests/components/yale_smart_alarm/fixtures/get_all.json @@ -4,7 +4,7 @@ "area": "1", "no": "1", "rf": null, - "address": "123", + "address": "1111", "type": "device_type.door_lock", "name": "Device1", "status1": "device_status.lock", @@ -48,13 +48,461 @@ "group_id": null, "group_name": null, "bypass": "0", - "device_id": "123", + "device_id": "1111", "status_temp_format": "C", "type_no": "72", "device_group": "002", "status_fault": [], "status_open": ["device_status.lock"], "trigger_by_zone": [] + }, + { + "area": "1", + "no": "2", + "rf": null, + "address": "2222", + "type": "device_type.door_lock", + "name": "Device2", + "status1": "device_status.unlock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:02", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": null, + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "2222", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": [], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "3", + "rf": null, + "address": "3333", + "type": "device_type.door_lock", + "name": "Device3", + "status1": "device_status.lock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:03", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": null, + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "3333", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.lock"], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "4", + "rf": null, + "address": "RF4", + "type": "device_type.door_contact", + "name": "Device4", + "status1": "device_status.dc_close", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "0", + "mac": "00:00:00:00:04", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "", + "minigw_syncing": "", + "minigw_configuration_data": "", + "minigw_product_data": "", + "minigw_lock_status": "", + "minigw_number_of_credentials_supported": "", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "4444", + "status_temp_format": "C", + "type_no": "4", + "device_group": "000", + "status_fault": [], + "status_open": ["device_status.dc_close"], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "5", + "rf": null, + "address": "RF5", + "type": "device_type.door_contact", + "name": "Device5", + "status1": "device_status.dc_open", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "0", + "mac": "00:00:00:00:05", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "", + "minigw_syncing": "", + "minigw_configuration_data": "", + "minigw_product_data": "", + "minigw_lock_status": "", + "minigw_number_of_credentials_supported": "", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "5555", + "status_temp_format": "C", + "type_no": "4", + "device_group": "000", + "status_fault": [], + "status_open": ["device_status.dc_open"], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "6", + "rf": null, + "address": "RF6", + "type": "device_type.door_contact", + "name": "Device6", + "status1": "unknwon", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "0", + "mac": "REDACTED", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "", + "minigw_syncing": "", + "minigw_configuration_data": "", + "minigw_product_data": "", + "minigw_lock_status": "", + "minigw_number_of_credentials_supported": "", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "6666", + "status_temp_format": "C", + "type_no": "4", + "device_group": "000", + "status_fault": [], + "status_open": [], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "7", + "rf": null, + "address": "7777", + "type": "device_type.door_lock", + "name": "Device7", + "status1": "device_status.lock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:07", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": "36", + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "7777", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.lock"], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "8", + "rf": null, + "address": "8888", + "type": "device_type.door_lock", + "name": "Device8", + "status1": "device_status.unlock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:08", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": "4", + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "8888", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.unlock"], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "9", + "rf": null, + "address": "9999", + "type": "device_type.door_lock", + "name": "Device9", + "status1": "device_status.error", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:09", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": "10", + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "9999", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.error"], + "trigger_by_zone": [] } ], "MODE": [ @@ -88,9 +536,9 @@ "area": "1", "no": "1", "rf": null, - "address": "124", + "address": "1111", "type": "device_type.door_lock", - "name": "Device2", + "name": "Device1", "status1": "device_status.lock", "status2": null, "status_switch": null, @@ -102,7 +550,7 @@ "status_hue": null, "status_saturation": null, "rssi": "9", - "mac": "00:00:00:00:02", + "mac": "00:00:00:00:01", "scene_trigger": "0", "status_total_energy": null, "device_id2": "", @@ -132,13 +580,461 @@ "group_id": null, "group_name": null, "bypass": "0", - "device_id": "124", + "device_id": "1111", "status_temp_format": "C", "type_no": "72", "device_group": "002", "status_fault": [], "status_open": ["device_status.lock"], "trigger_by_zone": [] + }, + { + "area": "1", + "no": "2", + "rf": null, + "address": "2222", + "type": "device_type.door_lock", + "name": "Device2", + "status1": "device_status.unlock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:02", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": null, + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "2222", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": [], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "3", + "rf": null, + "address": "3333", + "type": "device_type.door_lock", + "name": "Device3", + "status1": "device_status.lock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:03", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": null, + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "3333", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.lock"], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "4", + "rf": null, + "address": "RF4", + "type": "device_type.door_contact", + "name": "Device4", + "status1": "device_status.dc_close", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "0", + "mac": "00:00:00:00:04", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "", + "minigw_syncing": "", + "minigw_configuration_data": "", + "minigw_product_data": "", + "minigw_lock_status": "", + "minigw_number_of_credentials_supported": "", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "4444", + "status_temp_format": "C", + "type_no": "4", + "device_group": "000", + "status_fault": [], + "status_open": ["device_status.dc_close"], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "5", + "rf": null, + "address": "RF5", + "type": "device_type.door_contact", + "name": "Device5", + "status1": "device_status.dc_open", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "0", + "mac": "00:00:00:00:05", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "", + "minigw_syncing": "", + "minigw_configuration_data": "", + "minigw_product_data": "", + "minigw_lock_status": "", + "minigw_number_of_credentials_supported": "", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "5555", + "status_temp_format": "C", + "type_no": "4", + "device_group": "000", + "status_fault": [], + "status_open": ["device_status.dc_open"], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "6", + "rf": null, + "address": "RF6", + "type": "device_type.door_contact", + "name": "Device6", + "status1": "unknwon", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "0", + "mac": "REDACTED", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "", + "minigw_syncing": "", + "minigw_configuration_data": "", + "minigw_product_data": "", + "minigw_lock_status": "", + "minigw_number_of_credentials_supported": "", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "6666", + "status_temp_format": "C", + "type_no": "4", + "device_group": "000", + "status_fault": [], + "status_open": [], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "7", + "rf": null, + "address": "7777", + "type": "device_type.door_lock", + "name": "Device7", + "status1": "device_status.lock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:07", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": "36", + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "7777", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.lock"], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "8", + "rf": null, + "address": "8888", + "type": "device_type.door_lock", + "name": "Device8", + "status1": "device_status.unlock", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:08", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": "4", + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "8888", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.unlock"], + "trigger_by_zone": [] + }, + { + "area": "1", + "no": "9", + "rf": null, + "address": "9999", + "type": "device_type.door_lock", + "name": "Device9", + "status1": "device_status.error", + "status2": null, + "status_switch": null, + "status_power": null, + "status_temp": null, + "status_humi": null, + "status_dim_level": null, + "status_lux": "", + "status_hue": null, + "status_saturation": null, + "rssi": "9", + "mac": "00:00:00:00:09", + "scene_trigger": "0", + "status_total_energy": null, + "device_id2": "", + "extension": null, + "minigw_protocol": "DM", + "minigw_syncing": "0", + "minigw_configuration_data": "02FF000001000000000000000000001E000100", + "minigw_product_data": "21020120", + "minigw_lock_status": "10", + "minigw_number_of_credentials_supported": "10", + "sresp_button_3": null, + "sresp_button_1": null, + "sresp_button_2": null, + "sresp_button_4": null, + "ipcam_trigger_by_zone1": null, + "ipcam_trigger_by_zone2": null, + "ipcam_trigger_by_zone3": null, + "ipcam_trigger_by_zone4": null, + "scene_restore": null, + "thermo_mode": null, + "thermo_setpoint": null, + "thermo_c_setpoint": null, + "thermo_setpoint_away": null, + "thermo_c_setpoint_away": null, + "thermo_fan_mode": null, + "thermo_schd_setting": null, + "group_id": null, + "group_name": null, + "bypass": "0", + "device_id": "9999", + "status_temp_format": "C", + "type_no": "72", + "device_group": "002", + "status_fault": [], + "status_open": ["device_status.error"], + "trigger_by_zone": [] } ], "capture_latest": null, diff --git a/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr b/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr index faff1c5103a..ae720a611e3 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_diagnostics.ambr @@ -82,6 +82,496 @@ 'type': 'device_type.door_lock', 'type_no': '72', }), + dict({ + '_state': 'unlocked', + '_state2': 'unknown', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': None, + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '2', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.unlock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + '_state': 'locked', + '_state2': 'unknown', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': None, + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '3', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.lock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.lock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + '_state': 'closed', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '000', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '', + 'minigw_lock_status': '', + 'minigw_number_of_credentials_supported': '', + 'minigw_product_data': '', + 'minigw_protocol': '', + 'minigw_syncing': '', + 'name': '**REDACTED**', + 'no': '4', + 'rf': None, + 'rssi': '0', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.dc_close', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.dc_close', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_contact', + 'type_no': '4', + }), + dict({ + '_state': 'open', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '000', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '', + 'minigw_lock_status': '', + 'minigw_number_of_credentials_supported': '', + 'minigw_product_data': '', + 'minigw_protocol': '', + 'minigw_syncing': '', + 'name': '**REDACTED**', + 'no': '5', + 'rf': None, + 'rssi': '0', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.dc_open', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.dc_open', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_contact', + 'type_no': '4', + }), + dict({ + '_state': 'unavailable', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '000', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '', + 'minigw_lock_status': '', + 'minigw_number_of_credentials_supported': '', + 'minigw_product_data': '', + 'minigw_protocol': '', + 'minigw_syncing': '', + 'name': '**REDACTED**', + 'no': '6', + 'rf': None, + 'rssi': '0', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'unknwon', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_contact', + 'type_no': '4', + }), + dict({ + '_state': 'unlocked', + '_state2': 'closed', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '36', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '7', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.lock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.lock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + '_state': 'unlocked', + '_state2': 'open', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '4', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '8', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.unlock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.unlock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + '_state': 'unavailable', + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '10', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '9', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.error', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.error', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), ]), 'model': list([ dict({ @@ -162,6 +652,484 @@ 'type': 'device_type.door_lock', 'type_no': '72', }), + dict({ + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': None, + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '2', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.unlock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': None, + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '3', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.lock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.lock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '000', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '', + 'minigw_lock_status': '', + 'minigw_number_of_credentials_supported': '', + 'minigw_product_data': '', + 'minigw_protocol': '', + 'minigw_syncing': '', + 'name': '**REDACTED**', + 'no': '4', + 'rf': None, + 'rssi': '0', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.dc_close', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.dc_close', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_contact', + 'type_no': '4', + }), + dict({ + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '000', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '', + 'minigw_lock_status': '', + 'minigw_number_of_credentials_supported': '', + 'minigw_product_data': '', + 'minigw_protocol': '', + 'minigw_syncing': '', + 'name': '**REDACTED**', + 'no': '5', + 'rf': None, + 'rssi': '0', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.dc_open', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.dc_open', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_contact', + 'type_no': '4', + }), + dict({ + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '000', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '', + 'minigw_lock_status': '', + 'minigw_number_of_credentials_supported': '', + 'minigw_product_data': '', + 'minigw_protocol': '', + 'minigw_syncing': '', + 'name': '**REDACTED**', + 'no': '6', + 'rf': None, + 'rssi': '0', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'unknwon', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_contact', + 'type_no': '4', + }), + dict({ + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '36', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '7', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.lock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.lock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '4', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '8', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.unlock', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.unlock', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), + dict({ + 'address': '**REDACTED**', + 'area': '1', + 'bypass': '0', + 'device_group': '002', + 'device_id': '**REDACTED**', + 'device_id2': '', + 'extension': None, + 'group_id': None, + 'group_name': None, + 'ipcam_trigger_by_zone1': None, + 'ipcam_trigger_by_zone2': None, + 'ipcam_trigger_by_zone3': None, + 'ipcam_trigger_by_zone4': None, + 'mac': '**REDACTED**', + 'minigw_configuration_data': '02FF000001000000000000000000001E000100', + 'minigw_lock_status': '10', + 'minigw_number_of_credentials_supported': '10', + 'minigw_product_data': '21020120', + 'minigw_protocol': 'DM', + 'minigw_syncing': '0', + 'name': '**REDACTED**', + 'no': '9', + 'rf': None, + 'rssi': '9', + 'scene_restore': None, + 'scene_trigger': '0', + 'sresp_button_1': None, + 'sresp_button_2': None, + 'sresp_button_3': None, + 'sresp_button_4': None, + 'status1': 'device_status.error', + 'status2': None, + 'status_dim_level': None, + 'status_fault': list([ + ]), + 'status_hue': None, + 'status_humi': None, + 'status_lux': '', + 'status_open': list([ + 'device_status.error', + ]), + 'status_power': None, + 'status_saturation': None, + 'status_switch': None, + 'status_temp': None, + 'status_temp_format': 'C', + 'status_total_energy': None, + 'thermo_c_setpoint': None, + 'thermo_c_setpoint_away': None, + 'thermo_fan_mode': None, + 'thermo_mode': None, + 'thermo_schd_setting': None, + 'thermo_setpoint': None, + 'thermo_setpoint_away': None, + 'trigger_by_zone': list([ + ]), + 'type': 'device_type.door_lock', + 'type_no': '72', + }), ]), 'HISTORY': list([ dict({ diff --git a/tests/components/yale_smart_alarm/test_coordinator.py b/tests/components/yale_smart_alarm/test_coordinator.py new file mode 100644 index 00000000000..9ee09e9c0f2 --- /dev/null +++ b/tests/components/yale_smart_alarm/test_coordinator.py @@ -0,0 +1,123 @@ +"""The test for the sensibo coordinator.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any +from unittest.mock import Mock, patch + +import pytest +from yalesmartalarmclient.const import YALE_STATE_ARM_FULL +from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError + +from homeassistant.components.yale_smart_alarm.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import STATE_ALARM_ARMED_AWAY, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from .conftest import ENTRY_CONFIG, OPTIONS_CONFIG + +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.mark.parametrize( + "p_error", + [ + AuthenticationError(), + UnknownError(), + ConnectionError("Could not connect"), + TimeoutError(), + ], +) +async def test_coordinator_setup_errors( + hass: HomeAssistant, + load_json: dict[str, Any], + p_error: Exception, +) -> None: + """Test the Yale Smart Living coordinator with errors.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + options=OPTIONS_CONFIG, + entry_id="1", + unique_id="username", + version=1, + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.yale_smart_alarm.coordinator.YaleSmartAlarmClient", + autospec=True, + ) as mock_client_class: + mock_client_class.side_effect = p_error + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.yale_smart_alarm") + assert not state + + +async def test_coordinator_setup_and_update_errors( + hass: HomeAssistant, + load_config_entry: tuple[MockConfigEntry, Mock], + load_json: dict[str, Any], +) -> None: + """Test the Yale Smart Living coordinator with errors.""" + + client = load_config_entry[1] + + state = hass.states.get("alarm_control_panel.yale_smart_alarm") + assert state.state == STATE_ALARM_ARMED_AWAY + client.reset_mock() + + client.get_all.side_effect = ConnectionError("Could not connect") + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) + await hass.async_block_till_done() + client.get_all.assert_called_once() + state = hass.states.get("alarm_control_panel.yale_smart_alarm") + assert state.state == STATE_UNAVAILABLE + client.reset_mock() + + client.get_all.side_effect = ConnectionError("Could not connect") + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) + await hass.async_block_till_done() + client.get_all.assert_called_once() + state = hass.states.get("alarm_control_panel.yale_smart_alarm") + assert state.state == STATE_UNAVAILABLE + client.reset_mock() + + client.get_all.side_effect = TimeoutError("Could not connect") + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=3)) + await hass.async_block_till_done() + client.get_all.assert_called_once() + state = hass.states.get("alarm_control_panel.yale_smart_alarm") + assert state.state == STATE_UNAVAILABLE + client.reset_mock() + + client.get_all.side_effect = UnknownError("info") + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=4)) + await hass.async_block_till_done() + client.get_all.assert_called_once() + state = hass.states.get("alarm_control_panel.yale_smart_alarm") + assert state.state == STATE_UNAVAILABLE + client.reset_mock() + + client.get_all.side_effect = None + client.get_all.return_value = load_json + client.get_armed_status.return_value = YALE_STATE_ARM_FULL + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + client.get_all.assert_called_once() + state = hass.states.get("alarm_control_panel.yale_smart_alarm") + assert state.state == STATE_ALARM_ARMED_AWAY + client.reset_mock() + + client.get_all.side_effect = AuthenticationError("Can not authenticate") + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=6)) + await hass.async_block_till_done() + client.get_all.assert_called_once() + state = hass.states.get("alarm_control_panel.yale_smart_alarm") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/yale_smart_alarm/test_diagnostics.py b/tests/components/yale_smart_alarm/test_diagnostics.py index 8796eeb465b..dc4c5e8c8d7 100644 --- a/tests/components/yale_smart_alarm/test_diagnostics.py +++ b/tests/components/yale_smart_alarm/test_diagnostics.py @@ -1,11 +1,13 @@ """Test Yale Smart Living diagnostics.""" from __future__ import annotations +from unittest.mock import Mock + from syrupy.assertion import SnapshotAssertion -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -13,11 +15,11 @@ from tests.typing import ClientSessionGenerator async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - load_config_entry: ConfigEntry, + load_config_entry: tuple[MockConfigEntry, Mock], snapshot: SnapshotAssertion, ) -> None: """Test generating diagnostics for a config entry.""" - entry = load_config_entry + entry = load_config_entry[0] diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) From 31a8a62165829fc481b0a28452f53c8f100aed25 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 24 Aug 2023 11:45:14 +0200 Subject: [PATCH 0819/1151] SNMP sensor refactor to ManualTriggerSensorEntity (#98630) * SNMP to ManualTriggerSensorEntity * Mods --- homeassistant/components/snmp/sensor.py | 68 ++++++++++++++++++------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index fc8068fb532..85c69ddf76b 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -19,11 +19,15 @@ from pysnmp.hlapi.asyncio import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import CONF_STATE_CLASS, PLATFORM_SCHEMA from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_HOST, + CONF_ICON, + CONF_NAME, CONF_PORT, CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, @@ -31,9 +35,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant 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 ( + CONF_AVAILABILITY, + CONF_PICTURE, TEMPLATE_SENSOR_BASE_SCHEMA, - TemplateSensor, + ManualTriggerSensorEntity, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -64,6 +71,16 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) +TRIGGER_ENTITY_OPTIONS = ( + CONF_AVAILABILITY, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_PICTURE, + CONF_UNIQUE_ID, + CONF_STATE_CLASS, + CONF_UNIT_OF_MEASUREMENT, +) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_BASEOID): cv.string, @@ -106,7 +123,6 @@ async def async_setup_platform( privproto = config[CONF_PRIV_PROTOCOL] accept_errors = config.get(CONF_ACCEPT_ERRORS) default_value = config.get(CONF_DEFAULT_VALUE) - unique_id = config.get(CONF_UNIQUE_ID) try: # Try IPv4 first. @@ -151,35 +167,50 @@ async def async_setup_platform( _LOGGER.error("Please check the details in the configuration file") return + name = config.get(CONF_NAME, Template(DEFAULT_NAME, hass)) + trigger_entity_config = {CONF_NAME: name} + for key in TRIGGER_ENTITY_OPTIONS: + if key not in config: + continue + trigger_entity_config[key] = config[key] + + value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass + data = SnmpData(request_args, baseoid, accept_errors, default_value) - async_add_entities([SnmpSensor(hass, data, config, unique_id)], True) + async_add_entities([SnmpSensor(hass, data, trigger_entity_config, value_template)]) -class SnmpSensor(TemplateSensor): +class SnmpSensor(ManualTriggerSensorEntity): """Representation of a SNMP sensor.""" _attr_should_poll = True - def __init__(self, hass, data, config, unique_id): + def __init__( + self, + hass: HomeAssistant, + data: SnmpData, + config: ConfigType, + value_template: Template | None, + ) -> None: """Initialize the sensor.""" - super().__init__( - hass, config=config, unique_id=unique_id, fallback_name=DEFAULT_NAME - ) + super().__init__(hass, config) self.data = data self._state = None - self._value_template = config.get(CONF_VALUE_TEMPLATE) - if (value_template := self._value_template) is not None: - value_template.hass = hass + self._value_template = value_template - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state + async def async_added_to_hass(self) -> None: + """Handle adding to Home Assistant.""" + await super().async_added_to_hass() + await self.async_update() async def async_update(self) -> None: """Get the latest data and updates the states.""" await self.data.async_update() + raw_value = self.data.value + if (value := self.data.value) is None: value = STATE_UNKNOWN elif self._value_template is not None: @@ -187,13 +218,14 @@ class SnmpSensor(TemplateSensor): value, STATE_UNKNOWN ) - self._state = value + self._attr_native_value = value + self._process_manual_data(raw_value) class SnmpData: """Get the latest data and update the states.""" - def __init__(self, request_args, baseoid, accept_errors, default_value): + def __init__(self, request_args, baseoid, accept_errors, default_value) -> None: """Initialize the data object.""" self._request_args = request_args self._baseoid = baseoid From d282ba6bac32053daf76a7cfd333e160fdd426c6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Aug 2023 11:59:24 +0200 Subject: [PATCH 0820/1151] Use a single WS command for group preview (#98903) * Use a single WS command for group preview * Fix tests --- .../components/group/binary_sensor.py | 16 ++ homeassistant/components/group/config_flow.py | 159 ++++++------------ homeassistant/components/group/sensor.py | 17 ++ homeassistant/config_entries.py | 2 +- homeassistant/data_entry_flow.py | 5 +- .../helpers/schema_config_entry_flow.py | 6 +- tests/components/group/test_config_flow.py | 17 +- tests/test_config_entries.py | 3 +- 8 files changed, 102 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 53bf1affe00..d1e91db8f86 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -1,6 +1,8 @@ """Platform allowing several binary sensor to be grouped into one binary sensor.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -85,6 +87,20 @@ async def async_setup_entry( ) +@callback +def async_create_preview_binary_sensor( + name: str, validated_config: dict[str, Any] +) -> BinarySensorGroup: + """Create a preview sensor.""" + return BinarySensorGroup( + None, + name, + None, + validated_config[CONF_ENTITIES], + validated_config[CONF_ALL], + ) + + class BinarySensorGroup(GroupEntity, BinarySensorEntity): """Representation of a BinarySensorGroup.""" diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 869a4d33b5f..1d820b516af 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine, Mapping from functools import partial -from typing import Any, Literal, cast +from typing import Any, cast import voluptuous as vol @@ -21,10 +21,10 @@ from homeassistant.helpers.schema_config_entry_flow import ( entity_selector_without_own_entities, ) -from . import DOMAIN -from .binary_sensor import CONF_ALL, BinarySensorGroup +from . import DOMAIN, GroupEntity +from .binary_sensor import CONF_ALL, async_create_preview_binary_sensor from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC -from .sensor import SensorGroup +from .sensor import async_create_preview_sensor _STATISTIC_MEASURES = [ "min", @@ -171,8 +171,8 @@ CONFIG_FLOW = { "user": SchemaFlowMenuStep(GROUP_TYPES), "binary_sensor": SchemaFlowFormStep( BINARY_SENSOR_CONFIG_SCHEMA, + preview="group", validate_user_input=set_group_type("binary_sensor"), - preview="group_binary_sensor", ), "cover": SchemaFlowFormStep( basic_group_config_schema("cover"), @@ -196,8 +196,8 @@ CONFIG_FLOW = { ), "sensor": SchemaFlowFormStep( SENSOR_CONFIG_SCHEMA, + preview="group", validate_user_input=set_group_type("sensor"), - preview="group_sensor", ), "switch": SchemaFlowFormStep( basic_group_config_schema("switch"), @@ -210,22 +210,33 @@ OPTIONS_FLOW = { "init": SchemaFlowFormStep(next_step=choose_options_step), "binary_sensor": SchemaFlowFormStep( binary_sensor_options_schema, - preview="group_binary_sensor", + preview="group", ), "cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")), "fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")), "light": SchemaFlowFormStep(partial(light_switch_options_schema, "light")), "lock": SchemaFlowFormStep(partial(basic_group_options_schema, "lock")), "media_player": SchemaFlowFormStep( - partial(basic_group_options_schema, "media_player") + partial(basic_group_options_schema, "media_player"), + preview="group", ), "sensor": SchemaFlowFormStep( partial(sensor_options_schema, "sensor"), - preview="group_sensor", + preview="group", ), "switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")), } +PREVIEW_OPTIONS_SCHEMA: dict[str, vol.Schema] = {} + +CREATE_PREVIEW_ENTITY: dict[ + str, + Callable[[str, dict[str, Any]], GroupEntity], +] = { + "binary_sensor": async_create_preview_binary_sensor, + "sensor": async_create_preview_sensor, +} + class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config or options flow for groups.""" @@ -261,12 +272,20 @@ class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): ) _async_hide_members(hass, options[CONF_ENTITIES], hidden_by) - @callback @staticmethod - def async_setup_preview(hass: HomeAssistant) -> None: + async def async_setup_preview(hass: HomeAssistant) -> None: """Set up preview WS API.""" - websocket_api.async_register_command(hass, ws_preview_sensor) - websocket_api.async_register_command(hass, ws_preview_binary_sensor) + for group_type, form_step in OPTIONS_FLOW.items(): + if group_type not in GROUP_TYPES: + continue + schema = cast( + Callable[ + [SchemaCommonFlowHandler | None], Coroutine[Any, Any, vol.Schema] + ], + form_step.schema, + ) + PREVIEW_OPTIONS_SCHEMA[group_type] = await schema(None) + websocket_api.async_register_command(hass, ws_start_preview) def _async_hide_members( @@ -282,127 +301,51 @@ def _async_hide_members( registry.async_update_entity(entity_id, hidden_by=hidden_by) +@websocket_api.websocket_command( + { + vol.Required("type"): "group/start_preview", + vol.Required("flow_id"): str, + vol.Required("flow_type"): vol.Any("config_flow", "options_flow"), + vol.Required("user_input"): dict, + } +) @callback -def _async_handle_ws_preview( +def ws_start_preview( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any], - config_schema: vol.Schema, - options_schema: vol.Schema, - create_preview_entity: Callable[ - [Literal["config_flow", "options_flow"], str, dict[str, Any]], - BinarySensorGroup | SensorGroup, - ], ) -> None: """Generate a preview.""" if msg["flow_type"] == "config_flow": - validated = config_schema(msg["user_input"]) + flow_status = hass.config_entries.flow.async_get(msg["flow_id"]) + group_type = flow_status["step_id"] + form_step = cast(SchemaFlowFormStep, CONFIG_FLOW[group_type]) + schema = cast(vol.Schema, form_step.schema) + validated = schema(msg["user_input"]) name = validated["name"] else: - validated = options_schema(msg["user_input"]) flow_status = hass.config_entries.options.async_get(msg["flow_id"]) config_entry = hass.config_entries.async_get_entry(flow_status["handler"]) if not config_entry: raise HomeAssistantError + group_type = config_entry.options["group_type"] name = config_entry.options["name"] + validated = PREVIEW_OPTIONS_SCHEMA[group_type](msg["user_input"]) @callback def async_preview_updated(state: str, attributes: Mapping[str, Any]) -> None: """Forward config entry state events to websocket.""" connection.send_message( websocket_api.event_message( - msg["id"], {"state": state, "attributes": attributes} + msg["id"], + {"attributes": attributes, "group_type": group_type, "state": state}, ) ) - preview_entity = create_preview_entity(msg["flow_type"], name, validated) + preview_entity = CREATE_PREVIEW_ENTITY[group_type](name, validated) preview_entity.hass = hass connection.send_result(msg["id"]) connection.subscriptions[msg["id"]] = preview_entity.async_start_preview( async_preview_updated ) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "group/binary_sensor/start_preview", - vol.Required("flow_id"): str, - vol.Required("flow_type"): vol.Any("config_flow", "options_flow"), - vol.Required("user_input"): dict, - } -) -@websocket_api.async_response -async def ws_preview_binary_sensor( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] -) -> None: - """Generate a preview.""" - - def create_preview_binary_sensor( - flow_type: Literal["config_flow", "options_flow"], - name: str, - validated_config: dict[str, Any], - ) -> BinarySensorGroup: - """Create a preview sensor.""" - return BinarySensorGroup( - None, - name, - None, - validated_config[CONF_ENTITIES], - validated_config[CONF_ALL], - ) - - _async_handle_ws_preview( - hass, - connection, - msg, - BINARY_SENSOR_CONFIG_SCHEMA, - await binary_sensor_options_schema(None), - create_preview_binary_sensor, - ) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "group/sensor/start_preview", - vol.Required("flow_id"): str, - vol.Required("flow_type"): vol.Any("config_flow", "options_flow"), - vol.Required("user_input"): dict, - } -) -@websocket_api.async_response -async def ws_preview_sensor( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] -) -> None: - """Generate a preview.""" - - def create_preview_sensor( - flow_type: Literal["config_flow", "options_flow"], - name: str, - validated_config: dict[str, Any], - ) -> SensorGroup: - """Create a preview sensor.""" - ignore_non_numeric = ( - False - if flow_type == "config_flow" - else validated_config[CONF_IGNORE_NON_NUMERIC] - ) - return SensorGroup( - None, - name, - validated_config[CONF_ENTITIES], - ignore_non_numeric, - validated_config[CONF_TYPE], - None, - None, - None, - ) - - _async_handle_ws_preview( - hass, - connection, - msg, - SENSOR_CONFIG_SCHEMA, - await sensor_options_schema("sensor", None), - create_preview_sensor, - ) diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 57ada314707..10030ab647f 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -136,6 +136,23 @@ async def async_setup_entry( ) +@callback +def async_create_preview_sensor( + name: str, validated_config: dict[str, Any] +) -> SensorGroup: + """Create a preview sensor.""" + return SensorGroup( + None, + name, + validated_config[CONF_ENTITIES], + validated_config.get(CONF_IGNORE_NON_NUMERIC, False), + validated_config[CONF_TYPE], + None, + None, + None, + ) + + def calc_min( sensor_values: list[tuple[str, float, State]] ) -> tuple[dict[str, str | None], float | None]: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d3ff741e3e6..78b54929015 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1864,7 +1864,7 @@ class OptionsFlowManager(data_entry_flow.FlowManager): await _load_integration(self.hass, entry.domain, {}) if entry.domain not in self._preview: self._preview.add(entry.domain) - flow.async_setup_preview(self.hass) + await flow.async_setup_preview(self.hass) class OptionsFlow(data_entry_flow.FlowHandler): diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 04876590d2b..467fc3b5228 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -439,7 +439,7 @@ class FlowManager(abc.ABC): """Set up preview for a flow handler.""" if flow.handler not in self._preview: self._preview.add(flow.handler) - flow.async_setup_preview(self.hass) + await flow.async_setup_preview(self.hass) class FlowHandler: @@ -649,9 +649,8 @@ class FlowHandler: def async_remove(self) -> None: """Notification that the flow has been removed.""" - @callback @staticmethod - def async_setup_preview(hass: HomeAssistant) -> None: + async def async_setup_preview(hass: HomeAssistant) -> None: """Set up preview.""" diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index e9d86f79eec..20a5d8de5a8 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -292,9 +292,8 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): """Initialize config flow.""" self._common_handler = SchemaCommonFlowHandler(self, self.config_flow, None) - @callback @staticmethod - def async_setup_preview(hass: HomeAssistant) -> None: + async def async_setup_preview(hass: HomeAssistant) -> None: """Set up preview.""" @classmethod @@ -369,7 +368,8 @@ class SchemaOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): options_flow: Mapping[str, SchemaFlowStep], async_options_flow_finished: Callable[[HomeAssistant, Mapping[str, Any]], None] | None = None, - async_setup_preview: Callable[[HomeAssistant], None] | None = None, + async_setup_preview: Callable[[HomeAssistant], Coroutine[Any, Any, None]] + | None = None, ) -> None: """Initialize options flow. diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index ad084786366..a2845f098d3 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -490,11 +490,11 @@ async def test_config_flow_preview( assert result["type"] == FlowResultType.FORM assert result["step_id"] == domain assert result["errors"] is None - assert result["preview"] == f"group_{domain}" + assert result["preview"] == "group" await client.send_json_auto_id( { - "type": f"group/{domain}/start_preview", + "type": "group/start_preview", "flow_id": result["flow_id"], "flow_type": "config_flow", "user_input": {"name": "My group", "entities": input_entities} @@ -508,6 +508,7 @@ async def test_config_flow_preview( msg = await client.receive_json() assert msg["event"] == { "attributes": {"friendly_name": "My group"} | extra_attributes[0], + "group_type": domain, "state": "unavailable", } @@ -522,8 +523,10 @@ async def test_config_flow_preview( } | extra_attributes[0] | extra_attributes[1], + "group_type": domain, "state": group_state, } + assert len(hass.states.async_all()) == 2 @pytest.mark.parametrize( @@ -582,14 +585,14 @@ async def test_option_flow_preview( result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == FlowResultType.FORM assert result["errors"] is None - assert result["preview"] == f"group_{domain}" + assert result["preview"] == "group" hass.states.async_set(input_entities[0], input_states[0]) hass.states.async_set(input_entities[1], input_states[1]) await client.send_json_auto_id( { - "type": f"group/{domain}/start_preview", + "type": "group/start_preview", "flow_id": result["flow_id"], "flow_type": "options_flow", "user_input": {"entities": input_entities} | extra_user_input, @@ -603,8 +606,10 @@ async def test_option_flow_preview( assert msg["event"] == { "attributes": {"entity_id": input_entities, "friendly_name": "My group"} | extra_attributes, + "group_type": domain, "state": group_state, } + assert len(hass.states.async_all()) == 3 async def test_option_flow_sensor_preview_config_entry_removed( @@ -635,13 +640,13 @@ async def test_option_flow_sensor_preview_config_entry_removed( result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == FlowResultType.FORM assert result["errors"] is None - assert result["preview"] == "group_sensor" + assert result["preview"] == "group" await hass.config_entries.async_remove(config_entry.entry_id) await client.send_json_auto_id( { - "type": "group/sensor/start_preview", + "type": "group/start_preview", "flow_id": result["flow_id"], "flow_type": "options_flow", "user_input": { diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index f04f033b49f..680adcf1202 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3962,9 +3962,8 @@ async def test_preview_supported( """Mock Reauth.""" return self.async_show_form(step_id="next", preview="test") - @callback @staticmethod - def async_setup_preview(hass: HomeAssistant) -> None: + async def async_setup_preview(hass: HomeAssistant) -> None: """Set up preview.""" preview_calls.append(None) From 9a0507af3c5b969a3a05cd2c4019ea31ec1e86ab Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 24 Aug 2023 12:01:22 +0200 Subject: [PATCH 0821/1151] Bump reolink-aio to 0.7.8 (#98942) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index f350bb4f948..3ff25d1e7a0 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.7"] + "requirements": ["reolink-aio==0.7.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 601be9507ac..89088840dd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2288,7 +2288,7 @@ renault-api==0.2.0 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.7 +reolink-aio==0.7.8 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43ccd45c09c..212132857ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1681,7 +1681,7 @@ renault-api==0.2.0 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.7 +reolink-aio==0.7.8 # homeassistant.components.rflink rflink==0.0.65 From 849cfa3af818867c5d377e9e1337fb7d977a1468 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Aug 2023 05:04:00 -0500 Subject: [PATCH 0822/1151] Retry yeelight setup later if the wrong device is found (#98884) --- homeassistant/components/yeelight/__init__.py | 14 +++++++++++ homeassistant/components/yeelight/device.py | 20 ++++++++++----- tests/components/yeelight/test_init.py | 25 +++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index c07852629a9..cc9faa33194 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -144,6 +144,7 @@ async def _async_initialize( entry: ConfigEntry, device: YeelightDevice, ) -> None: + """Initialize a Yeelight device.""" entry_data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id] = {} await device.async_setup() entry_data[DATA_DEVICE] = device @@ -216,6 +217,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (asyncio.TimeoutError, OSError, BulbException) as ex: raise ConfigEntryNotReady from ex + found_unique_id = device.unique_id + expected_unique_id = entry.unique_id + if expected_unique_id and found_unique_id and found_unique_id != expected_unique_id: + # If the id of the device does not match the unique_id + # of the config entry, it likely means the DHCP lease has expired + # and the device has been assigned a new IP address. We need to + # wait for the next discovery to find the device at its new address + # and update the config entry so we do not mix up devices. + raise ConfigEntryNotReady( + f"Unexpected device found at {device.host}; " + f"expected {expected_unique_id}, found {found_unique_id}" + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Wait to install the reload listener until everything was successfully initialized diff --git a/homeassistant/components/yeelight/device.py b/homeassistant/components/yeelight/device.py index 0fabe693aa9..811a1904b04 100644 --- a/homeassistant/components/yeelight/device.py +++ b/homeassistant/components/yeelight/device.py @@ -3,12 +3,13 @@ from __future__ import annotations import asyncio import logging +from typing import Any from yeelight import BulbException -from yeelight.aio import KEY_CONNECTED +from yeelight.aio import KEY_CONNECTED, AsyncBulb from homeassistant.const import CONF_ID, CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -63,17 +64,19 @@ def update_needs_bg_power_workaround(data): class YeelightDevice: """Represents single Yeelight device.""" - def __init__(self, hass, host, config, bulb): + def __init__( + self, hass: HomeAssistant, host: str, config: dict[str, Any], bulb: AsyncBulb + ) -> None: """Initialize device.""" self._hass = hass self._config = config self._host = host self._bulb_device = bulb - self.capabilities = {} - self._device_type = None + self.capabilities: dict[str, Any] = {} + self._device_type: str | None = None self._available = True self._initialized = False - self._name = None + self._name: str | None = None @property def bulb(self): @@ -115,6 +118,11 @@ class YeelightDevice: """Return the firmware version.""" return self.capabilities.get("fw_ver") + @property + def unique_id(self) -> str | None: + """Return the unique ID of the device.""" + return self.capabilities.get("id") + @property def is_nightlight_supported(self) -> bool: """Return true / false if nightlight is supported. diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 906dbf50ace..b439ce04c25 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -618,3 +618,28 @@ async def test_async_setup_with_discovery_not_working(hass: HomeAssistant) -> No assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get("light.yeelight_color_0x15243f").state == STATE_ON + + +async def test_async_setup_retries_with_wrong_device( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the config entry enters a retry state with the wrong device.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_ID: "0x0000000000999999"}, + options={}, + unique_id="0x0000000000999999", + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=_mocked_bulb() + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert ( + "Unexpected device found at 192.168.1.239; expected 0x0000000000999999, " + "found 0x000000000015243f; Retrying in background" + ) in caplog.text From b69e8fda7778a99cbee0ac5b4804f10881d01c4d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 24 Aug 2023 12:14:39 +0200 Subject: [PATCH 0823/1151] Remove `TemplateSensor` from the `template_entity` helper (#98945) Clean off TemplateSensor --- homeassistant/components/template/sensor.py | 8 ++++++-- homeassistant/helpers/template_entity.py | 21 --------------------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 7a5df84a207..aa6788109ff 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, RestoreSensor, SensorDeviceClass, + SensorEntity, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( @@ -39,7 +40,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template_entity import ( TEMPLATE_SENSOR_BASE_SCHEMA, - TemplateSensor, + TemplateEntity, ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -196,7 +197,7 @@ async def async_setup_platform( ) -class SensorTemplate(TemplateSensor): +class SensorTemplate(TemplateEntity, SensorEntity): """Representation of a Template Sensor.""" _attr_should_poll = False @@ -209,6 +210,9 @@ class SensorTemplate(TemplateSensor): ) -> None: """Initialize the sensor.""" super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) + self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._attr_state_class = config.get(CONF_STATE_CLASS) self._template: template.Template = config[CONF_STATE] if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index 70a0ee1d16c..16dc212e8cc 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -454,27 +454,6 @@ class TemplateEntity(Entity): ) -class TemplateSensor(TemplateEntity, SensorEntity): - """Representation of a Template Sensor.""" - - def __init__( - self, - hass: HomeAssistant, - *, - config: dict[str, Any], - fallback_name: str | None, - unique_id: str | None, - ) -> None: - """Initialize the sensor.""" - super().__init__( - hass, config=config, fallback_name=fallback_name, unique_id=unique_id - ) - - self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) - self._attr_device_class = config.get(CONF_DEVICE_CLASS) - self._attr_state_class = config.get(CONF_STATE_CLASS) - - class TriggerBaseEntity(Entity): """Template Base entity based on trigger data.""" From 87dd18cc2e249b45fe1838e37b7988c460dd986b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 12:35:11 +0200 Subject: [PATCH 0824/1151] Remove obsolete yaml check in SQL (#98950) * Remove unique id check from SQL * Remove unique id check from SQL --- homeassistant/components/sql/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 0b32b10f972..dffb45bfd93 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -321,13 +321,12 @@ class SQLSensor(ManualTriggerSensorEntity): self._attr_extra_state_attributes = {} self._use_database_executor = use_database_executor self._lambda_stmt = _generate_lambda_stmt(query) - if not yaml: + if not yaml and (unique_id := trigger_entity_config.get(CONF_UNIQUE_ID)): self._attr_name = None self._attr_has_entity_name = True - if not yaml and trigger_entity_config.get(CONF_UNIQUE_ID): self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, trigger_entity_config[CONF_UNIQUE_ID])}, + identifiers={(DOMAIN, unique_id)}, manufacturer="SQL", name=self.name, ) From 0d013767ee498bafcb8fbe5d8a6a59dc28603e52 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Aug 2023 12:49:38 +0200 Subject: [PATCH 0825/1151] Add support for event groups (#98463) Co-authored-by: Martin Hjelmare --- homeassistant/components/group/config_flow.py | 6 + homeassistant/components/group/event.py | 180 ++++++++++++++++++ homeassistant/components/group/strings.json | 9 + tests/components/group/test_config_flow.py | 16 ++ tests/components/group/test_event.py | 138 ++++++++++++++ 5 files changed, 349 insertions(+) create mode 100644 homeassistant/components/group/event.py create mode 100644 tests/components/group/test_event.py diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 1d820b516af..28a7330a206 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -137,6 +137,7 @@ async def light_switch_options_schema( GROUP_TYPES = [ "binary_sensor", "cover", + "event", "fan", "light", "lock", @@ -178,6 +179,10 @@ CONFIG_FLOW = { basic_group_config_schema("cover"), validate_user_input=set_group_type("cover"), ), + "event": SchemaFlowFormStep( + basic_group_config_schema("event"), + validate_user_input=set_group_type("event"), + ), "fan": SchemaFlowFormStep( basic_group_config_schema("fan"), validate_user_input=set_group_type("fan"), @@ -213,6 +218,7 @@ OPTIONS_FLOW = { preview="group", ), "cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")), + "event": SchemaFlowFormStep(partial(basic_group_options_schema, "event")), "fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")), "light": SchemaFlowFormStep(partial(light_switch_options_schema, "light")), "lock": SchemaFlowFormStep(partial(basic_group_options_schema, "lock")), diff --git a/homeassistant/components/group/event.py b/homeassistant/components/group/event.py new file mode 100644 index 00000000000..81705c7f6f0 --- /dev/null +++ b/homeassistant/components/group/event.py @@ -0,0 +1,180 @@ +"""Platform allowing several event entities to be grouped into one event.""" +from __future__ import annotations + +import itertools + +import voluptuous as vol + +from homeassistant.components.event import ( + ATTR_EVENT_TYPE, + ATTR_EVENT_TYPES, + DOMAIN, + PLATFORM_SCHEMA, + EventEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + CONF_ENTITIES, + CONF_NAME, + CONF_UNIQUE_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType + +from . import GroupEntity + +DEFAULT_NAME = "Event group" + +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) + + +async def async_setup_platform( + _: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + __: DiscoveryInfoType | None = None, +) -> None: + """Set up the event group platform.""" + async_add_entities( + [ + EventGroup( + config.get(CONF_UNIQUE_ID), + config[CONF_NAME], + config[CONF_ENTITIES], + ) + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize event group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + async_add_entities( + [ + EventGroup( + config_entry.entry_id, + config_entry.title, + entities, + ) + ] + ) + + +class EventGroup(GroupEntity, EventEntity): + """Representation of an event group.""" + + _attr_available = False + _attr_should_poll = False + + def __init__( + self, + unique_id: str | None, + name: str, + entity_ids: list[str], + ) -> None: + """Initialize an event group.""" + self._entity_ids = entity_ids + self._attr_name = name + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} + self._attr_unique_id = unique_id + self._attr_event_types = [] + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def async_state_changed_listener( + event: EventType[EventStateChangedData], + ) -> None: + """Handle child updates.""" + if not self.hass.is_running: + return + + self.async_set_context(event.context) + + # Update all properties of the group + self.async_update_group_state() + + # Re-fire if one of the members fires an event, but only + # if the original state was not unavailable or unknown. + if ( + (old_state := event.data["old_state"]) + and old_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) + and (new_state := event.data["new_state"]) + and new_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) + and (event_type := new_state.attributes.get(ATTR_EVENT_TYPE)) + ): + event_attributes = new_state.attributes.copy() + + # We should not propagate the event properties as + # fired event attributes. + del event_attributes[ATTR_EVENT_TYPE] + del event_attributes[ATTR_EVENT_TYPES] + event_attributes.pop(ATTR_DEVICE_CLASS, None) + event_attributes.pop(ATTR_FRIENDLY_NAME, None) + + # Fire the group event + self._trigger_event(event_type, event_attributes) + + self.async_write_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) + ) + + await super().async_added_to_hass() + + @callback + def async_update_group_state(self) -> None: + """Query all members and determine the event group properties.""" + states = [ + state + for entity_id in self._entity_ids + if (state := self.hass.states.get(entity_id)) is not None + ] + + # None of the members are available + if not states: + self._attr_available = False + return + + # Gather and combine all possible event types from all entities + self._attr_event_types = list( + set( + itertools.chain.from_iterable( + state.attributes.get(ATTR_EVENT_TYPES, []) for state in states + ) + ) + ) + + # Set group as unavailable if all members are unavailable or missing + self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index 1c656b46b9e..5f3042c5bf7 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -8,6 +8,7 @@ "menu_options": { "binary_sensor": "Binary sensor group", "cover": "Cover group", + "event": "Event group", "fan": "Fan group", "light": "Light group", "lock": "Lock group", @@ -34,6 +35,14 @@ "name": "[%key:component::group::config::step::binary_sensor::data::name%]" } }, + "event": { + "title": "[%key:component::group::config::step::user::title%]", + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" + } + }, "fan": { "title": "[%key:component::group::config::step::user::title%]", "data": { diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index a2845f098d3..b244b37e072 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -6,6 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.group import DOMAIN, async_setup_entry +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er @@ -28,6 +29,18 @@ from tests.typing import WebSocketGenerator ("binary_sensor", "on", "on", {}, {}, {"all": False}, {}), ("binary_sensor", "on", "on", {}, {"all": True}, {"all": True}, {}), ("cover", "open", "open", {}, {}, {}, {}), + ( + "event", + STATE_UNKNOWN, + "2021-01-01T23:59:59.123+00:00", + { + "event_type": "single_press", + "event_types": ["single_press", "double_press"], + }, + {}, + {}, + {}, + ), ("fan", "on", "on", {}, {}, {}, {}), ("light", "on", "on", {}, {}, {}, {}), ("lock", "locked", "locked", {}, {}, {}, {}), @@ -122,6 +135,7 @@ async def test_config_flow( ( ("binary_sensor", {"all": False}), ("cover", {}), + ("event", {}), ("fan", {}), ("light", {}), ("lock", {}), @@ -194,6 +208,7 @@ def get_suggested(schema, key): ( ("binary_sensor", "on", {"all": False}, {}), ("cover", "open", {}, {}), + ("event", "2021-01-01T23:59:59.123+00:00", {}, {}), ("fan", "on", {}, {}), ("light", "on", {"all": False}, {}), ("lock", "locked", {}, {}), @@ -377,6 +392,7 @@ async def test_all_options( ( ("binary_sensor", {"all": False}), ("cover", {}), + ("event", {}), ("fan", {}), ("light", {}), ("lock", {}), diff --git a/tests/components/group/test_event.py b/tests/components/group/test_event.py new file mode 100644 index 00000000000..16ea11fe311 --- /dev/null +++ b/tests/components/group/test_event.py @@ -0,0 +1,138 @@ +"""The tests for the group event platform.""" + +from pytest_unordered import unordered + +from homeassistant.components.event import DOMAIN as EVENT_DOMAIN +from homeassistant.components.event.const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES +from homeassistant.components.group import DOMAIN +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + + +async def test_default_state(hass: HomeAssistant) -> None: + """Test event group default state.""" + await async_setup_component( + hass, + EVENT_DOMAIN, + { + EVENT_DOMAIN: { + "platform": DOMAIN, + "entities": ["event.button_1", "event.button_2"], + "name": "Remote control", + "unique_id": "unique_identifier", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("event.remote_control") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + hass.states.async_set( + "event.button_1", + "2021-01-01T23:59:59.123+00:00", + {"event_type": "double_press", "event_types": ["single_press", "double_press"]}, + ) + await hass.async_block_till_done() + + state = hass.states.get("event.remote_control") + assert state is not None + assert state.state == STATE_UNKNOWN + assert state.attributes.get(ATTR_ENTITY_ID) == ["event.button_1", "event.button_2"] + assert not state.attributes.get(ATTR_EVENT_TYPE) + assert state.attributes.get(ATTR_EVENT_TYPES) == unordered( + ["single_press", "double_press"] + ) + + # State changed + hass.states.async_set( + "event.button_1", + "2021-01-01T23:59:59.123+00:00", + {"event_type": "single_press", "event_types": ["single_press", "double_press"]}, + ) + await hass.async_block_till_done() + + state = hass.states.get("event.remote_control") + assert state is not None + assert state.state + assert state.attributes.get(ATTR_ENTITY_ID) == ["event.button_1", "event.button_2"] + assert state.attributes.get(ATTR_EVENT_TYPE) == "single_press" + assert state.attributes.get(ATTR_EVENT_TYPES) == unordered( + ["single_press", "double_press"] + ) + + # State changed, second remote came online + hass.states.async_set( + "event.button_2", + "2021-01-01T23:59:59.123+00:00", + {"event_type": "double_press", "event_types": ["double_press", "triple_press"]}, + ) + await hass.async_block_till_done() + + # State should be single_press, because button coming online is not an event + state = hass.states.get("event.remote_control") + assert state is not None + assert state.state + assert state.attributes.get(ATTR_ENTITY_ID) == ["event.button_1", "event.button_2"] + assert state.attributes.get(ATTR_EVENT_TYPE) == "single_press" + assert state.attributes.get(ATTR_EVENT_TYPES) == unordered( + ["single_press", "double_press", "triple_press"] + ) + + # State changed, now it fires an event + hass.states.async_set( + "event.button_2", + "2021-01-01T23:59:59.123+00:00", + { + "event_type": "triple_press", + "event_types": ["double_press", "triple_press"], + "device_class": "doorbell", + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("event.remote_control") + assert state is not None + assert state.state + assert state.attributes.get(ATTR_ENTITY_ID) == ["event.button_1", "event.button_2"] + assert state.attributes.get(ATTR_EVENT_TYPE) == "triple_press" + assert state.attributes.get(ATTR_EVENT_TYPES) == unordered( + ["single_press", "double_press", "triple_press"] + ) + assert ATTR_DEVICE_CLASS not in state.attributes + + # Mark button 1 unavailable + hass.states.async_set("event.button_1", STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get("event.remote_control") + assert state is not None + assert state.state + assert state.attributes.get(ATTR_ENTITY_ID) == ["event.button_1", "event.button_2"] + assert state.attributes.get(ATTR_EVENT_TYPE) == "triple_press" + assert state.attributes.get(ATTR_EVENT_TYPES) == unordered( + ["double_press", "triple_press"] + ) + + # Mark button 2 unavailable + hass.states.async_set("event.button_2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get("event.remote_control") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("event.remote_control") + assert entry + assert entry.unique_id == "unique_identifier" From b145352bbb59d2ffaec5c020231191841a5e2dcf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Aug 2023 13:44:43 +0200 Subject: [PATCH 0826/1151] Modernize meteo_france weather (#98022) * Modernize meteofrance weather * Remove options flow * Remove unused constant * Format code --------- Co-authored-by: Quentin POLLET --- .../components/meteo_france/config_flow.py | 39 ++------------ .../components/meteo_france/const.py | 1 - .../components/meteo_france/strings.json | 9 ---- .../components/meteo_france/weather.py | 52 ++++++++++++++----- .../meteo_france/test_config_flow.py | 42 +-------------- 5 files changed, 45 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py index d05c63ef684..ade6bedd362 100644 --- a/homeassistant/components/meteo_france/config_flow.py +++ b/homeassistant/components/meteo_france/config_flow.py @@ -7,11 +7,11 @@ from meteofrance_api.client import MeteoFranceClient import voluptuous as vol from homeassistant import config_entries -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import callback -from .const import CONF_CITY, DOMAIN, FORECAST_MODE, FORECAST_MODE_DAILY +from .const import CONF_CITY, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -25,14 +25,6 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Init MeteoFranceFlowHandler.""" self.places = [] - @staticmethod - @callback - def async_get_options_flow( - config_entry: ConfigEntry, - ) -> MeteoFranceOptionsFlowHandler: - """Get the options flow for this handler.""" - return MeteoFranceOptionsFlowHandler(config_entry) - @callback def _show_setup_form(self, user_input=None, errors=None): """Show the setup form to the user.""" @@ -114,30 +106,5 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class MeteoFranceOptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None): - """Handle options flow.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - data_schema = vol.Schema( - { - vol.Optional( - CONF_MODE, - default=self.config_entry.options.get( - CONF_MODE, FORECAST_MODE_DAILY - ), - ): vol.In(FORECAST_MODE) - } - ) - return self.async_show_form(step_id="init", data_schema=data_schema) - - def _build_place_key(place) -> str: return f"{place};{place.latitude};{place.longitude}" diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index f1e6ae8d0eb..e950dfe1fa8 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -33,7 +33,6 @@ MANUFACTURER = "Météo-France" CONF_CITY = "city" FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" -FORECAST_MODE = [FORECAST_MODE_HOURLY, FORECAST_MODE_DAILY] ATTR_NEXT_RAIN_1_HOUR_FORECAST = "1_hour_forecast" ATTR_NEXT_RAIN_DT_REF = "forecast_time_ref" diff --git a/homeassistant/components/meteo_france/strings.json b/homeassistant/components/meteo_france/strings.json index 944f2b32fab..7cb7d3efe53 100644 --- a/homeassistant/components/meteo_france/strings.json +++ b/homeassistant/components/meteo_france/strings.json @@ -21,14 +21,5 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", "unknown": "[%key:common::config_flow::error::unknown%]" } - }, - "options": { - "step": { - "init": { - "data": { - "mode": "Forecast mode" - } - } - } } } diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 6459827b601..d081a6e729b 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -2,7 +2,7 @@ import logging import time -from meteofrance_api.model.forecast import Forecast +from meteofrance_api.model.forecast import Forecast as MeteoFranceForecast from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -13,7 +13,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 ( @@ -23,7 +25,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, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -55,9 +57,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Meteo-France weather platform.""" - coordinator: DataUpdateCoordinator[Forecast] = hass.data[DOMAIN][entry.entry_id][ - COORDINATOR_FORECAST - ] + coordinator: DataUpdateCoordinator[MeteoFranceForecast] = hass.data[DOMAIN][ + entry.entry_id + ][COORDINATOR_FORECAST] async_add_entities( [ @@ -76,7 +78,7 @@ async def async_setup_entry( class MeteoFranceWeather( - CoordinatorEntity[DataUpdateCoordinator[Forecast]], WeatherEntity + CoordinatorEntity[DataUpdateCoordinator[MeteoFranceForecast]], WeatherEntity ): """Representation of a weather condition.""" @@ -85,14 +87,28 @@ class MeteoFranceWeather( _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) - def __init__(self, coordinator: DataUpdateCoordinator[Forecast], mode: str) -> None: + def __init__( + self, coordinator: DataUpdateCoordinator[MeteoFranceForecast], mode: str + ) -> None: """Initialise the platform with a data instance and station name.""" super().__init__(coordinator) self._city_name = self.coordinator.data.position["name"] self._mode = mode self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}" + @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 the unique id of the sensor.""" @@ -149,12 +165,11 @@ class MeteoFranceWeather( if wind_bearing != -1: return wind_bearing - @property - def forecast(self): + def _forecast(self, mode: str) -> list[Forecast]: """Return the forecast.""" - forecast_data = [] + forecast_data: list[Forecast] = [] - if self._mode == FORECAST_MODE_HOURLY: + if mode == FORECAST_MODE_HOURLY: today = time.time() for forecast in self.coordinator.data.forecast: # Can have data in the past @@ -186,7 +201,7 @@ class MeteoFranceWeather( { ATTR_FORECAST_TIME: self.coordinator.data.timestamp_to_locale_time( forecast["dt"] - ), + ).isoformat(), ATTR_FORECAST_CONDITION: format_condition( forecast["weather12H"]["desc"] ), @@ -199,3 +214,16 @@ class MeteoFranceWeather( } ) return forecast_data + + @property + def forecast(self) -> list[Forecast]: + """Return the forecast array.""" + return self._forecast(self._mode) + + async def async_forecast_daily(self) -> list[Forecast]: + """Return the daily forecast in native units.""" + return self._forecast(FORECAST_MODE_DAILY) + + async def async_forecast_hourly(self) -> list[Forecast]: + """Return the hourly forecast in native units.""" + return self._forecast(FORECAST_MODE_HOURLY) diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index e405d74ad53..80155d3311a 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -5,14 +5,9 @@ from meteofrance_api.model import Place import pytest from homeassistant import data_entry_flow -from homeassistant.components.meteo_france.const import ( - CONF_CITY, - DOMAIN, - FORECAST_MODE_DAILY, - FORECAST_MODE_HOURLY, -) +from homeassistant.components.meteo_france.const import CONF_CITY, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -212,36 +207,3 @@ async def test_abort_if_already_setup(hass: HomeAssistant, client_single) -> Non ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_options_flow(hass: HomeAssistant) -> None: - """Test config flow options.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_LATITUDE: CITY_1_LAT, CONF_LONGITUDE: CITY_1_LON}, - unique_id=f"{CITY_1_LAT}, {CITY_1_LON}", - ) - config_entry.add_to_hass(hass) - - assert config_entry.options == {} - - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - - # Default - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={}, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options[CONF_MODE] == FORECAST_MODE_DAILY - - # Manual - result = await hass.config_entries.options.async_init(config_entry.entry_id) - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONF_MODE: FORECAST_MODE_HOURLY}, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options[CONF_MODE] == FORECAST_MODE_HOURLY From 99e97782b6b742067ef8834e413c4d51124ba2ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Aug 2023 08:34:45 -0500 Subject: [PATCH 0827/1151] Improve performance of abort_entries_match (#98932) * Improve performance of abort_entries_match In #90406 a ChainMap was added which called __iter__ and __contains__ which ends up creating temp dicts for matching https://github.com/python/cpython/blob/174e9da0836844a2138cc8915dd305cb2cd7a583/Lib/collections/__init__.py#L1022 We can avoid this by removing the ChainMap since there are only two mappings to match on. This also means options no longer obscures data * adjust comment --- homeassistant/config_entries.py | 15 ++++++--------- tests/test_config_entries.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 78b54929015..a3b03407a14 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections import ChainMap from collections.abc import Callable, Coroutine, Generator, Iterable, Mapping from contextvars import ContextVar from copy import deepcopy @@ -1465,14 +1464,12 @@ def _async_abort_entries_match( if match_dict is None: match_dict = {} # Match any entry for entry in other_entries: - if all( - item - in ChainMap( - entry.options, # type: ignore[arg-type] - entry.data, # type: ignore[arg-type] - ).items() - for item in match_dict.items() - ): + options_items = entry.options.items() + data_items = entry.data.items() + for kv in match_dict.items(): + if kv not in options_items and kv not in data_items: + break + else: raise data_entry_flow.AbortFlow("already_configured") diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 680adcf1202..75b6377973b 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3379,11 +3379,13 @@ async def test_setup_retrying_during_shutdown(hass: HomeAssistant) -> None: ({"vendor": "zoo"}, "already_configured"), ({"ip": "9.9.9.9"}, "already_configured"), ({"ip": "7.7.7.7"}, "no_match"), # ignored - ({"vendor": "data"}, "no_match"), + # The next two data sets ensure options or data match + # as options previously shadowed data when matching. + ({"vendor": "data"}, "already_configured"), ( {"vendor": "options"}, "already_configured", - ), # ensure options takes precedence over data + ), ], ) async def test__async_abort_entries_match( @@ -3460,11 +3462,13 @@ async def test__async_abort_entries_match( ({"vendor": "zoo"}, "already_configured"), ({"ip": "9.9.9.9"}, "already_configured"), ({"ip": "7.7.7.7"}, "no_match"), # ignored - ({"vendor": "data"}, "no_match"), + # The next two data sets ensure options or data match + # as options previously shadowed data when matching. + ({"vendor": "data"}, "already_configured"), ( {"vendor": "options"}, "already_configured", - ), # ensure options takes precedence over data + ), ], ) async def test__async_abort_entries_match_options_flow( From 61c17291fb4674f4d5091b9f6cec5f87987be82e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 24 Aug 2023 15:37:04 +0200 Subject: [PATCH 0828/1151] Move TemplateEntity to template (#98957) * Move TemplateEntity to template * Rename template_entity in helpers --- .../components/command_line/binary_sensor.py | 2 +- .../components/command_line/cover.py | 2 +- .../components/command_line/sensor.py | 2 +- .../components/command_line/switch.py | 2 +- .../components/rest/binary_sensor.py | 2 +- homeassistant/components/rest/schema.py | 2 +- homeassistant/components/rest/sensor.py | 2 +- homeassistant/components/rest/switch.py | 2 +- homeassistant/components/scrape/__init__.py | 2 +- homeassistant/components/scrape/sensor.py | 2 +- homeassistant/components/snmp/sensor.py | 2 +- homeassistant/components/sql/__init__.py | 5 +- homeassistant/components/sql/sensor.py | 2 +- homeassistant/components/template/sensor.py | 6 +- .../components/template/template_entity.py | 390 ++++++++++- .../components/template/trigger_entity.py | 2 +- homeassistant/helpers/template_entity.py | 648 ------------------ .../helpers/trigger_template_entity.py | 267 ++++++++ tests/components/rest/test_switch.py | 2 +- tests/components/scrape/test_sensor.py | 5 +- tests/components/sql/__init__.py | 5 +- .../template/test_manual_trigger_entity.py | 2 +- 22 files changed, 683 insertions(+), 673 deletions(-) delete mode 100644 homeassistant/helpers/template_entity.py create mode 100644 homeassistant/helpers/trigger_template_entity.py diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index f2097178a95..1d6ee9046e8 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -29,7 +29,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template -from homeassistant.helpers.template_entity import ManualTriggerEntity +from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 553af2f0c86..2aa67cec641 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -30,7 +30,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template -from homeassistant.helpers.template_entity import ManualTriggerEntity +from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util, slugify diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index f04320b159e..a617d348c8d 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -35,7 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template -from homeassistant.helpers.template_entity import ( +from homeassistant.helpers.trigger_template_entity import ( CONF_AVAILABILITY, CONF_PICTURE, ManualTriggerSensorEntity, diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 8fbafd7a4d1..004a65643bb 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -32,7 +32,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template -from homeassistant.helpers.template_entity import ManualTriggerEntity +from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util, slugify diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 7ab632995ea..8c629e2240e 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -26,7 +26,7 @@ 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 ( +from homeassistant.helpers.trigger_template_entity import ( CONF_AVAILABILITY, CONF_PICTURE, ManualTriggerEntity, diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index 2f447b1c08c..d6011a43efd 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -27,7 +27,7 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template_entity import ( +from homeassistant.helpers.trigger_template_entity import ( CONF_AVAILABILITY, TEMPLATE_ENTITY_BASE_SCHEMA, TEMPLATE_SENSOR_BASE_SCHEMA, diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 63a9d6f210c..67f70a716b0 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -30,7 +30,7 @@ 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 ( +from homeassistant.helpers.trigger_template_entity import ( CONF_AVAILABILITY, CONF_PICTURE, ManualTriggerSensorEntity, diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 22570c3a245..102bb024924 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -33,7 +33,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.helpers.template_entity import ( +from homeassistant.helpers.trigger_template_entity import ( CONF_AVAILABILITY, CONF_PICTURE, TEMPLATE_ENTITY_BASE_SCHEMA, diff --git a/homeassistant/components/scrape/__init__.py b/homeassistant/components/scrape/__init__.py index bf2ccb16b03..bdfa3fd9c5a 100644 --- a/homeassistant/components/scrape/__init__.py +++ b/homeassistant/components/scrape/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template_entity import ( +from homeassistant.helpers.trigger_template_entity import ( CONF_AVAILABILITY, TEMPLATE_SENSOR_BASE_SCHEMA, ) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 2763d034804..77131ccb225 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -23,7 +23,7 @@ from homeassistant.exceptions import PlatformNotReady 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 ( +from homeassistant.helpers.trigger_template_entity import ( CONF_AVAILABILITY, CONF_PICTURE, TEMPLATE_SENSOR_BASE_SCHEMA, diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 85c69ddf76b..a5915183ad0 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -36,7 +36,7 @@ from homeassistant.core import HomeAssistant 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 ( +from homeassistant.helpers.trigger_template_entity import ( CONF_AVAILABILITY, CONF_PICTURE, TEMPLATE_SENSOR_BASE_SCHEMA, diff --git a/homeassistant/components/sql/__init__.py b/homeassistant/components/sql/__init__.py index 316e816fd6f..4658e19932c 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -24,7 +24,10 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template_entity import CONF_AVAILABILITY, CONF_PICTURE +from homeassistant.helpers.trigger_template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, +) from homeassistant.helpers.typing import ConfigType from .const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN, PLATFORMS diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index dffb45bfd93..f4f44d4f9a4 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -36,7 +36,7 @@ from homeassistant.helpers import issue_registry as ir 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 ( +from homeassistant.helpers.trigger_template_entity import ( CONF_AVAILABILITY, CONF_PICTURE, ManualTriggerSensorEntity, diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index aa6788109ff..36e54eaabc9 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -38,10 +38,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.template_entity import ( - TEMPLATE_SENSOR_BASE_SCHEMA, - TemplateEntity, -) +from homeassistant.helpers.trigger_template_entity import TEMPLATE_SENSOR_BASE_SCHEMA from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -52,6 +49,7 @@ from .const import ( ) from .template_entity import ( TEMPLATE_ENTITY_COMMON_SCHEMA, + TemplateEntity, rewrite_common_legacy_to_modern_conf, ) from .trigger_entity import TriggerEntity diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index fe1a53e6510..64112b0d3d4 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -1,7 +1,10 @@ """TemplateEntity utility class.""" from __future__ import annotations +from collections.abc import Callable +import contextlib import itertools +import logging from typing import Any import voluptuous as vol @@ -12,14 +15,30 @@ from homeassistant.const import ( CONF_ICON, CONF_ICON_TEMPLATE, CONF_NAME, + EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN, ) +from homeassistant.core import Context, CoreState, HomeAssistant, State, callback +from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template import Template -from homeassistant.helpers.template_entity import ( # noqa: F401 +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import ( + EventStateChangedData, + TrackTemplate, + TrackTemplateResult, + async_track_template_result, +) +from homeassistant.helpers.script import Script, _VarsType +from homeassistant.helpers.template import ( + Template, + TemplateStateFromEntityId, + result_as_boolean, +) +from homeassistant.helpers.trigger_template_entity import ( TEMPLATE_ENTITY_BASE_SCHEMA, - TemplateEntity, make_template_entity_base_schema, ) +from homeassistant.helpers.typing import ConfigType, EventType from .const import ( CONF_ATTRIBUTE_TEMPLATES, @@ -29,6 +48,8 @@ from .const import ( CONF_PICTURE, ) +_LOGGER = logging.getLogger(__name__) + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA = vol.Schema( { vol.Optional(CONF_AVAILABILITY): cv.template, @@ -113,3 +134,366 @@ def rewrite_common_legacy_to_modern_conf( entity_cfg[CONF_NAME] = Template(entity_cfg[CONF_NAME]) return entity_cfg + + +class _TemplateAttribute: + """Attribute value linked to template result.""" + + def __init__( + self, + entity: Entity, + attribute: str, + template: Template, + validator: Callable[[Any], Any] | None = None, + on_update: Callable[[Any], None] | None = None, + none_on_template_error: bool | None = False, + ) -> None: + """Template attribute.""" + self._entity = entity + self._attribute = attribute + self.template = template + self.validator = validator + self.on_update = on_update + self.async_update = None + self.none_on_template_error = none_on_template_error + + @callback + def async_setup(self) -> None: + """Config update path for the attribute.""" + if self.on_update: + return + + if not hasattr(self._entity, self._attribute): + raise AttributeError(f"Attribute '{self._attribute}' does not exist.") + + self.on_update = self._default_update + + @callback + def _default_update(self, result: str | TemplateError) -> None: + attr_result = None if isinstance(result, TemplateError) else result + setattr(self._entity, self._attribute, attr_result) + + @callback + def handle_result( + self, + event: EventType[EventStateChangedData] | None, + template: Template, + last_result: str | None | TemplateError, + result: str | TemplateError, + ) -> None: + """Handle a template result event callback.""" + if isinstance(result, TemplateError): + _LOGGER.error( + ( + "TemplateError('%s') " + "while processing template '%s' " + "for attribute '%s' in entity '%s'" + ), + result, + self.template, + self._attribute, + self._entity.entity_id, + ) + if self.none_on_template_error: + self._default_update(result) + else: + assert self.on_update + self.on_update(result) + return + + if not self.validator: + assert self.on_update + self.on_update(result) + return + + try: + validated = self.validator(result) + except vol.Invalid as ex: + _LOGGER.error( + ( + "Error validating template result '%s' " + "from template '%s' " + "for attribute '%s' in entity %s " + "validation message '%s'" + ), + result, + self.template, + self._attribute, + self._entity.entity_id, + ex.msg, + ) + assert self.on_update + self.on_update(None) + return + + assert self.on_update + self.on_update(validated) + return + + +class TemplateEntity(Entity): + """Entity that uses templates to calculate attributes.""" + + _attr_available = True + _attr_entity_picture = None + _attr_icon = None + + def __init__( + self, + hass: HomeAssistant, + *, + availability_template: Template | None = None, + icon_template: Template | None = None, + entity_picture_template: Template | None = None, + attribute_templates: dict[str, Template] | None = None, + config: ConfigType | None = None, + fallback_name: str | None = None, + unique_id: str | None = None, + ) -> None: + """Template Entity.""" + self._template_attrs: dict[Template, list[_TemplateAttribute]] = {} + self._async_update: Callable[[], None] | None = None + self._attr_extra_state_attributes = {} + self._self_ref_update_count = 0 + self._attr_unique_id = unique_id + if config is None: + self._attribute_templates = attribute_templates + self._availability_template = availability_template + self._icon_template = icon_template + self._entity_picture_template = entity_picture_template + self._friendly_name_template = None + else: + self._attribute_templates = config.get(CONF_ATTRIBUTES) + self._availability_template = config.get(CONF_AVAILABILITY) + self._icon_template = config.get(CONF_ICON) + self._entity_picture_template = config.get(CONF_PICTURE) + self._friendly_name_template = config.get(CONF_NAME) + + class DummyState(State): + """None-state for template entities not yet added to the state machine.""" + + def __init__(self) -> None: + """Initialize a new state.""" + super().__init__("unknown.unknown", STATE_UNKNOWN) + self.entity_id = None # type: ignore[assignment] + + @property + def name(self) -> str: + """Name of this state.""" + return "" + + variables = {"this": DummyState()} + + # Try to render the name as it can influence the entity ID + self._attr_name = fallback_name + if self._friendly_name_template: + self._friendly_name_template.hass = hass + with contextlib.suppress(TemplateError): + self._attr_name = self._friendly_name_template.async_render( + variables=variables, parse_result=False + ) + + # Templates will not render while the entity is unavailable, try to render the + # icon and picture templates. + if self._entity_picture_template: + self._entity_picture_template.hass = hass + with contextlib.suppress(TemplateError): + self._attr_entity_picture = self._entity_picture_template.async_render( + variables=variables, parse_result=False + ) + + if self._icon_template: + self._icon_template.hass = hass + with contextlib.suppress(TemplateError): + self._attr_icon = self._icon_template.async_render( + variables=variables, parse_result=False + ) + + @callback + def _update_available(self, result: str | TemplateError) -> None: + if isinstance(result, TemplateError): + self._attr_available = True + return + + self._attr_available = result_as_boolean(result) + + @callback + def _update_state(self, result: str | TemplateError) -> None: + if self._availability_template: + return + + self._attr_available = not isinstance(result, TemplateError) + + @callback + def _add_attribute_template( + self, attribute_key: str, attribute_template: Template + ) -> None: + """Create a template tracker for the attribute.""" + + def _update_attribute(result: str | TemplateError) -> None: + attr_result = None if isinstance(result, TemplateError) else result + self._attr_extra_state_attributes[attribute_key] = attr_result + + self.add_template_attribute( + attribute_key, attribute_template, None, _update_attribute + ) + + def add_template_attribute( + self, + attribute: str, + template: Template, + validator: Callable[[Any], Any] | None = None, + on_update: Callable[[Any], None] | None = None, + none_on_template_error: bool = False, + ) -> None: + """Call in the constructor to add a template linked to a attribute. + + Parameters + ---------- + attribute + The name of the attribute to link to. This attribute must exist + unless a custom on_update method is supplied. + template + The template to calculate. + validator + Validator function to parse the result and ensure it's valid. + on_update + Called to store the template result rather than storing it + the supplied attribute. Passed the result of the validator, or None + if the template or validator resulted in an error. + none_on_template_error + If True, the attribute will be set to None if the template errors. + + """ + assert self.hass is not None, "hass cannot be None" + template.hass = self.hass + template_attribute = _TemplateAttribute( + self, attribute, template, validator, on_update, none_on_template_error + ) + self._template_attrs.setdefault(template, []) + self._template_attrs[template].append(template_attribute) + + @callback + def _handle_results( + self, + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: + """Call back the results to the attributes.""" + if event: + self.async_set_context(event.context) + + entity_id = event and event.data["entity_id"] + + if entity_id and entity_id == self.entity_id: + self._self_ref_update_count += 1 + else: + self._self_ref_update_count = 0 + + if self._self_ref_update_count > len(self._template_attrs): + for update in updates: + _LOGGER.warning( + ( + "Template loop detected while processing event: %s, skipping" + " template render for Template[%s]" + ), + event, + update.template.template, + ) + return + + for update in updates: + for attr in self._template_attrs[update.template]: + attr.handle_result( + event, update.template, update.last_result, update.result + ) + + self.async_write_ha_state() + + async def _async_template_startup(self, *_: Any) -> None: + template_var_tups: list[TrackTemplate] = [] + has_availability_template = False + + variables = {"this": TemplateStateFromEntityId(self.hass, self.entity_id)} + + for template, attributes in self._template_attrs.items(): + template_var_tup = TrackTemplate(template, variables) + is_availability_template = False + for attribute in attributes: + # pylint: disable-next=protected-access + if attribute._attribute == "_attr_available": + has_availability_template = True + is_availability_template = True + attribute.async_setup() + # Insert the availability template first in the list + if is_availability_template: + template_var_tups.insert(0, template_var_tup) + else: + template_var_tups.append(template_var_tup) + + result_info = async_track_template_result( + self.hass, + template_var_tups, + self._handle_results, + has_super_template=has_availability_template, + ) + self.async_on_remove(result_info.async_remove) + self._async_update = result_info.async_refresh + result_info.async_refresh() + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + if self._availability_template is not None: + self.add_template_attribute( + "_attr_available", + self._availability_template, + None, + self._update_available, + ) + if self._attribute_templates is not None: + for key, value in self._attribute_templates.items(): + self._add_attribute_template(key, value) + if self._icon_template is not None: + self.add_template_attribute( + "_attr_icon", self._icon_template, vol.Or(cv.whitespace, cv.icon) + ) + if self._entity_picture_template is not None: + self.add_template_attribute( + "_attr_entity_picture", self._entity_picture_template + ) + if ( + self._friendly_name_template is not None + and not self._friendly_name_template.is_static + ): + self.add_template_attribute("_attr_name", self._friendly_name_template) + + if self.hass.state == CoreState.running: + await self._async_template_startup() + return + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self._async_template_startup + ) + + async def async_update(self) -> None: + """Call for forced update.""" + assert self._async_update + self._async_update() + + async def async_run_script( + self, + script: Script, + *, + run_variables: _VarsType | None = None, + context: Context | None = None, + ) -> None: + """Run an action script.""" + if run_variables is None: + run_variables = {} + await script.async_run( + run_variables={ + "this": TemplateStateFromEntityId(self.hass, self.entity_id), + **run_variables, + }, + context=context, + ) diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index 7d1a844fb3d..ca2f7240086 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.template_entity import TriggerBaseEntity +from homeassistant.helpers.trigger_template_entity import TriggerBaseEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import TriggerUpdateCoordinator diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py deleted file mode 100644 index 16dc212e8cc..00000000000 --- a/homeassistant/helpers/template_entity.py +++ /dev/null @@ -1,648 +0,0 @@ -"""TemplateEntity utility class.""" -from __future__ import annotations - -from collections.abc import Callable -import contextlib -import logging -from typing import Any - -import voluptuous as vol - -from homeassistant.components.sensor import ( - CONF_STATE_CLASS, - DEVICE_CLASSES_SCHEMA, - STATE_CLASSES_SCHEMA, - SensorEntity, -) -from homeassistant.const import ( - ATTR_ENTITY_PICTURE, - ATTR_FRIENDLY_NAME, - ATTR_ICON, - CONF_DEVICE_CLASS, - CONF_ICON, - CONF_NAME, - CONF_UNIQUE_ID, - CONF_UNIT_OF_MEASUREMENT, - EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN, -) -from homeassistant.core import Context, CoreState, HomeAssistant, State, callback -from homeassistant.exceptions import TemplateError -from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads - -from . import config_validation as cv -from .entity import Entity -from .event import ( - EventStateChangedData, - TrackTemplate, - TrackTemplateResult, - async_track_template_result, -) -from .script import Script, _VarsType -from .template import ( - Template, - TemplateStateFromEntityId, - attach as template_attach, - render_complex, - result_as_boolean, -) -from .typing import ConfigType, EventType - -_LOGGER = logging.getLogger(__name__) - -CONF_AVAILABILITY = "availability" -CONF_ATTRIBUTES = "attributes" -CONF_PICTURE = "picture" - -CONF_TO_ATTRIBUTE = { - CONF_ICON: ATTR_ICON, - CONF_NAME: ATTR_FRIENDLY_NAME, - CONF_PICTURE: ATTR_ENTITY_PICTURE, -} - -TEMPLATE_ENTITY_BASE_SCHEMA = vol.Schema( - { - vol.Optional(CONF_ICON): cv.template, - vol.Optional(CONF_NAME): cv.template, - vol.Optional(CONF_PICTURE): cv.template, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } -) - - -def make_template_entity_base_schema(default_name: str) -> vol.Schema: - """Return a schema with default name.""" - return vol.Schema( - { - vol.Optional(CONF_ICON): cv.template, - vol.Optional(CONF_NAME, default=default_name): cv.template, - vol.Optional(CONF_PICTURE): cv.template, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } - ) - - -TEMPLATE_SENSOR_BASE_SCHEMA = vol.Schema( - { - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - } -).extend(TEMPLATE_ENTITY_BASE_SCHEMA.schema) - - -class _TemplateAttribute: - """Attribute value linked to template result.""" - - def __init__( - self, - entity: Entity, - attribute: str, - template: Template, - validator: Callable[[Any], Any] | None = None, - on_update: Callable[[Any], None] | None = None, - none_on_template_error: bool | None = False, - ) -> None: - """Template attribute.""" - self._entity = entity - self._attribute = attribute - self.template = template - self.validator = validator - self.on_update = on_update - self.async_update = None - self.none_on_template_error = none_on_template_error - - @callback - def async_setup(self) -> None: - """Config update path for the attribute.""" - if self.on_update: - return - - if not hasattr(self._entity, self._attribute): - raise AttributeError(f"Attribute '{self._attribute}' does not exist.") - - self.on_update = self._default_update - - @callback - def _default_update(self, result: str | TemplateError) -> None: - attr_result = None if isinstance(result, TemplateError) else result - setattr(self._entity, self._attribute, attr_result) - - @callback - def handle_result( - self, - event: EventType[EventStateChangedData] | None, - template: Template, - last_result: str | None | TemplateError, - result: str | TemplateError, - ) -> None: - """Handle a template result event callback.""" - if isinstance(result, TemplateError): - _LOGGER.error( - ( - "TemplateError('%s') " - "while processing template '%s' " - "for attribute '%s' in entity '%s'" - ), - result, - self.template, - self._attribute, - self._entity.entity_id, - ) - if self.none_on_template_error: - self._default_update(result) - else: - assert self.on_update - self.on_update(result) - return - - if not self.validator: - assert self.on_update - self.on_update(result) - return - - try: - validated = self.validator(result) - except vol.Invalid as ex: - _LOGGER.error( - ( - "Error validating template result '%s' " - "from template '%s' " - "for attribute '%s' in entity %s " - "validation message '%s'" - ), - result, - self.template, - self._attribute, - self._entity.entity_id, - ex.msg, - ) - assert self.on_update - self.on_update(None) - return - - assert self.on_update - self.on_update(validated) - return - - -class TemplateEntity(Entity): - """Entity that uses templates to calculate attributes.""" - - _attr_available = True - _attr_entity_picture = None - _attr_icon = None - - def __init__( - self, - hass: HomeAssistant, - *, - availability_template: Template | None = None, - icon_template: Template | None = None, - entity_picture_template: Template | None = None, - attribute_templates: dict[str, Template] | None = None, - config: ConfigType | None = None, - fallback_name: str | None = None, - unique_id: str | None = None, - ) -> None: - """Template Entity.""" - self._template_attrs: dict[Template, list[_TemplateAttribute]] = {} - self._async_update: Callable[[], None] | None = None - self._attr_extra_state_attributes = {} - self._self_ref_update_count = 0 - self._attr_unique_id = unique_id - if config is None: - self._attribute_templates = attribute_templates - self._availability_template = availability_template - self._icon_template = icon_template - self._entity_picture_template = entity_picture_template - self._friendly_name_template = None - else: - self._attribute_templates = config.get(CONF_ATTRIBUTES) - self._availability_template = config.get(CONF_AVAILABILITY) - self._icon_template = config.get(CONF_ICON) - self._entity_picture_template = config.get(CONF_PICTURE) - self._friendly_name_template = config.get(CONF_NAME) - - class DummyState(State): - """None-state for template entities not yet added to the state machine.""" - - def __init__(self) -> None: - """Initialize a new state.""" - super().__init__("unknown.unknown", STATE_UNKNOWN) - self.entity_id = None # type: ignore[assignment] - - @property - def name(self) -> str: - """Name of this state.""" - return "" - - variables = {"this": DummyState()} - - # Try to render the name as it can influence the entity ID - self._attr_name = fallback_name - if self._friendly_name_template: - self._friendly_name_template.hass = hass - with contextlib.suppress(TemplateError): - self._attr_name = self._friendly_name_template.async_render( - variables=variables, parse_result=False - ) - - # Templates will not render while the entity is unavailable, try to render the - # icon and picture templates. - if self._entity_picture_template: - self._entity_picture_template.hass = hass - with contextlib.suppress(TemplateError): - self._attr_entity_picture = self._entity_picture_template.async_render( - variables=variables, parse_result=False - ) - - if self._icon_template: - self._icon_template.hass = hass - with contextlib.suppress(TemplateError): - self._attr_icon = self._icon_template.async_render( - variables=variables, parse_result=False - ) - - @callback - def _update_available(self, result: str | TemplateError) -> None: - if isinstance(result, TemplateError): - self._attr_available = True - return - - self._attr_available = result_as_boolean(result) - - @callback - def _update_state(self, result: str | TemplateError) -> None: - if self._availability_template: - return - - self._attr_available = not isinstance(result, TemplateError) - - @callback - def _add_attribute_template( - self, attribute_key: str, attribute_template: Template - ) -> None: - """Create a template tracker for the attribute.""" - - def _update_attribute(result: str | TemplateError) -> None: - attr_result = None if isinstance(result, TemplateError) else result - self._attr_extra_state_attributes[attribute_key] = attr_result - - self.add_template_attribute( - attribute_key, attribute_template, None, _update_attribute - ) - - def add_template_attribute( - self, - attribute: str, - template: Template, - validator: Callable[[Any], Any] | None = None, - on_update: Callable[[Any], None] | None = None, - none_on_template_error: bool = False, - ) -> None: - """Call in the constructor to add a template linked to a attribute. - - Parameters - ---------- - attribute - The name of the attribute to link to. This attribute must exist - unless a custom on_update method is supplied. - template - The template to calculate. - validator - Validator function to parse the result and ensure it's valid. - on_update - Called to store the template result rather than storing it - the supplied attribute. Passed the result of the validator, or None - if the template or validator resulted in an error. - none_on_template_error - If True, the attribute will be set to None if the template errors. - - """ - assert self.hass is not None, "hass cannot be None" - template.hass = self.hass - template_attribute = _TemplateAttribute( - self, attribute, template, validator, on_update, none_on_template_error - ) - self._template_attrs.setdefault(template, []) - self._template_attrs[template].append(template_attribute) - - @callback - def _handle_results( - self, - event: EventType[EventStateChangedData] | None, - updates: list[TrackTemplateResult], - ) -> None: - """Call back the results to the attributes.""" - if event: - self.async_set_context(event.context) - - entity_id = event and event.data["entity_id"] - - if entity_id and entity_id == self.entity_id: - self._self_ref_update_count += 1 - else: - self._self_ref_update_count = 0 - - if self._self_ref_update_count > len(self._template_attrs): - for update in updates: - _LOGGER.warning( - ( - "Template loop detected while processing event: %s, skipping" - " template render for Template[%s]" - ), - event, - update.template.template, - ) - return - - for update in updates: - for attr in self._template_attrs[update.template]: - attr.handle_result( - event, update.template, update.last_result, update.result - ) - - self.async_write_ha_state() - - async def _async_template_startup(self, *_: Any) -> None: - template_var_tups: list[TrackTemplate] = [] - has_availability_template = False - - variables = {"this": TemplateStateFromEntityId(self.hass, self.entity_id)} - - for template, attributes in self._template_attrs.items(): - template_var_tup = TrackTemplate(template, variables) - is_availability_template = False - for attribute in attributes: - # pylint: disable-next=protected-access - if attribute._attribute == "_attr_available": - has_availability_template = True - is_availability_template = True - attribute.async_setup() - # Insert the availability template first in the list - if is_availability_template: - template_var_tups.insert(0, template_var_tup) - else: - template_var_tups.append(template_var_tup) - - result_info = async_track_template_result( - self.hass, - template_var_tups, - self._handle_results, - has_super_template=has_availability_template, - ) - self.async_on_remove(result_info.async_remove) - self._async_update = result_info.async_refresh - result_info.async_refresh() - - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - if self._availability_template is not None: - self.add_template_attribute( - "_attr_available", - self._availability_template, - None, - self._update_available, - ) - if self._attribute_templates is not None: - for key, value in self._attribute_templates.items(): - self._add_attribute_template(key, value) - if self._icon_template is not None: - self.add_template_attribute( - "_attr_icon", self._icon_template, vol.Or(cv.whitespace, cv.icon) - ) - if self._entity_picture_template is not None: - self.add_template_attribute( - "_attr_entity_picture", self._entity_picture_template - ) - if ( - self._friendly_name_template is not None - and not self._friendly_name_template.is_static - ): - self.add_template_attribute("_attr_name", self._friendly_name_template) - - if self.hass.state == CoreState.running: - await self._async_template_startup() - return - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._async_template_startup - ) - - async def async_update(self) -> None: - """Call for forced update.""" - assert self._async_update - self._async_update() - - async def async_run_script( - self, - script: Script, - *, - run_variables: _VarsType | None = None, - context: Context | None = None, - ) -> None: - """Run an action script.""" - if run_variables is None: - run_variables = {} - await script.async_run( - run_variables={ - "this": TemplateStateFromEntityId(self.hass, self.entity_id), - **run_variables, - }, - context=context, - ) - - -class TriggerBaseEntity(Entity): - """Template Base entity based on trigger data.""" - - domain: str - extra_template_keys: tuple | None = None - extra_template_keys_complex: tuple | None = None - _unique_id: str | None - - def __init__( - self, - hass: HomeAssistant, - config: ConfigType, - ) -> None: - """Initialize the entity.""" - self.hass = hass - - self._set_unique_id(config.get(CONF_UNIQUE_ID)) - - self._config = config - - self._static_rendered = {} - self._to_render_simple = [] - self._to_render_complex: list[str] = [] - - for itm in ( - CONF_AVAILABILITY, - CONF_ICON, - CONF_NAME, - CONF_PICTURE, - ): - if itm not in config or config[itm] is None: - continue - if config[itm].is_static: - self._static_rendered[itm] = config[itm].template - else: - self._to_render_simple.append(itm) - - if self.extra_template_keys is not None: - self._to_render_simple.extend(self.extra_template_keys) - - if self.extra_template_keys_complex is not None: - self._to_render_complex.extend(self.extra_template_keys_complex) - - # We make a copy so our initial render is 'unknown' and not 'unavailable' - self._rendered = dict(self._static_rendered) - self._parse_result = {CONF_AVAILABILITY} - - @property - def name(self) -> str | None: - """Name of the entity.""" - return self._rendered.get(CONF_NAME) - - @property - def unique_id(self) -> str | None: - """Return unique ID of the entity.""" - return self._unique_id - - @property - def device_class(self): # type: ignore[no-untyped-def] - """Return device class of the entity.""" - return self._config.get(CONF_DEVICE_CLASS) - - @property - def icon(self) -> str | None: - """Return icon.""" - return self._rendered.get(CONF_ICON) - - @property - def entity_picture(self) -> str | None: - """Return entity picture.""" - return self._rendered.get(CONF_PICTURE) - - @property - def available(self) -> bool: - """Return availability of the entity.""" - return ( - self._rendered is not self._static_rendered - and - # Check against False so `None` is ok - self._rendered.get(CONF_AVAILABILITY) is not False - ) - - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return extra attributes.""" - return self._rendered.get(CONF_ATTRIBUTES) - - 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: - """Set unique id.""" - self._unique_id = unique_id - - def restore_attributes(self, last_state: State) -> None: - """Restore attributes.""" - for conf_key, attr in CONF_TO_ATTRIBUTE.items(): - if conf_key not in self._config or attr not in last_state.attributes: - continue - self._rendered[conf_key] = last_state.attributes[attr] - - if CONF_ATTRIBUTES in self._config: - extra_state_attributes = {} - for attr in self._config[CONF_ATTRIBUTES]: - if attr not in last_state.attributes: - continue - extra_state_attributes[attr] = last_state.attributes[attr] - self._rendered[CONF_ATTRIBUTES] = extra_state_attributes - - def _render_templates(self, variables: dict[str, Any]) -> None: - """Render templates.""" - try: - rendered = dict(self._static_rendered) - - for key in self._to_render_simple: - rendered[key] = self._config[key].async_render( - variables, - parse_result=key in self._parse_result, - ) - - for key in self._to_render_complex: - rendered[key] = render_complex( - self._config[key], - variables, - ) - - if CONF_ATTRIBUTES in self._config: - rendered[CONF_ATTRIBUTES] = render_complex( - self._config[CONF_ATTRIBUTES], - variables, - ) - - self._rendered = rendered - except TemplateError as err: - logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error( - "Error rendering %s template for %s: %s", key, self.entity_id, err - ) - self._rendered = self._static_rendered - - -class ManualTriggerEntity(TriggerBaseEntity): - """Template entity based on manual trigger data.""" - - def __init__( - self, - hass: HomeAssistant, - config: ConfigType, - ) -> None: - """Initialize the entity.""" - TriggerBaseEntity.__init__(self, hass, config) - # Need initial rendering on `name` as it influence the `entity_id` - self._rendered[CONF_NAME] = config[CONF_NAME].async_render( - {}, - parse_result=CONF_NAME in self._parse_result, - ) - - @callback - def _process_manual_data(self, value: Any | None = None) -> None: - """Process new data manually. - - Implementing class should call this last in update method to render templates. - Ex: self._process_manual_data(payload) - """ - - self.async_write_ha_state() - this = None - if state := self.hass.states.get(self.entity_id): - this = state.as_dict() - - run_variables: dict[str, Any] = {"value": value} - # Silently try if variable is a json and store result in `value_json` if it is. - with contextlib.suppress(*JSON_DECODE_EXCEPTIONS): - run_variables["value_json"] = json_loads(run_variables["value"]) - variables = {"this": this, **(run_variables or {})} - - self._render_templates(variables) - - -class ManualTriggerSensorEntity(ManualTriggerEntity, SensorEntity): - """Template entity based on manual trigger data for sensor.""" - - def __init__( - self, - hass: HomeAssistant, - config: ConfigType, - ) -> 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/homeassistant/helpers/trigger_template_entity.py b/homeassistant/helpers/trigger_template_entity.py new file mode 100644 index 00000000000..8fc99f5cb52 --- /dev/null +++ b/homeassistant/helpers/trigger_template_entity.py @@ -0,0 +1,267 @@ +"""TemplateEntity utility class.""" +from __future__ import annotations + +import contextlib +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + DEVICE_CLASSES_SCHEMA, + STATE_CLASSES_SCHEMA, + SensorEntity, +) +from homeassistant.const import ( + ATTR_ENTITY_PICTURE, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, +) +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.exceptions import TemplateError +from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads + +from . import config_validation as cv +from .entity import Entity +from .template import attach as template_attach, render_complex +from .typing import ConfigType + +CONF_AVAILABILITY = "availability" +CONF_ATTRIBUTES = "attributes" +CONF_PICTURE = "picture" + +CONF_TO_ATTRIBUTE = { + CONF_ICON: ATTR_ICON, + CONF_NAME: ATTR_FRIENDLY_NAME, + CONF_PICTURE: ATTR_ENTITY_PICTURE, +} + +TEMPLATE_ENTITY_BASE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_NAME): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) + + +def make_template_entity_base_schema(default_name: str) -> vol.Schema: + """Return a schema with default name.""" + return vol.Schema( + { + vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_NAME, default=default_name): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ) + + +TEMPLATE_SENSOR_BASE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } +).extend(TEMPLATE_ENTITY_BASE_SCHEMA.schema) + + +class TriggerBaseEntity(Entity): + """Template Base entity based on trigger data.""" + + domain: str + extra_template_keys: tuple | None = None + extra_template_keys_complex: tuple | None = None + _unique_id: str | None + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + ) -> None: + """Initialize the entity.""" + self.hass = hass + + self._set_unique_id(config.get(CONF_UNIQUE_ID)) + + self._config = config + + self._static_rendered = {} + self._to_render_simple = [] + self._to_render_complex: list[str] = [] + + for itm in ( + CONF_AVAILABILITY, + CONF_ICON, + CONF_NAME, + CONF_PICTURE, + ): + if itm not in config or config[itm] is None: + continue + if config[itm].is_static: + self._static_rendered[itm] = config[itm].template + else: + self._to_render_simple.append(itm) + + if self.extra_template_keys is not None: + self._to_render_simple.extend(self.extra_template_keys) + + if self.extra_template_keys_complex is not None: + self._to_render_complex.extend(self.extra_template_keys_complex) + + # We make a copy so our initial render is 'unknown' and not 'unavailable' + self._rendered = dict(self._static_rendered) + self._parse_result = {CONF_AVAILABILITY} + + @property + def name(self) -> str | None: + """Name of the entity.""" + return self._rendered.get(CONF_NAME) + + @property + def unique_id(self) -> str | None: + """Return unique ID of the entity.""" + return self._unique_id + + @property + def device_class(self): # type: ignore[no-untyped-def] + """Return device class of the entity.""" + return self._config.get(CONF_DEVICE_CLASS) + + @property + def icon(self) -> str | None: + """Return icon.""" + return self._rendered.get(CONF_ICON) + + @property + def entity_picture(self) -> str | None: + """Return entity picture.""" + return self._rendered.get(CONF_PICTURE) + + @property + def available(self) -> bool: + """Return availability of the entity.""" + return ( + self._rendered is not self._static_rendered + and + # Check against False so `None` is ok + self._rendered.get(CONF_AVAILABILITY) is not False + ) + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return extra attributes.""" + return self._rendered.get(CONF_ATTRIBUTES) + + 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: + """Set unique id.""" + self._unique_id = unique_id + + def restore_attributes(self, last_state: State) -> None: + """Restore attributes.""" + for conf_key, attr in CONF_TO_ATTRIBUTE.items(): + if conf_key not in self._config or attr not in last_state.attributes: + continue + self._rendered[conf_key] = last_state.attributes[attr] + + if CONF_ATTRIBUTES in self._config: + extra_state_attributes = {} + for attr in self._config[CONF_ATTRIBUTES]: + if attr not in last_state.attributes: + continue + extra_state_attributes[attr] = last_state.attributes[attr] + self._rendered[CONF_ATTRIBUTES] = extra_state_attributes + + def _render_templates(self, variables: dict[str, Any]) -> None: + """Render templates.""" + try: + rendered = dict(self._static_rendered) + + for key in self._to_render_simple: + rendered[key] = self._config[key].async_render( + variables, + parse_result=key in self._parse_result, + ) + + for key in self._to_render_complex: + rendered[key] = render_complex( + self._config[key], + variables, + ) + + if CONF_ATTRIBUTES in self._config: + rendered[CONF_ATTRIBUTES] = render_complex( + self._config[CONF_ATTRIBUTES], + variables, + ) + + self._rendered = rendered + except TemplateError as err: + logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error( + "Error rendering %s template for %s: %s", key, self.entity_id, err + ) + self._rendered = self._static_rendered + + +class ManualTriggerEntity(TriggerBaseEntity): + """Template entity based on manual trigger data.""" + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + ) -> None: + """Initialize the entity.""" + TriggerBaseEntity.__init__(self, hass, config) + # Need initial rendering on `name` as it influence the `entity_id` + self._rendered[CONF_NAME] = config[CONF_NAME].async_render( + {}, + parse_result=CONF_NAME in self._parse_result, + ) + + @callback + def _process_manual_data(self, value: Any | None = None) -> None: + """Process new data manually. + + Implementing class should call this last in update method to render templates. + Ex: self._process_manual_data(payload) + """ + + self.async_write_ha_state() + this = None + if state := self.hass.states.get(self.entity_id): + this = state.as_dict() + + run_variables: dict[str, Any] = {"value": value} + # Silently try if variable is a json and store result in `value_json` if it is. + with contextlib.suppress(*JSON_DECODE_EXCEPTIONS): + run_variables["value_json"] = json_loads(run_variables["value"]) + variables = {"this": this, **(run_variables or {})} + + self._render_templates(variables) + + +class ManualTriggerSensorEntity(ManualTriggerEntity, SensorEntity): + """Template entity based on manual trigger data for sensor.""" + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + ) -> 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_switch.py b/tests/components/rest/test_switch.py index 8bd13550960..d57cd41aa10 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -41,7 +41,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.template_entity import CONF_PICTURE +from homeassistant.helpers.trigger_template_entity import CONF_PICTURE from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index 60cde48e5bf..3ded3ce5bca 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -28,7 +28,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.template_entity import CONF_AVAILABILITY, CONF_PICTURE +from homeassistant.helpers.trigger_template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, +) from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util diff --git a/tests/components/sql/__init__.py b/tests/components/sql/__init__.py index 53356a85c4e..6a629f9603d 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -20,7 +20,10 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.template_entity import CONF_AVAILABILITY, CONF_PICTURE +from homeassistant.helpers.trigger_template_entity import ( + CONF_AVAILABILITY, + CONF_PICTURE, +) from tests.common import MockConfigEntry diff --git a/tests/components/template/test_manual_trigger_entity.py b/tests/components/template/test_manual_trigger_entity.py index 19210645a0f..a18827ecb4c 100644 --- a/tests/components/template/test_manual_trigger_entity.py +++ b/tests/components/template/test_manual_trigger_entity.py @@ -2,7 +2,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import template -from homeassistant.helpers.template_entity import ManualTriggerEntity +from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None: From 9da192c752c4627f35858b83a9c76d02d91cade8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 24 Aug 2023 17:38:22 +0300 Subject: [PATCH 0829/1151] Avoid use of `datetime.utc*` methods deprecated in Python 3.12 (#93684) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/feedreader/__init__.py | 6 +++--- homeassistant/components/filesize/sensor.py | 4 +--- homeassistant/components/http/__init__.py | 7 ++++--- .../components/local_calendar/diagnostics.py | 2 +- homeassistant/components/ovo_energy/__init__.py | 5 +++-- homeassistant/components/repetier/sensor.py | 6 +++--- homeassistant/components/starline/account.py | 5 ++++- homeassistant/components/stream/worker.py | 3 ++- .../components/traccar/device_tracker.py | 7 ++++--- script/version_bump.py | 9 +++------ tests/components/google/conftest.py | 5 ++--- tests/components/lacrosse_view/test_init.py | 4 ++-- tests/components/recorder/db_schema_0.py | 13 ++++++------- tests/components/scrape/test_init.py | 4 ++-- tests/components/scrape/test_sensor.py | 4 ++-- tests/components/stream/common.py | 4 ++-- tests/components/traccar/test_device_tracker.py | 6 +++--- tests/util/test_dt.py | 17 ++++++++++------- 18 files changed, 57 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 82312b8897c..eef84996d56 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.util import dt as dt_util _LOGGER = getLogger(__name__) @@ -207,7 +207,7 @@ class FeedManager: self._firstrun = False else: # Set last entry timestamp as epoch time if not available - self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple() + self._last_entry_timestamp = dt_util.utc_from_timestamp(0).timetuple() for entry in self._feed.entries: if ( self._firstrun @@ -286,6 +286,6 @@ class StoredData: def _async_save_data(self) -> dict[str, str]: """Save feed data to storage.""" return { - feed_id: utc_from_timestamp(timegm(struct_utc)).isoformat() + feed_id: dt_util.utc_from_timestamp(timegm(struct_utc)).isoformat() for feed_id, struct_utc in self._data.items() } diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 0526df81a02..0e600363640 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -102,9 +102,7 @@ class FileSizeCoordinator(DataUpdateCoordinator): raise UpdateFailed(f"Can not retrieve file statistics {error}") from error size = statinfo.st_size - last_updated = datetime.utcfromtimestamp(statinfo.st_mtime).replace( - tzinfo=dt_util.UTC - ) + last_updated = dt_util.utc_from_timestamp(statinfo.st_mtime) _LOGGER.debug("size %s, last updated %s", size, last_updated) data: dict[str, int | float | datetime] = { diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index ff287efb083..68f68d7f558 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -40,7 +40,7 @@ from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.setup import async_start_setup, async_when_setup_or_start -from homeassistant.util import ssl as ssl_util +from homeassistant.util import dt as dt_util, ssl as ssl_util from homeassistant.util.json import json_loads from .auth import async_setup_auth @@ -503,14 +503,15 @@ class HomeAssistantHTTP: x509.NameAttribute(NameOID.COMMON_NAME, host), ] ) + now = dt_util.utcnow() cert = ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(issuer) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.datetime.utcnow()) - .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=30)) + .not_valid_before(now) + .not_valid_after(now + datetime.timedelta(days=30)) .add_extension( x509.SubjectAlternativeName([x509.DNSName(host)]), critical=False, diff --git a/homeassistant/components/local_calendar/diagnostics.py b/homeassistant/components/local_calendar/diagnostics.py index 51b53ff0073..c3b9e5d151c 100644 --- a/homeassistant/components/local_calendar/diagnostics.py +++ b/homeassistant/components/local_calendar/diagnostics.py @@ -19,7 +19,7 @@ async def async_get_config_entry_diagnostics( payload: dict[str, Any] = { "now": dt_util.now().isoformat(), "timezone": str(dt_util.DEFAULT_TIME_ZONE), - "system_timezone": str(datetime.datetime.utcnow().astimezone().tzinfo), + "system_timezone": str(datetime.datetime.now().astimezone().tzinfo), } store = hass.data[DOMAIN][config_entry.entry_id] ics = await store.async_load() diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index f9547fc3493..3e2e868728d 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from datetime import datetime, timedelta +from datetime import timedelta import logging import aiohttp @@ -19,6 +19,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, UpdateFailed, ) +from homeassistant.util import dt as dt_util from .const import CONF_ACCOUNT, DATA_CLIENT, DATA_COORDINATOR, DOMAIN @@ -58,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise UpdateFailed(exception) from exception if not authenticated: raise ConfigEntryAuthFailed("Not authenticated with OVO Energy") - return await client.get_daily_usage(datetime.utcnow().strftime("%Y-%m")) + return await client.get_daily_usage(dt_util.utcnow().strftime("%Y-%m")) coordinator = DataUpdateCoordinator[OVODailyUsage]( hass, diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index 784555e6c73..578ca58b80f 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -1,7 +1,6 @@ """Support for monitoring Repetier Server Sensors.""" from __future__ import annotations -from datetime import datetime import logging import time @@ -10,6 +9,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType +from homeassistant.util import dt as dt_util from . import REPETIER_API, SENSOR_TYPES, UPDATE_SIGNAL, RepetierSensorEntityDescription @@ -170,7 +170,7 @@ class RepetierJobEndSensor(RepetierSensor): print_time = data["print_time"] from_start = data["from_start"] time_end = start + round(print_time, 0) - self._state = datetime.utcfromtimestamp(time_end) + self._state = dt_util.utc_from_timestamp(time_end) remaining = print_time - from_start remaining_secs = int(round(remaining, 0)) _LOGGER.debug( @@ -192,7 +192,7 @@ class RepetierJobStartSensor(RepetierSensor): job_name = data["job_name"] start = data["start"] from_start = data["from_start"] - self._state = datetime.utcfromtimestamp(start) + self._state = dt_util.utc_from_timestamp(start) elapsed_secs = int(round(from_start, 0)) _LOGGER.debug( "Job %s elapsed %s", diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index b6a6ae4a953..f0dea666085 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import dt as dt_util from .const import ( _LOGGER, @@ -141,7 +142,9 @@ class StarlineAccount: def gps_attrs(device: StarlineDevice) -> dict[str, Any]: """Attributes for device tracker.""" return { - "updated": datetime.utcfromtimestamp(device.position["ts"]).isoformat(), + "updated": dt_util.utc_from_timestamp(device.position["ts"]) + .replace(tzinfo=None) + .isoformat(), "online": device.online, } diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index c237a820e58..07d274e655c 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -14,6 +14,7 @@ import attr import av from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util from . import redact_credentials from .const import ( @@ -140,7 +141,7 @@ class StreamMuxer: self._part_has_keyframe = False self._stream_settings = stream_settings self._stream_state = stream_state - self._start_time = datetime.datetime.utcnow() + self._start_time = dt_util.utcnow() def make_new_av( self, diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index d15669745ef..f1236a66700 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from datetime import datetime, timedelta +from datetime import timedelta import logging from pytraccar import ( @@ -44,7 +44,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import slugify +from homeassistant.util import dt as dt_util, slugify from . import DOMAIN, TRACKER_UPDATE from .const import ( @@ -334,7 +334,8 @@ class TraccarScanner: async def import_events(self): """Import events from Traccar.""" - start_intervel = datetime.utcnow() + # get_reports_events requires naive UTC datetimes as of 1.0.0 + start_intervel = dt_util.utcnow().replace(tzinfo=None) events = await self._api.get_reports_events( devices=[device.id for device in self._devices], start_time=start_intervel, diff --git a/script/version_bump.py b/script/version_bump.py index ae01b1e6bed..4c4f8a97f09 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 """Helper script to bump the current version.""" import argparse -from datetime import datetime import re import subprocess from packaging.version import Version from homeassistant import const +from homeassistant.util import dt as dt_util def _bump_release(release, bump_type): @@ -86,10 +86,7 @@ def bump_version(version, bump_type): if not version.is_devrelease: raise ValueError("Can only be run on dev release") - to_change["dev"] = ( - "dev", - datetime.utcnow().date().isoformat().replace("-", ""), - ) + to_change["dev"] = ("dev", dt_util.utcnow().strftime("%Y%m%d")) else: assert False, f"Unsupported type: {bump_type}" @@ -206,7 +203,7 @@ def test_bump_version(): assert bump_version(Version("0.56.0.dev0"), "minor") == Version("0.56.0") assert bump_version(Version("0.56.2.dev0"), "minor") == Version("0.57.0") - today = datetime.utcnow().date().isoformat().replace("-", "") + today = dt_util.utcnow().strftime("%Y%m%d") assert bump_version(Version("0.56.0.dev0"), "nightly") == Version( f"0.56.0.dev{today}" ) diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 9516da8e841..57e542e8a21 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -20,6 +20,7 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.google import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -138,9 +139,7 @@ def token_scopes() -> list[str]: def token_expiry() -> datetime.datetime: """Expiration time for credentials used in the test.""" # OAuth library returns an offset-naive timestamp - return datetime.datetime.fromtimestamp( - datetime.datetime.utcnow().timestamp() - ) + datetime.timedelta(hours=1) + return dt_util.utcnow().replace(tzinfo=None) + datetime.timedelta(hours=1) @pytest.fixture diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py index 600fe1c9d24..557f8c4234a 100644 --- a/tests/components/lacrosse_view/test_init.py +++ b/tests/components/lacrosse_view/test_init.py @@ -92,7 +92,7 @@ async def test_new_token(hass: HomeAssistant) -> None: assert len(entries) == 1 assert entries[0].state == ConfigEntryState.LOADED - one_hour_after = datetime.utcnow() + timedelta(hours=1) + one_hour_after = datetime.now() + timedelta(hours=1) with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( "lacrosse_view.LaCrosse.get_sensors", @@ -122,7 +122,7 @@ async def test_failed_token(hass: HomeAssistant) -> None: assert len(entries) == 1 assert entries[0].state == ConfigEntryState.LOADED - one_hour_after = datetime.utcnow() + timedelta(hours=1) + one_hour_after = datetime.now() + timedelta(hours=1) with patch( "lacrosse_view.LaCrosse.login", side_effect=LoginError("Test") diff --git a/tests/components/recorder/db_schema_0.py b/tests/components/recorder/db_schema_0.py index 3fbf9cce5fc..6365ff6a7e7 100644 --- a/tests/components/recorder/db_schema_0.py +++ b/tests/components/recorder/db_schema_0.py @@ -4,7 +4,6 @@ This file contains the original models definitions before schema tracking was implemented. It is used to test the schema migration logic. """ -from datetime import datetime import json import logging @@ -40,7 +39,7 @@ class Events(Base): # type: ignore event_data = Column(Text) origin = Column(String(32)) time_fired = Column(DateTime(timezone=True)) - created = Column(DateTime(timezone=True), default=datetime.utcnow) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) @staticmethod def from_event(event): @@ -77,9 +76,9 @@ class States(Base): # type: ignore state = Column(String(255)) attributes = Column(Text) event_id = Column(Integer, ForeignKey("events.event_id")) - last_changed = Column(DateTime(timezone=True), default=datetime.utcnow) - last_updated = Column(DateTime(timezone=True), default=datetime.utcnow) - created = Column(DateTime(timezone=True), default=datetime.utcnow) + last_changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + last_updated = Column(DateTime(timezone=True), default=dt_util.utcnow) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) __table_args__ = ( Index("states__state_changes", "last_changed", "last_updated", "entity_id"), @@ -131,10 +130,10 @@ class RecorderRuns(Base): # type: ignore __tablename__ = "recorder_runs" run_id = Column(Integer, primary_key=True) - start = Column(DateTime(timezone=True), default=datetime.utcnow) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) end = Column(DateTime(timezone=True)) closed_incorrect = Column(Boolean, default=False) - created = Column(DateTime(timezone=True), default=datetime.utcnow) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) def entity_ids(self, point_in_time=None): """Return the entity ids that existed in this run. diff --git a/tests/components/scrape/test_init.py b/tests/components/scrape/test_init.py index 9b6122d6010..aa4be4cdef3 100644 --- a/tests/components/scrape/test_init.py +++ b/tests/components/scrape/test_init.py @@ -1,7 +1,6 @@ """Test Scrape component setup process.""" from __future__ import annotations -from datetime import datetime from unittest.mock import patch import pytest @@ -11,6 +10,7 @@ from homeassistant.components.scrape.const import DEFAULT_SCAN_INTERVAL, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from . import MockRestData, return_integration_config @@ -67,7 +67,7 @@ async def test_setup_no_data_fails_with_recovery( assert "Platform scrape not ready yet" in caplog.text mocker.payload = "test_scrape_sensor" - async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index 3ded3ce5bca..559c94633cd 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -1,7 +1,7 @@ """The tests for the Scrape sensor platform.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta from unittest.mock import patch import pytest @@ -247,7 +247,7 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: assert state.state == "Current Version: 2021.12.10" mocker.payload = "test_scrape_sensor_no_data" - async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) + async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index c75525c6061..7ea583c0ec3 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -1,5 +1,4 @@ """Collection of test helpers.""" -from datetime import datetime from fractions import Fraction import functools from functools import partial @@ -15,8 +14,9 @@ from homeassistant.components.stream.fmp4utils import ( XYW_ROW, find_box, ) +from homeassistant.util import dt as dt_util -FAKE_TIME = datetime.utcnow() +FAKE_TIME = dt_util.utcnow() # Segment with defaults filled in for use in tests DefaultSegment = partial( diff --git a/tests/components/traccar/test_device_tracker.py b/tests/components/traccar/test_device_tracker.py index 065b459354a..ed6cc3f629b 100644 --- a/tests/components/traccar/test_device_tracker.py +++ b/tests/components/traccar/test_device_tracker.py @@ -1,5 +1,4 @@ """The tests for the Traccar device tracker platform.""" -from datetime import datetime from unittest.mock import AsyncMock, patch from pytraccar import ReportsEventeModel @@ -17,6 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from tests.common import async_capture_events @@ -47,7 +47,7 @@ async def test_import_events_catch_all(hass: HomeAssistant) -> None: "maintenanceId": 1, "deviceId": device["id"], "type": "ignitionOn", - "eventTime": datetime.utcnow().isoformat(), + "eventTime": dt_util.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), "attributes": {}, } ), @@ -59,7 +59,7 @@ async def test_import_events_catch_all(hass: HomeAssistant) -> None: "maintenanceId": 1, "deviceId": device["id"], "type": "ignitionOff", - "eventTime": datetime.utcnow().isoformat(), + "eventTime": dt_util.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), "attributes": {}, } ), diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index e9cde21a265..28695a94400 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -1,7 +1,7 @@ """Test Home Assistant date util methods.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import time import pytest @@ -41,9 +41,9 @@ def test_set_default_time_zone() -> None: def test_utcnow() -> None: """Test the UTC now method.""" - assert abs(dt_util.utcnow().replace(tzinfo=None) - datetime.utcnow()) < timedelta( - seconds=1 - ) + assert abs( + dt_util.utcnow().replace(tzinfo=None) - datetime.now(UTC).replace(tzinfo=None) + ) < timedelta(seconds=1) def test_now() -> None: @@ -51,13 +51,14 @@ def test_now() -> None: dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) assert abs( - dt_util.as_utc(dt_util.now()).replace(tzinfo=None) - datetime.utcnow() + dt_util.as_utc(dt_util.now()).replace(tzinfo=None) + - datetime.now(UTC).replace(tzinfo=None) ) < timedelta(seconds=1) def test_as_utc_with_naive_object() -> None: """Test the now method.""" - utcnow = datetime.utcnow() + utcnow = datetime.now(UTC).replace(tzinfo=None) assert utcnow == dt_util.as_utc(utcnow).replace(tzinfo=None) @@ -82,7 +83,9 @@ def test_as_utc_with_local_object() -> None: def test_as_local_with_naive_object() -> None: """Test local time with native object.""" now = dt_util.now() - assert abs(now - dt_util.as_local(datetime.utcnow())) < timedelta(seconds=1) + assert abs( + now - dt_util.as_local(datetime.now(UTC).replace(tzinfo=None)) + ) < timedelta(seconds=1) def test_as_local_with_local_object() -> None: From d300f2d0cc92f0bb5a0eea17cc4617d450c34a41 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 16:39:14 +0200 Subject: [PATCH 0830/1151] Remove default model from upcloud (#98972) --- homeassistant/components/upcloud/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 7f832eb733f..174d35f07e0 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -243,7 +243,7 @@ class UpCloudServerEntity(CoordinatorEntity[UpCloudDataUpdateCoordinator]): assert self.coordinator.config_entry is not None return DeviceInfo( configuration_url="https://hub.upcloud.com", - default_model="Control Panel", + model="Control Panel", entry_type=DeviceEntryType.SERVICE, identifiers={ (DOMAIN, f"{self.coordinator.config_entry.data[CONF_USERNAME]}@hub") From 4e049f9bedefc5c253944d140c96d2b12a1983f3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 17:11:24 +0200 Subject: [PATCH 0831/1151] Use snapshot assertion in Tile diagnostic test (#98965) --- .../tile/snapshots/test_diagnostics.ambr | 26 ++++++++++++++++ tests/components/tile/test_diagnostics.py | 31 +++++-------------- 2 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 tests/components/tile/snapshots/test_diagnostics.ambr diff --git a/tests/components/tile/snapshots/test_diagnostics.ambr b/tests/components/tile/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..c04bd93315f --- /dev/null +++ b/tests/components/tile/snapshots/test_diagnostics.ambr @@ -0,0 +1,26 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'tiles': list([ + dict({ + 'accuracy': 13.496111, + 'altitude': '**REDACTED**', + 'archetype': 'WALLET', + 'dead': False, + 'firmware_version': '01.12.14.0', + 'hardware_version': '02.09', + 'kind': 'TILE', + 'last_timestamp': '2020-08-12T17:55:26', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'lost': False, + 'lost_timestamp': '1969-12-31T23:59:59.999000', + 'name': 'Wallet', + 'ring_state': 'STOPPED', + 'uuid': '**REDACTED**', + 'visible': True, + 'voip_state': 'OFFLINE', + }), + ]), + }) +# --- diff --git a/tests/components/tile/test_diagnostics.py b/tests/components/tile/test_diagnostics.py index a4aa42cc1fb..8af2c513202 100644 --- a/tests/components/tile/test_diagnostics.py +++ b/tests/components/tile/test_diagnostics.py @@ -1,5 +1,6 @@ """Test Tile diagnostics.""" -from homeassistant.components.diagnostics import REDACTED +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -11,28 +12,10 @@ async def test_entry_diagnostics( config_entry, hass_client: ClientSessionGenerator, setup_config_entry, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "tiles": [ - { - "accuracy": 13.496111, - "altitude": REDACTED, - "archetype": "WALLET", - "dead": False, - "firmware_version": "01.12.14.0", - "hardware_version": "02.09", - "kind": "TILE", - "last_timestamp": "2020-08-12T17:55:26", - "latitude": REDACTED, - "longitude": REDACTED, - "lost": False, - "lost_timestamp": "1969-12-31T23:59:59.999000", - "name": "Wallet", - "ring_state": "STOPPED", - "uuid": REDACTED, - "visible": True, - "voip_state": "OFFLINE", - } - ] - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From 53eb4d0ead920fb25c41d9f4600e0f4f23d9bdf7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Aug 2023 11:10:38 -0500 Subject: [PATCH 0832/1151] Bump dbus-fast to 1.94.0 (#98973) --- 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 db28e550d23..e6916d00881 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.9.0", - "dbus-fast==1.93.0" + "dbus-fast==1.94.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9d9e98e0c26..71f8a37ff40 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.9.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 -dbus-fast==1.93.0 +dbus-fast==1.94.0 fnv-hash-fast==0.4.0 ha-av==10.1.1 hass-nabucasa==0.70.0 diff --git a/requirements_all.txt b/requirements_all.txt index 89088840dd6..8ef933870a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.93.0 +dbus-fast==1.94.0 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 212132857ba..57ddafb4eba 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.93.0 +dbus-fast==1.94.0 # homeassistant.components.debugpy debugpy==1.6.7 From 7d35dcfa65f06f199a1c05f8f90810de9e995495 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 19:49:53 +0200 Subject: [PATCH 0833/1151] Make Sabnzbd entity translation clearer (#98938) --- homeassistant/components/sabnzbd/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sabnzbd/strings.json b/homeassistant/components/sabnzbd/strings.json index 8d3fb84fb9f..f8c831cd95a 100644 --- a/homeassistant/components/sabnzbd/strings.json +++ b/homeassistant/components/sabnzbd/strings.json @@ -26,7 +26,7 @@ "name": "Queue" }, "left": { - "name": "Left" + "name": "Left to download" }, "total_disk_space": { "name": "Total disk space" From 089f76099df11f36058d6c792861486b2ecce615 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 24 Aug 2023 19:50:25 +0200 Subject: [PATCH 0834/1151] Fix stream test aiohttp DeprecationWarning (#98962) --- tests/components/stream/test_ll_hls.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py index 17918ff93df..cd13ab340c2 100644 --- a/tests/components/stream/test_ll_hls.py +++ b/tests/components/stream/test_ll_hls.py @@ -7,6 +7,7 @@ import math import re from urllib.parse import urlparse +from aiohttp import web from dateutil import parser import pytest @@ -394,6 +395,9 @@ async def test_ll_hls_playlist_bad_msn_part( ) -> None: """Test some playlist requests with invalid _HLS_msn/_HLS_part.""" + async def _handler_bad_request(request): + raise web.HTTPBadRequest() + await async_setup_component( hass, "stream", @@ -413,6 +417,12 @@ async def test_ll_hls_playlist_bad_msn_part( hls_client = await hls_stream(stream) + # All GET calls to '/.../playlist.m3u8' should raise a HTTPBadRequest exception + hls_client.http_client.app.router._frozen = False + parsed_url = urlparse(stream.endpoint_url(HLS_PROVIDER)) + url = "/".join(parsed_url.path.split("/")[:-1]) + "/playlist.m3u8" + hls_client.http_client.app.router.add_route("GET", url, _handler_bad_request) + # If the Playlist URI contains an _HLS_part directive but no _HLS_msn # directive, the Server MUST return Bad Request, such as HTTP 400. From 998a390da5dfb22054242579f0d231325082012f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 19:53:09 +0200 Subject: [PATCH 0835/1151] Use device class in TPLink Omada Update entity (#98971) --- homeassistant/components/tplink_omada/update.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tplink_omada/update.py b/homeassistant/components/tplink_omada/update.py index 685ad9c5761..1e653a53aae 100644 --- a/homeassistant/components/tplink_omada/update.py +++ b/homeassistant/components/tplink_omada/update.py @@ -8,7 +8,11 @@ from tplink_omada_client.devices import OmadaFirmwareUpdate, OmadaListDevice from tplink_omada_client.exceptions import OmadaClientException, RequestFailed from tplink_omada_client.omadasiteclient import OmadaSiteClient -from homeassistant.components.update import UpdateEntity, UpdateEntityFeature +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -94,7 +98,7 @@ class OmadaDeviceUpdate( | UpdateEntityFeature.RELEASE_NOTES ) _attr_has_entity_name = True - _attr_name = "Firmware update" + _attr_device_class = UpdateDeviceClass.FIRMWARE def __init__( self, From 2066cf6b31b0b61bae95373ad3b6a59013c25459 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Aug 2023 19:54:04 +0200 Subject: [PATCH 0836/1151] Remove `group_type` from group preview events (#98952) --- homeassistant/components/group/config_flow.py | 3 +-- tests/components/group/test_config_flow.py | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 28a7330a206..a5bf9e0b972 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -343,8 +343,7 @@ def ws_start_preview( """Forward config entry state events to websocket.""" connection.send_message( websocket_api.event_message( - msg["id"], - {"attributes": attributes, "group_type": group_type, "state": state}, + msg["id"], {"attributes": attributes, "state": state} ) ) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index b244b37e072..a58e47cae71 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -524,7 +524,6 @@ async def test_config_flow_preview( msg = await client.receive_json() assert msg["event"] == { "attributes": {"friendly_name": "My group"} | extra_attributes[0], - "group_type": domain, "state": "unavailable", } @@ -539,7 +538,6 @@ async def test_config_flow_preview( } | extra_attributes[0] | extra_attributes[1], - "group_type": domain, "state": group_state, } assert len(hass.states.async_all()) == 2 @@ -622,7 +620,6 @@ async def test_option_flow_preview( assert msg["event"] == { "attributes": {"entity_id": input_entities, "friendly_name": "My group"} | extra_attributes, - "group_type": domain, "state": group_state, } assert len(hass.states.async_all()) == 3 From 969063ccf8fb4f314a07f090fc7a28f43dfe4a87 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 19:54:49 +0200 Subject: [PATCH 0837/1151] Use shorthand attributes for SRP Energy (#98953) --- homeassistant/components/srp_energy/sensor.py | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 946b2aedb13..d477b65b21d 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -101,6 +101,9 @@ class SrpEntity(SensorEntity): _attr_attribution = "Powered by SRP Energy" _attr_icon = "mdi:flash" + _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_device_class = SensorDeviceClass.ENERGY + _attr_state_class = SensorStateClass.TOTAL_INCREASING _attr_should_poll = False def __init__(self, coordinator) -> None: @@ -108,8 +111,6 @@ class SrpEntity(SensorEntity): self._name = SENSOR_NAME self.type = SENSOR_TYPE self.coordinator = coordinator - self._unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR - self._state = None @property def name(self) -> str: @@ -121,33 +122,16 @@ class SrpEntity(SensorEntity): """Return the state of the device.""" return self.coordinator.data - @property - def native_unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - @property def available(self) -> bool: """Return if entity is available.""" return self.coordinator.last_update_success - @property - def device_class(self) -> SensorDeviceClass: - """Return the device class.""" - return SensorDeviceClass.ENERGY - - @property - def state_class(self) -> SensorStateClass: - """Return the state class.""" - return SensorStateClass.TOTAL_INCREASING - async def async_added_to_hass(self) -> None: """When entity is added to hass.""" self.async_on_remove( self.coordinator.async_add_listener(self.async_write_ha_state) ) - if self.coordinator.data: - self._state = self.coordinator.data async def async_update(self) -> None: """Update the entity. From be78169065837ac2534213d12e8048c323730a9b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 19:56:02 +0200 Subject: [PATCH 0838/1151] Add entity translations to Risco (#98921) --- .../components/risco/alarm_control_panel.py | 4 ++-- homeassistant/components/risco/binary_sensor.py | 16 ++++++++-------- homeassistant/components/risco/entity.py | 4 ---- homeassistant/components/risco/strings.json | 15 +++++++++++++++ homeassistant/components/risco/switch.py | 4 ++-- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index 5b2d85b2bca..a72efe1629c 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -88,6 +88,8 @@ class RiscoAlarm(AlarmControlPanelEntity): """Representation of a Risco cloud partition.""" _attr_code_format = CodeFormat.NUMBER + _attr_has_entity_name = True + _attr_name = None def __init__( self, @@ -107,8 +109,6 @@ class RiscoAlarm(AlarmControlPanelEntity): self._code_disarm_required = options[CONF_CODE_DISARM_REQUIRED] self._risco_to_ha = options[CONF_RISCO_STATES_TO_HA] self._ha_to_risco = options[CONF_HA_STATES_TO_RISCO] - self._attr_has_entity_name = True - self._attr_name = None for state in self._ha_to_risco: self._attr_supported_features |= STATES_TO_SUPPORTED_FEATURES[state] diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index 423137d88b6..f60b0bf3c35 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -50,14 +50,13 @@ class RiscoCloudBinarySensor(RiscoCloudZoneEntity, BinarySensorEntity): """Representation of a Risco cloud zone as a binary sensor.""" _attr_device_class = BinarySensorDeviceClass.MOTION + _attr_name = None def __init__( self, coordinator: RiscoDataUpdateCoordinator, zone_id: int, zone: Zone ) -> None: """Init the zone.""" - super().__init__( - coordinator=coordinator, name=None, suffix="", zone_id=zone_id, zone=zone - ) + super().__init__(coordinator=coordinator, suffix="", zone_id=zone_id, zone=zone) @property def is_on(self) -> bool | None: @@ -69,12 +68,11 @@ class RiscoLocalBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): """Representation of a Risco local zone as a binary sensor.""" _attr_device_class = BinarySensorDeviceClass.MOTION + _attr_name = None def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: """Init the zone.""" - super().__init__( - system_id=system_id, name=None, suffix="", zone_id=zone_id, zone=zone - ) + super().__init__(system_id=system_id, suffix="", zone_id=zone_id, zone=zone) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: @@ -93,11 +91,12 @@ class RiscoLocalBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): class RiscoLocalAlarmedBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): """Representation whether a zone in Risco local is currently triggering an alarm.""" + _attr_translation_key = "alarmed" + def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: """Init the zone.""" super().__init__( system_id=system_id, - name="Alarmed", suffix="_alarmed", zone_id=zone_id, zone=zone, @@ -112,11 +111,12 @@ class RiscoLocalAlarmedBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): class RiscoLocalArmedBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): """Representation whether a zone in Risco local is currently armed.""" + _attr_translation_key = "armed" + def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: """Init the zone.""" super().__init__( system_id=system_id, - name="Armed", suffix="_armed", zone_id=zone_id, zone=zone, diff --git a/homeassistant/components/risco/entity.py b/homeassistant/components/risco/entity.py index 3a2c50e20af..7f8e3be698b 100644 --- a/homeassistant/components/risco/entity.py +++ b/homeassistant/components/risco/entity.py @@ -56,7 +56,6 @@ class RiscoCloudZoneEntity(RiscoCloudEntity): self, *, coordinator: RiscoDataUpdateCoordinator, - name: str | None, suffix: str, zone_id: int, zone: Zone, @@ -66,7 +65,6 @@ class RiscoCloudZoneEntity(RiscoCloudEntity): super().__init__(coordinator=coordinator, **kwargs) self._zone_id = zone_id self._zone = zone - self._attr_name = name device_unique_id = zone_unique_id(self._risco, zone_id) self._attr_unique_id = f"{device_unique_id}{suffix}" self._attr_device_info = DeviceInfo( @@ -90,7 +88,6 @@ class RiscoLocalZoneEntity(Entity): self, *, system_id: str, - name: str | None, suffix: str, zone_id: int, zone: Zone, @@ -100,7 +97,6 @@ class RiscoLocalZoneEntity(Entity): super().__init__(**kwargs) self._zone_id = zone_id self._zone = zone - self._attr_name = name device_unique_id = f"{system_id}_zone_{zone_id}_local" self._attr_unique_id = f"{device_unique_id}{suffix}" self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json index ed3d832cf0b..13dfd60b5b6 100644 --- a/homeassistant/components/risco/strings.json +++ b/homeassistant/components/risco/strings.json @@ -64,5 +64,20 @@ } } } + }, + "entity": { + "binary_sensor": { + "alarmed": { + "name": "Alarmed" + }, + "armed": { + "name": "Armed" + } + }, + "switch": { + "bypassed": { + "name": "Bypassed" + } + } } } diff --git a/homeassistant/components/risco/switch.py b/homeassistant/components/risco/switch.py index f0804abb68a..9b34479f8a2 100644 --- a/homeassistant/components/risco/switch.py +++ b/homeassistant/components/risco/switch.py @@ -40,6 +40,7 @@ class RiscoCloudSwitch(RiscoCloudZoneEntity, SwitchEntity): """Representation of a bypass switch for a Risco cloud zone.""" _attr_entity_category = EntityCategory.CONFIG + _attr_translation_key = "bypassed" def __init__( self, coordinator: RiscoDataUpdateCoordinator, zone_id: int, zone: Zone @@ -47,7 +48,6 @@ class RiscoCloudSwitch(RiscoCloudZoneEntity, SwitchEntity): """Init the zone.""" super().__init__( coordinator=coordinator, - name="Bypassed", suffix="_bypassed", zone_id=zone_id, zone=zone, @@ -76,12 +76,12 @@ class RiscoLocalSwitch(RiscoLocalZoneEntity, SwitchEntity): """Representation of a bypass switch for a Risco local zone.""" _attr_entity_category = EntityCategory.CONFIG + _attr_translation_key = "bypassed" def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: """Init the zone.""" super().__init__( system_id=system_id, - name="Bypassed", suffix="_bypassed", zone_id=zone_id, zone=zone, From 480db1f1e66ea633f41be548abf54e5926873357 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 19:58:54 +0200 Subject: [PATCH 0839/1151] Migrate Squeezebox to has entity name (#98948) --- .../components/squeezebox/media_player.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index d57ba8ba49d..c77126e4377 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -35,7 +35,7 @@ from homeassistant.helpers import ( entity_platform, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.device_registry import DeviceInfo, format_mac from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -236,6 +236,8 @@ class SqueezeBoxEntity(MediaPlayerEntity): | MediaPlayerEntityFeature.GROUPING | MediaPlayerEntityFeature.MEDIA_ENQUEUE ) + _attr_has_entity_name = True + _attr_name = None def __init__(self, player): """Initialize the SqueezeBox device.""" @@ -244,6 +246,10 @@ class SqueezeBoxEntity(MediaPlayerEntity): self._query_result = {} self._available = True self._remove_dispatcher = None + self._attr_unique_id = format_mac(self._player.player_id) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._attr_unique_id)}, name=self._player.name + ) @property def extra_state_attributes(self): @@ -256,16 +262,6 @@ class SqueezeBoxEntity(MediaPlayerEntity): return squeezebox_attr - @property - def name(self): - """Return the name of the device.""" - return self._player.name - - @property - def unique_id(self): - """Return a unique ID.""" - return format_mac(self._player.player_id) - @property def available(self): """Return True if device connected to LMS server.""" From 7575ffa24e3c1c84062735946188ea42b547bfb2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 19:59:34 +0200 Subject: [PATCH 0840/1151] Add entity translations to Tankerkoenig (#98961) --- .../components/tankerkoenig/__init__.py | 2 ++ .../components/tankerkoenig/binary_sensor.py | 4 +--- .../components/tankerkoenig/sensor.py | 5 ++--- .../components/tankerkoenig/strings.json | 18 ++++++++++++++++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index f1dbc26fc3a..39ae0c2fc16 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -170,6 +170,8 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): class TankerkoenigCoordinatorEntity(CoordinatorEntity): """Tankerkoenig base entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: TankerkoenigDataUpdateCoordinator, station: dict ) -> None: diff --git a/homeassistant/components/tankerkoenig/binary_sensor.py b/homeassistant/components/tankerkoenig/binary_sensor.py index 5f10b54f704..a6a79fd2d92 100644 --- a/homeassistant/components/tankerkoenig/binary_sensor.py +++ b/homeassistant/components/tankerkoenig/binary_sensor.py @@ -43,6 +43,7 @@ class StationOpenBinarySensorEntity(TankerkoenigCoordinatorEntity, BinarySensorE """Shows if a station is open or closed.""" _attr_device_class = BinarySensorDeviceClass.DOOR + _attr_translation_key = "status" def __init__( self, @@ -53,9 +54,6 @@ class StationOpenBinarySensorEntity(TankerkoenigCoordinatorEntity, BinarySensorE """Initialize the sensor.""" super().__init__(coordinator, station) self._station_id = station["id"] - self._attr_name = ( - f"{station['brand']} {station['street']} {station['houseNumber']} status" - ) self._attr_unique_id = f"{station['id']}_status" if show_on_map: self._attr_extra_state_attributes = { diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index 1638a8c3abb..af21ac4b6d6 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -20,7 +20,6 @@ from .const import ( ATTR_STREET, ATTRIBUTION, DOMAIN, - FUEL_TYPES, ) _LOGGER = logging.getLogger(__name__) @@ -59,6 +58,7 @@ class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity): _attr_attribution = ATTRIBUTION _attr_state_class = SensorStateClass.MEASUREMENT + _attr_native_unit_of_measurement = CURRENCY_EURO _attr_icon = "mdi:gas-station" def __init__(self, fuel_type, station, coordinator, show_on_map): @@ -66,8 +66,7 @@ class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity): super().__init__(coordinator, station) self._station_id = station["id"] self._fuel_type = fuel_type - self._attr_name = f"{station['brand']} {station['street']} {station['houseNumber']} {FUEL_TYPES[fuel_type]}" - self._attr_native_unit_of_measurement = CURRENCY_EURO + self._attr_translation_key = fuel_type self._attr_unique_id = f"{station['id']}_{fuel_type}" attrs = { ATTR_BRAND: station["brand"], diff --git a/homeassistant/components/tankerkoenig/strings.json b/homeassistant/components/tankerkoenig/strings.json index dea370f45b3..43d444b2c46 100644 --- a/homeassistant/components/tankerkoenig/strings.json +++ b/homeassistant/components/tankerkoenig/strings.json @@ -43,5 +43,23 @@ } } } + }, + "entity": { + "binary_sensor": { + "status": { + "name": "Status" + } + }, + "sensor": { + "e5": { + "name": "Super" + }, + "e10": { + "name": "Super E10" + }, + "diesel": { + "name": "Diesel" + } + } } } From a5cced1da95729924e11894ddaaddcf057e0b0a7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 20:07:02 +0200 Subject: [PATCH 0841/1151] Add device to Tile (#98964) --- homeassistant/components/tile/device_tracker.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 8dba892de83..81d3fb00c6e 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -13,6 +13,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( @@ -82,6 +83,8 @@ class TileDeviceTracker(CoordinatorEntity[DataUpdateCoordinator[None]], TrackerE """Representation of a network infrastructure device.""" _attr_icon = DEFAULT_ICON + _attr_has_entity_name = True + _attr_name = None def __init__( self, entry: ConfigEntry, coordinator: DataUpdateCoordinator[None], tile: Tile @@ -90,7 +93,6 @@ class TileDeviceTracker(CoordinatorEntity[DataUpdateCoordinator[None]], TrackerE super().__init__(coordinator) self._attr_extra_state_attributes = {} - self._attr_name = tile.name self._attr_unique_id = f"{entry.data[CONF_USERNAME]}_{tile.uuid}" self._entry = entry self._tile = tile @@ -110,6 +112,11 @@ class TileDeviceTracker(CoordinatorEntity[DataUpdateCoordinator[None]], TrackerE return super().location_accuracy return int(self._tile.accuracy) + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + return DeviceInfo(identifiers={(DOMAIN, self._tile.uuid)}, name=self._tile.name) + @property def latitude(self) -> float | None: """Return latitude value of the device.""" From 948b34b045a2364f14abba4dd3669829c8f7672f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 24 Aug 2023 20:09:14 +0200 Subject: [PATCH 0842/1151] Do not force update mqtt device_tracker (#98838) --- .../components/mqtt/device_tracker.py | 5 +++++ tests/components/mqtt/test_device_tracker.py | 22 ++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index dd4eca9878a..67355d9bca5 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -165,6 +165,11 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): }, ) + @property + def force_update(self) -> bool: + """Do not force updates if the state is the same.""" + return False + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index ddce53bfca0..8485e5578fe 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,6 +1,8 @@ """The tests for the MQTT device_tracker platform.""" +from datetime import UTC, datetime from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components import device_tracker, mqtt @@ -199,9 +201,10 @@ async def test_duplicate_device_tracker_removal( async def test_device_tracker_discovery_update( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, ) -> None: """Test for a discovery update event.""" + freezer.move_to("2023-08-22 19:15:00+00:00") await mqtt_mock_entry() async_fire_mqtt_message( hass, @@ -213,7 +216,9 @@ async def test_device_tracker_discovery_update( state = hass.states.get("device_tracker.beer") assert state is not None assert state.name == "Beer" + assert state.last_updated == datetime(2023, 8, 22, 19, 15, tzinfo=UTC) + freezer.move_to("2023-08-22 19:16:00+00:00") async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -224,6 +229,21 @@ async def test_device_tracker_discovery_update( state = hass.states.get("device_tracker.beer") assert state is not None assert state.name == "Cider" + assert state.last_updated == datetime(2023, 8, 22, 19, 16, tzinfo=UTC) + + freezer.move_to("2023-08-22 19:20:00+00:00") + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Cider", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is not None + assert state.name == "Cider" + # Entity was not updated as the state was not changed + assert state.last_updated == datetime(2023, 8, 22, 19, 16, tzinfo=UTC) async def test_cleanup_device_tracker( From 417fd0838a74a26f695ee413d8e59460983dcc96 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 21:05:00 +0200 Subject: [PATCH 0843/1151] Migrate Snooz to has entity name (#98940) --- homeassistant/components/snooz/fan.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/snooz/fan.py b/homeassistant/components/snooz/fan.py index a34989d1a03..c5b3e5b5b69 100644 --- a/homeassistant/components/snooz/fan.py +++ b/homeassistant/components/snooz/fan.py @@ -21,6 +21,7 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -71,15 +72,18 @@ async def async_setup_entry( class SnoozFan(FanEntity, RestoreEntity): """Fan representation of a Snooz device.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, data: SnoozConfigurationData) -> None: """Initialize a Snooz fan entity.""" self._device = data.device - self._attr_name = data.title self._attr_unique_id = data.device.address self._attr_supported_features = FanEntityFeature.SET_SPEED self._attr_should_poll = False self._is_on: bool | None = None self._percentage: int | None = None + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, data.device.address)}) @callback def _async_write_state_changed(self) -> None: From f2c475cf1ba34cd1e0c619b4de6212fe96b2643c Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 24 Aug 2023 15:13:42 -0400 Subject: [PATCH 0844/1151] Bump aiosomecomfort to 0.0.17 (#98978) * Clean up imports Add refresh after login in update * Bump somecomfort 0.0.17 Separate Somecomfort error to unauthorized * Add tests * Run Black format --- homeassistant/components/honeywell/climate.py | 24 +++++++++------ .../components/honeywell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/honeywell/test_climate.py | 30 +++++++++++++++---- 5 files changed, 43 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 6bfefcf3a8c..b23df9f1f4b 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -6,7 +6,8 @@ import datetime from typing import Any from aiohttp import ClientConnectionError -import aiosomecomfort +from aiosomecomfort import SomeComfortError, UnauthorizedError, UnexpectedResponse +from aiosomecomfort.device import Device as SomeComfortDevice from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, @@ -106,7 +107,7 @@ class HoneywellUSThermostat(ClimateEntity): def __init__( self, data: HoneywellData, - device: aiosomecomfort.device.Device, + device: SomeComfortDevice, cool_away_temp: int | None, heat_away_temp: int | None, ) -> None: @@ -312,7 +313,7 @@ class HoneywellUSThermostat(ClimateEntity): if mode == "heat": await self._device.set_setpoint_heat(temperature) - except aiosomecomfort.SomeComfortError as err: + except SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) async def async_set_temperature(self, **kwargs: Any) -> None: @@ -325,7 +326,7 @@ class HoneywellUSThermostat(ClimateEntity): if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): await self._device.set_setpoint_heat(temperature) - except aiosomecomfort.SomeComfortError as err: + except SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) async def async_set_fan_mode(self, fan_mode: str) -> None: @@ -354,7 +355,7 @@ class HoneywellUSThermostat(ClimateEntity): if mode in HEATING_MODES: await self._device.set_hold_heat(True, self._heat_away_temp) - except aiosomecomfort.SomeComfortError: + except SomeComfortError: _LOGGER.error( "Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f", mode, @@ -375,7 +376,7 @@ class HoneywellUSThermostat(ClimateEntity): if mode in HEATING_MODES: await self._device.set_hold_heat(True) - except aiosomecomfort.SomeComfortError: + except SomeComfortError: _LOGGER.error("Couldn't set permanent hold") else: _LOGGER.error("Invalid system mode returned: %s", mode) @@ -387,7 +388,7 @@ class HoneywellUSThermostat(ClimateEntity): # Disabling all hold modes await self._device.set_hold_cool(False) await self._device.set_hold_heat(False) - except aiosomecomfort.SomeComfortError: + except SomeComfortError: _LOGGER.error("Can not stop hold mode") async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -416,12 +417,14 @@ class HoneywellUSThermostat(ClimateEntity): try: await self._device.refresh() self._attr_available = True - except aiosomecomfort.SomeComfortError: + except UnauthorizedError: try: await self._data.client.login() + await self._device.refresh() + self._attr_available = True except ( - aiosomecomfort.SomeComfortError, + SomeComfortError, ClientConnectionError, asyncio.TimeoutError, ): @@ -429,3 +432,6 @@ class HoneywellUSThermostat(ClimateEntity): except (ClientConnectionError, asyncio.TimeoutError): self._attr_available = False + + except UnexpectedResponse: + pass diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index bb72c15cd46..a53eaaab8ce 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/honeywell", "iot_class": "cloud_polling", "loggers": ["somecomfort"], - "requirements": ["AIOSomecomfort==0.0.16"] + "requirements": ["AIOSomecomfort==0.0.17"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8ef933870a0..be073ac19ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ AEMET-OpenData==0.3.0 AIOAladdinConnect==0.1.57 # homeassistant.components.honeywell -AIOSomecomfort==0.0.16 +AIOSomecomfort==0.0.17 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57ddafb4eba..7b40e29d86e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ AEMET-OpenData==0.3.0 AIOAladdinConnect==0.1.57 # homeassistant.components.honeywell -AIOSomecomfort==0.0.16 +AIOSomecomfort==0.0.17 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index 4d6989d79e8..b8facc54d43 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -1010,8 +1010,8 @@ async def test_async_update_errors( await init_integration(hass, config_entry) - device.refresh.side_effect = aiosomecomfort.SomeComfortError - client.login.side_effect = aiosomecomfort.SomeComfortError + device.refresh.side_effect = aiosomecomfort.UnauthorizedError + client.login.side_effect = aiosomecomfort.AuthError entity_id = f"climate.{device.name}" state = hass.states.get(entity_id) assert state.state == "off" @@ -1037,6 +1037,28 @@ async def test_async_update_errors( state = hass.states.get(entity_id) assert state.state == "off" + device.refresh.side_effect = aiosomecomfort.UnexpectedResponse + client.login.side_effect = None + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == "off" + + device.refresh.side_effect = [aiosomecomfort.UnauthorizedError, None] + client.login.side_effect = None + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == "off" + # "reload integration" test device.refresh.side_effect = aiosomecomfort.SomeComfortError client.login.side_effect = aiosomecomfort.AuthError @@ -1046,9 +1068,8 @@ async def test_async_update_errors( ) await hass.async_block_till_done() - entity_id = f"climate.{device.name}" state = hass.states.get(entity_id) - assert state.state == "unavailable" + assert state.state == "off" device.refresh.side_effect = ClientConnectionError async_fire_time_changed( @@ -1057,7 +1078,6 @@ async def test_async_update_errors( ) await hass.async_block_till_done() - entity_id = f"climate.{device.name}" state = hass.states.get(entity_id) assert state.state == "unavailable" From b03ffe6a6ace990486e1657c8a05432acd4c76fe Mon Sep 17 00:00:00 2001 From: Michael Arthur Date: Fri, 25 Aug 2023 07:57:52 +1200 Subject: [PATCH 0845/1151] Electric Kiwi: Fix time for installations in UTC (#97881) --- homeassistant/components/electric_kiwi/sensor.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/electric_kiwi/sensor.py b/homeassistant/components/electric_kiwi/sensor.py index a3943437d4f..8c983b92dd5 100644 --- a/homeassistant/components/electric_kiwi/sensor.py +++ b/homeassistant/components/electric_kiwi/sensor.py @@ -46,16 +46,18 @@ class ElectricKiwiHOPSensorEntityDescription( def _check_and_move_time(hop: Hop, time: str) -> datetime: """Return the time a day forward if HOP end_time is in the past.""" date_time = datetime.combine( - datetime.today(), + dt_util.start_of_local_day(), datetime.strptime(time, "%I:%M %p").time(), - ).astimezone(dt_util.DEFAULT_TIME_ZONE) + dt_util.DEFAULT_TIME_ZONE, + ) end_time = datetime.combine( - datetime.today(), + dt_util.start_of_local_day(), datetime.strptime(hop.end.end_time, "%I:%M %p").time(), - ).astimezone(dt_util.DEFAULT_TIME_ZONE) + dt_util.DEFAULT_TIME_ZONE, + ) - if end_time < datetime.now().astimezone(dt_util.DEFAULT_TIME_ZONE): + if end_time < dt_util.now(): return date_time + timedelta(days=1) return date_time From d7adc2621dddc9b1f1b5f72223834fcc704ac5ab Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 22:03:26 +0200 Subject: [PATCH 0846/1151] Migrate Life360 to has entity name (#98796) --- homeassistant/components/life360/device_tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index 27b4ce291fd..ee097b9e989 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -100,6 +100,8 @@ class Life360DeviceTracker( _attr_attribution = ATTRIBUTION _attr_unique_id: str + _attr_has_entity_name = True + _attr_name = None def __init__( self, coordinator: Life360DataUpdateCoordinator, member_id: str @@ -111,7 +113,6 @@ class Life360DeviceTracker( self._data: Life360Member | None = coordinator.data.members[member_id] self._prev_data = self._data - self._attr_name = self._data.name self._name = self._data.name self._attr_entity_picture = self._data.entity_picture From 54ed8fc914dce70149dca071b133df85292bf82c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 24 Aug 2023 22:19:29 +0200 Subject: [PATCH 0847/1151] Use device class translations for 1-wire (#98813) --- homeassistant/components/onewire/sensor.py | 16 ------- homeassistant/components/onewire/strings.json | 15 ------ .../onewire/snapshots/test_sensor.ambr | 46 +++++++++---------- 3 files changed, 23 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 65bd542fc30..34ed66bd511 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -73,7 +73,6 @@ SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION = OneWireSensorEntityDescription( native_unit_of_measurement=UnitOfTemperature.CELSIUS, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="temperature", ) _LOGGER = logging.getLogger(__name__) @@ -89,7 +88,6 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=UnitOfTemperature.CELSIUS, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="temperature", ), OneWireSensorEntityDescription( key="TAI8570/pressure", @@ -98,7 +96,6 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=UnitOfPressure.MBAR, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="pressure", ), ), "22": (SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION,), @@ -111,7 +108,6 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="humidity", ), OneWireSensorEntityDescription( key="HIH3600/humidity", @@ -156,7 +152,6 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=UnitOfPressure.MBAR, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="pressure", ), OneWireSensorEntityDescription( key="S3-R1-A/illuminance", @@ -165,7 +160,6 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=LIGHT_LUX, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="illuminance", ), OneWireSensorEntityDescription( key="VAD", @@ -203,7 +197,6 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { override_key=_get_sensor_precision_family_28, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="temperature", ), ), "30": ( @@ -225,7 +218,6 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=UnitOfElectricPotential.VOLT, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="voltage", ), OneWireSensorEntityDescription( key="vis", @@ -261,7 +253,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="humidity", ), OneWireSensorEntityDescription( key="humidity/humidity_raw", @@ -277,7 +268,6 @@ HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=UnitOfTemperature.CELSIUS, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="temperature", ), ), "HB_MOISTURE_METER": tuple( @@ -303,7 +293,6 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=UnitOfTemperature.CELSIUS, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="temperature", ), OneWireSensorEntityDescription( key="EDS0066/pressure", @@ -311,7 +300,6 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=UnitOfPressure.MBAR, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="pressure", ), ), "EDS0068": ( @@ -321,7 +309,6 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=UnitOfTemperature.CELSIUS, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="temperature", ), OneWireSensorEntityDescription( key="EDS0068/pressure", @@ -329,7 +316,6 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=UnitOfPressure.MBAR, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="pressure", ), OneWireSensorEntityDescription( key="EDS0068/light", @@ -337,7 +323,6 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=LIGHT_LUX, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="illuminance", ), OneWireSensorEntityDescription( key="EDS0068/humidity", @@ -345,7 +330,6 @@ EDS_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { native_unit_of_measurement=PERCENTAGE, read_mode=READ_MODE_FLOAT, state_class=SensorStateClass.MEASUREMENT, - translation_key="humidity", ), ), } diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index f58731a2377..9e4120b68b2 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -68,9 +68,6 @@ "counter_b": { "name": "Counter B" }, - "humidity": { - "name": "[%key:component::sensor::entity_component::humidity::name%]" - }, "humidity_hih3600": { "name": "HIH3600 humidity" }, @@ -86,9 +83,6 @@ "humidity_raw": { "name": "Raw humidity" }, - "illuminance": { - "name": "[%key:component::sensor::entity_component::illuminance::name%]" - }, "moisture_1": { "name": "Moisture 1" }, @@ -101,18 +95,9 @@ "moisture_4": { "name": "Moisture 4" }, - "pressure": { - "name": "[%key:component::sensor::entity_component::pressure::name%]" - }, - "temperature": { - "name": "[%key:component::sensor::entity_component::temperature::name%]" - }, "thermocouple_temperature_k": { "name": "Thermocouple K temperature" }, - "voltage": { - "name": "[%key:component::sensor::entity_component::voltage::name%]" - }, "voltage_vad": { "name": "VAD voltage" }, diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index 6c18c1ec652..0664d7e5402 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -105,7 +105,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/10.111111111111/temperature', 'unit_of_measurement': , }), @@ -187,7 +187,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/12.111111111111/TAI8570/temperature', 'unit_of_measurement': , }), @@ -217,7 +217,7 @@ 'original_name': 'Pressure', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'pressure', + 'translation_key': None, 'unique_id': '/12.111111111111/TAI8570/pressure', 'unit_of_measurement': , }), @@ -589,7 +589,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/22.111111111111/temperature', 'unit_of_measurement': , }), @@ -671,7 +671,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/26.111111111111/temperature', 'unit_of_measurement': , }), @@ -701,7 +701,7 @@ 'original_name': 'Humidity', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'humidity', + 'translation_key': None, 'unique_id': '/26.111111111111/humidity', 'unit_of_measurement': '%', }), @@ -851,7 +851,7 @@ 'original_name': 'Pressure', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'pressure', + 'translation_key': None, 'unique_id': '/26.111111111111/B1-R1-A/pressure', 'unit_of_measurement': , }), @@ -881,7 +881,7 @@ 'original_name': 'Illuminance', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'illuminance', + 'translation_key': None, 'unique_id': '/26.111111111111/S3-R1-A/illuminance', 'unit_of_measurement': 'lx', }), @@ -1203,7 +1203,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/28.111111111111/temperature', 'unit_of_measurement': , }), @@ -1285,7 +1285,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/28.222222222222/temperature', 'unit_of_measurement': , }), @@ -1367,7 +1367,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/28.222222222223/temperature', 'unit_of_measurement': , }), @@ -1486,7 +1486,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/30.111111111111/temperature', 'unit_of_measurement': , }), @@ -1546,7 +1546,7 @@ 'original_name': 'Voltage', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'voltage', + 'translation_key': None, 'unique_id': '/30.111111111111/volt', 'unit_of_measurement': , }), @@ -1740,7 +1740,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/3B.111111111111/temperature', 'unit_of_measurement': , }), @@ -1822,7 +1822,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/42.111111111111/temperature', 'unit_of_measurement': , }), @@ -1904,7 +1904,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/7E.111111111111/EDS0068/temperature', 'unit_of_measurement': , }), @@ -1934,7 +1934,7 @@ 'original_name': 'Pressure', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'pressure', + 'translation_key': None, 'unique_id': '/7E.111111111111/EDS0068/pressure', 'unit_of_measurement': , }), @@ -1964,7 +1964,7 @@ 'original_name': 'Illuminance', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'illuminance', + 'translation_key': None, 'unique_id': '/7E.111111111111/EDS0068/light', 'unit_of_measurement': 'lx', }), @@ -1994,7 +1994,7 @@ 'original_name': 'Humidity', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'humidity', + 'translation_key': None, 'unique_id': '/7E.111111111111/EDS0068/humidity', 'unit_of_measurement': '%', }), @@ -2121,7 +2121,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/7E.222222222222/EDS0066/temperature', 'unit_of_measurement': , }), @@ -2151,7 +2151,7 @@ 'original_name': 'Pressure', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'pressure', + 'translation_key': None, 'unique_id': '/7E.222222222222/EDS0066/pressure', 'unit_of_measurement': , }), @@ -2248,7 +2248,7 @@ 'original_name': 'Humidity', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'humidity', + 'translation_key': None, 'unique_id': '/EF.111111111111/humidity/humidity_corrected', 'unit_of_measurement': '%', }), @@ -2308,7 +2308,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'supported_features': 0, - 'translation_key': 'temperature', + 'translation_key': None, 'unique_id': '/EF.111111111111/humidity/temperature', 'unit_of_measurement': , }), From 4d8941d4b7b798892d7f3183d57906ec4e2f6674 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 24 Aug 2023 22:40:45 +0200 Subject: [PATCH 0848/1151] Use snapshot assertion for onvif diagnostics test (#98982) --- .../onvif/snapshots/test_diagnostics.ambr | 74 ++++++++++++++++ tests/components/onvif/test_diagnostics.py | 84 ++----------------- 2 files changed, 80 insertions(+), 78 deletions(-) create mode 100644 tests/components/onvif/snapshots/test_diagnostics.ambr diff --git a/tests/components/onvif/snapshots/test_diagnostics.ambr b/tests/components/onvif/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..e10c8791ba9 --- /dev/null +++ b/tests/components/onvif/snapshots/test_diagnostics.ambr @@ -0,0 +1,74 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'config': dict({ + 'data': dict({ + 'host': '**REDACTED**', + 'name': 'TestCamera', + 'password': '**REDACTED**', + 'port': 80, + 'snapshot_auth': 'digest', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'onvif', + 'entry_id': '1', + 'options': dict({ + 'enable_webhooks': True, + 'extra_arguments': '-pred 1', + 'rtsp_transport': 'tcp', + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': 'aa:bb:cc:dd:ee', + 'version': 1, + }), + 'device': dict({ + 'capabilities': dict({ + 'events': False, + 'imaging': True, + 'ptz': True, + 'snapshot': False, + }), + 'info': dict({ + 'fw_version': 'TestFirmwareVersion', + 'mac': 'aa:bb:cc:dd:ee', + 'manufacturer': 'TestManufacturer', + 'model': 'TestModel', + 'serial_number': 'ABCDEFGHIJK', + }), + 'profiles': list([ + dict({ + 'index': 0, + 'name': 'profile1', + 'ptz': None, + 'token': 'dummy', + 'video': dict({ + 'encoding': 'any', + 'resolution': dict({ + 'height': 480, + 'width': 640, + }), + }), + 'video_source_token': None, + }), + ]), + 'services': dict({ + }), + 'xaddrs': dict({ + }), + }), + 'events': dict({ + 'pullpoint_manager_state': dict({ + '__type': "", + 'repr': '', + }), + 'webhook_manager_state': dict({ + '__type': "", + 'repr': '', + }), + }), + }) +# --- diff --git a/tests/components/onvif/test_diagnostics.py b/tests/components/onvif/test_diagnostics.py index f87a5f0eff6..af7a68a6e0d 100644 --- a/tests/components/onvif/test_diagnostics.py +++ b/tests/components/onvif/test_diagnostics.py @@ -1,93 +1,21 @@ """Test ONVIF diagnostics.""" -from unittest.mock import ANY +from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant -from . import ( - FIRMWARE_VERSION, - MAC, - MANUFACTURER, - MODEL, - SERIAL_NUMBER, - setup_onvif_integration, -) +from . import setup_onvif_integration from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator async def test_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test generating diagnostics for a config entry.""" entry, _, _ = await setup_onvif_integration(hass) - diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) - - assert diag == { - "config": { - "entry_id": "1", - "version": 1, - "domain": "onvif", - "title": "Mock Title", - "data": { - "name": "TestCamera", - "host": "**REDACTED**", - "port": 80, - "username": "**REDACTED**", - "password": "**REDACTED**", - "snapshot_auth": "digest", - }, - "options": { - "extra_arguments": "-pred 1", - "rtsp_transport": "tcp", - "enable_webhooks": True, - }, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": "aa:bb:cc:dd:ee", - "disabled_by": None, - }, - "device": { - "info": { - "manufacturer": MANUFACTURER, - "model": MODEL, - "fw_version": FIRMWARE_VERSION, - "serial_number": SERIAL_NUMBER, - "mac": MAC, - }, - "capabilities": { - "snapshot": False, - "events": False, - "ptz": True, - "imaging": True, - }, - "profiles": [ - { - "index": 0, - "token": "dummy", - "name": "profile1", - "video": { - "encoding": "any", - "resolution": {"width": 640, "height": 480}, - }, - "ptz": None, - "video_source_token": None, - } - ], - "services": ANY, - "xaddrs": ANY, - }, - "events": { - "pullpoint_manager_state": { - "__type": "", - "repr": "", - }, - "webhook_manager_state": { - "__type": "", - "repr": "", - }, - }, - } + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot From 3bcd1d5a1a444d603aa553a468c3e48afd52fed2 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 24 Aug 2023 23:26:21 +0200 Subject: [PATCH 0849/1151] Use snapshot assertion for iqvia diagnostics test (#98983) --- tests/components/iqvia/conftest.py | 7 +- .../iqvia/snapshots/test_diagnostics.ambr | 363 ++++++++++++++++++ tests/components/iqvia/test_diagnostics.py | 347 +---------------- 3 files changed, 381 insertions(+), 336 deletions(-) create mode 100644 tests/components/iqvia/snapshots/test_diagnostics.ambr diff --git a/tests/components/iqvia/conftest.py b/tests/components/iqvia/conftest.py index b6ac1724885..075d7249d36 100644 --- a/tests/components/iqvia/conftest.py +++ b/tests/components/iqvia/conftest.py @@ -13,7 +13,12 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=config[CONF_ZIP_CODE], data=config) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=config[CONF_ZIP_CODE], + data=config, + entry_id="690ac4b7e99855fc5ee7b987a758d5cb", + ) entry.add_to_hass(hass) return entry diff --git a/tests/components/iqvia/snapshots/test_diagnostics.ambr b/tests/components/iqvia/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..49006716fb3 --- /dev/null +++ b/tests/components/iqvia/snapshots/test_diagnostics.ambr @@ -0,0 +1,363 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'allergy_average_forecasted': dict({ + 'ForecastDate': '2018-06-12T00:00:00-04:00', + 'Location': dict({ + 'City': '**REDACTED**', + 'DisplayLocation': '**REDACTED**', + 'State': '**REDACTED**', + 'ZIP': '**REDACTED**', + 'periods': list([ + dict({ + 'Index': 6.6, + 'Period': '2018-06-12T13:47:12.897', + }), + dict({ + 'Index': 6.3, + 'Period': '2018-06-13T13:47:12.897', + }), + dict({ + 'Index': 7.6, + 'Period': '2018-06-14T13:47:12.897', + }), + dict({ + 'Index': 7.6, + 'Period': '2018-06-15T13:47:12.897', + }), + dict({ + 'Index': 7.3, + 'Period': '2018-06-16T13:47:12.897', + }), + ]), + }), + 'Type': 'pollen', + }), + 'allergy_index': dict({ + 'ForecastDate': '2018-06-12T00:00:00-04:00', + 'Location': dict({ + 'City': '**REDACTED**', + 'DisplayLocation': '**REDACTED**', + 'State': '**REDACTED**', + 'ZIP': '**REDACTED**', + 'periods': list([ + dict({ + 'Index': 7.2, + 'Period': '0001-01-01T00:00:00', + 'Triggers': list([ + dict({ + 'Genus': 'Juniperus', + 'LGID': 272, + 'Name': 'Juniper', + 'PlantType': 'Tree', + }), + dict({ + 'Genus': 'Grasses', + 'LGID': 346, + 'Name': 'Grasses', + 'PlantType': 'Grass', + }), + dict({ + 'Genus': 'Chenopods', + 'LGID': 63, + 'Name': 'Chenopods', + 'PlantType': 'Ragweed', + }), + ]), + 'Type': 'Yesterday', + }), + dict({ + 'Index': 6.6, + 'Period': '0001-01-01T00:00:00', + 'Triggers': list([ + dict({ + 'Genus': 'Juniperus', + 'LGID': 272, + 'Name': 'Juniper', + 'PlantType': 'Tree', + }), + dict({ + 'Genus': 'Grasses', + 'LGID': 346, + 'Name': 'Grasses', + 'PlantType': 'Grass', + }), + dict({ + 'Genus': 'Chenopods', + 'LGID': 63, + 'Name': 'Chenopods', + 'PlantType': 'Ragweed', + }), + ]), + 'Type': 'Today', + }), + dict({ + 'Index': 6.3, + 'Period': '0001-01-01T00:00:00', + 'Triggers': list([ + dict({ + 'Genus': 'Juniperus', + 'LGID': 272, + 'Name': 'Juniper', + 'PlantType': 'Tree', + }), + dict({ + 'Genus': 'Grasses', + 'LGID': 346, + 'Name': 'Grasses', + 'PlantType': 'Grass', + }), + dict({ + 'Genus': 'Chenopods', + 'LGID': 63, + 'Name': 'Chenopods', + 'PlantType': 'Ragweed', + }), + ]), + 'Type': 'Tomorrow', + }), + ]), + }), + 'Type': 'pollen', + }), + 'allergy_outlook': dict({ + 'Market': '**REDACTED**', + 'Outlook': 'The amount of pollen in the air for Wednesday...', + 'Season': 'Tree', + 'Trend': 'subsiding', + 'TrendID': 4, + 'ZIP': '**REDACTED**', + }), + 'asthma_average_forecasted': dict({ + 'ForecastDate': '2018-10-28T00:00:00-04:00', + 'Location': dict({ + 'City': '**REDACTED**', + 'DisplayLocation': '**REDACTED**', + 'State': '**REDACTED**', + 'ZIP': '**REDACTED**', + 'periods': list([ + dict({ + 'Idx': '4.5', + 'Index': 4.5, + 'Period': '2018-10-28T05:45:01.45', + }), + dict({ + 'Idx': '4.7', + 'Index': 4.7, + 'Period': '2018-10-29T05:45:01.45', + }), + dict({ + 'Idx': '5.0', + 'Index': 5, + 'Period': '2018-10-30T05:45:01.45', + }), + dict({ + 'Idx': '5.2', + 'Index': 5.2, + 'Period': '2018-10-31T05:45:01.45', + }), + dict({ + 'Idx': '5.5', + 'Index': 5.5, + 'Period': '2018-11-01T05:45:01.45', + }), + ]), + }), + 'Type': 'asthma', + }), + 'asthma_index': dict({ + 'ForecastDate': '2018-10-29T00:00:00-04:00', + 'Location': dict({ + 'City': '**REDACTED**', + 'DisplayLocation': '**REDACTED**', + 'State': '**REDACTED**', + 'ZIP': '**REDACTED**', + 'periods': list([ + dict({ + 'Idx': '4.1', + 'Index': 4.1, + 'Period': '0001-01-01T00:00:00', + 'Triggers': list([ + dict({ + 'Description': 'Ozone (O3) is a odorless, colorless ....', + 'LGID': 1, + 'Name': 'OZONE', + 'PPM': 42, + }), + dict({ + 'Description': 'Fine particles (PM2.5) are 2.5 ...', + 'LGID': 1, + 'Name': 'PM2.5', + 'PPM': 30, + }), + dict({ + 'Description': 'Coarse dust particles (PM10) are 2.5 ...', + 'LGID': 1, + 'Name': 'PM10', + 'PPM': 19, + }), + ]), + 'Type': 'Yesterday', + }), + dict({ + 'Idx': '4.5', + 'Index': 4.5, + 'Period': '0001-01-01T00:00:00', + 'Triggers': list([ + dict({ + 'Description': 'Fine particles (PM2.5) are 2.5 ...', + 'LGID': 3, + 'Name': 'PM2.5', + 'PPM': 105, + }), + dict({ + 'Description': 'Coarse dust particles (PM10) are 2.5 ...', + 'LGID': 2, + 'Name': 'PM10', + 'PPM': 65, + }), + dict({ + 'Description': 'Ozone (O3) is a odorless, colorless ...', + 'LGID': 1, + 'Name': 'OZONE', + 'PPM': 42, + }), + ]), + 'Type': 'Today', + }), + dict({ + 'Idx': '4.6', + 'Index': 4.6, + 'Period': '0001-01-01T00:00:00', + 'Triggers': list([ + ]), + 'Type': 'Tomorrow', + }), + ]), + }), + 'Type': 'asthma', + }), + 'disease_average_forecasted': dict({ + 'ForecastDate': '2018-06-12T00:00:00-04:00', + 'Location': dict({ + 'City': '**REDACTED**', + 'DisplayLocation': '**REDACTED**', + 'State': '**REDACTED**', + 'ZIP': '**REDACTED**', + 'periods': list([ + dict({ + 'Index': 2.4, + 'Period': '2018-06-12T05:13:51.817', + }), + dict({ + 'Index': 2.5, + 'Period': '2018-06-13T05:13:51.817', + }), + dict({ + 'Index': 2.5, + 'Period': '2018-06-14T05:13:51.817', + }), + dict({ + 'Index': 2.5, + 'Period': '2018-06-15T05:13:51.817', + }), + ]), + }), + 'Type': 'cold', + }), + 'disease_index': dict({ + 'ForecastDate': '2019-04-07T00:00:00-04:00', + 'Location': dict({ + 'City': '**REDACTED**', + 'DisplayLocation': '**REDACTED**', + 'State': '**REDACTED**', + 'ZIP': '**REDACTED**', + 'periods': list([ + dict({ + 'Idx': '6.8', + 'Index': 6.8, + 'Period': '2019-04-06T00:00:00', + 'Triggers': list([ + dict({ + 'Description': 'Influenza', + 'Idx': '3.1', + 'Index': 3.1, + 'Name': 'Flu', + }), + dict({ + 'Description': 'High Fever', + 'Idx': '6.2', + 'Index': 6.2, + 'Name': 'Fever', + }), + dict({ + 'Description': 'Strep & Sore throat', + 'Idx': '5.2', + 'Index': 5.2, + 'Name': 'Strep', + }), + dict({ + 'Description': 'Cough', + 'Idx': '7.8', + 'Index': 7.8, + 'Name': 'Cough', + }), + ]), + 'Type': 'Yesterday', + }), + dict({ + 'Idx': '6.7', + 'Index': 6.7, + 'Period': '2019-04-07T03:52:58', + 'Triggers': list([ + dict({ + 'Description': 'Influenza', + 'Idx': '3.1', + 'Index': 3.1, + 'Name': 'Flu', + }), + dict({ + 'Description': 'High Fever', + 'Idx': '5.9', + 'Index': 5.9, + 'Name': 'Fever', + }), + dict({ + 'Description': 'Strep & Sore throat', + 'Idx': '5.1', + 'Index': 5.1, + 'Name': 'Strep', + }), + dict({ + 'Description': 'Cough', + 'Idx': '7.7', + 'Index': 7.7, + 'Name': 'Cough', + }), + ]), + 'Type': 'Today', + }), + ]), + }), + 'Type': 'cold', + }), + }), + 'entry': dict({ + 'data': dict({ + 'zip_code': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'iqvia', + 'entry_id': '690ac4b7e99855fc5ee7b987a758d5cb', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/iqvia/test_diagnostics.py b/tests/components/iqvia/test_diagnostics.py index 2acf37d6642..bde2af57447 100644 --- a/tests/components/iqvia/test_diagnostics.py +++ b/tests/components/iqvia/test_diagnostics.py @@ -1,5 +1,6 @@ """Test IQVIA diagnostics.""" -from homeassistant.components.diagnostics import REDACTED +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -7,339 +8,15 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, config_entry, hass_client: ClientSessionGenerator, setup_iqvia + hass: HomeAssistant, + config_entry, + hass_client: ClientSessionGenerator, + setup_iqvia, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "entry_id": config_entry.entry_id, - "version": 1, - "domain": "iqvia", - "title": REDACTED, - "data": {"zip_code": REDACTED}, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - }, - "data": { - "allergy_average_forecasted": { - "Type": "pollen", - "ForecastDate": "2018-06-12T00:00:00-04:00", - "Location": { - "ZIP": REDACTED, - "City": REDACTED, - "State": REDACTED, - "periods": [ - {"Period": "2018-06-12T13:47:12.897", "Index": 6.6}, - {"Period": "2018-06-13T13:47:12.897", "Index": 6.3}, - {"Period": "2018-06-14T13:47:12.897", "Index": 7.6}, - {"Period": "2018-06-15T13:47:12.897", "Index": 7.6}, - {"Period": "2018-06-16T13:47:12.897", "Index": 7.3}, - ], - "DisplayLocation": REDACTED, - }, - }, - "allergy_index": { - "Type": "pollen", - "ForecastDate": "2018-06-12T00:00:00-04:00", - "Location": { - "ZIP": REDACTED, - "City": REDACTED, - "State": REDACTED, - "periods": [ - { - "Triggers": [ - { - "LGID": 272, - "Name": "Juniper", - "Genus": "Juniperus", - "PlantType": "Tree", - }, - { - "LGID": 346, - "Name": "Grasses", - "Genus": "Grasses", - "PlantType": "Grass", - }, - { - "LGID": 63, - "Name": "Chenopods", - "Genus": "Chenopods", - "PlantType": "Ragweed", - }, - ], - "Period": "0001-01-01T00:00:00", - "Type": "Yesterday", - "Index": 7.2, - }, - { - "Triggers": [ - { - "LGID": 272, - "Name": "Juniper", - "Genus": "Juniperus", - "PlantType": "Tree", - }, - { - "LGID": 346, - "Name": "Grasses", - "Genus": "Grasses", - "PlantType": "Grass", - }, - { - "LGID": 63, - "Name": "Chenopods", - "Genus": "Chenopods", - "PlantType": "Ragweed", - }, - ], - "Period": "0001-01-01T00:00:00", - "Type": "Today", - "Index": 6.6, - }, - { - "Triggers": [ - { - "LGID": 272, - "Name": "Juniper", - "Genus": "Juniperus", - "PlantType": "Tree", - }, - { - "LGID": 346, - "Name": "Grasses", - "Genus": "Grasses", - "PlantType": "Grass", - }, - { - "LGID": 63, - "Name": "Chenopods", - "Genus": "Chenopods", - "PlantType": "Ragweed", - }, - ], - "Period": "0001-01-01T00:00:00", - "Type": "Tomorrow", - "Index": 6.3, - }, - ], - "DisplayLocation": REDACTED, - }, - }, - "allergy_outlook": { - "Market": REDACTED, - "ZIP": REDACTED, - "TrendID": 4, - "Trend": "subsiding", - "Outlook": "The amount of pollen in the air for Wednesday...", - "Season": "Tree", - }, - "asthma_average_forecasted": { - "Type": "asthma", - "ForecastDate": "2018-10-28T00:00:00-04:00", - "Location": { - "ZIP": REDACTED, - "City": REDACTED, - "State": REDACTED, - "periods": [ - { - "Period": "2018-10-28T05:45:01.45", - "Index": 4.5, - "Idx": "4.5", - }, - { - "Period": "2018-10-29T05:45:01.45", - "Index": 4.7, - "Idx": "4.7", - }, - {"Period": "2018-10-30T05:45:01.45", "Index": 5, "Idx": "5.0"}, - { - "Period": "2018-10-31T05:45:01.45", - "Index": 5.2, - "Idx": "5.2", - }, - { - "Period": "2018-11-01T05:45:01.45", - "Index": 5.5, - "Idx": "5.5", - }, - ], - "DisplayLocation": REDACTED, - }, - }, - "asthma_index": { - "Type": "asthma", - "ForecastDate": "2018-10-29T00:00:00-04:00", - "Location": { - "ZIP": REDACTED, - "City": REDACTED, - "State": REDACTED, - "periods": [ - { - "Triggers": [ - { - "LGID": 1, - "Name": "OZONE", - "PPM": 42, - "Description": ( - "Ozone (O3) is a odorless, colorless ...." - ), - }, - { - "LGID": 1, - "Name": "PM2.5", - "PPM": 30, - "Description": "Fine particles (PM2.5) are 2.5 ...", - }, - { - "LGID": 1, - "Name": "PM10", - "PPM": 19, - "Description": ( - "Coarse dust particles (PM10) are 2.5 ..." - ), - }, - ], - "Period": "0001-01-01T00:00:00", - "Type": "Yesterday", - "Index": 4.1, - "Idx": "4.1", - }, - { - "Triggers": [ - { - "LGID": 3, - "Name": "PM2.5", - "PPM": 105, - "Description": "Fine particles (PM2.5) are 2.5 ...", - }, - { - "LGID": 2, - "Name": "PM10", - "PPM": 65, - "Description": ( - "Coarse dust particles (PM10) are 2.5 ..." - ), - }, - { - "LGID": 1, - "Name": "OZONE", - "PPM": 42, - "Description": ( - "Ozone (O3) is a odorless, colorless ..." - ), - }, - ], - "Period": "0001-01-01T00:00:00", - "Type": "Today", - "Index": 4.5, - "Idx": "4.5", - }, - { - "Triggers": [], - "Period": "0001-01-01T00:00:00", - "Type": "Tomorrow", - "Index": 4.6, - "Idx": "4.6", - }, - ], - "DisplayLocation": REDACTED, - }, - }, - "disease_average_forecasted": { - "Type": "cold", - "ForecastDate": "2018-06-12T00:00:00-04:00", - "Location": { - "ZIP": REDACTED, - "City": REDACTED, - "State": REDACTED, - "periods": [ - {"Period": "2018-06-12T05:13:51.817", "Index": 2.4}, - {"Period": "2018-06-13T05:13:51.817", "Index": 2.5}, - {"Period": "2018-06-14T05:13:51.817", "Index": 2.5}, - {"Period": "2018-06-15T05:13:51.817", "Index": 2.5}, - ], - "DisplayLocation": REDACTED, - }, - }, - "disease_index": { - "ForecastDate": "2019-04-07T00:00:00-04:00", - "Location": { - "City": REDACTED, - "DisplayLocation": REDACTED, - "State": REDACTED, - "ZIP": REDACTED, - "periods": [ - { - "Idx": "6.8", - "Index": 6.8, - "Period": "2019-04-06T00:00:00", - "Triggers": [ - { - "Description": "Influenza", - "Idx": "3.1", - "Index": 3.1, - "Name": "Flu", - }, - { - "Description": "High Fever", - "Idx": "6.2", - "Index": 6.2, - "Name": "Fever", - }, - { - "Description": "Strep & Sore throat", - "Idx": "5.2", - "Index": 5.2, - "Name": "Strep", - }, - { - "Description": "Cough", - "Idx": "7.8", - "Index": 7.8, - "Name": "Cough", - }, - ], - "Type": "Yesterday", - }, - { - "Idx": "6.7", - "Index": 6.7, - "Period": "2019-04-07T03:52:58", - "Triggers": [ - { - "Description": "Influenza", - "Idx": "3.1", - "Index": 3.1, - "Name": "Flu", - }, - { - "Description": "High Fever", - "Idx": "5.9", - "Index": 5.9, - "Name": "Fever", - }, - { - "Description": "Strep & Sore throat", - "Idx": "5.1", - "Index": 5.1, - "Name": "Strep", - }, - { - "Description": "Cough", - "Idx": "7.7", - "Index": 7.7, - "Name": "Cough", - }, - ], - "Type": "Today", - }, - ], - }, - "Type": "cold", - }, - }, - } + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) From 588db501fbf11aedecd549540661397557916e35 Mon Sep 17 00:00:00 2001 From: Marty Sun Date: Fri, 25 Aug 2023 06:48:49 +0800 Subject: [PATCH 0850/1151] Add new integration Yardian (#97326) Co-authored-by: J. Nick Koston --- .coveragerc | 3 + CODEOWNERS | 1 + homeassistant/components/yardian/__init__.py | 40 ++++ .../components/yardian/config_flow.py | 73 +++++++ homeassistant/components/yardian/const.py | 7 + .../components/yardian/coordinator.py | 73 +++++++ .../components/yardian/manifest.json | 9 + homeassistant/components/yardian/strings.json | 20 ++ homeassistant/components/yardian/switch.py | 71 +++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + tests/components/yardian/conftest.py | 14 ++ tests/components/yardian/test_config_flow.py | 188 ++++++++++++++++++ 14 files changed, 509 insertions(+) create mode 100644 homeassistant/components/yardian/__init__.py create mode 100644 homeassistant/components/yardian/config_flow.py create mode 100644 homeassistant/components/yardian/const.py create mode 100644 homeassistant/components/yardian/coordinator.py create mode 100644 homeassistant/components/yardian/manifest.json create mode 100644 homeassistant/components/yardian/strings.json create mode 100644 homeassistant/components/yardian/switch.py create mode 100644 tests/components/yardian/conftest.py create mode 100644 tests/components/yardian/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 5155cac79f1..5753bc13195 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1521,6 +1521,9 @@ omit = homeassistant/components/yamaha_musiccast/select.py homeassistant/components/yamaha_musiccast/switch.py homeassistant/components/yandex_transport/sensor.py + homeassistant/components/yardian/__init__.py + homeassistant/components/yardian/coordinator.py + homeassistant/components/yardian/switch.py homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py homeassistant/components/yolink/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index e3e42b75280..dd52cb196a6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1444,6 +1444,7 @@ build.json @home-assistant/supervisor /tests/components/yamaha_musiccast/ @vigonotion @micha91 /homeassistant/components/yandex_transport/ @rishatik92 @devbis /tests/components/yandex_transport/ @rishatik92 @devbis +/homeassistant/components/yardian/ @h3l1o5 /homeassistant/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015 /tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015 /homeassistant/components/yeelightsunflower/ @lindsaymarkward diff --git a/homeassistant/components/yardian/__init__.py b/homeassistant/components/yardian/__init__.py new file mode 100644 index 00000000000..d6cee9015b8 --- /dev/null +++ b/homeassistant/components/yardian/__init__.py @@ -0,0 +1,40 @@ +"""The Yardian integration.""" +from __future__ import annotations + +from pyyardian import AsyncYardianClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN +from .coordinator import YardianUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.SWITCH] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Yardian from a config entry.""" + + host = entry.data[CONF_HOST] + access_token = entry.data[CONF_ACCESS_TOKEN] + + controller = AsyncYardianClient(async_get_clientsession(hass), host, access_token) + coordinator = YardianUpdateCoordinator(hass, entry, controller) + 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) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data.get(DOMAIN, {}).pop(entry.entry_id, None) + + return unload_ok diff --git a/homeassistant/components/yardian/config_flow.py b/homeassistant/components/yardian/config_flow.py new file mode 100644 index 00000000000..99258965f21 --- /dev/null +++ b/homeassistant/components/yardian/config_flow.py @@ -0,0 +1,73 @@ +"""Config flow for Yardian integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from pyyardian import ( + AsyncYardianClient, + DeviceInfo, + NetworkException, + NotAuthorizedException, +) +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, PRODUCT_NAME + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_ACCESS_TOKEN): str, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Yardian.""" + + VERSION = 1 + + async def async_fetch_device_info(self, host: str, access_token: str) -> DeviceInfo: + """Fetch device info from Yardian.""" + yarcli = AsyncYardianClient( + async_get_clientsession(self.hass), + host, + access_token, + ) + return await yarcli.fetch_device_info() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + try: + device_info = await self.async_fetch_device_info( + user_input["host"], user_input["access_token"] + ) + except NotAuthorizedException: + errors["base"] = "invalid_auth" + except NetworkException: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(device_info["yid"]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + data=user_input | device_info, + title=PRODUCT_NAME, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/yardian/const.py b/homeassistant/components/yardian/const.py new file mode 100644 index 00000000000..b4e75f2367b --- /dev/null +++ b/homeassistant/components/yardian/const.py @@ -0,0 +1,7 @@ +"""Constants for the Yardian integration.""" + +DOMAIN = "yardian" +MANUFACTURER = "Aeon Matrix" +PRODUCT_NAME = "Yardian Smart Sprinkler" + +DEFAULT_WATERING_DURATION = 6 diff --git a/homeassistant/components/yardian/coordinator.py b/homeassistant/components/yardian/coordinator.py new file mode 100644 index 00000000000..526ee3c42ab --- /dev/null +++ b/homeassistant/components/yardian/coordinator.py @@ -0,0 +1,73 @@ +"""Update coordinators for Yardian.""" + +from __future__ import annotations + +import asyncio +import datetime +import logging + +from pyyardian import ( + AsyncYardianClient, + NetworkException, + NotAuthorizedException, + YardianDeviceState, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = datetime.timedelta(seconds=30) + + +class YardianUpdateCoordinator(DataUpdateCoordinator[YardianDeviceState]): + """Coordinator for Yardian API calls.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + controller: AsyncYardianClient, + ) -> None: + """Initialize Yardian API communication.""" + super().__init__( + hass, + _LOGGER, + name=entry.title, + update_method=self._async_update_data, + update_interval=SCAN_INTERVAL, + always_update=False, + ) + + self.controller = controller + self.yid = entry.data["yid"] + self._name = entry.title + self._model = entry.data["model"] + + @property + def device_info(self) -> DeviceInfo: + """Return information about the device.""" + return DeviceInfo( + name=self._name, + identifiers={(DOMAIN, self.yid)}, + manufacturer=MANUFACTURER, + model=self._model, + ) + + async def _async_update_data(self) -> YardianDeviceState: + """Fetch data from Yardian device.""" + try: + async with asyncio.timeout(10): + return await self.controller.fetch_device_state() + + except asyncio.TimeoutError as e: + raise UpdateFailed("Communication with Device was time out") from e + except NotAuthorizedException as e: + raise UpdateFailed("Invalid access token") from e + except NetworkException as e: + raise UpdateFailed("Failed to communicate with Device") from e diff --git a/homeassistant/components/yardian/manifest.json b/homeassistant/components/yardian/manifest.json new file mode 100644 index 00000000000..a20315278b4 --- /dev/null +++ b/homeassistant/components/yardian/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "yardian", + "name": "Yardian", + "codeowners": ["@h3l1o5"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/yardian", + "iot_class": "local_polling", + "requirements": ["pyyardian==1.1.0"] +} diff --git a/homeassistant/components/yardian/strings.json b/homeassistant/components/yardian/strings.json new file mode 100644 index 00000000000..6577c99456c --- /dev/null +++ b/homeassistant/components/yardian/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "access_token": "[%key:common::config_flow::data::access_token%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/yardian/switch.py b/homeassistant/components/yardian/switch.py new file mode 100644 index 00000000000..af5703e0fd4 --- /dev/null +++ b/homeassistant/components/yardian/switch.py @@ -0,0 +1,71 @@ +"""Support for Yardian integration.""" +from __future__ import annotations + +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_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DEFAULT_WATERING_DURATION, DOMAIN +from .coordinator import YardianUpdateCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entry for a Yardian irrigation switches.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + YardianSwitch( + coordinator, + i, + ) + for i in range(len(coordinator.data.zones)) + ) + + +class YardianSwitch(CoordinatorEntity[YardianUpdateCoordinator], SwitchEntity): + """Representation of a Yardian switch.""" + + _attr_icon = "mdi:water" + _attr_has_entity_name = True + + def __init__(self, coordinator: YardianUpdateCoordinator, zone_id) -> None: + """Initialize a Yardian Switch Device.""" + super().__init__(coordinator) + self._zone_id = zone_id + self._attr_unique_id = f"{coordinator.yid}-{zone_id}" + self._attr_device_info = coordinator.device_info + + @property + def name(self) -> str: + """Return the zone name.""" + return self.coordinator.data.zones[self._zone_id][0] + + @property + def is_on(self) -> bool: + """Return true if switch is on.""" + return self._zone_id in self.coordinator.data.active_zones + + @property + def available(self) -> bool: + """Return the switch is available or not.""" + return self.coordinator.data.zones[self._zone_id][1] == 1 + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self.coordinator.controller.start_irrigation( + self._zone_id, + kwargs.get("duration", DEFAULT_WATERING_DURATION), + ) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self.coordinator.controller.stop_irrigation() + await self.coordinator.async_request_refresh() diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 82c2d82f423..93d7ec1fbdc 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -535,6 +535,7 @@ FLOWS = { "yale_smart_alarm", "yalexs_ble", "yamaha_musiccast", + "yardian", "yeelight", "yolink", "youless", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 75540a3af83..07960a97fe5 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6518,6 +6518,12 @@ } } }, + "yardian": { + "name": "Yardian", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "yeelight": { "name": "Yeelight", "integrations": { diff --git a/requirements_all.txt b/requirements_all.txt index be073ac19ab..0c96dc89643 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2248,6 +2248,9 @@ pyws66i==1.1 # homeassistant.components.xeoma pyxeoma==1.4.1 +# homeassistant.components.yardian +pyyardian==1.1.0 + # homeassistant.components.qrcode pyzbar==0.1.7 diff --git a/tests/components/yardian/conftest.py b/tests/components/yardian/conftest.py new file mode 100644 index 00000000000..d4f289c4242 --- /dev/null +++ b/tests/components/yardian/conftest.py @@ -0,0 +1,14 @@ +"""Common fixtures for the Yardian tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.yardian.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/yardian/test_config_flow.py b/tests/components/yardian/test_config_flow.py new file mode 100644 index 00000000000..5f1fcc940cc --- /dev/null +++ b/tests/components/yardian/test_config_flow.py @@ -0,0 +1,188 @@ +"""Test the Yardian config flow.""" +from unittest.mock import AsyncMock, patch + +import pytest +from pyyardian import NetworkException, NotAuthorizedException + +from homeassistant import config_entries +from homeassistant.components.yardian.const import DOMAIN, PRODUCT_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.yardian.config_flow.AsyncYardianClient.fetch_device_info", + return_value={"name": "fake_name", "yid": "fake_yid"}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "fake_host", + "access_token": "fake_token", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == PRODUCT_NAME + assert result2["data"] == { + "host": "fake_host", + "access_token": "fake_token", + "name": "fake_name", + "yid": "fake_yid", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.yardian.config_flow.AsyncYardianClient.fetch_device_info", + side_effect=NotAuthorizedException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "fake_host", + "access_token": "fake_token", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + # Should be recoverable after hits error + with patch( + "homeassistant.components.yardian.config_flow.AsyncYardianClient.fetch_device_info", + return_value={"name": "fake_name", "yid": "fake_yid"}, + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "fake_host", + "access_token": "fake_token", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == PRODUCT_NAME + assert result3["data"] == { + "host": "fake_host", + "access_token": "fake_token", + "name": "fake_name", + "yid": "fake_yid", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.yardian.config_flow.AsyncYardianClient.fetch_device_info", + side_effect=NetworkException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "fake_host", + "access_token": "fake_token", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + # Should be recoverable after hits error + with patch( + "homeassistant.components.yardian.config_flow.AsyncYardianClient.fetch_device_info", + return_value={"name": "fake_name", "yid": "fake_yid"}, + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "fake_host", + "access_token": "fake_token", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == PRODUCT_NAME + assert result3["data"] == { + "host": "fake_host", + "access_token": "fake_token", + "name": "fake_name", + "yid": "fake_yid", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_uncategorized_error( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we handle uncategorized error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.yardian.config_flow.AsyncYardianClient.fetch_device_info", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "fake_host", + "access_token": "fake_token", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + # Should be recoverable after hits error + with patch( + "homeassistant.components.yardian.config_flow.AsyncYardianClient.fetch_device_info", + return_value={"name": "fake_name", "yid": "fake_yid"}, + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "fake_host", + "access_token": "fake_token", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == PRODUCT_NAME + assert result3["data"] == { + "host": "fake_host", + "access_token": "fake_token", + "name": "fake_name", + "yid": "fake_yid", + } + assert len(mock_setup_entry.mock_calls) == 1 From 72e6f790866b9823cece39c1bd4c7647620ed3c6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 Aug 2023 03:23:43 +0200 Subject: [PATCH 0851/1151] Replace remaining utcnow calls + add ruff check (#97964) --- homeassistant/components/whois/sensor.py | 6 +++++- pyproject.toml | 2 ++ tests/components/unifi/test_sensor.py | 13 ++++++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 72c366bb0bc..beca3540e8e 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -22,6 +22,7 @@ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) +from homeassistant.util import dt as dt_util from .const import ATTR_EXPIRES, ATTR_NAME_SERVERS, ATTR_REGISTRAR, ATTR_UPDATED, DOMAIN @@ -45,7 +46,10 @@ def _days_until_expiration(domain: Domain) -> int | None: if domain.expiration_date is None: return None # We need to cast here, as (unlike Pyright) mypy isn't able to determine the type. - return cast(int, (domain.expiration_date - domain.expiration_date.utcnow()).days) + return cast( + int, + (domain.expiration_date - dt_util.utcnow().replace(tzinfo=None)).days, + ) def _ensure_timezone(timestamp: datetime | None) -> datetime | None: diff --git a/pyproject.toml b/pyproject.toml index 2ae9c96734c..38501a024f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -515,6 +515,8 @@ select = [ "C", # complexity "COM818", # Trailing comma on bare tuple prohibited "D", # docstrings + "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() + "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) "E", # pycodestyle "F", # pyflakes/autoflake "G", # flake8-logging-format diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index cf6b74b9765..da2c0b46f76 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -8,7 +8,11 @@ from aiounifi.websocket import WebsocketState import pytest from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass +from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, + SCAN_INTERVAL, + SensorDeviceClass, +) from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, @@ -19,7 +23,6 @@ from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL from homeassistant.helpers.entity_registry import RegistryEntryDisabler import homeassistant.util.dt as dt_util @@ -686,7 +689,7 @@ async def test_wlan_client_sensors( ssid_1 = hass.states.get("sensor.ssid_1") assert ssid_1.state == "1" - async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() ssid_1 = hass.states.get("sensor.ssid_1") @@ -697,7 +700,7 @@ async def test_wlan_client_sensors( wireless_client_1["essid"] = "SSID" mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_1) - async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() ssid_1 = hass.states.get("sensor.ssid_1") @@ -708,7 +711,7 @@ async def test_wlan_client_sensors( wireless_client_2["last_seen"] = 0 mock_unifi_websocket(message=MessageKey.CLIENT, data=wireless_client_2) - async_fire_time_changed(hass, datetime.utcnow() + DEFAULT_SCAN_INTERVAL) + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() ssid_1 = hass.states.get("sensor.ssid_1") From a741298461292440c439423863d69fdaa2c03cd0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 24 Aug 2023 20:11:58 -0600 Subject: [PATCH 0852/1151] Bump `simplisafe-python` to 2023.08.0 (#98991) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index d137824b3db..d0d2a4c5689 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -13,5 +13,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["simplipy"], - "requirements": ["simplisafe-python==2023.05.0"] + "requirements": ["simplisafe-python==2023.08.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0c96dc89643..1ec9810d4ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2402,7 +2402,7 @@ simplehound==0.3 simplepush==2.1.1 # homeassistant.components.simplisafe -simplisafe-python==2023.05.0 +simplisafe-python==2023.08.0 # homeassistant.components.sisyphus sisyphus-control==3.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b40e29d86e..4eb7c10b607 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1756,7 +1756,7 @@ simplehound==0.3 simplepush==2.1.1 # homeassistant.components.simplisafe -simplisafe-python==2023.05.0 +simplisafe-python==2023.08.0 # homeassistant.components.slack slackclient==2.5.0 From 3e02fb1f077d66d59988ed66a795aa9446ad5c75 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 08:59:33 +0200 Subject: [PATCH 0853/1151] Add preview support to all groups (#98951) --- homeassistant/components/group/config_flow.py | 55 ++++++++++++++++--- homeassistant/components/group/cover.py | 12 ++++ homeassistant/components/group/event.py | 13 +++++ homeassistant/components/group/fan.py | 10 ++++ homeassistant/components/group/light.py | 13 +++++ homeassistant/components/group/lock.py | 10 ++++ .../components/group/media_player.py | 45 +++++++++++++-- homeassistant/components/group/switch.py | 13 +++++ tests/components/group/test_config_flow.py | 49 +++++++++++++---- 9 files changed, 194 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index a5bf9e0b972..9eb973b9609 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -24,7 +24,14 @@ from homeassistant.helpers.schema_config_entry_flow import ( from . import DOMAIN, GroupEntity from .binary_sensor import CONF_ALL, async_create_preview_binary_sensor from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC +from .cover import async_create_preview_cover +from .event import async_create_preview_event +from .fan import async_create_preview_fan +from .light import async_create_preview_light +from .lock import async_create_preview_lock +from .media_player import MediaPlayerGroup, async_create_preview_media_player from .sensor import async_create_preview_sensor +from .switch import async_create_preview_switch _STATISTIC_MEASURES = [ "min", @@ -122,7 +129,7 @@ SENSOR_CONFIG_SCHEMA = basic_group_config_schema( async def light_switch_options_schema( - domain: str, handler: SchemaCommonFlowHandler + domain: str, handler: SchemaCommonFlowHandler | None ) -> vol.Schema: """Generate options schema.""" return (await basic_group_options_schema(domain, handler)).extend( @@ -177,26 +184,32 @@ CONFIG_FLOW = { ), "cover": SchemaFlowFormStep( basic_group_config_schema("cover"), + preview="group", validate_user_input=set_group_type("cover"), ), "event": SchemaFlowFormStep( basic_group_config_schema("event"), + preview="group", validate_user_input=set_group_type("event"), ), "fan": SchemaFlowFormStep( basic_group_config_schema("fan"), + preview="group", validate_user_input=set_group_type("fan"), ), "light": SchemaFlowFormStep( basic_group_config_schema("light"), + preview="group", validate_user_input=set_group_type("light"), ), "lock": SchemaFlowFormStep( basic_group_config_schema("lock"), + preview="group", validate_user_input=set_group_type("lock"), ), "media_player": SchemaFlowFormStep( basic_group_config_schema("media_player"), + preview="group", validate_user_input=set_group_type("media_player"), ), "sensor": SchemaFlowFormStep( @@ -206,6 +219,7 @@ CONFIG_FLOW = { ), "switch": SchemaFlowFormStep( basic_group_config_schema("switch"), + preview="group", validate_user_input=set_group_type("switch"), ), } @@ -217,11 +231,26 @@ OPTIONS_FLOW = { binary_sensor_options_schema, preview="group", ), - "cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")), - "event": SchemaFlowFormStep(partial(basic_group_options_schema, "event")), - "fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")), - "light": SchemaFlowFormStep(partial(light_switch_options_schema, "light")), - "lock": SchemaFlowFormStep(partial(basic_group_options_schema, "lock")), + "cover": SchemaFlowFormStep( + partial(basic_group_options_schema, "cover"), + preview="group", + ), + "event": SchemaFlowFormStep( + partial(basic_group_options_schema, "event"), + preview="group", + ), + "fan": SchemaFlowFormStep( + partial(basic_group_options_schema, "fan"), + preview="group", + ), + "light": SchemaFlowFormStep( + partial(light_switch_options_schema, "light"), + preview="group", + ), + "lock": SchemaFlowFormStep( + partial(basic_group_options_schema, "lock"), + preview="group", + ), "media_player": SchemaFlowFormStep( partial(basic_group_options_schema, "media_player"), preview="group", @@ -230,17 +259,27 @@ OPTIONS_FLOW = { partial(sensor_options_schema, "sensor"), preview="group", ), - "switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")), + "switch": SchemaFlowFormStep( + partial(light_switch_options_schema, "switch"), + preview="group", + ), } PREVIEW_OPTIONS_SCHEMA: dict[str, vol.Schema] = {} CREATE_PREVIEW_ENTITY: dict[ str, - Callable[[str, dict[str, Any]], GroupEntity], + Callable[[str, dict[str, Any]], GroupEntity | MediaPlayerGroup], ] = { "binary_sensor": async_create_preview_binary_sensor, + "cover": async_create_preview_cover, + "event": async_create_preview_event, + "fan": async_create_preview_fan, + "light": async_create_preview_light, + "lock": async_create_preview_lock, + "media_player": async_create_preview_media_player, "sensor": async_create_preview_sensor, + "switch": async_create_preview_switch, } diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 0fe67a9bccd..dbb49222bb0 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -96,6 +96,18 @@ async def async_setup_entry( ) +@callback +def async_create_preview_cover( + name: str, validated_config: dict[str, Any] +) -> CoverGroup: + """Create a preview sensor.""" + return CoverGroup( + None, + name, + validated_config[CONF_ENTITIES], + ) + + class CoverGroup(GroupEntity, CoverEntity): """Representation of a CoverGroup.""" diff --git a/homeassistant/components/group/event.py b/homeassistant/components/group/event.py index 81705c7f6f0..ca0c88867fe 100644 --- a/homeassistant/components/group/event.py +++ b/homeassistant/components/group/event.py @@ -2,6 +2,7 @@ from __future__ import annotations import itertools +from typing import Any import voluptuous as vol @@ -87,6 +88,18 @@ async def async_setup_entry( ) +@callback +def async_create_preview_event( + name: str, validated_config: dict[str, Any] +) -> EventGroup: + """Create a preview sensor.""" + return EventGroup( + None, + name, + validated_config[CONF_ENTITIES], + ) + + class EventGroup(GroupEntity, EventEntity): """Representation of an event group.""" diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index 79ce6fe0d87..4ee788c8402 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -96,6 +96,16 @@ async def async_setup_entry( async_add_entities([FanGroup(config_entry.entry_id, config_entry.title, entities)]) +@callback +def async_create_preview_fan(name: str, validated_config: dict[str, Any]) -> FanGroup: + """Create a preview sensor.""" + return FanGroup( + None, + name, + validated_config[CONF_ENTITIES], + ) + + class FanGroup(GroupEntity, FanEntity): """Representation of a FanGroup.""" diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index c6369d876a4..38da7088c2e 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -110,6 +110,19 @@ async def async_setup_entry( ) +@callback +def async_create_preview_light( + name: str, validated_config: dict[str, Any] +) -> LightGroup: + """Create a preview sensor.""" + return LightGroup( + None, + name, + validated_config[CONF_ENTITIES], + validated_config.get(CONF_ALL, False), + ) + + FORWARDED_ATTRIBUTES = frozenset( { ATTR_BRIGHTNESS, diff --git a/homeassistant/components/group/lock.py b/homeassistant/components/group/lock.py index ec0ff13ee15..5558eab5475 100644 --- a/homeassistant/components/group/lock.py +++ b/homeassistant/components/group/lock.py @@ -90,6 +90,16 @@ async def async_setup_entry( ) +@callback +def async_create_preview_lock(name: str, validated_config: dict[str, Any]) -> LockGroup: + """Create a preview sensor.""" + return LockGroup( + None, + name, + validated_config[CONF_ENTITIES], + ) + + class LockGroup(GroupEntity, LockEntity): """Representation of a lock group.""" diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index f0d076ec130..3960f400614 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -1,7 +1,7 @@ """Platform allowing several media players to be grouped into one media player.""" from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Callable, Mapping from contextlib import suppress from typing import Any @@ -44,7 +44,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( @@ -107,6 +107,18 @@ async def async_setup_entry( ) +@callback +def async_create_preview_media_player( + name: str, validated_config: dict[str, Any] +) -> MediaPlayerGroup: + """Create a preview sensor.""" + return MediaPlayerGroup( + None, + name, + validated_config[CONF_ENTITIES], + ) + + class MediaPlayerGroup(MediaPlayerEntity): """Representation of a Media Group.""" @@ -139,7 +151,8 @@ class MediaPlayerGroup(MediaPlayerEntity): self.async_update_supported_features( event.data["entity_id"], event.data["new_state"] ) - self.async_update_state() + self.async_update_group_state() + self.async_write_ha_state() @callback def async_update_supported_features( @@ -208,6 +221,26 @@ class MediaPlayerGroup(MediaPlayerEntity): else: self._features[KEY_ENQUEUE].discard(entity_id) + @callback + def async_start_preview( + self, + preview_callback: Callable[[str, Mapping[str, Any]], None], + ) -> CALLBACK_TYPE: + """Render a preview.""" + + @callback + def async_state_changed_listener( + event: EventType[EventStateChangedData] | None, + ) -> None: + """Handle child updates.""" + self.async_update_group_state() + preview_callback(*self._async_generate_attributes()) + + async_state_changed_listener(None) + return async_track_state_change_event( + self.hass, self._entities, async_state_changed_listener + ) + async def async_added_to_hass(self) -> None: """Register listeners.""" for entity_id in self._entities: @@ -216,7 +249,8 @@ class MediaPlayerGroup(MediaPlayerEntity): async_track_state_change_event( self.hass, self._entities, self.async_on_state_change ) - self.async_update_state() + self.async_update_group_state() + self.async_write_ha_state() @property def name(self) -> str: @@ -391,7 +425,7 @@ class MediaPlayerGroup(MediaPlayerEntity): await self.async_set_volume_level(max(0, volume_level - 0.1)) @callback - def async_update_state(self) -> None: + def async_update_group_state(self) -> None: """Query all members and determine the media group state.""" states = [ state.state @@ -455,4 +489,3 @@ class MediaPlayerGroup(MediaPlayerEntity): supported_features |= MediaPlayerEntityFeature.MEDIA_ENQUEUE self._attr_supported_features = supported_features - self.async_write_ha_state() diff --git a/homeassistant/components/group/switch.py b/homeassistant/components/group/switch.py index bef42824d86..64bc9a99636 100644 --- a/homeassistant/components/group/switch.py +++ b/homeassistant/components/group/switch.py @@ -85,6 +85,19 @@ async def async_setup_entry( ) +@callback +def async_create_preview_switch( + name: str, validated_config: dict[str, Any] +) -> SwitchGroup: + """Create a preview sensor.""" + return SwitchGroup( + None, + name, + validated_config[CONF_ENTITIES], + validated_config.get(CONF_ALL, False), + ) + + class SwitchGroup(GroupEntity, SwitchEntity): """Representation of a switch group.""" diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index a58e47cae71..d0e90fe61bd 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -466,17 +466,34 @@ async def test_options_flow_hides_members( assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by +COVER_ATTRS = [{"supported_features": 0}, {}] +EVENT_ATTRS = [{"event_types": []}, {"event_type": None}] +FAN_ATTRS = [{"supported_features": 0}, {"assumed_state": True}] +LIGHT_ATTRS = [ + { + "icon": "mdi:lightbulb-group", + "supported_color_modes": ["onoff"], + "supported_features": 0, + }, + {"color_mode": "onoff"}, +] +LOCK_ATTRS = [{"supported_features": 1}, {}] +MEDIA_PLAYER_ATTRS = [{"supported_features": 0}, {}] +SENSOR_ATTRS = [{"icon": "mdi:calculator"}, {"max_entity_id": "sensor.input_two"}] + + @pytest.mark.parametrize( ("domain", "extra_user_input", "input_states", "group_state", "extra_attributes"), [ ("binary_sensor", {"all": True}, ["on", "off"], "off", [{}, {}]), - ( - "sensor", - {"type": "max"}, - ["10", "20"], - "20.0", - [{"icon": "mdi:calculator"}, {"max_entity_id": "sensor.input_two"}], - ), + ("cover", {}, ["open", "closed"], "open", COVER_ATTRS), + ("event", {}, ["", ""], "unknown", EVENT_ATTRS), + ("fan", {}, ["on", "off"], "on", FAN_ATTRS), + ("light", {}, ["on", "off"], "on", LIGHT_ATTRS), + ("lock", {}, ["unlocked", "locked"], "unlocked", LOCK_ATTRS), + ("media_player", {}, ["on", "off"], "on", MEDIA_PLAYER_ATTRS), + ("sensor", {"type": "max"}, ["10", "20"], "20.0", SENSOR_ATTRS), + ("switch", {}, ["on", "off"], "on", [{}, {}]), ], ) async def test_config_flow_preview( @@ -553,15 +570,22 @@ async def test_config_flow_preview( "extra_attributes", ), [ - ("binary_sensor", {"all": True}, {"all": False}, ["on", "off"], "on", {}), + ("binary_sensor", {"all": True}, {"all": False}, ["on", "off"], "on", [{}, {}]), + ("cover", {}, {}, ["open", "closed"], "open", COVER_ATTRS), + ("event", {}, {}, ["", ""], "unknown", EVENT_ATTRS), + ("fan", {}, {}, ["on", "off"], "on", FAN_ATTRS), + ("light", {}, {}, ["on", "off"], "on", LIGHT_ATTRS), + ("lock", {}, {}, ["unlocked", "locked"], "unlocked", LOCK_ATTRS), + ("media_player", {}, {}, ["on", "off"], "on", MEDIA_PLAYER_ATTRS), ( "sensor", {"type": "min"}, {"type": "max"}, ["10", "20"], "20.0", - {"icon": "mdi:calculator", "max_entity_id": "sensor.input_two"}, + SENSOR_ATTRS, ), + ("switch", {}, {}, ["on", "off"], "on", [{}, {}]), ], ) async def test_option_flow_preview( @@ -575,8 +599,6 @@ async def test_option_flow_preview( extra_attributes: dict[str, Any], ) -> None: """Test the option flow preview.""" - client = await hass_ws_client(hass) - input_entities = [f"{domain}.input_one", f"{domain}.input_two"] # Setup the config entry @@ -596,6 +618,8 @@ async def test_option_flow_preview( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + client = await hass_ws_client(hass) + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == FlowResultType.FORM assert result["errors"] is None @@ -619,7 +643,8 @@ async def test_option_flow_preview( msg = await client.receive_json() assert msg["event"] == { "attributes": {"entity_id": input_entities, "friendly_name": "My group"} - | extra_attributes, + | extra_attributes[0] + | extra_attributes[1], "state": group_state, } assert len(hass.states.async_all()) == 3 From 960d66e168fa55738adf8dab09d1215803a4365e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 25 Aug 2023 00:43:11 -0700 Subject: [PATCH 0854/1151] Bump ical to 5.0.1 (#98998) --- homeassistant/components/local_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index b56acffe4e2..acc2ac80caa 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==4.5.4"] + "requirements": ["ical==5.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1ec9810d4ad..dd7805741a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1027,7 +1027,7 @@ ibeacon-ble==1.0.1 ibmiotf==0.3.4 # homeassistant.components.local_calendar -ical==4.5.4 +ical==5.0.1 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4eb7c10b607..6433987c4d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -801,7 +801,7 @@ iaqualink==0.5.0 ibeacon-ble==1.0.1 # homeassistant.components.local_calendar -ical==4.5.4 +ical==5.0.1 # homeassistant.components.ping icmplib==3.0 From e7b60374192571fa1591fe026c04a2d3854668b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 25 Aug 2023 10:46:34 +0300 Subject: [PATCH 0855/1151] Remove unnnecessary pylint configs from components [e-l]* (#99004) --- .../components/eddystone_temperature/sensor.py | 1 - homeassistant/components/emulated_hue/config.py | 2 +- homeassistant/components/eq3btsmart/climate.py | 2 +- homeassistant/components/esphome/bluetooth/client.py | 4 +--- homeassistant/components/esphome/entity.py | 2 +- homeassistant/components/esphome/light.py | 2 -- homeassistant/components/eufy/light.py | 1 - .../components/google_assistant/report_state.py | 2 +- homeassistant/components/google_mail/config_flow.py | 4 +--- homeassistant/components/gtfs/sensor.py | 1 - homeassistant/components/hassio/http.py | 1 - homeassistant/components/hassio/websocket_api.py | 2 -- homeassistant/components/html5/notify.py | 1 - homeassistant/components/hue/v1/light.py | 2 +- homeassistant/components/ios/__init__.py | 1 - homeassistant/components/ios/notify.py | 1 - homeassistant/components/keyboard/__init__.py | 2 +- homeassistant/components/keyboard_remote/__init__.py | 2 -- homeassistant/components/konnected/__init__.py | 1 - homeassistant/components/limitlessled/light.py | 2 -- homeassistant/components/linode/binary_sensor.py | 2 +- homeassistant/components/linode/switch.py | 2 +- homeassistant/components/lirc/__init__.py | 1 - homeassistant/components/logger/__init__.py | 1 - tests/components/emulated_hue/test_hue_api.py | 2 -- tests/components/flux/test_switch.py | 10 ---------- tests/components/fritz/conftest.py | 2 +- tests/components/group/test_init.py | 1 - tests/components/history/test_init.py | 1 - tests/components/history/test_init_db_schema_30.py | 1 - tests/components/history/test_websocket_api.py | 1 - .../components/history/test_websocket_api_schema_32.py | 1 - tests/components/homematicip_cloud/test_device.py | 4 ++-- tests/components/hyperion/test_config_flow.py | 1 - tests/components/insteon/test_init.py | 1 - tests/components/lock/test_init.py | 1 - tests/components/logbook/test_init.py | 1 - 37 files changed, 13 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 5a86d45e9f0..347ee1b242f 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -7,7 +7,6 @@ from __future__ import annotations import logging -# pylint: disable=import-error from beacontools import BeaconScanner, EddystoneFilter, EddystoneTLMFrame import voluptuous as vol diff --git a/homeassistant/components/emulated_hue/config.py b/homeassistant/components/emulated_hue/config.py index 104e05605cb..379f0bec9d7 100644 --- a/homeassistant/components/emulated_hue/config.py +++ b/homeassistant/components/emulated_hue/config.py @@ -225,7 +225,7 @@ class Config: @callback def _clear_exposed_cache(self, event: EventType[EventStateChangedData]) -> None: """Clear the cache of exposed states.""" - self.get_exposed_states.cache_clear() # pylint: disable=no-member + self.get_exposed_states.cache_clear() def is_state_exposed(self, state: State) -> bool: """Cache determine if an entity should be exposed on the emulated bridge.""" diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 1ac4531a376..700bc61293f 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -import eq3bt as eq3 # pylint: disable=import-error +import eq3bt as eq3 import voluptuous as vol from homeassistant.components.climate import ( diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 4ce8909587e..ad43ca5df7d 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -51,9 +51,7 @@ CCCD_INDICATE_BYTES = b"\x02\x00" DEFAULT_MAX_WRITE_WITHOUT_RESPONSE = DEFAULT_MTU - GATT_HEADER_SIZE _LOGGER = logging.getLogger(__name__) -_WrapFuncType = TypeVar( # pylint: disable=invalid-name - "_WrapFuncType", bound=Callable[..., Any] -) +_WrapFuncType = TypeVar("_WrapFuncType", bound=Callable[..., Any]) def mac_to_int(address: str) -> int: diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 8b69d011804..db300ab1b28 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable import functools import math -from typing import Any, Generic, TypeVar, cast # pylint: disable=unused-import +from typing import Any, Generic, TypeVar, cast from aioesphomeapi import ( EntityCategory as EsphomeEntityCategory, diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 95fe864eea8..e170d8b3948 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -181,7 +181,6 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): try_keep_current_mode = False if (rgbw_ha := kwargs.get(ATTR_RGBW_COLOR)) is not None: - # pylint: disable-next=invalid-name *rgb, w = tuple(x / 255 for x in rgbw_ha) # type: ignore[assignment] color_bri = max(rgb) # normalize rgb @@ -194,7 +193,6 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): try_keep_current_mode = False if (rgbww_ha := kwargs.get(ATTR_RGBWW_COLOR)) is not None: - # pylint: disable-next=invalid-name *rgb, cw, ww = tuple(x / 255 for x in rgbww_ha) # type: ignore[assignment] color_bri = max(rgb) # normalize rgb diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index 625b5cda0ba..5185dcd8818 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -134,7 +134,6 @@ class EufyHomeLight(LightEntity): """Turn the specified light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) - # pylint: disable-next=invalid-name hs = kwargs.get(ATTR_HS_COLOR) if brightness is not None: diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 109ea61dbab..5248ce7c4da 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -147,6 +147,6 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig def unsub_all(): unsub() if unsub_pending: - unsub_pending() # pylint: disable=not-callable + unsub_pending() return unsub_all diff --git a/homeassistant/components/google_mail/config_flow.py b/homeassistant/components/google_mail/config_flow.py index 0552f57bf5c..b57947302cc 100644 --- a/homeassistant/components/google_mail/config_flow.py +++ b/homeassistant/components/google_mail/config_flow.py @@ -60,9 +60,7 @@ class OAuth2FlowHandler( def _get_profile() -> str: """Get profile from inside the executor.""" - users = build( # pylint: disable=no-member - "gmail", "v1", credentials=credentials - ).users() + users = build("gmail", "v1", credentials=credentials).users() return users.getProfile(userId="me").execute()["emailAddress"] credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 6f8daf2918d..87d2b55aa24 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -505,7 +505,6 @@ def setup_platform( joined_path = os.path.join(gtfs_dir, sqlite_file) gtfs = pygtfs.Schedule(joined_path) - # pylint: disable=no-member if not gtfs.feeds: pygtfs.append_feed(gtfs, os.path.join(gtfs_dir, data)) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 0e18a009323..5bcdb6896cd 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -82,7 +82,6 @@ NO_STORE = re.compile( r"|app/entrypoint.js" r")$" ) -# pylint: enable=implicit-str-concat # fmt: on RESPONSE_HEADERS_FILTER = { diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index ac0395ebd9f..8f44f7f2843 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -41,7 +41,6 @@ SCHEMA_WEBSOCKET_EVENT = vol.Schema( ) # Endpoints needed for ingress can't require admin because addons can set `panel_admin: false` -# pylint: disable=implicit-str-concat # fmt: off WS_NO_ADMIN_ENDPOINTS = re.compile( r"^(?:" @@ -50,7 +49,6 @@ WS_NO_ADMIN_ENDPOINTS = re.compile( r")$" # noqa: ISC001 ) # fmt: on -# pylint: enable=implicit-str-concat _LOGGER: logging.Logger = logging.getLogger(__package__) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 931d446b2a0..d65a4c42488 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -113,7 +113,6 @@ SUBSCRIPTION_SCHEMA = vol.All( dict, vol.Schema( { - # pylint: disable=no-value-for-parameter vol.Required(ATTR_ENDPOINT): vol.Url(), vol.Required(ATTR_KEYS): KEYS_SCHEMA, vol.Optional(ATTR_EXPIRATIONTIME): vol.Any(None, cv.positive_int), diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index 8ae09ef9d47..18440f68239 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -224,7 +224,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Once we do a rooms update, we cancel the listener # until the next time lights are added bridge.reset_jobs.remove(cancel_update_rooms_listener) - cancel_update_rooms_listener() # pylint: disable=not-callable + cancel_update_rooms_listener() cancel_update_rooms_listener = None @callback diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index 052ed9f94a0..dd5ea743d57 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -293,7 +293,6 @@ async def async_setup_entry( return True -# pylint: disable=invalid-name class iOSPushConfigView(HomeAssistantView): """A view that provides the push categories configuration.""" diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index 519bb87d98a..2f42edb4bc1 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -25,7 +25,6 @@ _LOGGER = logging.getLogger(__name__) PUSH_URL = "https://ios-push.home-assistant.io/push" -# pylint: disable=invalid-name def log_rate_limits(hass, target, resp, level=20): """Output rate limit log line at given level.""" rate_limits = resp["rateLimits"] diff --git a/homeassistant/components/keyboard/__init__.py b/homeassistant/components/keyboard/__init__.py index f4e7f9e0424..d129505515d 100644 --- a/homeassistant/components/keyboard/__init__.py +++ b/homeassistant/components/keyboard/__init__.py @@ -1,5 +1,5 @@ """Support to emulate keyboard presses on host machine.""" -from pykeyboard import PyKeyboard # pylint: disable=import-error +from pykeyboard import PyKeyboard import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index df3b6f0e427..eecde05d1f4 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -1,5 +1,4 @@ """Receive signals from a keyboard and use it as a remote control.""" -# pylint: disable=import-error from __future__ import annotations import asyncio @@ -331,7 +330,6 @@ class KeyboardRemote: _LOGGER.debug("Start device monitoring") await self.hass.async_add_executor_job(self.dev.grab) async for event in self.dev.async_read_loop(): - # pylint: disable=no-member if event.type is ecodes.EV_KEY: if event.value in self.key_values: _LOGGER.debug( diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 119c7c946a5..fa8a35d7a64 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -197,7 +197,6 @@ DEVICE_SCHEMA_YAML = vol.All( import_device_validator, ) -# pylint: disable=no-value-for-parameter CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 801f104bd3b..6677768dd00 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -278,7 +278,6 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): return ColorMode.COLOR_TEMP return ColorMode.HS - # pylint: disable=arguments-differ @state(False) def turn_off(self, transition_time: int, pipeline: Pipeline, **kwargs: Any) -> None: """Turn off a group.""" @@ -286,7 +285,6 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): pipeline.transition(transition_time, brightness=0.0) pipeline.off() - # pylint: disable=arguments-differ @state(True) def turn_on(self, transition_time: int, pipeline: Pipeline, **kwargs: Any) -> None: """Turn on (or adjust property of) a group.""" diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index 2c63bbc0bc8..17a68e9be9c 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -61,7 +61,7 @@ class LinodeBinarySensor(BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.MOVING - def __init__(self, li, node_id): # pylint: disable=invalid-name + def __init__(self, li, node_id): """Initialize a new Linode sensor.""" self._linode = li self._node_id = node_id diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py index 183abbc068c..b59e8f901e5 100644 --- a/homeassistant/components/linode/switch.py +++ b/homeassistant/components/linode/switch.py @@ -57,7 +57,7 @@ def setup_platform( class LinodeSwitch(SwitchEntity): """Representation of a Linode Node switch.""" - def __init__(self, li, node_id): # pylint: disable=invalid-name + def __init__(self, li, node_id): """Initialize a new Linode sensor.""" self._linode = li self._node_id = node_id diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index 181783b6bbd..1b9688906ff 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -1,5 +1,4 @@ """Support for LIRC devices.""" -# pylint: disable=import-error import logging import threading import time diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index cd2761510d3..e7f3d6b78f1 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -126,7 +126,6 @@ def _get_logger_class(hass_overrides: dict[str, int]) -> type[logging.Logger]: super().setLevel(level) - # pylint: disable=invalid-name def orig_setLevel(self, level: int | str) -> None: """Set the log level.""" super().setLevel(level) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index b42b40b2739..24acde0709a 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1130,7 +1130,6 @@ async def test_put_light_state_fan(hass_hue, hue_client) -> None: assert round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 100 -# pylint: disable=invalid-name async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client) -> None: """Test the form with urlencoded content.""" entity_number = ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] @@ -1215,7 +1214,6 @@ async def test_get_empty_groups_state(hue_client) -> None: assert result_json == {} -# pylint: disable=invalid-name async def perform_put_test_on_ceiling_lights( hass_hue, hue_client, content_type=CONTENT_TYPE_JSON ): diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index a7d7439226c..ed8a4756031 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -302,7 +302,6 @@ async def test_flux_before_sunrise_known_location( assert call.data[light.ATTR_XY_COLOR] == [0.606, 0.379] -# pylint: disable=invalid-name async def test_flux_after_sunrise_before_sunset( hass: HomeAssistant, enable_custom_integrations: None ) -> None: @@ -361,7 +360,6 @@ async def test_flux_after_sunrise_before_sunset( assert call.data[light.ATTR_XY_COLOR] == [0.439, 0.37] -# pylint: disable=invalid-name async def test_flux_after_sunset_before_stop( hass: HomeAssistant, enable_custom_integrations: None ) -> None: @@ -421,7 +419,6 @@ async def test_flux_after_sunset_before_stop( assert call.data[light.ATTR_XY_COLOR] == [0.506, 0.385] -# pylint: disable=invalid-name async def test_flux_after_stop_before_sunrise( hass: HomeAssistant, enable_custom_integrations: None ) -> None: @@ -480,7 +477,6 @@ async def test_flux_after_stop_before_sunrise( assert call.data[light.ATTR_XY_COLOR] == [0.606, 0.379] -# pylint: disable=invalid-name async def test_flux_with_custom_start_stop_times( hass: HomeAssistant, enable_custom_integrations: None ) -> None: @@ -603,7 +599,6 @@ async def test_flux_before_sunrise_stop_next_day( assert call.data[light.ATTR_XY_COLOR] == [0.606, 0.379] -# pylint: disable=invalid-name async def test_flux_after_sunrise_before_sunset_stop_next_day( hass: HomeAssistant, enable_custom_integrations: None ) -> None: @@ -666,7 +661,6 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day( assert call.data[light.ATTR_XY_COLOR] == [0.439, 0.37] -# pylint: disable=invalid-name @pytest.mark.parametrize("x", [0, 1]) async def test_flux_after_sunset_before_midnight_stop_next_day( hass: HomeAssistant, x, enable_custom_integrations: None @@ -730,7 +724,6 @@ async def test_flux_after_sunset_before_midnight_stop_next_day( assert call.data[light.ATTR_XY_COLOR] == [0.588, 0.386] -# pylint: disable=invalid-name async def test_flux_after_sunset_after_midnight_stop_next_day( hass: HomeAssistant, enable_custom_integrations: None ) -> None: @@ -793,7 +786,6 @@ async def test_flux_after_sunset_after_midnight_stop_next_day( assert call.data[light.ATTR_XY_COLOR] == [0.601, 0.382] -# pylint: disable=invalid-name async def test_flux_after_stop_before_sunrise_stop_next_day( hass: HomeAssistant, enable_custom_integrations: None ) -> None: @@ -856,7 +848,6 @@ async def test_flux_after_stop_before_sunrise_stop_next_day( assert call.data[light.ATTR_XY_COLOR] == [0.606, 0.379] -# pylint: disable=invalid-name async def test_flux_with_custom_colortemps( hass: HomeAssistant, enable_custom_integrations: None ) -> None: @@ -918,7 +909,6 @@ async def test_flux_with_custom_colortemps( assert call.data[light.ATTR_XY_COLOR] == [0.469, 0.378] -# pylint: disable=invalid-name async def test_flux_with_custom_brightness( hass: HomeAssistant, enable_custom_integrations: None ) -> None: diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index acb135d01bb..08dce14f18d 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -26,7 +26,7 @@ class FritzServiceMock(Service): self.serviceId = serviceId -class FritzConnectionMock: # pylint: disable=too-few-public-methods +class FritzConnectionMock: """FritzConnection mocking.""" def __init__(self, services): diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index e4d737b04e2..3ea75fbce06 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -616,7 +616,6 @@ async def test_service_group_services_add_remove_entities(hass: HomeAssistant) - assert "person.one" not in list(group_state.attributes["entity_id"]) -# pylint: disable=invalid-name async def test_service_group_set_group_remove_group(hass: HomeAssistant) -> None: """Check if service are available.""" with assert_setup_component(0, "group"): diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 04384834282..356fbb86b01 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1,5 +1,4 @@ """The tests the History component.""" -# pylint: disable=invalid-name from datetime import timedelta from http import HTTPStatus import json diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py index 32358e95e41..caf151cafe7 100644 --- a/tests/components/history/test_init_db_schema_30.py +++ b/tests/components/history/test_init_db_schema_30.py @@ -1,7 +1,6 @@ """The tests the History component.""" from __future__ import annotations -# pylint: disable=invalid-name from datetime import timedelta from http import HTTPStatus import json diff --git a/tests/components/history/test_websocket_api.py b/tests/components/history/test_websocket_api.py index 87489486614..9ba47303e53 100644 --- a/tests/components/history/test_websocket_api.py +++ b/tests/components/history/test_websocket_api.py @@ -1,5 +1,4 @@ """The tests the History component websocket_api.""" -# pylint: disable=protected-access,invalid-name import asyncio from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/history/test_websocket_api_schema_32.py b/tests/components/history/test_websocket_api_schema_32.py index aebf5aa7ac2..6ef6f7225c1 100644 --- a/tests/components/history/test_websocket_api_schema_32.py +++ b/tests/components/history/test_websocket_api_schema_32.py @@ -1,5 +1,4 @@ """The tests the History component websocket_api.""" -# pylint: disable=protected-access,invalid-name import pytest diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index d84fe690df6..24842ab8beb 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -244,13 +244,13 @@ async def test_hmip_reset_energy_counter_services( blocking=True, ) assert hmip_device.mock_calls[-1][0] == "reset_energy_counter" - assert len(hmip_device._connection.mock_calls) == 2 # pylint: disable=W0212 + assert len(hmip_device._connection.mock_calls) == 2 await hass.services.async_call( "homematicip_cloud", "reset_energy_counter", {"entity_id": "all"}, blocking=True ) assert hmip_device.mock_calls[-1][0] == "reset_energy_counter" - assert len(hmip_device._connection.mock_calls) == 4 # pylint: disable=W0212 + assert len(hmip_device._connection.mock_calls) == 4 async def test_hmip_multi_area_device( diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index f9cef677ead..97b705ef731 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -763,7 +763,6 @@ async def test_options_priority(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True, ) - # pylint: disable-next=unsubscriptable-object assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py index 1c4e2abf123..15f529babd8 100644 --- a/tests/components/insteon/test_init.py +++ b/tests/components/insteon/test_init.py @@ -44,7 +44,6 @@ async def test_setup_entry(hass: HomeAssistant) -> None: await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - # pylint: disable-next=no-member assert insteon.devices.async_save.call_count == 1 assert mock_close.called diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py index 0d33881c46c..24b13d48a1e 100644 --- a/tests/components/lock/test_init.py +++ b/tests/components/lock/test_init.py @@ -73,7 +73,6 @@ async def test_lock_default(hass: HomeAssistant) -> None: async def test_lock_states(hass: HomeAssistant) -> None: """Test lock entity states.""" - # pylint: disable=protected-access lock = MockLockEntity() lock.hass = hass diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 2a93e6e1d4c..eaa2a1e4192 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1,5 +1,4 @@ """The tests for the logbook component.""" -# pylint: disable=invalid-name import asyncio import collections from collections.abc import Callable From 07494f129cb60bbe7bb6fa33e58f57a617047fab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Aug 2023 10:18:43 +0200 Subject: [PATCH 0856/1151] Bump actions/checkout from 3.5.3 to 3.6.0 (#99003) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.3 to 3.6.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.5.3...v3.6.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ .github/workflows/ci.yaml | 28 ++++++++++++++-------------- .github/workflows/translations.yml | 2 +- .github/workflows/wheels.yml | 6 +++--- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index b5d37be44bc..3296f33f84c 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -24,7 +24,7 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 with: fetch-depth: 0 @@ -56,7 +56,7 @@ jobs: if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.7.0 @@ -98,7 +98,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' @@ -254,7 +254,7 @@ jobs: - green steps: - name: Checkout the repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set build additional args run: | @@ -293,7 +293,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -331,7 +331,7 @@ jobs: id-token: write steps: - name: Checkout the repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Install Cosign uses: sigstore/cosign-installer@v3.1.1 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a96a0602473..26811f31962 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Generate partial Python venv restore key id: generate_python_cache_key run: >- @@ -220,7 +220,7 @@ jobs: - info steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.7.0 @@ -265,7 +265,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.7.0 id: python @@ -311,7 +311,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.7.0 id: python @@ -360,7 +360,7 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.7.0 id: python @@ -454,7 +454,7 @@ jobs: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.7.0 @@ -522,7 +522,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.7.0 @@ -554,7 +554,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.7.0 @@ -587,7 +587,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.7.0 @@ -631,7 +631,7 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.7.0 @@ -713,7 +713,7 @@ jobs: bluez \ ffmpeg - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.7.0 @@ -865,7 +865,7 @@ jobs: ffmpeg \ libmariadb-dev-compat - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.7.0 @@ -989,7 +989,7 @@ jobs: ffmpeg \ postgresql-server-dev-14 - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.7.0 @@ -1084,7 +1084,7 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Download all coverage artifacts uses: actions/download-artifact@v3 - name: Upload coverage to Codecov (full coverage) diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index 1d77ac8f130..5affa459f52 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v4.7.0 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 16bd347d7cf..01823199c17 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -26,7 +26,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Get information id: info @@ -84,7 +84,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Download env_file uses: actions/download-artifact@v3 @@ -122,7 +122,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v3.6.0 - name: Download env_file uses: actions/download-artifact@v3 From 48b6b1c11af463a367330e89ebb6865bd9bf439b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 10:25:03 +0200 Subject: [PATCH 0857/1151] Modernize openweathermap weather (#99002) --- .../components/openweathermap/weather.py | 27 ++++++++++++++++--- .../weather_update_coordinator.py | 14 +++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index c6f95555954..bf1ae5ca7da 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -17,7 +17,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, Forecast, - WeatherEntity, + SingleCoordinatorWeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -26,10 +27,9 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +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 .const import ( ATTR_API_CLOUDS, @@ -60,6 +60,8 @@ from .const import ( DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, + FORECAST_MODE_DAILY, + FORECAST_MODE_ONECALL_DAILY, MANUFACTURER, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -96,7 +98,7 @@ async def async_setup_entry( async_add_entities([owm_weather], False) -class OpenWeatherMapWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): +class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]): """Implementation of an OpenWeatherMap sensor.""" _attr_attribution = ATTRIBUTION @@ -123,6 +125,13 @@ class OpenWeatherMapWeather(CoordinatorEntity[WeatherUpdateCoordinator], Weather manufacturer=MANUFACTURER, name=DEFAULT_NAME, ) + if weather_coordinator.forecast_mode in ( + FORECAST_MODE_DAILY, + FORECAST_MODE_ONECALL_DAILY, + ): + self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY + else: # FORECAST_MODE_DAILY or FORECAST_MODE_ONECALL_HOURLY + self._attr_supported_features = WeatherEntityFeature.FORECAST_HOURLY @property def condition(self) -> str | None: @@ -187,3 +196,13 @@ class OpenWeatherMapWeather(CoordinatorEntity[WeatherUpdateCoordinator], Weather for forecast in api_forecasts ] return cast(list[Forecast], forecasts) + + @callback + def _async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + return self.forecast + + @callback + def _async_forecast_hourly(self) -> list[Forecast] | None: + """Return the hourly forecast in native units.""" + return self.forecast diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index cf0c941f0df..56519c46fd9 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -68,7 +68,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): self._owm_client = owm self._latitude = latitude self._longitude = longitude - self._forecast_mode = forecast_mode + self.forecast_mode = forecast_mode self._forecast_limit = None if forecast_mode == FORECAST_MODE_DAILY: self._forecast_limit = 15 @@ -90,7 +90,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): async def _get_owm_weather(self): """Poll weather data from OWM.""" - if self._forecast_mode in ( + if self.forecast_mode in ( FORECAST_MODE_ONECALL_HOURLY, FORECAST_MODE_ONECALL_DAILY, ): @@ -106,17 +106,17 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _get_legacy_weather_and_forecast(self): """Get weather and forecast data from OWM.""" - interval = self._get_forecast_interval() + interval = self._get_legacy_forecast_interval() weather = self._owm_client.weather_at_coords(self._latitude, self._longitude) forecast = self._owm_client.forecast_at_coords( self._latitude, self._longitude, interval, self._forecast_limit ) return LegacyWeather(weather.weather, forecast.forecast.weathers) - def _get_forecast_interval(self): + def _get_legacy_forecast_interval(self): """Get the correct forecast interval depending on the forecast mode.""" interval = "daily" - if self._forecast_mode == FORECAST_MODE_HOURLY: + if self.forecast_mode == FORECAST_MODE_HOURLY: interval = "3h" return interval @@ -153,9 +153,9 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _get_forecast_from_weather_response(self, weather_response): """Extract the forecast data from the weather response.""" forecast_arg = "forecast" - if self._forecast_mode == FORECAST_MODE_ONECALL_HOURLY: + if self.forecast_mode == FORECAST_MODE_ONECALL_HOURLY: forecast_arg = "forecast_hourly" - elif self._forecast_mode == FORECAST_MODE_ONECALL_DAILY: + elif self.forecast_mode == FORECAST_MODE_ONECALL_DAILY: forecast_arg = "forecast_daily" return [ self._convert_forecast(x) for x in getattr(weather_response, forecast_arg) From c2713f0aed643954b61aef5f714209d3d250a4ea Mon Sep 17 00:00:00 2001 From: Niels Perfors Date: Fri, 25 Aug 2023 10:27:35 +0200 Subject: [PATCH 0858/1151] Upgrade Verisure to 2.6.6 (#98258) --- homeassistant/components/verisure/coordinator.py | 14 +++++++++++--- homeassistant/components/verisure/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index 3779af4fd16..f31d36aa2da 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -47,7 +47,7 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): try: await self.hass.async_add_executor_job(self.verisure.login_cookie) except VerisureLoginError as ex: - LOGGER.error("Could not log in to verisure, %s", ex) + LOGGER.error("Credentials expired for Verisure, %s", ex) raise ConfigEntryAuthFailed("Credentials expired for Verisure") from ex except VerisureError as ex: LOGGER.error("Could not log in to verisure, %s", ex) @@ -63,8 +63,16 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): """Fetch data from Verisure.""" try: await self.hass.async_add_executor_job(self.verisure.update_cookie) - except VerisureLoginError as ex: - raise ConfigEntryAuthFailed("Credentials expired for Verisure") from ex + except VerisureLoginError: + LOGGER.debug("Cookie expired, acquiring new cookies") + try: + await self.hass.async_add_executor_job(self.verisure.login_cookie) + except VerisureLoginError as ex: + LOGGER.error("Credentials expired for Verisure, %s", ex) + raise ConfigEntryAuthFailed("Credentials expired for Verisure") from ex + except VerisureError as ex: + LOGGER.error("Could not log in to verisure, %s", ex) + raise ConfigEntryAuthFailed("Could not log in to verisure") from ex except VerisureError as ex: raise UpdateFailed("Unable to update cookie") from ex try: diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 98440f67e4c..7c9e7057b0c 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["verisure"], - "requirements": ["vsure==2.6.4"] + "requirements": ["vsure==2.6.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd7805741a2..2aaba30788b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2647,7 +2647,7 @@ volkszaehler==0.4.0 volvooncall==0.10.3 # homeassistant.components.verisure -vsure==2.6.4 +vsure==2.6.6 # homeassistant.components.vasttrafik vtjp==0.1.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6433987c4d4..752e320aa93 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1941,7 +1941,7 @@ voip-utils==0.1.0 volvooncall==0.10.3 # homeassistant.components.verisure -vsure==2.6.4 +vsure==2.6.6 # homeassistant.components.vulcan vulcan-api==2.3.0 From 3ebf96143a65392bdb64ebc5a1500927f6fae06e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Aug 2023 03:31:43 -0500 Subject: [PATCH 0859/1151] Improve performance of bluetooth coordinators (#98997) --- .../components/bluetooth/update_coordinator.py | 16 ++++++++++++++-- .../bluetooth/test_passive_update_processor.py | 6 ++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index 88263aa0a58..9c38bf2f520 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -39,6 +39,7 @@ class BasePassiveBluetoothCoordinator(ABC): self.mode = mode self._last_unavailable_time = 0.0 self._last_name = address + self._available = async_address_present(hass, address, connectable) @callback def async_start(self) -> CALLBACK_TYPE: @@ -85,7 +86,17 @@ class BasePassiveBluetoothCoordinator(ABC): @property def available(self) -> bool: """Return if the device is available.""" - return async_address_present(self.hass, self.address, self.connectable) + return self._available + + @callback + def _async_handle_bluetooth_event_internal( + self, + service_info: BluetoothServiceInfoBleak, + change: BluetoothChange, + ) -> None: + """Handle a bluetooth event.""" + self._available = True + self._async_handle_bluetooth_event(service_info, change) @callback def _async_start(self) -> None: @@ -93,7 +104,7 @@ class BasePassiveBluetoothCoordinator(ABC): self._on_stop.append( async_register_callback( self.hass, - self._async_handle_bluetooth_event, + self._async_handle_bluetooth_event_internal, BluetoothCallbackMatcher( address=self.address, connectable=self.connectable ), @@ -123,3 +134,4 @@ class BasePassiveBluetoothCoordinator(ABC): """Handle the device going unavailable.""" self._last_unavailable_time = service_info.time self._last_name = service_info.name + self._available = False diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index d11f5cd5ccd..c96fbfbfc99 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -406,7 +406,7 @@ async def test_exception_from_update_method( """Generate mock data.""" nonlocal run_count run_count += 1 - if run_count == 1: + if run_count == 2: raise Exception("Test exception") return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE @@ -436,6 +436,7 @@ async def test_exception_from_update_method( processor.async_add_listener(MagicMock()) inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert processor.available is True # We should go unavailable once we get an exception @@ -473,7 +474,7 @@ async def test_bad_data_from_update_method( """Generate mock data.""" nonlocal run_count run_count += 1 - if run_count == 1: + if run_count == 2: return "bad_data" return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE @@ -503,6 +504,7 @@ async def test_bad_data_from_update_method( processor.async_add_listener(MagicMock()) inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert processor.available is True # We should go unavailable once we get bad data From 475fd770198baf8272a749d110c6e5e6bca20052 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 25 Aug 2023 10:33:02 +0200 Subject: [PATCH 0860/1151] Extract SRP Energy coordinator to separate file (#98956) --- .../components/srp_energy/__init__.py | 11 +- .../components/srp_energy/coordinator.py | 77 +++++++++++++ homeassistant/components/srp_energy/sensor.py | 107 ++---------------- tests/components/srp_energy/test_sensor.py | 30 +++++ 4 files changed, 124 insertions(+), 101 deletions(-) create mode 100644 homeassistant/components/srp_energy/coordinator.py diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index ea80a29d990..98d1cdd421a 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -5,7 +5,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN, LOGGER +from .const import CONF_IS_TOU, DOMAIN, LOGGER +from .coordinator import SRPEnergyDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] @@ -24,8 +25,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_password, ) + coordinator = SRPEnergyDataUpdateCoordinator( + hass, api_instance, entry.data[CONF_IS_TOU] + ) + + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = api_instance + hass.data[DOMAIN][entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/srp_energy/coordinator.py b/homeassistant/components/srp_energy/coordinator.py new file mode 100644 index 00000000000..9637f176886 --- /dev/null +++ b/homeassistant/components/srp_energy/coordinator.py @@ -0,0 +1,77 @@ +"""DataUpdateCoordinator for the srp_energy integration.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta + +from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout +from srpenergy.client import SrpEnergyClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util + +from .const import DOMAIN, LOGGER, MIN_TIME_BETWEEN_UPDATES, PHOENIX_TIME_ZONE + + +class SRPEnergyDataUpdateCoordinator(DataUpdateCoordinator[float]): + """A srp_energy Data Update Coordinator.""" + + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, client: SrpEnergyClient, is_time_of_use: bool + ) -> None: + """Initialize the srp_energy data coordinator.""" + self._client = client + self._is_time_of_use = is_time_of_use + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + + async def _async_update_data(self) -> float: + """Fetch data from API endpoint. + + This is the place to pre-process the data to lookup tables + so entities can quickly look up their data. + """ + LOGGER.debug("async_update_data enter") + try: + # Fetch srp_energy data + phx_time_zone = dt_util.get_time_zone(PHOENIX_TIME_ZONE) + end_date = dt_util.now(phx_time_zone) + start_date = end_date - timedelta(days=1) + + async with asyncio.timeout(10): + hourly_usage = await self.hass.async_add_executor_job( + self._client.usage, + start_date, + end_date, + self._is_time_of_use, + ) + + LOGGER.debug( + "async_update_data: Received %s record(s) from %s to %s", + len(hourly_usage) if hourly_usage else "None", + start_date, + end_date, + ) + + previous_daily_usage = 0.0 + for _, _, _, kwh, _ in hourly_usage: + previous_daily_usage += float(kwh) + + LOGGER.debug( + "async_update_data: previous_daily_usage %s", + previous_daily_usage, + ) + + return previous_daily_usage + except TimeoutError as timeout_err: + raise UpdateFailed("Timeout communicating with API") from timeout_err + except (ConnectError, HTTPError, Timeout, ValueError, TypeError) as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index d477b65b21d..601baaee8ca 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -1,11 +1,6 @@ """Support for SRP Energy Sensor.""" from __future__ import annotations -import asyncio -from datetime import timedelta - -from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -15,88 +10,22 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfEnergy from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util import dt as dt_util +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - CONF_IS_TOU, - DEFAULT_NAME, - DOMAIN, - LOGGER, - MIN_TIME_BETWEEN_UPDATES, - PHOENIX_TIME_ZONE, - SENSOR_NAME, - SENSOR_TYPE, -) +from . import SRPEnergyDataUpdateCoordinator +from .const import DEFAULT_NAME, DOMAIN, SENSOR_NAME async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the SRP Energy Usage sensor.""" - # API object stored here by __init__.py - api = hass.data[DOMAIN][entry.entry_id] - is_time_of_use = entry.data[CONF_IS_TOU] - - async def async_update_data(): - """Fetch data from API endpoint. - - This is the place to pre-process the data to lookup tables - so entities can quickly look up their data. - """ - LOGGER.debug("async_update_data enter") - try: - # Fetch srp_energy data - phx_time_zone = dt_util.get_time_zone(PHOENIX_TIME_ZONE) - end_date = dt_util.now(phx_time_zone) - start_date = end_date - timedelta(days=1) - - async with asyncio.timeout(10): - hourly_usage = await hass.async_add_executor_job( - api.usage, - start_date, - end_date, - is_time_of_use, - ) - - LOGGER.debug( - "async_update_data: Received %s record(s) from %s to %s", - len(hourly_usage) if hourly_usage else "None", - start_date, - end_date, - ) - - previous_daily_usage = 0.0 - for _, _, _, kwh, _ in hourly_usage: - previous_daily_usage += float(kwh) - - LOGGER.debug( - "async_update_data: previous_daily_usage %s", - previous_daily_usage, - ) - - return previous_daily_usage - except TimeoutError as timeout_err: - raise UpdateFailed("Timeout communicating with API") from timeout_err - except (ConnectError, HTTPError, Timeout, ValueError, TypeError) as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err - - coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name="sensor", - update_method=async_update_data, - update_interval=MIN_TIME_BETWEEN_UPDATES, - ) - - # Fetch initial data so we have data when entities subscribe - await coordinator.async_refresh() + coordinator: SRPEnergyDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities([SrpEntity(coordinator)]) -class SrpEntity(SensorEntity): +class SrpEntity(CoordinatorEntity[SRPEnergyDataUpdateCoordinator], SensorEntity): """Implementation of a Srp Energy Usage sensor.""" _attr_attribution = "Powered by SRP Energy" @@ -104,13 +33,11 @@ class SrpEntity(SensorEntity): _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR _attr_device_class = SensorDeviceClass.ENERGY _attr_state_class = SensorStateClass.TOTAL_INCREASING - _attr_should_poll = False - def __init__(self, coordinator) -> None: + def __init__(self, coordinator: SRPEnergyDataUpdateCoordinator) -> None: """Initialize the SrpEntity class.""" + super().__init__(coordinator) self._name = SENSOR_NAME - self.type = SENSOR_TYPE - self.coordinator = coordinator @property def name(self) -> str: @@ -118,24 +45,6 @@ class SrpEntity(SensorEntity): return f"{DEFAULT_NAME} {self._name}" @property - def native_value(self) -> StateType: + def native_value(self) -> float: """Return the state of the device.""" return self.coordinator.data - - @property - def available(self) -> bool: - """Return if entity is available.""" - return self.coordinator.last_update_success - - async def async_added_to_hass(self) -> None: - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self) -> None: - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 3310e9ce9cd..0e3075c6ac8 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -1,4 +1,9 @@ """Tests for the srp_energy sensor platform.""" +from unittest.mock import patch + +import pytest +from requests.models import HTTPError + from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( @@ -10,6 +15,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry + async def test_loading_sensors(hass: HomeAssistant, init_integration) -> None: """Test the srp energy sensors.""" @@ -37,3 +44,26 @@ async def test_srp_entity(hass: HomeAssistant, init_integration) -> None: assert usage_state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert usage_state.attributes.get(ATTR_ICON) == "mdi:flash" + + +@pytest.mark.parametrize("error", [TimeoutError, HTTPError]) +async def test_srp_entity_update_failed( + hass: HomeAssistant, + error: Exception, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the SrpEntity.""" + + with patch( + "homeassistant.components.srp_energy.SrpEnergyClient", autospec=True + ) as srp_energy_mock: + client = srp_energy_mock.return_value + client.validate.return_value = True + client.usage.side_effect = error + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + usage_state = hass.states.get("sensor.home_energy_usage") + assert usage_state is None From 11c5e3534aeae67a3e0c3650030a00175f3f1672 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 25 Aug 2023 10:52:07 +0200 Subject: [PATCH 0861/1151] Add unique id to srp energy entity (#99008) --- homeassistant/components/srp_energy/sensor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 601baaee8ca..a7f0f97b636 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -22,7 +22,7 @@ async def async_setup_entry( """Set up the SRP Energy Usage sensor.""" coordinator: SRPEnergyDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([SrpEntity(coordinator)]) + async_add_entities([SrpEntity(coordinator, entry)]) class SrpEntity(CoordinatorEntity[SRPEnergyDataUpdateCoordinator], SensorEntity): @@ -34,9 +34,12 @@ class SrpEntity(CoordinatorEntity[SRPEnergyDataUpdateCoordinator], SensorEntity) _attr_device_class = SensorDeviceClass.ENERGY _attr_state_class = SensorStateClass.TOTAL_INCREASING - def __init__(self, coordinator: SRPEnergyDataUpdateCoordinator) -> None: + def __init__( + self, coordinator: SRPEnergyDataUpdateCoordinator, config_entry: ConfigEntry + ) -> None: """Initialize the SrpEntity class.""" super().__init__(coordinator) + self._attr_unique_id = f"{config_entry.entry_id}_total_usage" self._name = SENSOR_NAME @property From da9fc495ca9f5b526f53e94de78f069d7342d360 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 25 Aug 2023 11:19:40 +0200 Subject: [PATCH 0862/1151] Improve SRP Energy coordinator (#99010) * Improve SRP Energy coordinator * Use time instead of asyncio --- .../components/srp_energy/coordinator.py | 54 +++++++++---------- tests/components/srp_energy/test_sensor.py | 29 ++++++++-- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/srp_energy/coordinator.py b/homeassistant/components/srp_energy/coordinator.py index 9637f176886..a72ea4d3334 100644 --- a/homeassistant/components/srp_energy/coordinator.py +++ b/homeassistant/components/srp_energy/coordinator.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from datetime import timedelta -from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout from srpenergy.client import SrpEnergyClient from homeassistant.config_entries import ConfigEntry @@ -14,6 +13,8 @@ from homeassistant.util import dt as dt_util from .const import DOMAIN, LOGGER, MIN_TIME_BETWEEN_UPDATES, PHOENIX_TIME_ZONE +TIMEOUT = 10 + class SRPEnergyDataUpdateCoordinator(DataUpdateCoordinator[float]): """A srp_energy Data Update Coordinator.""" @@ -40,38 +41,35 @@ class SRPEnergyDataUpdateCoordinator(DataUpdateCoordinator[float]): so entities can quickly look up their data. """ LOGGER.debug("async_update_data enter") + # Fetch srp_energy data + phx_time_zone = dt_util.get_time_zone(PHOENIX_TIME_ZONE) + end_date = dt_util.now(phx_time_zone) + start_date = end_date - timedelta(days=1) try: - # Fetch srp_energy data - phx_time_zone = dt_util.get_time_zone(PHOENIX_TIME_ZONE) - end_date = dt_util.now(phx_time_zone) - start_date = end_date - timedelta(days=1) - - async with asyncio.timeout(10): + async with asyncio.timeout(TIMEOUT): hourly_usage = await self.hass.async_add_executor_job( self._client.usage, start_date, end_date, self._is_time_of_use, ) - - LOGGER.debug( - "async_update_data: Received %s record(s) from %s to %s", - len(hourly_usage) if hourly_usage else "None", - start_date, - end_date, - ) - - previous_daily_usage = 0.0 - for _, _, _, kwh, _ in hourly_usage: - previous_daily_usage += float(kwh) - - LOGGER.debug( - "async_update_data: previous_daily_usage %s", - previous_daily_usage, - ) - - return previous_daily_usage - except TimeoutError as timeout_err: - raise UpdateFailed("Timeout communicating with API") from timeout_err - except (ConnectError, HTTPError, Timeout, ValueError, TypeError) as err: + except (ValueError, TypeError) as err: raise UpdateFailed(f"Error communicating with API: {err}") from err + + LOGGER.debug( + "async_update_data: Received %s record(s) from %s to %s", + len(hourly_usage) if hourly_usage else "None", + start_date, + end_date, + ) + + previous_daily_usage = 0.0 + for _, _, _, kwh, _ in hourly_usage: + previous_daily_usage += float(kwh) + + LOGGER.debug( + "async_update_data: previous_daily_usage %s", + previous_daily_usage, + ) + + return previous_daily_usage diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 0e3075c6ac8..1ae213e4bf1 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -1,7 +1,7 @@ """Tests for the srp_energy sensor platform.""" +import time from unittest.mock import patch -import pytest from requests.models import HTTPError from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass @@ -46,10 +46,8 @@ async def test_srp_entity(hass: HomeAssistant, init_integration) -> None: assert usage_state.attributes.get(ATTR_ICON) == "mdi:flash" -@pytest.mark.parametrize("error", [TimeoutError, HTTPError]) async def test_srp_entity_update_failed( hass: HomeAssistant, - error: Exception, mock_config_entry: MockConfigEntry, ) -> None: """Test the SrpEntity.""" @@ -59,7 +57,30 @@ async def test_srp_entity_update_failed( ) as srp_energy_mock: client = srp_energy_mock.return_value client.validate.return_value = True - client.usage.side_effect = error + client.usage.side_effect = HTTPError + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + usage_state = hass.states.get("sensor.home_energy_usage") + assert usage_state is None + + +async def test_srp_entity_timeout( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the SrpEntity timing out.""" + + with patch( + "homeassistant.components.srp_energy.SrpEnergyClient", autospec=True + ) as srp_energy_mock, patch( + "homeassistant.components.srp_energy.coordinator.TIMEOUT", 0 + ): + client = srp_energy_mock.return_value + client.validate.return_value = True + client.usage = lambda _, __, ___: time.sleep(1) mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) From bab7d289a62039bfb3e072231d5dee6b6d75fe8a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 25 Aug 2023 11:42:55 +0200 Subject: [PATCH 0863/1151] Reolink fix unknown value in select enums (#99012) --- homeassistant/components/reolink/select.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/select.py b/homeassistant/components/reolink/select.py index e9dc151f33b..84d39b3d8e2 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +import logging from typing import Any from reolink_aio.api import ( @@ -23,6 +24,8 @@ from . import ReolinkData from .const import DOMAIN from .entity import ReolinkChannelCoordinatorEntity +_LOGGER = logging.getLogger(__name__) + @dataclass class ReolinkSelectEntityDescriptionMixin: @@ -135,6 +138,7 @@ class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity): """Initialize Reolink select entity.""" super().__init__(reolink_data, channel) self.entity_description = entity_description + self._log_error = True self._attr_unique_id = ( f"{self._host.unique_id}_{channel}_{entity_description.key}" @@ -151,7 +155,16 @@ class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity): if self.entity_description.value is None: return None - return self.entity_description.value(self._host.api, self._channel) + try: + option = self.entity_description.value(self._host.api, self._channel) + except ValueError: + if self._log_error: + _LOGGER.exception("Reolink '%s' has an unknown value", self.name) + self._log_error = False + return None + + self._log_error = True + return option async def async_select_option(self, option: str) -> None: """Change the selected option.""" From 3ebd7d2fd133954a24b644eeb8e8a7ccd11b31d4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 Aug 2023 12:27:23 +0200 Subject: [PATCH 0864/1151] Fix asyncio DeprecationWarning [3.12] (#98989) * Fix asyncio DeprecationWarning [3.12] * Use AsyncMock * Rewrite ffmpeg tests * Remove test classes * Rename test file --- tests/components/ffmpeg/test_binary_sensor.py | 127 +++++++++++++++++ tests/components/ffmpeg/test_sensor.py | 130 ------------------ .../minecraft_server/test_config_flow.py | 8 +- 3 files changed, 129 insertions(+), 136 deletions(-) create mode 100644 tests/components/ffmpeg/test_binary_sensor.py delete mode 100644 tests/components/ffmpeg/test_sensor.py diff --git a/tests/components/ffmpeg/test_binary_sensor.py b/tests/components/ffmpeg/test_binary_sensor.py new file mode 100644 index 00000000000..6eec115d6f0 --- /dev/null +++ b/tests/components/ffmpeg/test_binary_sensor.py @@ -0,0 +1,127 @@ +"""The tests for Home Assistant ffmpeg binary sensor.""" +from unittest.mock import AsyncMock, patch + +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import assert_setup_component + +CONFIG_NOISE = { + "binary_sensor": {"platform": "ffmpeg_noise", "input": "testinputvideo"} +} +CONFIG_MOTION = { + "binary_sensor": {"platform": "ffmpeg_motion", "input": "testinputvideo"} +} + + +# -- ffmpeg noise binary_sensor -- + + +async def test_noise_setup_component(hass: HomeAssistant) -> None: + """Set up ffmpeg component.""" + with assert_setup_component(1, "binary_sensor"): + await async_setup_component(hass, "binary_sensor", CONFIG_NOISE) + await hass.async_block_till_done() + + assert hass.data["ffmpeg"].binary == "ffmpeg" + assert hass.states.get("binary_sensor.ffmpeg_noise") is not None + + +@patch("haffmpeg.sensor.SensorNoise.open_sensor", side_effect=AsyncMock()) +async def test_noise_setup_component_start(mock_start, hass: HomeAssistant): + """Set up ffmpeg component.""" + with assert_setup_component(1, "binary_sensor"): + await async_setup_component(hass, "binary_sensor", CONFIG_NOISE) + await hass.async_block_till_done() + + assert hass.data["ffmpeg"].binary == "ffmpeg" + assert hass.states.get("binary_sensor.ffmpeg_noise") is not None + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert mock_start.called + + entity = hass.states.get("binary_sensor.ffmpeg_noise") + assert entity.state == "unavailable" + + +@patch("haffmpeg.sensor.SensorNoise") +async def test_noise_setup_component_start_callback(mock_ffmpeg, hass: HomeAssistant): + """Set up ffmpeg component.""" + mock_ffmpeg().open_sensor.side_effect = AsyncMock() + mock_ffmpeg().close = AsyncMock() + with assert_setup_component(1, "binary_sensor"): + await async_setup_component(hass, "binary_sensor", CONFIG_NOISE) + await hass.async_block_till_done() + + assert hass.data["ffmpeg"].binary == "ffmpeg" + assert hass.states.get("binary_sensor.ffmpeg_noise") is not None + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.ffmpeg_noise") + assert entity.state == "off" + + hass.async_add_job(mock_ffmpeg.call_args[0][1], True) + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.ffmpeg_noise") + assert entity.state == "on" + + +# -- ffmpeg motion binary_sensor -- + + +async def test_motion_setup_component(hass: HomeAssistant) -> None: + """Set up ffmpeg component.""" + with assert_setup_component(1, "binary_sensor"): + await async_setup_component(hass, "binary_sensor", CONFIG_MOTION) + await hass.async_block_till_done() + + assert hass.data["ffmpeg"].binary == "ffmpeg" + assert hass.states.get("binary_sensor.ffmpeg_motion") is not None + + +@patch("haffmpeg.sensor.SensorMotion.open_sensor", side_effect=AsyncMock()) +async def test_motion_setup_component_start(mock_start, hass: HomeAssistant): + """Set up ffmpeg component.""" + with assert_setup_component(1, "binary_sensor"): + await async_setup_component(hass, "binary_sensor", CONFIG_MOTION) + await hass.async_block_till_done() + + assert hass.data["ffmpeg"].binary == "ffmpeg" + assert hass.states.get("binary_sensor.ffmpeg_motion") is not None + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert mock_start.called + + entity = hass.states.get("binary_sensor.ffmpeg_motion") + assert entity.state == "unavailable" + + +@patch("haffmpeg.sensor.SensorMotion") +async def test_motion_setup_component_start_callback(mock_ffmpeg, hass: HomeAssistant): + """Set up ffmpeg component.""" + mock_ffmpeg().open_sensor.side_effect = AsyncMock() + mock_ffmpeg().close = AsyncMock() + with assert_setup_component(1, "binary_sensor"): + await async_setup_component(hass, "binary_sensor", CONFIG_MOTION) + await hass.async_block_till_done() + + assert hass.data["ffmpeg"].binary == "ffmpeg" + assert hass.states.get("binary_sensor.ffmpeg_motion") is not None + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.ffmpeg_motion") + assert entity.state == "off" + + hass.async_add_job(mock_ffmpeg.call_args[0][1], True) + await hass.async_block_till_done() + + entity = hass.states.get("binary_sensor.ffmpeg_motion") + assert entity.state == "on" diff --git a/tests/components/ffmpeg/test_sensor.py b/tests/components/ffmpeg/test_sensor.py deleted file mode 100644 index a6c9c1f441a..00000000000 --- a/tests/components/ffmpeg/test_sensor.py +++ /dev/null @@ -1,130 +0,0 @@ -"""The tests for Home Assistant ffmpeg binary sensor.""" -from unittest.mock import patch - -from homeassistant.setup import setup_component - -from tests.common import assert_setup_component, get_test_home_assistant, mock_coro - - -class TestFFmpegNoiseSetup: - """Test class for ffmpeg.""" - - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - self.config = { - "binary_sensor": {"platform": "ffmpeg_noise", "input": "testinputvideo"} - } - - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_component(self): - """Set up ffmpeg component.""" - with assert_setup_component(1, "binary_sensor"): - setup_component(self.hass, "binary_sensor", self.config) - self.hass.block_till_done() - - assert self.hass.data["ffmpeg"].binary == "ffmpeg" - assert self.hass.states.get("binary_sensor.ffmpeg_noise") is not None - - @patch("haffmpeg.sensor.SensorNoise.open_sensor", return_value=mock_coro()) - def test_setup_component_start(self, mock_start): - """Set up ffmpeg component.""" - with assert_setup_component(1, "binary_sensor"): - setup_component(self.hass, "binary_sensor", self.config) - self.hass.block_till_done() - - assert self.hass.data["ffmpeg"].binary == "ffmpeg" - assert self.hass.states.get("binary_sensor.ffmpeg_noise") is not None - - self.hass.start() - assert mock_start.called - - entity = self.hass.states.get("binary_sensor.ffmpeg_noise") - assert entity.state == "unavailable" - - @patch("haffmpeg.sensor.SensorNoise") - def test_setup_component_start_callback(self, mock_ffmpeg): - """Set up ffmpeg component.""" - with assert_setup_component(1, "binary_sensor"): - setup_component(self.hass, "binary_sensor", self.config) - self.hass.block_till_done() - - assert self.hass.data["ffmpeg"].binary == "ffmpeg" - assert self.hass.states.get("binary_sensor.ffmpeg_noise") is not None - - self.hass.start() - - entity = self.hass.states.get("binary_sensor.ffmpeg_noise") - assert entity.state == "off" - - self.hass.add_job(mock_ffmpeg.call_args[0][1], True) - self.hass.block_till_done() - - entity = self.hass.states.get("binary_sensor.ffmpeg_noise") - assert entity.state == "on" - - -class TestFFmpegMotionSetup: - """Test class for ffmpeg.""" - - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - self.config = { - "binary_sensor": {"platform": "ffmpeg_motion", "input": "testinputvideo"} - } - - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_component(self): - """Set up ffmpeg component.""" - with assert_setup_component(1, "binary_sensor"): - setup_component(self.hass, "binary_sensor", self.config) - self.hass.block_till_done() - - assert self.hass.data["ffmpeg"].binary == "ffmpeg" - assert self.hass.states.get("binary_sensor.ffmpeg_motion") is not None - - @patch("haffmpeg.sensor.SensorMotion.open_sensor", return_value=mock_coro()) - def test_setup_component_start(self, mock_start): - """Set up ffmpeg component.""" - with assert_setup_component(1, "binary_sensor"): - setup_component(self.hass, "binary_sensor", self.config) - self.hass.block_till_done() - - assert self.hass.data["ffmpeg"].binary == "ffmpeg" - assert self.hass.states.get("binary_sensor.ffmpeg_motion") is not None - - self.hass.start() - assert mock_start.called - - entity = self.hass.states.get("binary_sensor.ffmpeg_motion") - assert entity.state == "unavailable" - - @patch("haffmpeg.sensor.SensorMotion") - def test_setup_component_start_callback(self, mock_ffmpeg): - """Set up ffmpeg component.""" - with assert_setup_component(1, "binary_sensor"): - setup_component(self.hass, "binary_sensor", self.config) - self.hass.block_till_done() - - assert self.hass.data["ffmpeg"].binary == "ffmpeg" - assert self.hass.states.get("binary_sensor.ffmpeg_motion") is not None - - self.hass.start() - - entity = self.hass.states.get("binary_sensor.ffmpeg_motion") - assert entity.state == "off" - - self.hass.add_job(mock_ffmpeg.call_args[0][1], True) - self.hass.block_till_done() - - entity = self.hass.states.get("binary_sensor.ffmpeg_motion") - assert entity.state == "on" diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index ac5ae7dbc6e..3a201f15bf3 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -1,7 +1,6 @@ """Test the Minecraft Server config flow.""" -import asyncio -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import aiodns from mcstatus.status_response import JavaStatusResponse @@ -72,9 +71,6 @@ USER_INPUT_PORT_TOO_LARGE = { CONF_HOST: "mc.dummyserver.com:65536", } -SRV_RECORDS = asyncio.Future() -SRV_RECORDS.set_result([QueryMock()]) - async def test_show_config_form(hass: HomeAssistant) -> None: """Test if initial configuration form is shown.""" @@ -173,7 +169,7 @@ async def test_connection_succeeded_with_srv_record(hass: HomeAssistant) -> None """Test config entry in case of a successful connection with a SRV record.""" with patch( "aiodns.DNSResolver.query", - return_value=SRV_RECORDS, + side_effect=AsyncMock(return_value=[QueryMock()]), ), patch( "mcstatus.server.JavaServer.async_status", return_value=JavaStatusResponse( From 3f2d2a85b7fb238ad2f2de5f2d349901e9c157ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 25 Aug 2023 12:53:26 +0200 Subject: [PATCH 0865/1151] Update AEMET-OpenData to v0.4.0 (#99015) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update AEMET-OpenData to v0.4.0 Signed-off-by: Álvaro Fernández Rojas * Trigger Github CI --------- Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/__init__.py | 5 +++-- homeassistant/components/aemet/config_flow.py | 8 +++----- homeassistant/components/aemet/manifest.json | 2 +- .../components/aemet/weather_update_coordinator.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 68e7bb6c5e0..772dcd0276b 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,7 +1,7 @@ """The AEMET OpenData component.""" import logging -from aemet_opendata.interface import AEMET +from aemet_opendata.interface import AEMET, ConnectionOptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME @@ -28,7 +28,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: longitude = entry.data[CONF_LONGITUDE] station_updates = entry.options.get(CONF_STATION_UPDATES, True) - aemet = AEMET(aiohttp_client.async_get_clientsession(hass), api_key) + options = ConnectionOptions(api_key, station_updates) + aemet = AEMET(aiohttp_client.async_get_clientsession(hass), options) weather_coordinator = WeatherUpdateCoordinator( hass, aemet, latitude, longitude, station_updates ) diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py index 129f513025a..4f3531b19e7 100644 --- a/homeassistant/components/aemet/config_flow.py +++ b/homeassistant/components/aemet/config_flow.py @@ -1,8 +1,8 @@ """Config flow for AEMET OpenData.""" from __future__ import annotations -from aemet_opendata import AEMET from aemet_opendata.exceptions import AuthError +from aemet_opendata.interface import AEMET, ConnectionOptions import voluptuous as vol from homeassistant import config_entries @@ -40,10 +40,8 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(f"{latitude}-{longitude}") self._abort_if_unique_id_configured() - aemet = AEMET( - aiohttp_client.async_get_clientsession(self.hass), - user_input[CONF_API_KEY], - ) + options = ConnectionOptions(user_input[CONF_API_KEY], False) + aemet = AEMET(aiohttp_client.async_get_clientsession(self.hass), options) try: await aemet.get_conventional_observation_stations(False) except AuthError: diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index a460d9e16bc..4d1b25908ef 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.3.0"] + "requirements": ["AEMET-OpenData==0.4.0"] } diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index d44160116f2..66a1a2eb891 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -11,6 +11,7 @@ from aemet_opendata.const import ( AEMET_ATTR_DAY, AEMET_ATTR_DIRECTION, AEMET_ATTR_ELABORATED, + AEMET_ATTR_FEEL_TEMPERATURE, AEMET_ATTR_FORECAST, AEMET_ATTR_HUMIDITY, AEMET_ATTR_ID, @@ -32,7 +33,6 @@ from aemet_opendata.const import ( AEMET_ATTR_STATION_TEMPERATURE, AEMET_ATTR_STORM_PROBABILITY, AEMET_ATTR_TEMPERATURE, - AEMET_ATTR_TEMPERATURE_FEELING, AEMET_ATTR_WIND, AEMET_ATTR_WIND_GUST, ATTR_DATA, @@ -563,7 +563,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): @staticmethod def _get_temperature_feeling(day_data, hour): """Get temperature from weather data.""" - val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE_FEELING], hour) + val = get_forecast_hour_value(day_data[AEMET_ATTR_FEEL_TEMPERATURE], hour) return format_int(val) def _get_town_id(self): diff --git a/requirements_all.txt b/requirements_all.txt index 2aaba30788b..b915a38368f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.3.0 +AEMET-OpenData==0.4.0 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 752e320aa93..a292cfb78ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.3.0 +AEMET-OpenData==0.4.0 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 From d79e8b7a029d268ac9dcd0808fcc2e42b6f892b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Aug 2023 06:35:31 -0500 Subject: [PATCH 0866/1151] Avoid fetching state and charging state multiple time for hkc icon (#98995) --- .../components/homekit_controller/sensor.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index d7230de0832..5803b8aa839 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -466,21 +466,23 @@ class HomeKitBatterySensor(HomeKitSensor): @property def icon(self) -> str: """Return the sensor icon.""" - if not self.available or self.state is None: + native_value = self.native_value + if not self.available or native_value is None: return "mdi:battery-unknown" # This is similar to the logic in helpers.icon, but we have delegated the # decision about what mdi:battery-alert is to the device. icon = "mdi:battery" - if self.is_charging and self.state > 10: - percentage = int(round(self.state / 20 - 0.01)) * 20 + is_charging = self.is_charging + if is_charging and native_value > 10: + percentage = int(round(native_value / 20 - 0.01)) * 20 icon += f"-charging-{percentage}" - elif self.is_charging: + elif is_charging: icon += "-outline" elif self.is_low_battery: icon += "-alert" - elif self.state < 95: - percentage = max(int(round(self.state / 10 - 0.01)) * 10, 10) + elif native_value < 95: + percentage = max(int(round(native_value / 10 - 0.01)) * 10, 10) icon += f"-{percentage}" return icon From 4fb00e448c45bee93a04f321feb45db7bd9a6b2d Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 25 Aug 2023 13:40:08 +0200 Subject: [PATCH 0867/1151] Use snapshot assertion for rdw diagnostics test (#99027) --- .../rdw/snapshots/test_diagnostics.ambr | 30 ++++++++++++++++ tests/components/rdw/test_diagnostics.py | 35 ++++--------------- 2 files changed, 36 insertions(+), 29 deletions(-) create mode 100644 tests/components/rdw/snapshots/test_diagnostics.ambr diff --git a/tests/components/rdw/snapshots/test_diagnostics.ambr b/tests/components/rdw/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..6da03b67245 --- /dev/null +++ b/tests/components/rdw/snapshots/test_diagnostics.ambr @@ -0,0 +1,30 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'apk_expiration': '2022-01-04', + 'ascription_date': '2021-11-04', + 'ascription_possible': True, + 'brand': 'Skoda', + 'energy_label': 'A', + 'engine_capacity': 999, + 'exported': False, + 'first_admission': '2013-01-04', + 'interior': 'hatchback', + 'last_odometer_registration_year': 2021, + 'liability_insured': False, + 'license_plate': '11ZKZ3', + 'list_price': 10697, + 'mass_driveable': 940, + 'mass_empty': 840, + 'model': 'Citigo', + 'number_of_cylinders': 3, + 'number_of_doors': 0, + 'number_of_seats': 4, + 'number_of_wheelchair_seats': 0, + 'number_of_wheels': 4, + 'odometer_judgement': 'Logisch', + 'pending_recall': False, + 'taxi': None, + 'vehicle_type': 'Personenauto', + }) +# --- diff --git a/tests/components/rdw/test_diagnostics.py b/tests/components/rdw/test_diagnostics.py index 0e21779ff37..28b7714fcce 100644 --- a/tests/components/rdw/test_diagnostics.py +++ b/tests/components/rdw/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the RDW integration.""" +from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant @@ -11,34 +12,10 @@ async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - assert await get_diagnostics_for_config_entry( - hass, hass_client, init_integration - ) == { - "apk_expiration": "2022-01-04", - "ascription_date": "2021-11-04", - "ascription_possible": True, - "brand": "Skoda", - "energy_label": "A", - "engine_capacity": 999, - "exported": False, - "interior": "hatchback", - "last_odometer_registration_year": 2021, - "liability_insured": False, - "license_plate": "11ZKZ3", - "list_price": 10697, - "first_admission": "2013-01-04", - "mass_empty": 840, - "mass_driveable": 940, - "model": "Citigo", - "number_of_cylinders": 3, - "number_of_doors": 0, - "number_of_seats": 4, - "number_of_wheelchair_seats": 0, - "number_of_wheels": 4, - "odometer_judgement": "Logisch", - "pending_recall": False, - "taxi": None, - "vehicle_type": "Personenauto", - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) From 8161810159a560c114576d8533a67a38f80f1a45 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 14:03:51 +0200 Subject: [PATCH 0868/1151] Use freezegun in opensky tests (#99039) --- tests/components/opensky/test_sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/components/opensky/test_sensor.py b/tests/components/opensky/test_sensor.py index eb17721929c..b637a0d0356 100644 --- a/tests/components/opensky/test_sensor.py +++ b/tests/components/opensky/test_sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import json from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from python_opensky import StatesResponse from syrupy import SnapshotAssertion @@ -16,7 +17,6 @@ from homeassistant.const import CONF_PLATFORM, CONF_RADIUS, Platform from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util from .conftest import ComponentSetup @@ -78,6 +78,7 @@ async def test_sensor_altitude( async def test_sensor_updating( hass: HomeAssistant, config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, setup_integration: ComponentSetup, snapshot: SnapshotAssertion, ): @@ -97,8 +98,8 @@ async def test_sensor_updating( hass.bus.async_listen(EVENT_OPENSKY_EXIT, event_listener) async def skip_time_and_check_events() -> None: - future = dt_util.utcnow() + timedelta(minutes=15) - async_fire_time_changed(hass, future) + freezer.tick(timedelta(minutes=15)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert events == snapshot From 64306ec053a603284b70a54341e33e98edde00a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 15:58:52 +0200 Subject: [PATCH 0869/1151] Use freezegun in solaredge tests (#99043) --- .../components/solaredge/test_coordinator.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/components/solaredge/test_coordinator.py b/tests/components/solaredge/test_coordinator.py index 7b746a2ae05..550040a9b25 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -1,6 +1,8 @@ """Tests for the SolarEdge coordinator services.""" from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.solaredge.const import ( CONF_SITE_ID, DEFAULT_NAME, @@ -10,7 +12,6 @@ from homeassistant.components.solaredge.const import ( from homeassistant.components.solaredge.sensor import SENSOR_TYPES from homeassistant.const import CONF_API_KEY, CONF_NAME, STATE_UNKNOWN from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -20,7 +21,7 @@ API_KEY = "a1b2c3d4e5f6g7h8" @patch("homeassistant.components.solaredge.Solaredge") async def test_solaredgeoverviewdataservice_energy_values_validity( - mock_solaredge, hass: HomeAssistant + mock_solaredge, hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: """Test overview energy data validity.""" mock_config_entry = MockConfigEntry( @@ -46,7 +47,8 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( } } mock_solaredge().get_overview.return_value = mock_overview_data - async_fire_time_changed(hass, dt_util.utcnow() + OVERVIEW_UPDATE_DELAY) + freezer.tick(OVERVIEW_UPDATE_DELAY) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.solaredge_lifetime_energy") assert state @@ -55,7 +57,8 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # Invalid energy values, lifeTimeData energy is lower than last year, month or day. mock_overview_data["overview"]["lifeTimeData"]["energy"] = 0 mock_solaredge().get_overview.return_value = mock_overview_data - async_fire_time_changed(hass, dt_util.utcnow() + OVERVIEW_UPDATE_DELAY) + freezer.tick(OVERVIEW_UPDATE_DELAY) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.solaredge_lifetime_energy") @@ -65,7 +68,8 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # New valid energy values update mock_overview_data["overview"]["lifeTimeData"]["energy"] = 100001 mock_solaredge().get_overview.return_value = mock_overview_data - async_fire_time_changed(hass, dt_util.utcnow() + OVERVIEW_UPDATE_DELAY) + freezer.tick(OVERVIEW_UPDATE_DELAY) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.solaredge_lifetime_energy") @@ -75,7 +79,8 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # Invalid energy values, lastYearData energy is lower than last month or day. mock_overview_data["overview"]["lastYearData"]["energy"] = 0 mock_solaredge().get_overview.return_value = mock_overview_data - async_fire_time_changed(hass, dt_util.utcnow() + OVERVIEW_UPDATE_DELAY) + freezer.tick(OVERVIEW_UPDATE_DELAY) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.solaredge_energy_this_year") @@ -92,7 +97,8 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_overview_data["overview"]["lastMonthData"]["energy"] = 0.0 mock_overview_data["overview"]["lastDayData"]["energy"] = 0.0 mock_solaredge().get_overview.return_value = mock_overview_data - async_fire_time_changed(hass, dt_util.utcnow() + OVERVIEW_UPDATE_DELAY) + freezer.tick(OVERVIEW_UPDATE_DELAY) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.solaredge_lifetime_energy") From 65d555b138a2349fe1eb97f93e6186ce63a5bc7f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 15:59:08 +0200 Subject: [PATCH 0870/1151] Use freezegun in qnap_qsw tests (#99041) --- tests/components/qnap_qsw/test_coordinator.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/components/qnap_qsw/test_coordinator.py b/tests/components/qnap_qsw/test_coordinator.py index 61d1fa04200..b0163f7b7ec 100644 --- a/tests/components/qnap_qsw/test_coordinator.py +++ b/tests/components/qnap_qsw/test_coordinator.py @@ -3,6 +3,7 @@ from unittest.mock import patch from aioqsw.exceptions import APIError, QswError +from freezegun.api import FrozenDateTimeFactory from homeassistant.components.qnap_qsw.const import DOMAIN from homeassistant.components.qnap_qsw.coordinator import ( @@ -11,7 +12,6 @@ from homeassistant.components.qnap_qsw.coordinator import ( ) from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.util.dt import utcnow from .util import ( CONFIG, @@ -31,7 +31,9 @@ from .util import ( from tests.common import MockConfigEntry, async_fire_time_changed -async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: +async def test_coordinator_client_connector_error( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test ClientConnectorError on coordinator update.""" entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) @@ -99,7 +101,8 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_users_login.reset_mock() mock_system_sensor.side_effect = QswError - async_fire_time_changed(hass, utcnow() + DATA_SCAN_INTERVAL) + freezer.tick(DATA_SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_system_sensor.assert_called_once() @@ -110,14 +113,16 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: assert state.state == STATE_UNAVAILABLE mock_firmware_update_check.side_effect = APIError - async_fire_time_changed(hass, utcnow() + FW_SCAN_INTERVAL) + freezer.tick(FW_SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_firmware_update_check.assert_called_once() mock_firmware_update_check.reset_mock() mock_firmware_update_check.side_effect = QswError - async_fire_time_changed(hass, utcnow() + FW_SCAN_INTERVAL) + freezer.tick(FW_SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_firmware_update_check.assert_called_once() From 346674a1a8508284cef4a764c80628ebb9bf6853 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 15:59:30 +0200 Subject: [PATCH 0871/1151] Use freezegun in wled tests (#99048) --- tests/components/wled/conftest.py | 10 +++++++++- tests/components/wled/test_button.py | 2 +- tests/components/wled/test_light.py | 16 +++++++++++----- tests/components/wled/test_number.py | 9 ++++++--- tests/components/wled/test_select.py | 17 ++++++++++++----- tests/components/wled/test_switch.py | 9 ++++++--- 6 files changed, 45 insertions(+), 18 deletions(-) diff --git a/tests/components/wled/conftest.py b/tests/components/wled/conftest.py index 824801fe44b..bbbdd4e1cbe 100644 --- a/tests/components/wled/conftest.py +++ b/tests/components/wled/conftest.py @@ -2,6 +2,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from wled import Device as WLEDDevice @@ -67,7 +68,10 @@ def mock_wled(device_fixture: str) -> Generator[MagicMock, None, None]: @pytest.fixture async def init_integration( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_wled: MagicMock + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, + mock_wled: MagicMock, ) -> MockConfigEntry: """Set up the WLED integration for testing.""" mock_config_entry.add_to_hass(hass) @@ -75,4 +79,8 @@ async def init_integration( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() + # Let some time pass so coordinators can be reliably triggered by bumping + # time by SCAN_INTERVAL + freezer.tick(1) + return mock_config_entry diff --git a/tests/components/wled/test_button.py b/tests/components/wled/test_button.py index c1f3165e5bc..92a13baf43c 100644 --- a/tests/components/wled/test_button.py +++ b/tests/components/wled/test_button.py @@ -13,7 +13,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er pytestmark = [ pytest.mark.usefixtures("init_integration"), - pytest.mark.freeze_time("2021-11-04 17:37:00+01:00"), + pytest.mark.freeze_time("2021-11-04 17:36:59+01:00"), ] diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 16aba21392b..678b4a44459 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -2,6 +2,7 @@ import json from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory import pytest from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError @@ -27,7 +28,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture @@ -177,6 +177,7 @@ async def test_master_change_state( @pytest.mark.parametrize("device_fixture", ["rgb_single_segment"]) async def test_dynamically_handle_segments( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, mock_wled: MagicMock, ) -> None: """Test if a new/deleted segment is dynamically added/removed.""" @@ -190,7 +191,8 @@ async def test_dynamically_handle_segments( json.loads(load_fixture("wled/rgb.json")) ) - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (master := hass.states.get("light.wled_rgb_light_master")) @@ -202,7 +204,8 @@ async def test_dynamically_handle_segments( # Test adding if segment shows up again, including the master entity mock_wled.update.return_value = return_value - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (master := hass.states.get("light.wled_rgb_light_master")) @@ -216,6 +219,7 @@ async def test_dynamically_handle_segments( @pytest.mark.parametrize("device_fixture", ["rgb_single_segment"]) async def test_single_segment_behavior( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, mock_wled: MagicMock, ) -> None: """Test the behavior of the integration with a single segment.""" @@ -228,7 +232,8 @@ async def test_single_segment_behavior( # Test segment brightness takes master into account device.state.brightness = 100 device.state.segments[0].brightness = 255 - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (state := hass.states.get("light.wled_rgb_light")) @@ -236,7 +241,8 @@ async def test_single_segment_behavior( # Test segment is off when master is off device.state.on = False - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("light.wled_rgb_light") assert state diff --git a/tests/components/wled/test_number.py b/tests/components/wled/test_number.py index 59f2fb12332..e91ec4f2e66 100644 --- a/tests/components/wled/test_number.py +++ b/tests/components/wled/test_number.py @@ -2,6 +2,7 @@ import json from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError @@ -16,7 +17,6 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, load_fixture @@ -113,6 +113,7 @@ async def test_numbers( ) async def test_speed_dynamically_handle_segments( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, mock_wled: MagicMock, entity_id_segment0: str, entity_id_segment1: str, @@ -130,7 +131,8 @@ async def test_speed_dynamically_handle_segments( json.loads(load_fixture("wled/rgb.json")) ) - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (segment0 := hass.states.get(entity_id_segment0)) @@ -140,7 +142,8 @@ async def test_speed_dynamically_handle_segments( # Test remove segment again... mock_wled.update.return_value = return_value - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (segment0 := hass.states.get(entity_id_segment0)) diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py index caf1fa24868..219ec945021 100644 --- a/tests/components/wled/test_select.py +++ b/tests/components/wled/test_select.py @@ -2,6 +2,7 @@ import json from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError @@ -17,7 +18,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, load_fixture @@ -125,6 +125,7 @@ async def test_color_palette_state( @pytest.mark.parametrize("device_fixture", ["rgb_single_segment"]) async def test_color_palette_dynamically_handle_segments( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, mock_wled: MagicMock, ) -> None: """Test if a new/deleted segment is dynamically added/removed.""" @@ -137,7 +138,8 @@ async def test_color_palette_dynamically_handle_segments( json.loads(load_fixture("wled/rgb.json")) ) - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (segment0 := hass.states.get("select.wled_rgb_light_color_palette")) @@ -149,7 +151,8 @@ async def test_color_palette_dynamically_handle_segments( # Test adding if segment shows up again, including the master entity mock_wled.update.return_value = return_value - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (segment0 := hass.states.get("select.wled_rgb_light_color_palette")) @@ -175,13 +178,15 @@ async def test_playlist_unavailable_without_playlists(hass: HomeAssistant) -> No @pytest.mark.parametrize("device_fixture", ["rgbw"]) async def test_old_style_preset_active( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, mock_wled: MagicMock, ) -> None: """Test unknown preset returned (when old style/unknown) preset is active.""" # Set device preset state to a random number mock_wled.update.return_value.state.preset = 99 - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (state := hass.states.get("select.wled_rgbw_light_preset")) @@ -191,13 +196,15 @@ async def test_old_style_preset_active( @pytest.mark.parametrize("device_fixture", ["rgbw"]) async def test_old_style_playlist_active( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, mock_wled: MagicMock, ) -> None: """Test when old style playlist cycle is active.""" # Set device playlist to 0, which meant "cycle" previously. mock_wled.update.return_value.state.playlist = 0 - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (state := hass.states.get("select.wled_rgbw_light_playlist")) diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index 70804e07eb9..40b7783fc04 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -2,6 +2,7 @@ import json from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError @@ -19,7 +20,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, load_fixture @@ -132,6 +132,7 @@ async def test_switch_state( @pytest.mark.parametrize("device_fixture", ["rgb_single_segment"]) async def test_switch_dynamically_handle_segments( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, mock_wled: MagicMock, ) -> None: """Test if a new/deleted segment is dynamically added/removed.""" @@ -146,7 +147,8 @@ async def test_switch_dynamically_handle_segments( json.loads(load_fixture("wled/rgb.json")) ) - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (segment0 := hass.states.get("switch.wled_rgb_light_reverse")) @@ -156,7 +158,8 @@ async def test_switch_dynamically_handle_segments( # Test remove segment again... mock_wled.update.return_value = return_value - async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert (segment0 := hass.states.get("switch.wled_rgb_light_reverse")) From 943db9e0d5c75ba20004f7cd2b299da84ee9aabd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 15:59:52 +0200 Subject: [PATCH 0872/1151] Use freezegun in devolo_home_network tests (#99029) --- .../devolo_home_network/test_binary_sensor.py | 9 +++-- .../test_device_tracker.py | 15 +++++--- .../devolo_home_network/test_sensor.py | 9 +++-- .../devolo_home_network/test_switch.py | 34 ++++++++++--------- .../devolo_home_network/test_update.py | 17 +++++++--- 5 files changed, 52 insertions(+), 32 deletions(-) diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py index 7a6395c20f1..17d95fc51a3 100644 --- a/tests/components/devolo_home_network/test_binary_sensor.py +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock from devolo_plc_api.exceptions.device import DeviceUnavailable +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -13,7 +14,6 @@ from homeassistant.components.devolo_home_network.const import ( from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from . import configure_integration from .const import PLCNET_ATTACHED @@ -40,6 +40,7 @@ async def test_update_attached_to_router( hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, ) -> None: """Test state change of a attached_to_router binary sensor device.""" @@ -57,7 +58,8 @@ async def test_update_attached_to_router( mock_device.plcnet.async_get_network_overview = AsyncMock( side_effect=DeviceUnavailable ) - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + freezer.tick(LONG_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(state_key) @@ -68,7 +70,8 @@ async def test_update_attached_to_router( mock_device.plcnet.async_get_network_overview = AsyncMock( return_value=PLCNET_ATTACHED ) - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + freezer.tick(LONG_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(state_key) diff --git a/tests/components/devolo_home_network/test_device_tracker.py b/tests/components/devolo_home_network/test_device_tracker.py index 324f8b44041..8f58b1154de 100644 --- a/tests/components/devolo_home_network/test_device_tracker.py +++ b/tests/components/devolo_home_network/test_device_tracker.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock from devolo_plc_api.exceptions.device import DeviceUnavailable +from freezegun.api import FrozenDateTimeFactory from syrupy.assertion import SnapshotAssertion from homeassistant.components.device_tracker import DOMAIN as PLATFORM @@ -12,7 +13,6 @@ from homeassistant.components.devolo_home_network.const import ( from homeassistant.const import STATE_NOT_HOME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from . import configure_integration from .const import CONNECTED_STATIONS, DISCOVERY_INFO, NO_CONNECTED_STATIONS @@ -28,6 +28,7 @@ async def test_device_tracker( hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, ) -> None: """Test device tracker states.""" @@ -37,13 +38,15 @@ async def test_device_tracker( entry = configure_integration(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + freezer.tick(LONG_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() # Enable entity entity_registry.async_update_entity(state_key, disabled_by=None) await hass.async_block_till_done() - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + freezer.tick(LONG_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get(state_key) == snapshot @@ -52,7 +55,8 @@ async def test_device_tracker( mock_device.device.async_get_wifi_connected_station = AsyncMock( return_value=NO_CONNECTED_STATIONS ) - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + freezer.tick(LONG_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(state_key) @@ -63,7 +67,8 @@ async def test_device_tracker( mock_device.device.async_get_wifi_connected_station = AsyncMock( side_effect=DeviceUnavailable ) - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + freezer.tick(LONG_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(state_key) diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index dc7842e5fbd..230457f5617 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import AsyncMock from devolo_plc_api.exceptions.device import DeviceUnavailable +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -14,7 +15,6 @@ from homeassistant.components.sensor import DOMAIN from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from . import configure_integration from .mock import MockDevice @@ -62,6 +62,7 @@ async def test_sensor( hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, name: str, get_method: str, @@ -80,7 +81,8 @@ async def test_sensor( # Emulate device failure setattr(mock_device.device, get_method, AsyncMock(side_effect=DeviceUnavailable)) setattr(mock_device.plcnet, get_method, AsyncMock(side_effect=DeviceUnavailable)) - async_fire_time_changed(hass, dt_util.utcnow() + interval) + freezer.tick(interval) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(state_key) @@ -89,7 +91,8 @@ async def test_sensor( # Emulate state change mock_device.reset() - async_fire_time_changed(hass, dt_util.utcnow() + interval) + freezer.tick(interval) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(state_key) diff --git a/tests/components/devolo_home_network/test_switch.py b/tests/components/devolo_home_network/test_switch.py index 00c06a6acc1..c77a77e87de 100644 --- a/tests/components/devolo_home_network/test_switch.py +++ b/tests/components/devolo_home_network/test_switch.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch from devolo_plc_api.device_api import WifiGuestAccessGet from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -24,7 +25,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.update_coordinator import REQUEST_REFRESH_DEFAULT_COOLDOWN -from homeassistant.util import dt as dt_util from . import configure_integration from .mock import MockDevice @@ -75,6 +75,7 @@ async def test_update_enable_guest_wifi( hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, ) -> None: """Test state change of a enable_guest_wifi switch device.""" @@ -92,7 +93,8 @@ async def test_update_enable_guest_wifi( mock_device.device.async_get_wifi_guest_access.return_value = WifiGuestAccessGet( enabled=True ) - async_fire_time_changed(hass, dt_util.utcnow() + SHORT_UPDATE_INTERVAL) + freezer.tick(SHORT_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(state_key) @@ -116,9 +118,8 @@ async def test_update_enable_guest_wifi( assert state.state == STATE_OFF turn_off.assert_called_once_with(False) - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) - ) + freezer.tick(REQUEST_REFRESH_DEFAULT_COOLDOWN) + async_fire_time_changed(hass) await hass.async_block_till_done() # Switch on @@ -138,9 +139,8 @@ async def test_update_enable_guest_wifi( assert state.state == STATE_ON turn_on.assert_called_once_with(True) - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) - ) + freezer.tick(REQUEST_REFRESH_DEFAULT_COOLDOWN) + async_fire_time_changed(hass) await hass.async_block_till_done() # Device unavailable @@ -164,6 +164,7 @@ async def test_update_enable_leds( hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, ) -> None: """Test state change of a enable_leds switch device.""" @@ -179,7 +180,8 @@ async def test_update_enable_leds( # Emulate state change mock_device.device.async_get_led_setting.return_value = True - async_fire_time_changed(hass, dt_util.utcnow() + SHORT_UPDATE_INTERVAL) + freezer.tick(SHORT_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(state_key) @@ -201,9 +203,8 @@ async def test_update_enable_leds( assert state.state == STATE_OFF turn_off.assert_called_once_with(False) - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) - ) + freezer.tick(REQUEST_REFRESH_DEFAULT_COOLDOWN) + async_fire_time_changed(hass) await hass.async_block_till_done() # Switch on @@ -221,9 +222,8 @@ async def test_update_enable_leds( assert state.state == STATE_ON turn_on.assert_called_once_with(True) - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) - ) + freezer.tick(REQUEST_REFRESH_DEFAULT_COOLDOWN) + async_fire_time_changed(hass) await hass.async_block_till_done() # Device unavailable @@ -253,6 +253,7 @@ async def test_update_enable_leds( async def test_device_failure( hass: HomeAssistant, mock_device: MockDevice, + freezer: FrozenDateTimeFactory, name: str, get_method: str, update_interval: timedelta, @@ -270,7 +271,8 @@ async def test_device_failure( api = getattr(mock_device.device, get_method) api.side_effect = DeviceUnavailable - async_fire_time_changed(hass, dt_util.utcnow() + update_interval) + freezer.tick(update_interval) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(state_key) diff --git a/tests/components/devolo_home_network/test_update.py b/tests/components/devolo_home_network/test_update.py index f5ef0bc9381..97d313d9273 100644 --- a/tests/components/devolo_home_network/test_update.py +++ b/tests/components/devolo_home_network/test_update.py @@ -1,6 +1,7 @@ """Tests for the devolo Home Network update.""" from devolo_plc_api.device_api import UPDATE_NOT_AVAILABLE, UpdateFirmwareCheck from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.devolo_home_network.const import ( @@ -18,7 +19,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory -from homeassistant.util import dt as dt_util from . import configure_integration from .const import FIRMWARE_UPDATE_AVAILABLE @@ -41,7 +41,10 @@ async def test_update_setup(hass: HomeAssistant) -> None: async def test_update_firmware( - hass: HomeAssistant, mock_device: MockDevice, entity_registry: er.EntityRegistry + hass: HomeAssistant, + mock_device: MockDevice, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test updating a device.""" entry = configure_integration(hass) @@ -75,7 +78,8 @@ async def test_update_firmware( mock_device.device.async_check_firmware_available.return_value = ( UpdateFirmwareCheck(result=UPDATE_NOT_AVAILABLE) ) - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + freezer.tick(LONG_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(state_key) @@ -86,7 +90,9 @@ async def test_update_firmware( async def test_device_failure_check( - hass: HomeAssistant, mock_device: MockDevice + hass: HomeAssistant, + mock_device: MockDevice, + freezer: FrozenDateTimeFactory, ) -> None: """Test device failure during check.""" entry = configure_integration(hass) @@ -100,7 +106,8 @@ async def test_device_failure_check( assert state is not None mock_device.device.async_check_firmware_available.side_effect = DeviceUnavailable - async_fire_time_changed(hass, dt_util.utcnow() + LONG_UPDATE_INTERVAL) + freezer.tick(LONG_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(state_key) From e0af9de87730d42e815b89614c6074db4ff24734 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:00:11 +0200 Subject: [PATCH 0873/1151] Use freezegun in motioneye tests (#99038) --- tests/components/motioneye/test_sensor.py | 22 ++++++++++------- tests/components/motioneye/test_switch.py | 29 ++++++++++++++--------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/tests/components/motioneye/test_sensor.py b/tests/components/motioneye/test_sensor.py index 5494e69d9e9..659738ef2c5 100644 --- a/tests/components/motioneye/test_sensor.py +++ b/tests/components/motioneye/test_sensor.py @@ -3,6 +3,7 @@ import copy from datetime import timedelta from unittest.mock import AsyncMock, patch +from freezegun.api import FrozenDateTimeFactory from motioneye_client.const import KEY_ACTIONS from homeassistant.components.motioneye import get_motioneye_device_identifier @@ -14,7 +15,6 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util from . import ( TEST_CAMERA, @@ -28,7 +28,9 @@ from . import ( from tests.common import async_fire_time_changed -async def test_sensor_actions(hass: HomeAssistant) -> None: +async def test_sensor_actions( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test the actions sensor.""" register_test_entity( hass, @@ -51,7 +53,8 @@ async def test_sensor_actions(hass: HomeAssistant) -> None: # When the next refresh is called return the updated values. client.async_get_cameras = AsyncMock(return_value={"cameras": [updated_camera]}) - async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + freezer.tick(DEFAULT_SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID) @@ -60,7 +63,8 @@ async def test_sensor_actions(hass: HomeAssistant) -> None: assert entity_state.attributes.get(KEY_ACTIONS) == ["one"] del updated_camera[KEY_ACTIONS] - async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + freezer.tick(DEFAULT_SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID) @@ -99,7 +103,9 @@ async def test_sensor_device_info(hass: HomeAssistant) -> None: assert TEST_SENSOR_ACTION_ENTITY_ID in entities_from_device -async def test_sensor_actions_can_be_enabled(hass: HomeAssistant) -> None: +async def test_sensor_actions_can_be_enabled( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Verify the action sensor can be enabled.""" client = create_mock_motioneye_client() await setup_mock_motioneye_config_entry(hass, client=client) @@ -122,10 +128,8 @@ async def test_sensor_actions_can_be_enabled(hass: HomeAssistant) -> None: assert not updated_entry.disabled await hass.async_block_till_done() - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), - ) + freezer.tick(timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1)) + async_fire_time_changed(hass) await hass.async_block_till_done() entity_state = hass.states.get(TEST_SENSOR_ACTION_ENTITY_ID) diff --git a/tests/components/motioneye/test_switch.py b/tests/components/motioneye/test_switch.py index f0fe4f1faba..cc193f5fb60 100644 --- a/tests/components/motioneye/test_switch.py +++ b/tests/components/motioneye/test_switch.py @@ -3,6 +3,7 @@ import copy from datetime import timedelta from unittest.mock import AsyncMock, call, patch +from freezegun.api import FrozenDateTimeFactory from motioneye_client.const import ( KEY_MOTION_DETECTION, KEY_MOVIES, @@ -19,7 +20,6 @@ from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util from . import ( TEST_CAMERA, @@ -34,7 +34,9 @@ from . import ( from tests.common import async_fire_time_changed -async def test_switch_turn_on_off(hass: HomeAssistant) -> None: +async def test_switch_turn_on_off( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test turning the switch on and off.""" client = create_mock_motioneye_client() await setup_mock_motioneye_config_entry(hass, client=client) @@ -60,7 +62,8 @@ async def test_switch_turn_on_off(hass: HomeAssistant) -> None: blocking=True, ) - async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + freezer.tick(DEFAULT_SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() # Verify correct parameters are passed to the library. @@ -85,7 +88,8 @@ async def test_switch_turn_on_off(hass: HomeAssistant) -> None: # Verify correct parameters are passed to the library. assert client.async_set_camera.call_args == call(TEST_CAMERA_ID, TEST_CAMERA) - async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + freezer.tick(DEFAULT_SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() # Verify the switch turns on. @@ -94,7 +98,9 @@ async def test_switch_turn_on_off(hass: HomeAssistant) -> None: assert entity_state.state == "on" -async def test_switch_state_update_from_coordinator(hass: HomeAssistant) -> None: +async def test_switch_state_update_from_coordinator( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test that coordinator data impacts state.""" client = create_mock_motioneye_client() await setup_mock_motioneye_config_entry(hass, client=client) @@ -108,7 +114,8 @@ async def test_switch_state_update_from_coordinator(hass: HomeAssistant) -> None updated_cameras["cameras"][0][KEY_MOTION_DETECTION] = False client.async_get_cameras = AsyncMock(return_value=updated_cameras) - async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) + freezer.tick(DEFAULT_SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() # Verify the switch turns off. @@ -144,7 +151,9 @@ async def test_switch_has_correct_entities(hass: HomeAssistant) -> None: assert not entity_state -async def test_disabled_switches_can_be_enabled(hass: HomeAssistant) -> None: +async def test_disabled_switches_can_be_enabled( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Verify disabled switches can be enabled.""" client = create_mock_motioneye_client() await setup_mock_motioneye_config_entry(hass, client=client) @@ -174,10 +183,8 @@ async def test_disabled_switches_can_be_enabled(hass: HomeAssistant) -> None: assert not updated_entry.disabled await hass.async_block_till_done() - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), - ) + freezer.tick(timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1)) + async_fire_time_changed(hass) await hass.async_block_till_done() entity_state = hass.states.get(entity_id) From ee073e9e3e8f9d151da6ebfee3b9d3e3f18ac45e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:00:30 +0200 Subject: [PATCH 0874/1151] Use freezegun in lacrosse_view tests (#99036) --- tests/components/lacrosse_view/test_init.py | 26 ++++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py index 557f8c4234a..2b3f5927bd2 100644 --- a/tests/components/lacrosse_view/test_init.py +++ b/tests/components/lacrosse_view/test_init.py @@ -1,8 +1,8 @@ """Test the LaCrosse View initialization.""" -from datetime import datetime, timedelta +from datetime import timedelta from unittest.mock import patch -from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory from lacrosse_view import HTTPError, LoginError from homeassistant.components.lacrosse_view.const import DOMAIN @@ -74,7 +74,7 @@ async def test_http_error(hass: HomeAssistant) -> None: assert entries[0].state == ConfigEntryState.SETUP_RETRY -async def test_new_token(hass: HomeAssistant) -> None: +async def test_new_token(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: """Test new token.""" config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) @@ -92,19 +92,20 @@ async def test_new_token(hass: HomeAssistant) -> None: assert len(entries) == 1 assert entries[0].state == ConfigEntryState.LOADED - one_hour_after = datetime.now() + timedelta(hours=1) - with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR], - ), freeze_time(one_hour_after): - async_fire_time_changed(hass, one_hour_after) + ): + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() login.assert_called_once() -async def test_failed_token(hass: HomeAssistant) -> None: +async def test_failed_token( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test if a reauth flow occurs when token refresh fails.""" config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) @@ -122,12 +123,9 @@ async def test_failed_token(hass: HomeAssistant) -> None: assert len(entries) == 1 assert entries[0].state == ConfigEntryState.LOADED - one_hour_after = datetime.now() + timedelta(hours=1) - - with patch( - "lacrosse_view.LaCrosse.login", side_effect=LoginError("Test") - ), freeze_time(one_hour_after): - async_fire_time_changed(hass, one_hour_after) + with patch("lacrosse_view.LaCrosse.login", side_effect=LoginError("Test")): + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() entries = hass.config_entries.async_entries(DOMAIN) From f18a277cac431b1878daa10cd2592df22eec3c4c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:00:50 +0200 Subject: [PATCH 0875/1151] Use freezegun in ws66i tests (#99049) --- tests/components/ws66i/test_media_player.py | 47 ++++++++++++++------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/tests/components/ws66i/test_media_player.py b/tests/components/ws66i/test_media_player.py index 2cdab824040..c4a10197a34 100644 --- a/tests/components/ws66i/test_media_player.py +++ b/tests/components/ws66i/test_media_player.py @@ -2,6 +2,8 @@ from collections import defaultdict from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, @@ -31,7 +33,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_fire_time_changed @@ -174,7 +175,7 @@ async def _call_media_player_service(hass, name, data): ) -async def test_update(hass: HomeAssistant) -> None: +async def test_update(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: """Test updating values from ws66i.""" ws66i = MockWs66i() _ = await _setup_ws66i_with_options(hass, ws66i) @@ -191,7 +192,8 @@ async def test_update(hass: HomeAssistant) -> None: ws66i.set_volume(11, MAX_VOL) with patch.object(MockWs66i, "open") as method_call: - async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + freezer.tick(POLL_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert not method_call.called @@ -203,7 +205,9 @@ async def test_update(hass: HomeAssistant) -> None: assert state.attributes[ATTR_INPUT_SOURCE] == "three" -async def test_failed_update(hass: HomeAssistant) -> None: +async def test_failed_update( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test updating failure from ws66i.""" ws66i = MockWs66i() _ = await _setup_ws66i_with_options(hass, ws66i) @@ -219,23 +223,27 @@ async def test_failed_update(hass: HomeAssistant) -> None: ws66i.set_source(11, 3) ws66i.set_volume(11, MAX_VOL) - async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + freezer.tick(POLL_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() # Failed update, close called with patch.object(MockWs66i, "zone_status", return_value=None): - async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + freezer.tick(POLL_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.is_state(ZONE_1_ID, STATE_UNAVAILABLE) # A connection re-attempt fails with patch.object(MockWs66i, "zone_status", return_value=None): - async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + freezer.tick(POLL_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() # A connection re-attempt succeeds - async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + freezer.tick(POLL_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() # confirm entity is back on @@ -295,14 +303,17 @@ async def test_select_source(hass: HomeAssistant) -> None: assert ws66i.zones[11].source == 3 -async def test_source_select(hass: HomeAssistant) -> None: +async def test_source_select( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test source selection simulated from keypad.""" ws66i = MockWs66i() _ = await _setup_ws66i_with_options(hass, ws66i) ws66i.set_source(11, 5) - async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + freezer.tick(POLL_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ZONE_1_ID) @@ -341,7 +352,9 @@ async def test_mute_volume(hass: HomeAssistant) -> None: assert ws66i.zones[11].mute -async def test_volume_up_down(hass: HomeAssistant) -> None: +async def test_volume_up_down( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test increasing volume by one.""" ws66i = MockWs66i() _ = await _setup_ws66i(hass, ws66i) @@ -354,26 +367,30 @@ async def test_volume_up_down(hass: HomeAssistant) -> None: await _call_media_player_service( hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} ) - async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + freezer.tick(POLL_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() # should not go below zero assert ws66i.zones[11].volume == 0 await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) - async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + freezer.tick(POLL_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert ws66i.zones[11].volume == 1 await _call_media_player_service( hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} ) - async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + freezer.tick(POLL_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert ws66i.zones[11].volume == MAX_VOL await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) - async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + freezer.tick(POLL_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() # should not go above 38 (MAX_VOL) assert ws66i.zones[11].volume == MAX_VOL From f99743bedb44513fdf332edbbca752af035b8947 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:01:28 +0200 Subject: [PATCH 0876/1151] Use freezegun in tomorrowio tests (#99044) --- tests/components/tomorrowio/test_init.py | 82 +++++++++++------------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/tests/components/tomorrowio/test_init.py b/tests/components/tomorrowio/test_init.py index 5fd954859b1..fe17bbe79b7 100644 --- a/tests/components/tomorrowio/test_init.py +++ b/tests/components/tomorrowio/test_init.py @@ -1,6 +1,7 @@ """Tests for Tomorrow.io init.""" from datetime import timedelta -from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory from homeassistant.components.tomorrowio.config_flow import ( _get_config_schema, @@ -11,7 +12,6 @@ from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from .const import MIN_CONFIG @@ -42,10 +42,9 @@ async def test_load_and_unload(hass: HomeAssistant) -> None: async def test_update_intervals( - hass: HomeAssistant, tomorrowio_config_entry_update + hass: HomeAssistant, freezer: FrozenDateTimeFactory, tomorrowio_config_entry_update ) -> None: """Test coordinator update intervals.""" - now = dt_util.utcnow() data = _get_config_schema(hass, SOURCE_USER)(MIN_CONFIG) data[CONF_NAME] = "test" config_entry = MockConfigEntry( @@ -56,63 +55,58 @@ async def test_update_intervals( version=1, ) config_entry.add_to_hass(hass) - with patch("homeassistant.helpers.update_coordinator.utcnow", return_value=now): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(tomorrowio_config_entry_update.call_args_list) == 1 + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 1 tomorrowio_config_entry_update.reset_mock() # Before the update interval, no updates yet - future = now + timedelta(minutes=30) - with patch("homeassistant.helpers.update_coordinator.utcnow", return_value=future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - assert len(tomorrowio_config_entry_update.call_args_list) == 0 + freezer.tick(timedelta(minutes=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 0 tomorrowio_config_entry_update.reset_mock() # On the update interval, we get a new update - future = now + timedelta(minutes=32) - with patch("homeassistant.helpers.update_coordinator.utcnow", return_value=future): - async_fire_time_changed(hass, now + timedelta(minutes=32)) - await hass.async_block_till_done() - assert len(tomorrowio_config_entry_update.call_args_list) == 1 + freezer.tick(timedelta(minutes=2)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 1 - tomorrowio_config_entry_update.reset_mock() + tomorrowio_config_entry_update.reset_mock() - # Adding a second config entry should cause the update interval to double - config_entry_2 = MockConfigEntry( - domain=DOMAIN, - data=data, - options={CONF_TIMESTEP: 1}, - unique_id=f"{_get_unique_id(hass, data)}_1", - version=1, - ) - config_entry_2.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry_2.entry_id) - await hass.async_block_till_done() - assert config_entry.data[CONF_API_KEY] == config_entry_2.data[CONF_API_KEY] - # We should get an immediate call once the new config entry is setup for a - # partial update - assert len(tomorrowio_config_entry_update.call_args_list) == 1 + # Adding a second config entry should cause the update interval to double + config_entry_2 = MockConfigEntry( + domain=DOMAIN, + data=data, + options={CONF_TIMESTEP: 1}, + unique_id=f"{_get_unique_id(hass, data)}_1", + version=1, + ) + config_entry_2.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry_2.entry_id) + await hass.async_block_till_done() + assert config_entry.data[CONF_API_KEY] == config_entry_2.data[CONF_API_KEY] + # We should get an immediate call once the new config entry is setup for a + # partial update + assert len(tomorrowio_config_entry_update.call_args_list) == 1 tomorrowio_config_entry_update.reset_mock() # We should get no new calls on our old interval - future = now + timedelta(minutes=64) - with patch("homeassistant.helpers.update_coordinator.utcnow", return_value=future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - assert len(tomorrowio_config_entry_update.call_args_list) == 0 + freezer.tick(timedelta(minutes=32)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 0 tomorrowio_config_entry_update.reset_mock() # We should get two calls on our new interval, one for each entry - future = now + timedelta(minutes=96) - with patch("homeassistant.helpers.update_coordinator.utcnow", return_value=future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - assert len(tomorrowio_config_entry_update.call_args_list) == 2 + freezer.tick(timedelta(minutes=32)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 2 tomorrowio_config_entry_update.reset_mock() From 81aa35a9ce38a887c028393a3e30b55f4fe48f86 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:01:48 +0200 Subject: [PATCH 0877/1151] Use freezegun in version tests (#99047) --- tests/components/version/common.py | 9 +++++---- tests/components/version/test_sensor.py | 11 ++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/components/version/common.py b/tests/components/version/common.py index 3e3ae6c3970..c4759604a44 100644 --- a/tests/components/version/common.py +++ b/tests/components/version/common.py @@ -4,6 +4,8 @@ from __future__ import annotations from typing import Any, Final from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory + from homeassistant import config_entries from homeassistant.components.version.const import ( DEFAULT_CONFIGURATION, @@ -14,7 +16,6 @@ from homeassistant.components.version.const import ( ) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -37,6 +38,7 @@ TEST_DEFAULT_IMPORT_CONFIG: Final = { async def mock_get_version_update( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, version: str = MOCK_VERSION, data: dict[str, Any] = MOCK_VERSION_DATA, side_effect: Exception = None, @@ -47,9 +49,8 @@ async def mock_get_version_update( return_value=(version, data), side_effect=side_effect, ): - async_fire_time_changed( - hass, dt_util.utcnow() + UPDATE_COORDINATOR_UPDATE_INTERVAL - ) + freezer.tick(UPDATE_COORDINATOR_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() diff --git a/tests/components/version/test_sensor.py b/tests/components/version/test_sensor.py index 1c7f9040b22..0a3e89494f1 100644 --- a/tests/components/version/test_sensor.py +++ b/tests/components/version/test_sensor.py @@ -1,6 +1,7 @@ """The test for the version sensor platform.""" from __future__ import annotations +from freezegun.api import FrozenDateTimeFactory from pyhaversion.exceptions import HaVersionException import pytest @@ -19,15 +20,19 @@ async def test_version_sensor(hass: HomeAssistant) -> None: assert "channel" not in state.attributes -async def test_update(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: +async def test_update( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: """Test updates.""" await setup_version_integration(hass) assert hass.states.get("sensor.local_installation").state == MOCK_VERSION - await mock_get_version_update(hass, version="1970.1.1") + await mock_get_version_update(hass, freezer, version="1970.1.1") assert hass.states.get("sensor.local_installation").state == "1970.1.1" assert "Error fetching version data" not in caplog.text - await mock_get_version_update(hass, side_effect=HaVersionException) + await mock_get_version_update(hass, freezer, side_effect=HaVersionException) assert hass.states.get("sensor.local_installation").state == "unavailable" assert "Error fetching version data" in caplog.text From 676f59fdedaedc257e1a3931993549d78c17aba6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:02:07 +0200 Subject: [PATCH 0878/1151] Use freezegun in trafikverket_ferry tests (#99045) --- .../trafikverket_ferry/test_coordinator.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/components/trafikverket_ferry/test_coordinator.py b/tests/components/trafikverket_ferry/test_coordinator.py index 591486474d3..c0fbe7537cc 100644 --- a/tests/components/trafikverket_ferry/test_coordinator.py +++ b/tests/components/trafikverket_ferry/test_coordinator.py @@ -24,6 +24,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_coordinator( hass: HomeAssistant, entity_registry_enabled_by_default: None, + freezer: FrozenDateTimeFactory, monkeypatch: pytest.MonkeyPatch, get_ferries: list[FerryStop], ) -> None: @@ -59,7 +60,8 @@ async def test_coordinator( datetime(dt_util.now().year + 2, 5, 1, 12, 0, tzinfo=dt_util.UTC), ) - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=6)) + freezer.tick(timedelta(minutes=6)) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_data.assert_called_once() state1 = hass.states.get("sensor.harbor1_departure_from") @@ -71,7 +73,8 @@ async def test_coordinator( mock_data.reset_mock() mock_data.side_effect = NoFerryFound() - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=6)) + freezer.tick(timedelta(minutes=6)) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_data.assert_called_once() state1 = hass.states.get("sensor.harbor1_departure_from") @@ -80,7 +83,8 @@ async def test_coordinator( mock_data.return_value = get_ferries mock_data.side_effect = None - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=6)) + freezer.tick(timedelta(minutes=6)) + async_fire_time_changed(hass) await hass.async_block_till_done() # mock_data.assert_called_once() state1 = hass.states.get("sensor.harbor1_departure_from") @@ -88,7 +92,8 @@ async def test_coordinator( mock_data.reset_mock() mock_data.side_effect = InvalidAuthentication() - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=6)) + freezer.tick(timedelta(minutes=6)) + async_fire_time_changed(hass) await hass.async_block_till_done() mock_data.assert_called_once() state1 = hass.states.get("sensor.harbor1_departure_from") From 75743ed947602cbb0d80b56ab3033de2e3be47de Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:02:25 +0200 Subject: [PATCH 0879/1151] Use freezegun in here_travel_time tests (#99032) --- .../here_travel_time/test_sensor.py | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 91439a11d95..28228788cf5 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta from unittest.mock import MagicMock, patch +from freezegun.api import FrozenDateTimeFactory from here_routing import ( HERERoutingError, HERERoutingTooManyRequestsError, @@ -64,7 +65,6 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow from .conftest import RESPONSE, TRANSIT_RESPONSE from .const import ( @@ -662,7 +662,9 @@ async def test_transit_errors( async def test_routing_rate_limit( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, ) -> None: """Test that rate limiting is applied when encountering HTTP 429.""" with patch( @@ -689,9 +691,8 @@ async def test_routing_rate_limit( "Rate limit for this service has been reached" ), ): - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=DEFAULT_SCAN_INTERVAL + 1) - ) + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL + 1)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("sensor.test_distance").state == "unavailable" @@ -701,18 +702,17 @@ async def test_routing_rate_limit( "here_routing.HERERoutingApi.route", return_value=RESPONSE, ): - async_fire_time_changed( - hass, - utcnow() - + timedelta(seconds=DEFAULT_SCAN_INTERVAL * BACKOFF_MULTIPLIER + 1), - ) + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL * BACKOFF_MULTIPLIER + 1)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("sensor.test_distance").state == "13.682" assert "Resetting update interval to" in caplog.text async def test_transit_rate_limit( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, ) -> None: """Test that rate limiting is applied when encountering HTTP 429.""" with patch( @@ -747,9 +747,8 @@ async def test_transit_rate_limit( "Rate limit for this service has been reached" ), ): - async_fire_time_changed( - hass, utcnow() + timedelta(seconds=DEFAULT_SCAN_INTERVAL + 1) - ) + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL + 1)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("sensor.test_distance").state == "unavailable" @@ -759,11 +758,8 @@ async def test_transit_rate_limit( "here_transit.HERETransitApi.route", return_value=TRANSIT_RESPONSE, ): - async_fire_time_changed( - hass, - utcnow() - + timedelta(seconds=DEFAULT_SCAN_INTERVAL * BACKOFF_MULTIPLIER + 1), - ) + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL * BACKOFF_MULTIPLIER + 1)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("sensor.test_distance").state == "1.883" assert "Resetting update interval to" in caplog.text From cb8842b1fb56872ad6a62ee75e181de4755e7fea Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:02:47 +0200 Subject: [PATCH 0880/1151] Use freezegun in landisgyr_heat_meter tests (#99037) --- tests/components/landisgyr_heat_meter/test_sensor.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py index e28ebe695b3..5ed2a397ccd 100644 --- a/tests/components/landisgyr_heat_meter/test_sensor.py +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -2,6 +2,7 @@ import datetime from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest import serial from syrupy import SnapshotAssertion @@ -122,7 +123,9 @@ async def test_create_sensors( @patch(API_HEAT_METER_SERVICE) -async def test_exception_on_polling(mock_heat_meter, hass: HomeAssistant) -> None: +async def test_exception_on_polling( + mock_heat_meter, hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test sensor.""" entry_data = { "device": "/dev/USB0", @@ -148,7 +151,8 @@ async def test_exception_on_polling(mock_heat_meter, hass: HomeAssistant) -> Non # Now 'disable' the connection and wait for polling and see if it fails mock_heat_meter().read.side_effect = serial.serialutil.SerialException - async_fire_time_changed(hass, dt_util.utcnow() + POLLING_INTERVAL) + freezer.tick(POLLING_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.heat_meter_heat_usage_gj") assert state.state == STATE_UNAVAILABLE @@ -159,7 +163,8 @@ async def test_exception_on_polling(mock_heat_meter, hass: HomeAssistant) -> Non mock_heat_meter().read.return_value = mock_heat_meter_response mock_heat_meter().read.side_effect = None - async_fire_time_changed(hass, dt_util.utcnow() + POLLING_INTERVAL) + freezer.tick(POLLING_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.heat_meter_heat_usage_gj") assert state From e6728f2f19534c8692d1f5c00aa6bffc885fa16a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:03:12 +0200 Subject: [PATCH 0881/1151] Use freezegun in kraken tests (#99035) --- tests/components/kraken/test_sensor.py | 47 ++++++++++++-------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/tests/components/kraken/test_sensor.py b/tests/components/kraken/test_sensor.py index 1435e0d6b04..8efac3017e0 100644 --- a/tests/components/kraken/test_sensor.py +++ b/tests/components/kraken/test_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from pykrakenapi.pykrakenapi import KrakenAPIError from homeassistant.components.kraken.const import ( @@ -13,7 +14,6 @@ from homeassistant.components.kraken.const import ( from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util from .const import ( MISSING_PAIR_TICKER_INFORMATION_RESPONSE, @@ -25,11 +25,9 @@ from .const import ( from tests.common import MockConfigEntry, async_fire_time_changed -async def test_sensor(hass: HomeAssistant) -> None: +async def test_sensor(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: """Test that sensor has a value.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + with patch( "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", return_value=TRADEABLE_ASSET_PAIR_RESPONSE, ), patch( @@ -230,11 +228,11 @@ async def test_sensor(hass: HomeAssistant) -> None: assert xbt_usd_opening_price_today.state == "0.0003513" -async def test_sensors_available_after_restart(hass: HomeAssistant) -> None: +async def test_sensors_available_after_restart( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test that all sensors are added again after a restart.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + with patch( "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", return_value=TRADEABLE_ASSET_PAIR_RESPONSE, ), patch( @@ -271,11 +269,11 @@ async def test_sensors_available_after_restart(hass: HomeAssistant) -> None: assert sensor.state == "0.0003494" -async def test_sensors_added_after_config_update(hass: HomeAssistant) -> None: +async def test_sensors_added_after_config_update( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test that sensors are added when another tracked asset pair is added.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + with patch( "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", return_value=TRADEABLE_ASSET_PAIR_RESPONSE, ), patch( @@ -309,19 +307,18 @@ async def test_sensors_added_after_config_update(hass: HomeAssistant) -> None: CONF_TRACKED_ASSET_PAIRS: [DEFAULT_TRACKED_ASSET_PAIR, "ADA/XBT"], }, ) - async_fire_time_changed( - hass, utcnow + timedelta(seconds=DEFAULT_SCAN_INTERVAL * 2) - ) + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL * 2)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("sensor.ada_xbt_ask") -async def test_missing_pair_marks_sensor_unavailable(hass: HomeAssistant) -> None: +async def test_missing_pair_marks_sensor_unavailable( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test that a missing tradable asset pair marks the sensor unavailable.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + with patch( "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", return_value=TRADEABLE_ASSET_PAIR_RESPONSE, ) as tradeable_asset_pairs_mock, patch( @@ -353,16 +350,14 @@ async def test_missing_pair_marks_sensor_unavailable(hass: HomeAssistant) -> Non ticket_information_mock.side_effect = KrakenAPIError( "EQuery:Unknown asset pair" ) - async_fire_time_changed( - hass, utcnow + timedelta(seconds=DEFAULT_SCAN_INTERVAL * 2) - ) + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL * 2)) + async_fire_time_changed(hass) await hass.async_block_till_done() ticket_information_mock.side_effect = None ticket_information_mock.return_value = MISSING_PAIR_TICKER_INFORMATION_RESPONSE - async_fire_time_changed( - hass, utcnow + timedelta(seconds=DEFAULT_SCAN_INTERVAL * 2) - ) + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL * 2)) + async_fire_time_changed(hass) await hass.async_block_till_done() sensor = hass.states.get("sensor.xbt_usd_ask") From 1f9c1802332d5396e512083f4378894dbf34188f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:03:29 +0200 Subject: [PATCH 0882/1151] Use freezegun in iotawatt tests (#99034) --- tests/components/iotawatt/test_sensor.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/components/iotawatt/test_sensor.py b/tests/components/iotawatt/test_sensor.py index d9017955c75..5646115f59a 100644 --- a/tests/components/iotawatt/test_sensor.py +++ b/tests/components/iotawatt/test_sensor.py @@ -1,6 +1,8 @@ """Test setting up sensors.""" from datetime import timedelta +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -15,14 +17,15 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow from . import INPUT_SENSOR, OUTPUT_SENSOR from tests.common import async_fire_time_changed -async def test_sensor_type_input(hass: HomeAssistant, mock_iotawatt) -> None: +async def test_sensor_type_input( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_iotawatt +) -> None: """Test input sensors work.""" assert await async_setup_component(hass, "iotawatt", {}) await hass.async_block_till_done() @@ -31,7 +34,8 @@ async def test_sensor_type_input(hass: HomeAssistant, mock_iotawatt) -> None: # Discover this sensor during a regular update. mock_iotawatt.getSensors.return_value["sensors"]["my_sensor_key"] = INPUT_SENSOR - async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == 1 @@ -47,13 +51,16 @@ async def test_sensor_type_input(hass: HomeAssistant, mock_iotawatt) -> None: assert state.attributes["type"] == "Input" mock_iotawatt.getSensors.return_value["sensors"].pop("my_sensor_key") - async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("sensor.my_sensor") is None -async def test_sensor_type_output(hass: HomeAssistant, mock_iotawatt) -> None: +async def test_sensor_type_output( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_iotawatt +) -> None: """Tests the sensor type of Output.""" mock_iotawatt.getSensors.return_value["sensors"][ "my_watthour_sensor_key" @@ -73,7 +80,8 @@ async def test_sensor_type_output(hass: HomeAssistant, mock_iotawatt) -> None: assert state.attributes["type"] == "Output" mock_iotawatt.getSensors.return_value["sensors"].pop("my_watthour_sensor_key") - async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("sensor.my_watthour_sensor") is None From 3b07181d8760c2bff0244b0d49221802051c10ac Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:03:51 +0200 Subject: [PATCH 0883/1151] Use freezegun in fully_kiosk tests (#99031) --- tests/components/fully_kiosk/test_binary_sensor.py | 9 ++++++--- tests/components/fully_kiosk/test_sensor.py | 8 ++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/components/fully_kiosk/test_binary_sensor.py b/tests/components/fully_kiosk/test_binary_sensor.py index 5b88854b020..db37139b0ba 100644 --- a/tests/components/fully_kiosk/test_binary_sensor.py +++ b/tests/components/fully_kiosk/test_binary_sensor.py @@ -1,6 +1,7 @@ """Test the Fully Kiosk Browser binary sensors.""" from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory from fullykiosk import FullyKioskError from homeassistant.components.binary_sensor import BinarySensorDeviceClass @@ -15,13 +16,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed async def test_binary_sensors( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, mock_fully_kiosk: MagicMock, init_integration: MockConfigEntry, ) -> None: @@ -76,7 +77,8 @@ async def test_binary_sensors( # Test unknown/missing data mock_fully_kiosk.getDeviceInfo.return_value = {} - async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("binary_sensor.amazon_fire_plugged_in") @@ -85,7 +87,8 @@ async def test_binary_sensors( # Test failed update mock_fully_kiosk.getDeviceInfo.side_effect = FullyKioskError("error", "status") - async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("binary_sensor.amazon_fire_plugged_in") diff --git a/tests/components/fully_kiosk/test_sensor.py b/tests/components/fully_kiosk/test_sensor.py index cc8b30640b5..05fd002a205 100644 --- a/tests/components/fully_kiosk/test_sensor.py +++ b/tests/components/fully_kiosk/test_sensor.py @@ -1,6 +1,7 @@ """Test the Fully Kiosk Browser sensors.""" from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory from fullykiosk import FullyKioskError from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL @@ -25,6 +26,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_sensors_sensors( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, mock_fully_kiosk: MagicMock, init_integration: MockConfigEntry, ) -> None: @@ -141,7 +143,8 @@ async def test_sensors_sensors( # Test unknown/missing data mock_fully_kiosk.getDeviceInfo.return_value = {} - async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") @@ -150,7 +153,8 @@ async def test_sensors_sensors( # Test failed update mock_fully_kiosk.getDeviceInfo.side_effect = FullyKioskError("error", "status") - async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") From dd39e8fe64de533b3329fc76843ee220a3ea1dcb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:04:28 +0200 Subject: [PATCH 0884/1151] Use freezegun in hue tests (#99033) --- tests/components/hue/test_sensor_v1.py | 31 ++++++++++++-------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/tests/components/hue/test_sensor_v1.py b/tests/components/hue/test_sensor_v1.py index d5ac8406f24..1edaf18774f 100644 --- a/tests/components/hue/test_sensor_v1.py +++ b/tests/components/hue/test_sensor_v1.py @@ -3,6 +3,7 @@ import asyncio from unittest.mock import Mock import aiohue +from freezegun.api import FrozenDateTimeFactory from homeassistant.components import hue from homeassistant.components.hue.const import ATTR_HUE_EVENT @@ -10,7 +11,6 @@ from homeassistant.components.hue.v1 import sensor_base from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get -from homeassistant.util import dt as dt_util from .conftest import create_mock_bridge, setup_platform @@ -448,7 +448,9 @@ async def test_update_unauthorized(hass: HomeAssistant, mock_bridge_v1) -> None: assert len(mock_bridge_v1.handle_unauthorized_error.mock_calls) == 1 -async def test_hue_events(hass: HomeAssistant, mock_bridge_v1, device_reg) -> None: +async def test_hue_events( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_bridge_v1, device_reg +) -> None: """Test that hue remotes fire events when pressed.""" mock_bridge_v1.mock_sensor_responses.append(SENSOR_RESPONSE) @@ -475,9 +477,8 @@ async def test_hue_events(hass: HomeAssistant, mock_bridge_v1, device_reg) -> No mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - async_fire_time_changed( - hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL - ) + freezer.tick(sensor_base.SensorManager.SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(mock_bridge_v1.mock_requests) == 2 @@ -504,9 +505,8 @@ async def test_hue_events(hass: HomeAssistant, mock_bridge_v1, device_reg) -> No mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - async_fire_time_changed( - hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL - ) + freezer.tick(sensor_base.SensorManager.SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(mock_bridge_v1.mock_requests) == 3 @@ -530,9 +530,8 @@ async def test_hue_events(hass: HomeAssistant, mock_bridge_v1, device_reg) -> No mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - async_fire_time_changed( - hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL - ) + freezer.tick(sensor_base.SensorManager.SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(mock_bridge_v1.mock_requests) == 4 @@ -575,9 +574,8 @@ async def test_hue_events(hass: HomeAssistant, mock_bridge_v1, device_reg) -> No mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - async_fire_time_changed( - hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL - ) + freezer.tick(sensor_base.SensorManager.SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(mock_bridge_v1.mock_requests) == 5 @@ -589,9 +587,8 @@ async def test_hue_events(hass: HomeAssistant, mock_bridge_v1, device_reg) -> No mock_bridge_v1.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - async_fire_time_changed( - hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL - ) + freezer.tick(sensor_base.SensorManager.SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() hue_aurora_device = device_reg.async_get_device( From 5617a738c0847e37a5c81bc5ace4cd5da3ab4f08 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:04:51 +0200 Subject: [PATCH 0885/1151] Use freezegun in airly tests (#99028) --- tests/components/airly/test_init.py | 80 ++++++++++++++--------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index f360beb8c51..9b69607e6aa 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -1,7 +1,7 @@ """Test init of Airly integration.""" from typing import Any -from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM @@ -11,7 +11,6 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.util.dt import utcnow from . import API_POINT_URL, init_integration @@ -99,7 +98,9 @@ async def test_config_with_turned_off_station( async def test_update_interval( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, ) -> None: """Test correct update interval when the number of configured instances changes.""" REMAINING_REQUESTS = 15 @@ -135,50 +136,47 @@ async def test_update_interval( assert entry.state is ConfigEntryState.LOADED update_interval = set_update_interval(instances, REMAINING_REQUESTS) - future = utcnow() + update_interval - with patch("homeassistant.util.dt.utcnow") as mock_utcnow: - mock_utcnow.return_value = future - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + freezer.tick(update_interval) + async_fire_time_changed(hass) + await hass.async_block_till_done() - # call_count should increase by one because we have one instance configured - assert aioclient_mock.call_count == 2 + # call_count should increase by one because we have one instance configured + assert aioclient_mock.call_count == 2 - # Now we add the second Airly instance - entry = MockConfigEntry( - domain=DOMAIN, - title="Work", - unique_id="66.66-111.11", - data={ - "api_key": "foo", - "latitude": 66.66, - "longitude": 111.11, - "name": "Work", - }, - ) + # Now we add the second Airly instance + entry = MockConfigEntry( + domain=DOMAIN, + title="Work", + unique_id="66.66-111.11", + data={ + "api_key": "foo", + "latitude": 66.66, + "longitude": 111.11, + "name": "Work", + }, + ) - aioclient_mock.get( - "https://airapi.airly.eu/v2/measurements/point?lat=66.660000&lng=111.110000", - text=load_fixture("valid_station.json", "airly"), - headers=HEADERS, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - instances = 2 + aioclient_mock.get( + "https://airapi.airly.eu/v2/measurements/point?lat=66.660000&lng=111.110000", + text=load_fixture("valid_station.json", "airly"), + headers=HEADERS, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + instances = 2 - assert aioclient_mock.call_count == 3 - assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - assert entry.state is ConfigEntryState.LOADED + assert aioclient_mock.call_count == 3 + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + assert entry.state is ConfigEntryState.LOADED - update_interval = set_update_interval(instances, REMAINING_REQUESTS) - future = utcnow() + update_interval - mock_utcnow.return_value = future - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + update_interval = set_update_interval(instances, REMAINING_REQUESTS) + freezer.tick(update_interval) + async_fire_time_changed(hass) + await hass.async_block_till_done() - # call_count should increase by two because we have two instances configured - assert aioclient_mock.call_count == 5 + # call_count should increase by two because we have two instances configured + assert aioclient_mock.call_count == 5 async def test_unload_entry( From 0d3663c52a7e59001bc8f5f59c3f09179cd437c2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:05:16 +0200 Subject: [PATCH 0886/1151] Use freezegun in fronius tests (#99030) --- tests/components/fronius/test_coordinator.py | 38 +++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/tests/components/fronius/test_coordinator.py b/tests/components/fronius/test_coordinator.py index a0e420c5b52..d4f42fadb06 100644 --- a/tests/components/fronius/test_coordinator.py +++ b/tests/components/fronius/test_coordinator.py @@ -1,13 +1,13 @@ """Test the Fronius update coordinators.""" from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from pyfronius import BadStatusError, FroniusError from homeassistant.components.fronius.coordinator import ( FroniusInverterUpdateCoordinator, ) from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from . import mock_responses, setup_fronius_integration @@ -16,7 +16,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_adaptive_update_interval( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, ) -> None: """Test coordinators changing their update interval when inverter not available.""" with patch("pyfronius.Fronius.current_inverter_data") as mock_inverter_data: @@ -25,9 +27,8 @@ async def test_adaptive_update_interval( mock_inverter_data.assert_called_once() mock_inverter_data.reset_mock() - async_fire_time_changed( - hass, dt_util.utcnow() + FroniusInverterUpdateCoordinator.default_interval - ) + freezer.tick(FroniusInverterUpdateCoordinator.default_interval) + async_fire_time_changed(hass, None) await hass.async_block_till_done() mock_inverter_data.assert_called_once() mock_inverter_data.reset_mock() @@ -35,33 +36,28 @@ async def test_adaptive_update_interval( mock_inverter_data.side_effect = FroniusError() # first 3 bad requests at default interval - 4th has different interval for _ in range(3): - async_fire_time_changed( - hass, - dt_util.utcnow() + FroniusInverterUpdateCoordinator.default_interval, - ) + freezer.tick(FroniusInverterUpdateCoordinator.default_interval) + async_fire_time_changed(hass, None) await hass.async_block_till_done() assert mock_inverter_data.call_count == 3 mock_inverter_data.reset_mock() - async_fire_time_changed( - hass, dt_util.utcnow() + FroniusInverterUpdateCoordinator.error_interval - ) + freezer.tick(FroniusInverterUpdateCoordinator.error_interval) + async_fire_time_changed(hass, None) await hass.async_block_till_done() assert mock_inverter_data.call_count == 1 mock_inverter_data.reset_mock() mock_inverter_data.side_effect = None # next successful request resets to default interval - async_fire_time_changed( - hass, dt_util.utcnow() + FroniusInverterUpdateCoordinator.error_interval - ) + freezer.tick(FroniusInverterUpdateCoordinator.error_interval) + async_fire_time_changed(hass, None) await hass.async_block_till_done() mock_inverter_data.assert_called_once() mock_inverter_data.reset_mock() - async_fire_time_changed( - hass, dt_util.utcnow() + FroniusInverterUpdateCoordinator.default_interval - ) + freezer.tick(FroniusInverterUpdateCoordinator.default_interval) + async_fire_time_changed(hass, None) await hass.async_block_till_done() mock_inverter_data.assert_called_once() mock_inverter_data.reset_mock() @@ -70,10 +66,8 @@ async def test_adaptive_update_interval( mock_inverter_data.side_effect = BadStatusError("mock_endpoint", 8) # first 3 requests at default interval - 4th has different interval for _ in range(3): - async_fire_time_changed( - hass, - dt_util.utcnow() + FroniusInverterUpdateCoordinator.default_interval, - ) + freezer.tick(FroniusInverterUpdateCoordinator.default_interval) + async_fire_time_changed(hass, None) await hass.async_block_till_done() # BadStatusError does 3 silent retries for inverter endpoint * 3 request intervals = 9 assert mock_inverter_data.call_count == 9 From c827af5826fae1e1f44dbaa515b3ee23bfdedd84 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:05:44 +0200 Subject: [PATCH 0887/1151] Use freezegun in uptimerobot tests (#99046) --- tests/components/uptimerobot/test_init.py | 41 ++++++++++++++++------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index bba5af07be3..67fac2437f0 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -1,6 +1,7 @@ """Test the UptimeRobot init.""" from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException @@ -12,7 +13,6 @@ from homeassistant.components.uptimerobot.const import ( from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.util import dt as dt_util from .common import ( MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, @@ -93,7 +93,9 @@ async def test_reauthentication_trigger_key_read_only( async def test_reauthentication_trigger_after_setup( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, ) -> None: """Test reauthentication trigger.""" mock_config_entry = await setup_uptimerobot_integration(hass) @@ -106,7 +108,8 @@ async def test_reauthentication_trigger_after_setup( "pyuptimerobot.UptimeRobot.async_get_monitors", side_effect=UptimeRobotAuthenticationException, ): - async_fire_time_changed(hass, dt_util.utcnow() + COORDINATOR_UPDATE_INTERVAL) + freezer.tick(COORDINATOR_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() @@ -125,7 +128,10 @@ async def test_reauthentication_trigger_after_setup( assert flow["context"]["entry_id"] == mock_config_entry.entry_id -async def test_integration_reload(hass: HomeAssistant) -> None: +async def test_integration_reload( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: """Test integration reload.""" mock_entry = await setup_uptimerobot_integration(hass) @@ -134,7 +140,8 @@ async def test_integration_reload(hass: HomeAssistant) -> None: return_value=mock_uptimerobot_api_response(), ): assert await hass.config_entries.async_reload(mock_entry.entry_id) - async_fire_time_changed(hass, dt_util.utcnow() + COORDINATOR_UPDATE_INTERVAL) + freezer.tick(COORDINATOR_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() entry = hass.config_entries.async_get_entry(mock_entry.entry_id) @@ -143,7 +150,9 @@ async def test_integration_reload(hass: HomeAssistant) -> None: async def test_update_errors( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, ) -> None: """Test errors during updates.""" await setup_uptimerobot_integration(hass) @@ -152,7 +161,8 @@ async def test_update_errors( "pyuptimerobot.UptimeRobot.async_get_monitors", side_effect=UptimeRobotException, ): - async_fire_time_changed(hass, dt_util.utcnow() + COORDINATOR_UPDATE_INTERVAL) + freezer.tick(COORDINATOR_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert ( hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state @@ -163,7 +173,8 @@ async def test_update_errors( "pyuptimerobot.UptimeRobot.async_get_monitors", return_value=mock_uptimerobot_api_response(), ): - async_fire_time_changed(hass, dt_util.utcnow() + COORDINATOR_UPDATE_INTERVAL) + freezer.tick(COORDINATOR_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state == STATE_ON @@ -171,7 +182,8 @@ async def test_update_errors( "pyuptimerobot.UptimeRobot.async_get_monitors", return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), ): - async_fire_time_changed(hass, dt_util.utcnow() + COORDINATOR_UPDATE_INTERVAL) + freezer.tick(COORDINATOR_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert ( hass.states.get(UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY).state @@ -181,7 +193,10 @@ async def test_update_errors( assert "Error fetching uptimerobot data: test error from API" in caplog.text -async def test_device_management(hass: HomeAssistant) -> None: +async def test_device_management( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: """Test that we are adding and removing devices for monitors returned from the API.""" mock_entry = await setup_uptimerobot_integration(hass) dev_reg = dr.async_get(hass) @@ -201,7 +216,8 @@ async def test_device_management(hass: HomeAssistant) -> None: data=[MOCK_UPTIMEROBOT_MONITOR, {**MOCK_UPTIMEROBOT_MONITOR, "id": 12345}] ), ): - async_fire_time_changed(hass, dt_util.utcnow() + COORDINATOR_UPDATE_INTERVAL) + freezer.tick(COORDINATOR_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() devices = dr.async_entries_for_config_entry(dev_reg, mock_entry.entry_id) @@ -218,7 +234,8 @@ async def test_device_management(hass: HomeAssistant) -> None: "pyuptimerobot.UptimeRobot.async_get_monitors", return_value=mock_uptimerobot_api_response(), ): - async_fire_time_changed(hass, dt_util.utcnow() + COORDINATOR_UPDATE_INTERVAL) + freezer.tick(COORDINATOR_UPDATE_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() await hass.async_block_till_done() From 452caee41a8f480a952e2a03200757c3e0e10bd0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:06:14 +0200 Subject: [PATCH 0888/1151] Use freezegun in pvpc_hourly_pricing tests (#99040) --- .../pvpc_hourly_pricing/test_config_flow.py | 145 +++++++++--------- 1 file changed, 73 insertions(+), 72 deletions(-) diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 8623830f0dd..e22ab03eb60 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for the pvpc_hourly_pricing config_flow.""" from datetime import datetime, timedelta -from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory from homeassistant import config_entries, data_entry_flow from homeassistant.components.pvpc_hourly_pricing import ( @@ -25,7 +25,9 @@ _MOCK_TIME_VALID_RESPONSES = datetime(2023, 1, 6, 12, 0, tzinfo=dt_util.UTC) async def test_config_flow( - hass: HomeAssistant, pvpc_aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + pvpc_aioclient_mock: AiohttpClientMocker, ) -> None: """Test config flow for pvpc_hourly_pricing. @@ -35,6 +37,7 @@ async def test_config_flow( - Check removal and add again to check state restoration - Configure options to change power and tariff to "2.0TD" """ + freezer.move_to(_MOCK_TIME_VALID_RESPONSES) hass.config.set_time_zone("Europe/Madrid") tst_config = { CONF_NAME: "test", @@ -43,84 +46,82 @@ async def test_config_flow( ATTR_POWER_P3: 5.75, } - with freeze_time(_MOCK_TIME_VALID_RESPONSES) as mock_time: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], tst_config - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + result = await hass.config_entries.flow.async_configure( + result["flow_id"], tst_config + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() - state = hass.states.get("sensor.test") - check_valid_state(state, tariff=TARIFFS[1]) - assert pvpc_aioclient_mock.call_count == 1 + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + check_valid_state(state, tariff=TARIFFS[1]) + assert pvpc_aioclient_mock.call_count == 1 - # Check abort when configuring another with same tariff - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], tst_config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert pvpc_aioclient_mock.call_count == 1 + # Check abort when configuring another with same tariff + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + result = await hass.config_entries.flow.async_configure( + result["flow_id"], tst_config + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert pvpc_aioclient_mock.call_count == 1 - # Check removal - registry = er.async_get(hass) - registry_entity = registry.async_get("sensor.test") - assert await hass.config_entries.async_remove(registry_entity.config_entry_id) + # Check removal + registry = er.async_get(hass) + registry_entity = registry.async_get("sensor.test") + assert await hass.config_entries.async_remove(registry_entity.config_entry_id) - # and add it again with UI - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + # and add it again with UI + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], tst_config - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + result = await hass.config_entries.flow.async_configure( + result["flow_id"], tst_config + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() - state = hass.states.get("sensor.test") - check_valid_state(state, tariff=TARIFFS[1]) - assert pvpc_aioclient_mock.call_count == 2 - assert state.attributes["period"] == "P3" - assert state.attributes["next_period"] == "P2" - assert state.attributes["available_power"] == 5750 + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + check_valid_state(state, tariff=TARIFFS[1]) + assert pvpc_aioclient_mock.call_count == 2 + assert state.attributes["period"] == "P3" + assert state.attributes["next_period"] == "P2" + assert state.attributes["available_power"] == 5750 - # check options flow - current_entries = hass.config_entries.async_entries(DOMAIN) - assert len(current_entries) == 1 - config_entry = current_entries[0] + # check options flow + current_entries = hass.config_entries.async_entries(DOMAIN) + assert len(current_entries) == 1 + config_entry = current_entries[0] - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6}, - ) - await hass.async_block_till_done() - state = hass.states.get("sensor.test") - check_valid_state(state, tariff=TARIFFS[1]) - assert pvpc_aioclient_mock.call_count == 3 - assert state.attributes["period"] == "P3" - assert state.attributes["next_period"] == "P2" - assert state.attributes["available_power"] == 4600 + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6}, + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + check_valid_state(state, tariff=TARIFFS[1]) + assert pvpc_aioclient_mock.call_count == 3 + assert state.attributes["period"] == "P3" + assert state.attributes["next_period"] == "P2" + assert state.attributes["available_power"] == 4600 - # check update failed - ts_future = _MOCK_TIME_VALID_RESPONSES + timedelta(days=1) - mock_time.move_to(ts_future) - async_fire_time_changed(hass, ts_future) - await hass.async_block_till_done() - state = hass.states.get("sensor.test") - check_valid_state(state, tariff=TARIFFS[0], value="unavailable") - assert "period" not in state.attributes - assert pvpc_aioclient_mock.call_count == 4 + # check update failed + freezer.tick(timedelta(days=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + check_valid_state(state, tariff=TARIFFS[0], value="unavailable") + assert "period" not in state.attributes + assert pvpc_aioclient_mock.call_count == 4 From b0952bc54a3feba67b7a94e0fa3b06b93d077c67 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:06:43 +0200 Subject: [PATCH 0889/1151] Use freezegun in shelly tests (#99042) --- tests/components/shelly/__init__.py | 18 +-- tests/components/shelly/test_binary_sensor.py | 12 +- tests/components/shelly/test_coordinator.py | 106 ++++++++---------- tests/components/shelly/test_sensor.py | 10 +- tests/components/shelly/test_update.py | 17 +-- 5 files changed, 80 insertions(+), 83 deletions(-) diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index 67f47b0e7e3..464118ac99b 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -7,6 +7,7 @@ from datetime import timedelta from typing import Any from unittest.mock import Mock +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.shelly.const import ( @@ -20,7 +21,6 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.entity_registry import async_get -from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -78,17 +78,21 @@ def inject_rpc_device_event( mock_rpc_device.mock_event() -async def mock_rest_update(hass: HomeAssistant, seconds=REST_SENSORS_UPDATE_INTERVAL): +async def mock_rest_update( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + seconds=REST_SENSORS_UPDATE_INTERVAL, +): """Move time to create REST sensors update event.""" - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=seconds)) + freezer.tick(timedelta(seconds=seconds)) + async_fire_time_changed(hass) await hass.async_block_till_done() -async def mock_polling_rpc_update(hass: HomeAssistant): +async def mock_polling_rpc_update(hass: HomeAssistant, freezer: FrozenDateTimeFactory): """Move time to create polling RPC sensors update event.""" - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=RPC_SENSORS_POLLING_INTERVAL) - ) + freezer.tick(timedelta(seconds=RPC_SENSORS_POLLING_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index ebc5089f884..a54b5398b11 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -1,4 +1,6 @@ """Tests for Shelly binary sensor platform.""" +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.shelly.const import SLEEP_PERIOD_MULTIPLIER from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN @@ -54,7 +56,7 @@ async def test_block_binary_sensor_extra_state_attr( async def test_block_rest_binary_sensor( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch ) -> None: """Test block REST binary sensor.""" entity_id = register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") @@ -64,13 +66,13 @@ async def test_block_rest_binary_sensor( assert hass.states.get(entity_id).state == STATE_OFF monkeypatch.setitem(mock_block_device.status["cloud"], "connected", True) - await mock_rest_update(hass) + await mock_rest_update(hass, freezer) assert hass.states.get(entity_id).state == STATE_ON async def test_block_rest_binary_sensor_connected_battery_devices( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch ) -> None: """Test block REST binary sensor for connected battery devices.""" entity_id = register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") @@ -84,11 +86,11 @@ async def test_block_rest_binary_sensor_connected_battery_devices( monkeypatch.setitem(mock_block_device.status["cloud"], "connected", True) # Verify no update on fast intervals - await mock_rest_update(hass) + await mock_rest_update(hass, freezer) assert hass.states.get(entity_id).state == STATE_OFF # Verify update on slow intervals - await mock_rest_update(hass, seconds=SLEEP_PERIOD_MULTIPLIER * 3600) + await mock_rest_update(hass, freezer, seconds=SLEEP_PERIOD_MULTIPLIER * 3600) assert hass.states.get(entity_id).state == STATE_ON diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 5a8bb234f30..a7fa64962e9 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError +from freezegun.api import FrozenDateTimeFactory from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -26,7 +27,6 @@ from homeassistant.helpers.device_registry import ( async_get as async_get_dev_reg, ) import homeassistant.helpers.issue_registry as ir -from homeassistant.util import dt as dt_util from . import ( MOCK_MAC, @@ -46,7 +46,7 @@ DEVICE_BLOCK_ID = 4 async def test_block_reload_on_cfg_change( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch ) -> None: """Test block reload on config change.""" await init_integration(hass, 1) @@ -66,16 +66,15 @@ async def test_block_reload_on_cfg_change( assert hass.states.get("switch.test_name_channel_1") is not None # Wait for debouncer - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) - ) + freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("switch.test_name_channel_1") is None async def test_block_no_reload_on_bulb_changes( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch ) -> None: """Test block no reload on bulb mode/effect change.""" await init_integration(hass, 1, model="SHBLB-1") @@ -96,9 +95,8 @@ async def test_block_no_reload_on_bulb_changes( assert hass.states.get("switch.test_name_channel_1") is not None # Wait for debouncer - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) - ) + freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("switch.test_name_channel_1") is not None @@ -112,16 +110,15 @@ async def test_block_no_reload_on_bulb_changes( assert hass.states.get("switch.test_name_channel_1") is not None # Wait for debouncer - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) - ) + freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("switch.test_name_channel_1") is not None async def test_block_polling_auth_error( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch ) -> None: """Test block device polling authentication error.""" monkeypatch.setattr( @@ -134,9 +131,8 @@ async def test_block_polling_auth_error( assert entry.state == ConfigEntryState.LOADED # Move time to generate polling - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15) - ) + freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED @@ -154,7 +150,7 @@ async def test_block_polling_auth_error( async def test_block_rest_update_auth_error( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch ) -> None: """Test block REST update authentication error.""" register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") @@ -170,7 +166,7 @@ async def test_block_rest_update_auth_error( assert entry.state == ConfigEntryState.LOADED - await mock_rest_update(hass) + await mock_rest_update(hass, freezer) assert entry.state == ConfigEntryState.LOADED @@ -187,7 +183,7 @@ async def test_block_rest_update_auth_error( async def test_block_polling_connection_error( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch ) -> None: """Test block device polling connection error.""" monkeypatch.setattr( @@ -200,16 +196,15 @@ async def test_block_polling_connection_error( assert hass.states.get("switch.test_name_channel_1").state == STATE_ON # Move time to generate polling - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15) - ) + freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 15)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("switch.test_name_channel_1").state == STATE_UNAVAILABLE async def test_block_rest_update_connection_error( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch ) -> None: """Test block REST update connection error.""" entity_id = register_entity(hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud") @@ -217,7 +212,7 @@ async def test_block_rest_update_connection_error( monkeypatch.setitem(mock_block_device.status, "uptime", 1) await init_integration(hass, 1) - await mock_rest_update(hass) + await mock_rest_update(hass, freezer) assert hass.states.get(entity_id).state == STATE_ON monkeypatch.setattr( @@ -225,13 +220,13 @@ async def test_block_rest_update_connection_error( "update_shelly", AsyncMock(side_effect=DeviceConnectionError), ) - await mock_rest_update(hass) + await mock_rest_update(hass, freezer) assert hass.states.get(entity_id).state == STATE_UNAVAILABLE async def test_block_sleeping_device_no_periodic_updates( - hass: HomeAssistant, mock_block_device + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device ) -> None: """Test block sleeping device no periodic updates.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" @@ -244,9 +239,8 @@ async def test_block_sleeping_device_no_periodic_updates( assert hass.states.get(entity_id).state == "22.1" # Move time to generate polling - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000) - ) + freezer.tick(timedelta(seconds=UPDATE_PERIOD_MULTIPLIER * 1000)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE @@ -322,7 +316,7 @@ async def test_block_button_click_event( async def test_rpc_reload_on_cfg_change( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch ) -> None: """Test RPC reload on config change.""" await init_integration(hass, 2) @@ -356,16 +350,15 @@ async def test_rpc_reload_on_cfg_change( assert hass.states.get("switch.test_name_test_switch_0") is not None # Wait for debouncer - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) - ) + freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("switch.test_name_test_switch_0") is None async def test_rpc_reload_with_invalid_auth( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch ) -> None: """Test RPC when InvalidAuthError is raising during config entry reload.""" with patch( @@ -398,9 +391,8 @@ async def test_rpc_reload_with_invalid_auth( await hass.async_block_till_done() # Move time to generate reconnect - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=RPC_RECONNECT_INTERVAL) - ) + freezer.tick(timedelta(seconds=RPC_RECONNECT_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED @@ -455,7 +447,7 @@ async def test_rpc_click_event( async def test_rpc_update_entry_sleep_period( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch ) -> None: """Test RPC update entry sleep period.""" entry = await init_integration(hass, 2, sleep_period=600) @@ -475,16 +467,15 @@ async def test_rpc_update_entry_sleep_period( # Move time to generate sleep period update monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 3600) - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER) - ) + freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert entry.data["sleep_period"] == 3600 async def test_rpc_sleeping_device_no_periodic_updates( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch ) -> None: """Test RPC sleeping device no periodic updates.""" entity_id = f"{SENSOR_DOMAIN}.test_name_temperature" @@ -504,16 +495,15 @@ async def test_rpc_sleeping_device_no_periodic_updates( assert hass.states.get(entity_id).state == "22.9" # Move time to generate polling - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000) - ) + freezer.tick(timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE async def test_rpc_reconnect_auth_error( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch ) -> None: """Test RPC reconnect authentication error.""" entry = await init_integration(hass, 2) @@ -530,9 +520,8 @@ async def test_rpc_reconnect_auth_error( assert entry.state == ConfigEntryState.LOADED # Move time to generate reconnect - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=RPC_RECONNECT_INTERVAL) - ) + freezer.tick(timedelta(seconds=RPC_RECONNECT_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED @@ -550,7 +539,7 @@ async def test_rpc_reconnect_auth_error( async def test_rpc_polling_auth_error( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch ) -> None: """Test RPC polling authentication error.""" register_entity(hass, SENSOR_DOMAIN, "test_name_rssi", "wifi-rssi") @@ -566,7 +555,7 @@ async def test_rpc_polling_auth_error( assert entry.state == ConfigEntryState.LOADED - await mock_polling_rpc_update(hass) + await mock_polling_rpc_update(hass, freezer) assert entry.state == ConfigEntryState.LOADED @@ -583,7 +572,7 @@ async def test_rpc_polling_auth_error( async def test_rpc_reconnect_error( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch ) -> None: """Test RPC reconnect error.""" await init_integration(hass, 2) @@ -600,16 +589,15 @@ async def test_rpc_reconnect_error( ) # Move time to generate reconnect - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=RPC_RECONNECT_INTERVAL) - ) + freezer.tick(timedelta(seconds=RPC_RECONNECT_INTERVAL)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get("switch.test_name_test_switch_0").state == STATE_UNAVAILABLE async def test_rpc_polling_connection_error( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch ) -> None: """Test RPC polling connection error.""" entity_id = register_entity(hass, SENSOR_DOMAIN, "test_name_rssi", "wifi-rssi") @@ -625,13 +613,13 @@ async def test_rpc_polling_connection_error( assert hass.states.get(entity_id).state == "-63" - await mock_polling_rpc_update(hass) + await mock_polling_rpc_update(hass, freezer) assert hass.states.get(entity_id).state == STATE_UNAVAILABLE async def test_rpc_polling_disconnected( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch ) -> None: """Test RPC polling device disconnected.""" entity_id = register_entity(hass, SENSOR_DOMAIN, "test_name_rssi", "wifi-rssi") @@ -641,6 +629,6 @@ async def test_rpc_polling_disconnected( assert hass.states.get(entity_id).state == "-63" - await mock_polling_rpc_update(hass) + await mock_polling_rpc_update(hass, freezer) assert hass.states.get(entity_id).state == STATE_UNAVAILABLE diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index fe79b1d010a..630ee551e89 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -1,4 +1,6 @@ """Tests for Shelly sensor platform.""" +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.shelly.const import DOMAIN from homeassistant.const import ( @@ -89,7 +91,7 @@ async def test_power_factory_without_unit_migration( async def test_block_rest_sensor( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch ) -> None: """Test block REST sensor.""" entity_id = register_entity(hass, SENSOR_DOMAIN, "test_name_rssi", "rssi") @@ -98,7 +100,7 @@ async def test_block_rest_sensor( assert hass.states.get(entity_id).state == "-64" monkeypatch.setitem(mock_block_device.status["wifi_sta"], "rssi", -71) - await mock_rest_update(hass) + await mock_rest_update(hass, freezer) assert hass.states.get(entity_id).state == "-71" @@ -304,7 +306,7 @@ async def test_rpc_sensor_error( async def test_rpc_polling_sensor( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch ) -> None: """Test RPC polling sensor.""" entity_id = register_entity(hass, SENSOR_DOMAIN, "test_name_rssi", "wifi-rssi") @@ -313,7 +315,7 @@ async def test_rpc_polling_sensor( assert hass.states.get(entity_id).state == "-63" mutate_rpc_device_status(monkeypatch, mock_rpc_device, "wifi", "rssi", "-70") - await mock_polling_rpc_update(hass) + await mock_polling_rpc_update(hass, freezer) assert hass.states.get(entity_id).state == "-70" diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index ed5dd81339e..1ff2ac99814 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.shelly.const import DOMAIN @@ -37,7 +38,7 @@ from tests.common import mock_restore_cache async def test_block_update( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch ) -> None: """Test block device update entity.""" entity_registry = async_get(hass) @@ -75,7 +76,7 @@ async def test_block_update( assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2") - await mock_rest_update(hass) + await mock_rest_update(hass, freezer) state = hass.states.get("update.test_name_firmware_update") assert state.state == STATE_OFF @@ -85,7 +86,7 @@ async def test_block_update( async def test_block_beta_update( - hass: HomeAssistant, mock_block_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_block_device, monkeypatch ) -> None: """Test block device beta update entity.""" entity_registry = async_get(hass) @@ -108,7 +109,7 @@ async def test_block_beta_update( assert state.attributes[ATTR_IN_PROGRESS] is False monkeypatch.setitem(mock_block_device.status["update"], "beta_version", "2b") - await mock_rest_update(hass) + await mock_rest_update(hass, freezer) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_ON @@ -131,7 +132,7 @@ async def test_block_beta_update( assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_block_device.status["update"], "old_version", "2b") - await mock_rest_update(hass) + await mock_rest_update(hass, freezer) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_OFF @@ -389,7 +390,7 @@ async def test_rpc_restored_sleeping_update_no_last_state( async def test_rpc_beta_update( - hass: HomeAssistant, mock_rpc_device, monkeypatch + hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_rpc_device, monkeypatch ) -> None: """Test RPC device beta update entity.""" entity_registry = async_get(hass) @@ -425,7 +426,7 @@ async def test_rpc_beta_update( "beta": {"version": "2b"}, }, ) - await mock_rest_update(hass) + await mock_rest_update(hass, freezer) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_ON @@ -448,7 +449,7 @@ async def test_rpc_beta_update( assert state.attributes[ATTR_IN_PROGRESS] is True monkeypatch.setitem(mock_rpc_device.shelly, "ver", "2b") - await mock_rest_update(hass) + await mock_rest_update(hass, freezer) state = hass.states.get("update.test_name_beta_firmware_update") assert state.state == STATE_OFF From f96c1516f83194ce94d1ffc743386452481fa584 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 25 Aug 2023 16:46:10 +0200 Subject: [PATCH 0890/1151] Use snapshot assertion for gios diagnostics test (#98984) --- tests/components/gios/__init__.py | 1 + .../gios/fixtures/diagnostics_data.json | 50 ------------- .../gios/snapshots/test_diagnostics.ambr | 72 +++++++++++++++++++ tests/components/gios/test_diagnostics.py | 30 ++------ 4 files changed, 79 insertions(+), 74 deletions(-) delete mode 100644 tests/components/gios/fixtures/diagnostics_data.json create mode 100644 tests/components/gios/snapshots/test_diagnostics.ambr diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 6c39ee35303..946cceac786 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -21,6 +21,7 @@ async def init_integration( title="Home", unique_id="123", data={"station_id": 123, "name": "Home"}, + entry_id="86129426118ae32020417a53712d6eef", ) indexes = json.loads(load_fixture("gios/indexes.json")) diff --git a/tests/components/gios/fixtures/diagnostics_data.json b/tests/components/gios/fixtures/diagnostics_data.json deleted file mode 100644 index feee534ec31..00000000000 --- a/tests/components/gios/fixtures/diagnostics_data.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "aqi": { - "name": "AQI", - "id": null, - "index": null, - "value": "good" - }, - "c6h6": { - "name": "benzene", - "id": 658, - "index": "very_good", - "value": 0.23789 - }, - "co": { - "name": "carbon monoxide", - "id": 660, - "index": "good", - "value": 251.874 - }, - "no2": { - "name": "nitrogen dioxide", - "id": 665, - "index": "good", - "value": 7.13411 - }, - "o3": { - "name": "ozone", - "id": 667, - "index": "good", - "value": 95.7768 - }, - "pm10": { - "name": "particulate matter 10", - "id": 14395, - "index": "good", - "value": 16.8344 - }, - "pm25": { - "name": "particulate matter 2.5", - "id": 670, - "index": "good", - "value": 4 - }, - "so2": { - "name": "sulfur dioxide", - "id": 672, - "index": "very_good", - "value": 4.35478 - } -} diff --git a/tests/components/gios/snapshots/test_diagnostics.ambr b/tests/components/gios/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..67691602fcf --- /dev/null +++ b/tests/components/gios/snapshots/test_diagnostics.ambr @@ -0,0 +1,72 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config_entry': dict({ + 'data': dict({ + 'name': 'Home', + 'station_id': 123, + }), + 'disabled_by': None, + 'domain': 'gios', + 'entry_id': '86129426118ae32020417a53712d6eef', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Home', + 'unique_id': '123', + 'version': 1, + }), + 'coordinator_data': dict({ + 'aqi': dict({ + 'id': None, + 'index': None, + 'name': 'AQI', + 'value': 'good', + }), + 'c6h6': dict({ + 'id': 658, + 'index': 'very_good', + 'name': 'benzene', + 'value': 0.23789, + }), + 'co': dict({ + 'id': 660, + 'index': 'good', + 'name': 'carbon monoxide', + 'value': 251.874, + }), + 'no2': dict({ + 'id': 665, + 'index': 'good', + 'name': 'nitrogen dioxide', + 'value': 7.13411, + }), + 'o3': dict({ + 'id': 667, + 'index': 'good', + 'name': 'ozone', + 'value': 95.7768, + }), + 'pm10': dict({ + 'id': 14395, + 'index': 'good', + 'name': 'particulate matter 10', + 'value': 16.8344, + }), + 'pm25': dict({ + 'id': 670, + 'index': 'good', + 'name': 'particulate matter 2.5', + 'value': 4, + }), + 'so2': dict({ + 'id': 672, + 'index': 'very_good', + 'name': 'sulfur dioxide', + 'value': 4.35478, + }), + }), + }) +# --- diff --git a/tests/components/gios/test_diagnostics.py b/tests/components/gios/test_diagnostics.py index 0b9560a96e1..903de4872a2 100644 --- a/tests/components/gios/test_diagnostics.py +++ b/tests/components/gios/test_diagnostics.py @@ -1,39 +1,21 @@ """Test GIOS diagnostics.""" -import json + +from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant from . import init_integration -from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" entry = await init_integration(hass) - coordinator_data = json.loads(load_fixture("diagnostics_data.json", "gios")) - - result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - - assert result["config_entry"] == { - "entry_id": entry.entry_id, - "version": 1, - "domain": "gios", - "title": "Home", - "data": { - "station_id": 123, - "name": "Home", - }, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": "123", - "disabled_by": None, - } - assert result["coordinator_data"] == coordinator_data + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot From 27f7399071787451bed73b196333d786830843e5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 16:46:23 +0200 Subject: [PATCH 0891/1151] Modernize accuweather weather (#99001) --- .../components/accuweather/weather.py | 15 +- .../accuweather/snapshots/test_weather.ambr | 225 ++++++++++++++++++ tests/components/accuweather/test_weather.py | 96 +++++++- 3 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 tests/components/accuweather/snapshots/test_weather.ambr diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 518714b3874..d446b4b58d9 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -17,7 +17,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_UV_INDEX, ATTR_FORECAST_WIND_BEARING, Forecast, - WeatherEntity, + SingleCoordinatorWeatherEntity, + WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -27,9 +28,8 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utc_from_timestamp from . import AccuWeatherDataUpdateCoordinator @@ -58,7 +58,7 @@ async def async_setup_entry( class AccuWeatherEntity( - CoordinatorEntity[AccuWeatherDataUpdateCoordinator], WeatherEntity + SingleCoordinatorWeatherEntity[AccuWeatherDataUpdateCoordinator] ): """Define an AccuWeather entity.""" @@ -76,6 +76,8 @@ class AccuWeatherEntity( self._attr_unique_id = coordinator.location_key self._attr_attribution = ATTRIBUTION self._attr_device_info = coordinator.device_info + if self.coordinator.forecast: + self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY @property def condition(self) -> str | None: @@ -174,3 +176,8 @@ class AccuWeatherEntity( } for item in self.coordinator.data[ATTR_FORECAST] ] + + @callback + def _async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" + return self.forecast diff --git a/tests/components/accuweather/snapshots/test_weather.ambr b/tests/components/accuweather/snapshots/test_weather.ambr new file mode 100644 index 00000000000..521393af71b --- /dev/null +++ b/tests/components/accuweather/snapshots/test_weather.ambr @@ -0,0 +1,225 @@ +# serializer version: 1 +# name: test_forecast_service + dict({ + 'forecast': list([ + dict({ + 'apparent_temperature': 29.8, + 'cloud_coverage': 58, + 'condition': 'lightning-rainy', + 'datetime': '2020-07-26T05:00:00+00:00', + 'precipitation': 2.5, + 'precipitation_probability': 60, + 'temperature': 29.5, + 'templow': 15.4, + 'uv_index': 5, + 'wind_bearing': 166, + 'wind_gust_speed': 29.6, + 'wind_speed': 13.0, + }), + dict({ + 'apparent_temperature': 28.9, + 'cloud_coverage': 52, + 'condition': 'partlycloudy', + 'datetime': '2020-07-27T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'temperature': 26.2, + 'templow': 15.9, + 'uv_index': 7, + 'wind_bearing': 297, + 'wind_gust_speed': 14.8, + 'wind_speed': 9.3, + }), + dict({ + 'apparent_temperature': 31.6, + 'cloud_coverage': 65, + 'condition': 'partlycloudy', + 'datetime': '2020-07-28T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'temperature': 31.7, + 'templow': 16.8, + 'uv_index': 7, + 'wind_bearing': 198, + 'wind_gust_speed': 24.1, + 'wind_speed': 16.7, + }), + dict({ + 'apparent_temperature': 26.5, + 'cloud_coverage': 45, + 'condition': 'partlycloudy', + 'datetime': '2020-07-29T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 9, + 'temperature': 24.0, + 'templow': 11.7, + 'uv_index': 6, + 'wind_bearing': 293, + 'wind_gust_speed': 24.1, + 'wind_speed': 13.0, + }), + dict({ + 'apparent_temperature': 22.2, + 'cloud_coverage': 50, + 'condition': 'partlycloudy', + 'datetime': '2020-07-30T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'temperature': 21.4, + 'templow': 12.2, + 'uv_index': 7, + 'wind_bearing': 280, + 'wind_gust_speed': 27.8, + 'wind_speed': 18.5, + }), + ]), + }) +# --- +# name: test_forecast_subscription + list([ + dict({ + 'apparent_temperature': 29.8, + 'cloud_coverage': 58, + 'condition': 'lightning-rainy', + 'datetime': '2020-07-26T05:00:00+00:00', + 'precipitation': 2.5, + 'precipitation_probability': 60, + 'temperature': 29.5, + 'templow': 15.4, + 'uv_index': 5, + 'wind_bearing': 166, + 'wind_gust_speed': 29.6, + 'wind_speed': 13.0, + }), + dict({ + 'apparent_temperature': 28.9, + 'cloud_coverage': 52, + 'condition': 'partlycloudy', + 'datetime': '2020-07-27T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'temperature': 26.2, + 'templow': 15.9, + 'uv_index': 7, + 'wind_bearing': 297, + 'wind_gust_speed': 14.8, + 'wind_speed': 9.3, + }), + dict({ + 'apparent_temperature': 31.6, + 'cloud_coverage': 65, + 'condition': 'partlycloudy', + 'datetime': '2020-07-28T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'temperature': 31.7, + 'templow': 16.8, + 'uv_index': 7, + 'wind_bearing': 198, + 'wind_gust_speed': 24.1, + 'wind_speed': 16.7, + }), + dict({ + 'apparent_temperature': 26.5, + 'cloud_coverage': 45, + 'condition': 'partlycloudy', + 'datetime': '2020-07-29T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 9, + 'temperature': 24.0, + 'templow': 11.7, + 'uv_index': 6, + 'wind_bearing': 293, + 'wind_gust_speed': 24.1, + 'wind_speed': 13.0, + }), + dict({ + 'apparent_temperature': 22.2, + 'cloud_coverage': 50, + 'condition': 'partlycloudy', + 'datetime': '2020-07-30T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'temperature': 21.4, + 'templow': 12.2, + 'uv_index': 7, + 'wind_bearing': 280, + 'wind_gust_speed': 27.8, + 'wind_speed': 18.5, + }), + ]) +# --- +# name: test_forecast_subscription.1 + list([ + dict({ + 'apparent_temperature': 29.8, + 'cloud_coverage': 58, + 'condition': 'lightning-rainy', + 'datetime': '2020-07-26T05:00:00+00:00', + 'precipitation': 2.5, + 'precipitation_probability': 60, + 'temperature': 29.5, + 'templow': 15.4, + 'uv_index': 5, + 'wind_bearing': 166, + 'wind_gust_speed': 29.6, + 'wind_speed': 13.0, + }), + dict({ + 'apparent_temperature': 28.9, + 'cloud_coverage': 52, + 'condition': 'partlycloudy', + 'datetime': '2020-07-27T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'temperature': 26.2, + 'templow': 15.9, + 'uv_index': 7, + 'wind_bearing': 297, + 'wind_gust_speed': 14.8, + 'wind_speed': 9.3, + }), + dict({ + 'apparent_temperature': 31.6, + 'cloud_coverage': 65, + 'condition': 'partlycloudy', + 'datetime': '2020-07-28T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'temperature': 31.7, + 'templow': 16.8, + 'uv_index': 7, + 'wind_bearing': 198, + 'wind_gust_speed': 24.1, + 'wind_speed': 16.7, + }), + dict({ + 'apparent_temperature': 26.5, + 'cloud_coverage': 45, + 'condition': 'partlycloudy', + 'datetime': '2020-07-29T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 9, + 'temperature': 24.0, + 'templow': 11.7, + 'uv_index': 6, + 'wind_bearing': 293, + 'wind_gust_speed': 24.1, + 'wind_speed': 13.0, + }), + dict({ + 'apparent_temperature': 22.2, + 'cloud_coverage': 50, + 'condition': 'partlycloudy', + 'datetime': '2020-07-30T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'temperature': 21.4, + 'templow': 12.2, + 'uv_index': 7, + 'wind_bearing': 280, + 'wind_gust_speed': 27.8, + 'wind_speed': 18.5, + }), + ]) +# --- diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index b9e66d51874..1d970e322e4 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -2,6 +2,9 @@ from datetime import timedelta from unittest.mock import PropertyMock, patch +from freezegun.api import FrozenDateTimeFactory +from syrupy.assertion import SnapshotAssertion + from homeassistant.components.accuweather.const import ATTRIBUTION from homeassistant.components.weather import ( ATTR_FORECAST, @@ -27,8 +30,16 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_GUST_SPEED, ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + WeatherEntityFeature, +) +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, ) -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -41,6 +52,7 @@ from tests.common import ( load_json_array_fixture, load_json_object_fixture, ) +from tests.typing import WebSocketGenerator async def test_weather_without_forecast(hass: HomeAssistant) -> None: @@ -64,6 +76,7 @@ async def test_weather_without_forecast(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_WEATHER_WIND_GUST_SPEED) == 20.3 assert state.attributes.get(ATTR_WEATHER_UV_INDEX) == 6 assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ATTR_SUPPORTED_FEATURES not in state.attributes entry = registry.async_get("weather.home") assert entry @@ -90,6 +103,9 @@ async def test_weather_with_forecast(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_WEATHER_WIND_GUST_SPEED) == 20.3 assert state.attributes.get(ATTR_WEATHER_UV_INDEX) == 6 assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] == WeatherEntityFeature.FORECAST_DAILY + ) forecast = state.attributes.get(ATTR_FORECAST)[0] assert forecast.get(ATTR_FORECAST_CONDITION) == "lightning-rainy" assert forecast.get(ATTR_FORECAST_PRECIPITATION) == 2.5 @@ -186,3 +202,81 @@ async def test_unsupported_condition_icon_data(hass: HomeAssistant) -> None: state = hass.states.get("weather.home") assert state.attributes.get(ATTR_FORECAST_CONDITION) is None + + +async def test_forecast_service( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test multiple forecast.""" + await init_integration(hass, forecast=True) + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECAST, + { + "entity_id": "weather.home", + "type": "daily", + }, + blocking=True, + return_response=True, + ) + assert response["forecast"] != [] + assert response == snapshot + + +async def test_forecast_subscription( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test multiple forecast.""" + client = await hass_ws_client(hass) + + await init_integration(hass, forecast=True) + + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": "daily", + "entity_id": "weather.home", + } + ) + 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 != [] + assert forecast1 == snapshot + + current = load_json_object_fixture("accuweather/current_conditions_data.json") + forecast = load_json_array_fixture("accuweather/forecast_data.json") + + with patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ), patch( + "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", + return_value=forecast, + ), patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ): + freezer.tick(timedelta(minutes=80) + 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 != [] + assert forecast2 == snapshot From 49897341ba992770186e886d85ece788c3bc362a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 25 Aug 2023 17:56:22 +0200 Subject: [PATCH 0892/1151] Add lawn_mower platform to MQTT (#98831) * Add MQTT lawn_mower platform * Use separate command topics and templates * Remove unrelated change --- .../components/mqtt/abbreviations.py | 8 + .../components/mqtt/config_integration.py | 5 + homeassistant/components/mqtt/const.py | 2 + homeassistant/components/mqtt/discovery.py | 1 + homeassistant/components/mqtt/lawn_mower.py | 254 +++++ tests/components/mqtt/test_lawn_mower.py | 888 ++++++++++++++++++ 6 files changed, 1158 insertions(+) create mode 100644 homeassistant/components/mqtt/lawn_mower.py create mode 100644 tests/components/mqtt/test_lawn_mower.py diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 43f14eba1c5..eb9ab56208e 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -3,6 +3,8 @@ ABBREVIATIONS = { "act_t": "action_topic", "act_tpl": "action_template", + "act_stat_t": "activity_state_topic", + "act_val_tpl": "activity_value_template", "atype": "automation_type", "aux_cmd_t": "aux_command_topic", "aux_stat_tpl": "aux_state_template", @@ -54,6 +56,8 @@ ABBREVIATIONS = { "dir_val_tpl": "direction_value_template", "dock_t": "docked_topic", "dock_tpl": "docked_template", + "dock_cmd_t": "dock_command_topic", + "dock_cmd_tpl": "dock_command_template", "e": "encoding", "en": "enabled_by_default", "ent_cat": "entity_category", @@ -121,6 +125,8 @@ ABBREVIATIONS = { "osc_cmd_tpl": "oscillation_command_template", "osc_stat_t": "oscillation_state_topic", "osc_val_tpl": "oscillation_value_template", + "pause_cmd_t": "pause_command_topic", + "pause_mw_cmd_tpl": "pause_command_template", "pct_cmd_t": "percentage_command_topic", "pct_cmd_tpl": "percentage_command_template", "pct_stat_t": "percentage_state_topic", @@ -215,6 +221,8 @@ ABBREVIATIONS = { "stat_tpl": "state_template", "stat_val_tpl": "state_value_template", "step": "step", + "strt_mw_cmd_t": "start_mowing_command_topic", + "strt_mw_cmd_tpl": "start_mowing_command_template", "stype": "subtype", "sug_dsp_prc": "suggested_display_precision", "sup_dur": "support_duration", diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index cd4470ef22d..79e977a90cd 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -26,6 +26,7 @@ from . import ( fan as fan_platform, humidifier as humidifier_platform, image as image_platform, + lawn_mower as lawn_mower_platform, light as light_platform, lock as lock_platform, number as number_platform, @@ -99,6 +100,10 @@ CONFIG_SCHEMA_BASE = vol.Schema( cv.ensure_list, [image_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), + Platform.LAWN_MOWER.value: vol.All( + cv.ensure_list, + [lawn_mower_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] + ), Platform.LOCK.value: vol.All( cv.ensure_list, [lock_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index c0589f60cbe..685e45700b5 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -134,6 +134,7 @@ PLATFORMS = [ Platform.FAN, Platform.HUMIDIFIER, Platform.IMAGE, + Platform.LAWN_MOWER, Platform.LIGHT, Platform.LOCK, Platform.NUMBER, @@ -161,6 +162,7 @@ RELOADABLE_PLATFORMS = [ Platform.HUMIDIFIER, Platform.IMAGE, Platform.LIGHT, + Platform.LAWN_MOWER, Platform.LOCK, Platform.NUMBER, Platform.SCENE, diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index e701937a048..37885b628d2 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -61,6 +61,7 @@ SUPPORTED_COMPONENTS = [ "fan", "humidifier", "image", + "lawn_mower", "light", "lock", "number", diff --git a/homeassistant/components/mqtt/lawn_mower.py b/homeassistant/components/mqtt/lawn_mower.py new file mode 100644 index 00000000000..44db3581f8b --- /dev/null +++ b/homeassistant/components/mqtt/lawn_mower.py @@ -0,0 +1,254 @@ +"""Support for MQTT lawn mowers.""" +from __future__ import annotations + +from collections.abc import Callable +import contextlib +import functools +import logging + +import voluptuous as vol + +from homeassistant.components import lawn_mower +from homeassistant.components.lawn_mower import ( + LawnMowerActivity, + LawnMowerEntity, + LawnMowerEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import subscription +from .config import MQTT_BASE_SCHEMA +from .const import ( + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + DEFAULT_OPTIMISTIC, + DEFAULT_RETAIN, +) +from .debug_info import log_messages +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) +from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic + +_LOGGER = logging.getLogger(__name__) + +CONF_ACTIVITY_STATE_TOPIC = "activity_state_topic" +CONF_ACTIVITY_VALUE_TEMPLATE = "activity_value_template" +CONF_DOCK_COMMAND_TOPIC = "dock_command_topic" +CONF_DOCK_COMMAND_TEMPLATE = "dock_command_template" +CONF_PAUSE_COMMAND_TOPIC = "pause_command_topic" +CONF_PAUSE_COMMAND_TEMPLATE = "pause_command_template" +CONF_START_MOWING_COMMAND_TOPIC = "start_mowing_command_topic" +CONF_START_MOWING_COMMAND_TEMPLATE = "start_mowing_command_template" + +DEFAULT_NAME = "MQTT Lawn Mower" +ENTITY_ID_FORMAT = lawn_mower.DOMAIN + ".{}" + +MQTT_LAWN_MOWER_ATTRIBUTES_BLOCKED: frozenset[str] = frozenset() + +FEATURE_DOCK = "dock" +FEATURE_PAUSE = "pause" +FEATURE_START_MOWING = "start_mowing" + +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( + { + vol.Optional(CONF_ACTIVITY_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_ACTIVITY_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_DOCK_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_DOCK_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PAUSE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_PAUSE_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_START_MOWING_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_START_MOWING_COMMAND_TOPIC): valid_publish_topic, + }, +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + +DISCOVERY_SCHEMA = vol.All(PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT lawn mower through YAML and through MQTT discovery.""" + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper(hass, lawn_mower.DOMAIN, setup, DISCOVERY_SCHEMA) + + +async def _async_setup_entity( + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, +) -> None: + """Set up the MQTT lawn mower.""" + async_add_entities([MqttLawnMower(hass, config, config_entry, discovery_data)]) + + +class MqttLawnMower(MqttEntity, LawnMowerEntity, RestoreEntity): + """Representation of an MQTT lawn mower.""" + + _default_name = DEFAULT_NAME + _entity_id_format = ENTITY_ID_FORMAT + _attributes_extra_blocked = MQTT_LAWN_MOWER_ATTRIBUTES_BLOCKED + _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]] + _command_topics: dict[str, str] + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] + _optimistic: bool = False + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: + """Initialize the MQTT lawn mower.""" + self._attr_current_option = None + LawnMowerEntity.__init__(self) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + + @staticmethod + def config_schema() -> vol.Schema: + """Return the config schema.""" + return DISCOVERY_SCHEMA + + def _setup_from_config(self, config: ConfigType) -> None: + """(Re)Setup the entity.""" + self._optimistic = config[CONF_OPTIMISTIC] + + self._value_template = MqttValueTemplate( + config.get(CONF_ACTIVITY_VALUE_TEMPLATE), entity=self + ).async_render_with_possible_json_value + supported_features = LawnMowerEntityFeature(0) + self._command_topics = {} + if CONF_DOCK_COMMAND_TOPIC in config: + self._command_topics[FEATURE_DOCK] = config[CONF_DOCK_COMMAND_TOPIC] + supported_features |= LawnMowerEntityFeature.DOCK + if CONF_PAUSE_COMMAND_TOPIC in config: + self._command_topics[FEATURE_PAUSE] = config[CONF_PAUSE_COMMAND_TOPIC] + supported_features |= LawnMowerEntityFeature.PAUSE + if CONF_START_MOWING_COMMAND_TOPIC in config: + self._command_topics[FEATURE_START_MOWING] = config[ + CONF_START_MOWING_COMMAND_TOPIC + ] + supported_features |= LawnMowerEntityFeature.START_MOWING + self._attr_supported_features = supported_features + self._command_templates = {} + self._command_templates[FEATURE_DOCK] = MqttCommandTemplate( + config.get(CONF_DOCK_COMMAND_TEMPLATE), entity=self + ).async_render + self._command_templates[FEATURE_PAUSE] = MqttCommandTemplate( + config.get(CONF_PAUSE_COMMAND_TEMPLATE), entity=self + ).async_render + self._command_templates[FEATURE_START_MOWING] = MqttCommandTemplate( + config.get(CONF_START_MOWING_COMMAND_TEMPLATE), entity=self + ).async_render + + def _prepare_subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + + @callback + @log_messages(self.hass, self.entity_id) + def message_received(msg: ReceiveMessage) -> None: + """Handle new MQTT messages.""" + payload = str(self._value_template(msg.payload)) + if not payload: + _LOGGER.debug( + "Invalid empty activity payload from topic %s, for entity %s", + msg.topic, + self.entity_id, + ) + return + if payload.lower() == "none": + self._attr_activity = None + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + return + + try: + self._attr_activity = LawnMowerActivity(payload) + except ValueError: + _LOGGER.error( + "Invalid activity for %s: '%s' (valid activies: %s)", + self.entity_id, + payload, + [option.value for option in LawnMowerActivity], + ) + return + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + if self._config.get(CONF_ACTIVITY_STATE_TOPIC) is None: + # Force into optimistic mode. + self._optimistic = True + else: + self._sub_state = subscription.async_prepare_subscribe_topics( + self.hass, + self._sub_state, + { + CONF_ACTIVITY_STATE_TOPIC: { + "topic": self._config.get(CONF_ACTIVITY_STATE_TOPIC), + "msg_callback": message_received, + "qos": self._config[CONF_QOS], + "encoding": self._config[CONF_ENCODING] or None, + } + }, + ) + + async def _subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + await subscription.async_subscribe_topics(self.hass, self._sub_state) + + if self._optimistic and (last_state := await self.async_get_last_state()): + with contextlib.suppress(ValueError): + self._attr_activity = LawnMowerActivity(last_state.state) + + @property + def assumed_state(self) -> bool: + """Return true if we do optimistic updates.""" + return self._optimistic + + async def _async_operate(self, option: str, activity: LawnMowerActivity) -> None: + """Execute operation.""" + payload = self._command_templates[option](option) + if self._optimistic: + self._attr_activity = activity + self.async_write_ha_state() + + await self.async_publish( + self._command_topics[option], + payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + self._config[CONF_ENCODING], + ) + + async def async_start_mowing(self) -> None: + """Start or resume mowing.""" + await self._async_operate("start_mowing", LawnMowerActivity.MOWING) + + async def async_dock(self) -> None: + """Dock the mower.""" + await self._async_operate("dock", LawnMowerActivity.DOCKED) + + async def async_pause(self) -> None: + """Pause the lawn mower.""" + await self._async_operate("pause", LawnMowerActivity.PAUSED) diff --git a/tests/components/mqtt/test_lawn_mower.py b/tests/components/mqtt/test_lawn_mower.py new file mode 100644 index 00000000000..b7130cac3bf --- /dev/null +++ b/tests/components/mqtt/test_lawn_mower.py @@ -0,0 +1,888 @@ +"""The tests for mqtt lawn_mower component.""" +import copy +import json +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant.components import lawn_mower, mqtt +from homeassistant.components.lawn_mower import ( + DOMAIN as LAWN_MOWER_DOMAIN, + SERVICE_DOCK, + SERVICE_PAUSE, + SERVICE_START_MOWING, + LawnMowerEntityFeature, +) +from homeassistant.components.mqtt.lawn_mower import MQTT_LAWN_MOWER_ATTRIBUTES_BLOCKED +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant, State + +from .test_common import ( + help_test_availability_when_connection_lost, + help_test_availability_without_topic, + help_test_custom_availability_payload, + help_test_default_availability_payload, + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_setup, + help_test_discovery_update, + help_test_discovery_update_attr, + help_test_discovery_update_unchanged, + help_test_encoding_subscribable_topics, + help_test_entity_debug_info_message, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, + help_test_publishing_with_custom_encoding, + help_test_reloadable, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_unique_id, + help_test_unload_config_entry_with_platform, + help_test_update_with_json_attrs_bad_json, + help_test_update_with_json_attrs_not_dict, +) + +from tests.common import async_fire_mqtt_message, mock_restore_cache +from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient + +ATTR_ACTIVITY = "activity" + +DEFAULT_FEATURES = ( + LawnMowerEntityFeature.START_MOWING + | LawnMowerEntityFeature.PAUSE + | LawnMowerEntityFeature.DOCK +) + +DEFAULT_CONFIG = { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: { + "name": "test", + "dock_command_topic": "dock-test-topic", + "pause_command_topic": "pause-test-topic", + "start_mowing_command_topic": "start_mowing-test-topic", + } + } +} + + +@pytest.fixture(autouse=True) +def lawn_mower_platform_only(): + """Only setup the lawn_mower platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LAWN_MOWER]): + yield + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: { + "activity_state_topic": "test/lawn_mower_stat", + "dock_command_topic": "dock-test-topic", + "pause_command_topic": "pause-test-topic", + "start_mowing_command_topic": "start_mowing-test-topic", + "name": "Test Lawn Mower", + } + } + } + ], +) +async def test_run_lawn_mower_setup_and_state_updates( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test that it sets up correctly fetches the given payload.""" + await mqtt_mock_entry() + + async_fire_mqtt_message(hass, "test/lawn_mower_stat", "mowing") + + await hass.async_block_till_done() + + state = hass.states.get("lawn_mower.test_lawn_mower") + assert state.state == "mowing" + + async_fire_mqtt_message(hass, "test/lawn_mower_stat", "docked") + + await hass.async_block_till_done() + + state = hass.states.get("lawn_mower.test_lawn_mower") + assert state.state == "docked" + + # empty payloads are ignored + async_fire_mqtt_message(hass, "test/lawn_mower_stat", "") + + await hass.async_block_till_done() + + state = hass.states.get("lawn_mower.test_lawn_mower") + assert state.state == "docked" + + +@pytest.mark.parametrize( + ("hass_config", "expected_features"), + [ + ( + DEFAULT_CONFIG, + DEFAULT_FEATURES, + ), + ( + { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: { + "pause_command_topic": "pause-test-topic", + "name": "test", + } + } + }, + LawnMowerEntityFeature.PAUSE, + ), + ( + { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: { + "dock_command_topic": "dock-test-topic", + "start_mowing_command_topic": "start_mowing-test-topic", + "name": "test", + } + } + }, + LawnMowerEntityFeature.START_MOWING | LawnMowerEntityFeature.DOCK, + ), + ], +) +async def test_supported_features( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_features: LawnMowerEntityFeature | None, +) -> None: + """Test conditional enablement of supported features.""" + await mqtt_mock_entry() + assert ( + hass.states.get("lawn_mower.test").attributes["supported_features"] + == expected_features + ) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: { + "activity_state_topic": "test/lawn_mower_stat", + "name": "Test Lawn Mower", + "activity_value_template": "{{ value_json.val }}", + } + } + } + ], +) +async def test_value_template( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test that it fetches the given payload with a template.""" + await mqtt_mock_entry() + + async_fire_mqtt_message(hass, "test/lawn_mower_stat", '{"val":"mowing"}') + + await hass.async_block_till_done() + + state = hass.states.get("lawn_mower.test_lawn_mower") + assert state.state == "mowing" + + async_fire_mqtt_message(hass, "test/lawn_mower_stat", '{"val":"paused"}') + + await hass.async_block_till_done() + + state = hass.states.get("lawn_mower.test_lawn_mower") + assert state.state == "paused" + + async_fire_mqtt_message(hass, "test/lawn_mower_stat", '{"val": null}') + + await hass.async_block_till_done() + + state = hass.states.get("lawn_mower.test_lawn_mower") + assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG], +) +async def test_run_lawn_mower_service_optimistic( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test that service calls work in optimistic mode.""" + + fake_state = State("lawn_mower.test", "docked") + mock_restore_cache(hass, (fake_state,)) + + mqtt_mock = await mqtt_mock_entry() + + state = hass.states.get("lawn_mower.test") + assert state.state == "docked" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + lawn_mower.DOMAIN, + SERVICE_START_MOWING, + {ATTR_ENTITY_ID: "lawn_mower.test"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "start_mowing-test-topic", "start_mowing", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lawn_mower.test") + assert state.state == "mowing" + + await hass.services.async_call( + lawn_mower.DOMAIN, + SERVICE_PAUSE, + {ATTR_ENTITY_ID: "lawn_mower.test"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "pause-test-topic", "pause", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lawn_mower.test") + assert state.state == "paused" + + await hass.services.async_call( + lawn_mower.DOMAIN, + SERVICE_DOCK, + {ATTR_ENTITY_ID: "lawn_mower.test"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with("dock-test-topic", "dock", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lawn_mower.test") + assert state.state == "docked" + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: { + "pause_command_topic": "test/lawn_mower_pause_cmd", + "name": "Test Lawn Mower", + } + } + } + ], +) +async def test_restore_lawn_mower_from_invalid_state( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test that restoring the state skips invalid values.""" + fake_state = State("lawn_mower.test_lawn_mower", "unknown") + mock_restore_cache(hass, (fake_state,)) + + await mqtt_mock_entry() + + state = hass.states.get("lawn_mower.test_lawn_mower") + assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: { + "name": "Test Lawn Mower", + "dock_command_topic": "test/lawn_mower_dock_cmd", + "dock_command_template": '{"action": "{{ value }}"}', + "pause_command_topic": "test/lawn_mower_pause_cmd", + "pause_command_template": '{"action": "{{ value }}"}', + "start_mowing_command_topic": "test/lawn_mower_start_mowing_cmd", + "start_mowing_command_template": '{"action": "{{ value }}"}', + } + } + } + ], +) +async def test_run_lawn_mower_service_optimistic_with_command_templates( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test that service calls work in optimistic mode and with a command_template.""" + fake_state = State("lawn_mower.test_lawn_mower", "docked") + mock_restore_cache(hass, (fake_state,)) + + mqtt_mock = await mqtt_mock_entry() + + state = hass.states.get("lawn_mower.test_lawn_mower") + assert state.state == "docked" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + lawn_mower.DOMAIN, + SERVICE_START_MOWING, + {ATTR_ENTITY_ID: "lawn_mower.test_lawn_mower"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "test/lawn_mower_start_mowing_cmd", '{"action": "start_mowing"}', 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lawn_mower.test_lawn_mower") + assert state.state == "mowing" + + await hass.services.async_call( + lawn_mower.DOMAIN, + SERVICE_PAUSE, + {ATTR_ENTITY_ID: "lawn_mower.test_lawn_mower"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "test/lawn_mower_pause_cmd", '{"action": "pause"}', 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lawn_mower.test_lawn_mower") + assert state.state == "paused" + + await hass.services.async_call( + lawn_mower.DOMAIN, + SERVICE_DOCK, + {ATTR_ENTITY_ID: "lawn_mower.test_lawn_mower"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "test/lawn_mower_dock_cmd", '{"action": "dock"}', 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lawn_mower.test_lawn_mower") + assert state.state == "docked" + + +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_availability_when_connection_lost( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock_entry, lawn_mower.DOMAIN + ) + + +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_availability_without_topic( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_custom_availability_payload( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_attribute_via_mqtt_json_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_blocked_attribute_via_mqtt_json_message( + hass, + mqtt_mock_entry, + lawn_mower.DOMAIN, + DEFAULT_CONFIG, + MQTT_LAWN_MOWER_ATTRIBUTES_BLOCKED, + ) + + +async def test_setting_attribute_with_template( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, + mqtt_mock_entry, + caplog, + lawn_mower.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_update_with_json_attrs_bad_json( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_json( + hass, + mqtt_mock_entry, + caplog, + lawn_mower.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_discovery_update_attr( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, + mqtt_mock_entry, + caplog, + lawn_mower.DOMAIN, + DEFAULT_CONFIG, + ) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: [ + { + "name": "Test 1", + "activity_state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "activity_state_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + } + ], +) +async def test_unique_id( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test unique id action only creates one lawn_mower per unique_id.""" + await help_test_unique_id(hass, mqtt_mock_entry, lawn_mower.DOMAIN) + + +async def test_discovery_removal_lawn_mower( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test removal of discovered lawn_mower.""" + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][lawn_mower.DOMAIN]) + await help_test_discovery_removal( + hass, mqtt_mock_entry, caplog, lawn_mower.DOMAIN, data + ) + + +async def test_discovery_update_lawn_mower( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered lawn_mower.""" + config1 = { + "name": "Beer", + "activity_state_topic": "test-topic", + "command_topic": "test-topic", + "actions": ["milk", "beer"], + } + config2 = { + "name": "Milk", + "activity_state_topic": "test-topic", + "command_topic": "test-topic", + "actions": ["milk"], + } + + await help_test_discovery_update( + hass, mqtt_mock_entry, caplog, lawn_mower.DOMAIN, config1, config2 + ) + + +async def test_discovery_update_unchanged_lawn_mower( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered lawn_mower.""" + data1 = '{ "name": "Beer", "activity_state_topic": "test-topic", "command_topic": "test-topic", "actions": ["milk", "beer"]}' + with patch( + "homeassistant.components.mqtt.lawn_mower.MqttLawnMower.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, + mqtt_mock_entry, + caplog, + lawn_mower.DOMAIN, + data1, + discovery_update, + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling of bad discovery message.""" + data1 = '{ "invalid" }' + data2 = '{ "name": "Milk", "activity_state_topic": "test-topic", "pause_command_topic": "test-topic"}' + + await help_test_discovery_broken( + hass, mqtt_mock_entry, caplog, lawn_mower.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT lawn_mower device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT lawn_mower device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_subscriptions( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT subscriptions are managed when entity_id is updated.""" + config = { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: { + "name": "test", + "activity_state_topic": "test-topic", + "availability_topic": "avty-topic", + } + } + } + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock_entry, lawn_mower.DOMAIN, config, ["avty-topic", "test-topic"] + ) + + +async def test_entity_id_update_discovery_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock_entry, lawn_mower.DOMAIN, DEFAULT_CONFIG + ) + + +@pytest.mark.parametrize( + ("service", "command_payload", "state_payload", "state_topic", "command_topic"), + [ + ( + SERVICE_START_MOWING, + "start_mowing", + "mowing", + "test/lawn_mower_stat", + "start_mowing-test-topic", + ), + ( + SERVICE_PAUSE, + "pause", + "paused", + "test/lawn_mower_stat", + "pause-test-topic", + ), + ( + SERVICE_DOCK, + "dock", + "docked", + "test/lawn_mower_stat", + "dock-test-topic", + ), + ], +) +async def test_entity_debug_info_message( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + service: str, + command_payload: str, + state_payload: str, + state_topic: str, + command_topic: str, +) -> None: + """Test MQTT debug info.""" + config = { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: { + "activity_state_topic": "test/lawn_mower_stat", + "dock_command_topic": "dock-test-topic", + "pause_command_topic": "pause-test-topic", + "start_mowing_command_topic": "start_mowing-test-topic", + "name": "test", + } + } + } + await help_test_entity_debug_info_message( + hass, + mqtt_mock_entry, + lawn_mower.DOMAIN, + config, + service=service, + command_payload=command_payload, + state_payload=state_payload, + state_topic=state_topic, + command_topic=command_topic, + ) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + lawn_mower.DOMAIN: { + "dock_command_topic": "dock-test-topic", + "pause_command_topic": "pause-test-topic", + "start_mowing_command_topic": "start_mowing-test-topic", + "activity_state_topic": "test/lawn_mower_stat", + "name": "Test Lawn Mower", + } + } + } + ], +) +async def test_mqtt_payload_not_a_valid_activity_warning( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test warning for MQTT payload which is not a valid activity.""" + await mqtt_mock_entry() + + async_fire_mqtt_message(hass, "test/lawn_mower_stat", "painting") + + await hass.async_block_till_done() + + assert ( + "Invalid activity for lawn_mower.test_lawn_mower: 'painting' (valid activies: ['error', 'paused', 'mowing', 'docked'])" + in caplog.text + ) + + +@pytest.mark.parametrize( + ("service", "topic", "parameters", "payload", "template"), + [ + ( + SERVICE_START_MOWING, + "start_mowing_command_topic", + {}, + "start_mowing", + "start_mowing_command_template", + ), + ( + SERVICE_PAUSE, + "pause_command_topic", + {}, + "pause", + "pause_command_template", + ), + ( + SERVICE_DOCK, + "dock_command_topic", + {}, + "dock", + "dock_command_template", + ), + ], +) +async def test_publishing_with_custom_encoding( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + service: str, + topic: str, + parameters: dict[str, Any], + payload: str, + template: str | None, +) -> None: + """Test publishing MQTT payload with different encoding.""" + domain = lawn_mower.DOMAIN + config = DEFAULT_CONFIG + + await help_test_publishing_with_custom_encoding( + hass, + mqtt_mock_entry, + caplog, + domain, + config, + service, + topic, + parameters, + payload, + template, + ) + + +async def test_reloadable( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test reloading the MQTT platform.""" + domain = lawn_mower.DOMAIN + config = DEFAULT_CONFIG + await help_test_reloadable(hass, mqtt_client_mock, domain, config) + + +@pytest.mark.parametrize( + ("topic", "value", "attribute", "attribute_value"), + [ + ("activity_state_topic", "paused", None, "paused"), + ("activity_state_topic", "docked", None, "docked"), + ("activity_state_topic", "mowing", None, "mowing"), + ], +) +async def test_encoding_subscribable_topics( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + topic: str, + value: str, + attribute: str | None, + attribute_value: Any, +) -> None: + """Test handling of incoming encoded payload.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][lawn_mower.DOMAIN]) + config["actions"] = ["milk", "beer"] + await help_test_encoding_subscribable_topics( + hass, + mqtt_mock_entry, + lawn_mower.DOMAIN, + config, + topic, + value, + attribute, + attribute_value, + ) + + +@pytest.mark.parametrize( + "hass_config", + [DEFAULT_CONFIG, {"mqtt": [DEFAULT_CONFIG["mqtt"]]}], + ids=["platform_key", "listed"], +) +async def test_setup_manual_entity_from_yaml( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test setup manual configured MQTT entity.""" + await mqtt_mock_entry() + platform = lawn_mower.DOMAIN + assert hass.states.get(f"{platform}.test") + + +async def test_unload_entry( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test unloading the config entry.""" + domain = lawn_mower.DOMAIN + config = DEFAULT_CONFIG + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry, domain, config + ) + + +async def test_persistent_state_after_reconfig( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test of the state is persistent after reconfiguring the lawn_mower activity.""" + await mqtt_mock_entry() + discovery_data = '{ "name": "Garden", "activity_state_topic": "test-topic", "command_topic": "test-topic"}' + await help_test_discovery_setup(hass, LAWN_MOWER_DOMAIN, discovery_data, "garden") + + # assign an initial state + async_fire_mqtt_message(hass, "test-topic", "docked") + state = hass.states.get("lawn_mower.garden") + assert state.state == "docked" + + # change the config + discovery_data = '{ "name": "Garden", "activity_state_topic": "test-topic2", "command_topic": "test-topic"}' + await help_test_discovery_setup(hass, LAWN_MOWER_DOMAIN, discovery_data, "garden") + + # assert the state persistent + state = hass.states.get("lawn_mower.garden") + assert state.state == "docked" From 8768c390213130d8d6ab1eea15e7e8fd6533fe6c Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 25 Aug 2023 12:28:48 -0500 Subject: [PATCH 0893/1151] Wake word cleanup (#98652) * Make arguments for async_pipeline_from_audio_stream keyword-only after hass * Use a bytearray ring buffer * Move generator outside * Move stt stream generator outside * Clean up execute * Refactor VAD to use bytearray * More tests * Refactor chunk_samples to be more correct and robust * Change AudioBuffer to use append instead of setitem * Cleanup --------- Co-authored-by: Paulus Schoutsen --- .../components/assist_pipeline/__init__.py | 1 + .../components/assist_pipeline/pipeline.py | 183 ++++++++++-------- .../components/assist_pipeline/ring_buffer.py | 57 ++++++ .../components/assist_pipeline/vad.py | 153 ++++++++++----- .../components/wake_word/__init__.py | 2 - .../assist_pipeline/snapshots/test_init.ambr | 6 + tests/components/assist_pipeline/test_init.py | 90 +++++---- .../assist_pipeline/test_ring_buffer.py | 38 ++++ tests/components/assist_pipeline/test_vad.py | 91 ++++++++- 9 files changed, 458 insertions(+), 163 deletions(-) create mode 100644 homeassistant/components/assist_pipeline/ring_buffer.py create mode 100644 tests/components/assist_pipeline/test_ring_buffer.py diff --git a/homeassistant/components/assist_pipeline/__init__.py b/homeassistant/components/assist_pipeline/__init__.py index c2d25da2162..4c2fe01036f 100644 --- a/homeassistant/components/assist_pipeline/__init__.py +++ b/homeassistant/components/assist_pipeline/__init__.py @@ -52,6 +52,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_pipeline_from_audio_stream( hass: HomeAssistant, + *, context: Context, event_callback: PipelineEventCallback, stt_metadata: stt.SpeechMetadata, diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 320812b2039..3759fc12c75 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -49,6 +49,7 @@ from .error import ( WakeWordDetectionError, WakeWordTimeoutError, ) +from .ring_buffer import RingBuffer from .vad import VoiceActivityTimeout, VoiceCommandSegmenter _LOGGER = logging.getLogger(__name__) @@ -425,7 +426,6 @@ 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( @@ -448,7 +448,7 @@ class PipelineRun: async def wake_word_detection( self, stream: AsyncIterable[bytes], - audio_buffer: list[bytes], + audio_chunks_for_stt: list[bytes], ) -> wake_word.DetectionResult | None: """Run wake-word-detection portion of pipeline. Returns detection result.""" metadata_dict = asdict( @@ -484,46 +484,29 @@ class PipelineRun: # 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 chunk buffer. This audio will be forwarded to speech-to-text + # after wake-word-detection. + num_audio_bytes_to_buffer = int( + wake_word_settings.audio_seconds_to_buffer * 16000 * 2 # 16-bit @ 16Khz ) - 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" - ) + stt_audio_buffer: RingBuffer | None = None + if num_audio_bytes_to_buffer > 0: + stt_audio_buffer = RingBuffer(num_audio_bytes_to_buffer) try: # Detect wake word(s) result = await self.wake_word_provider.async_process_audio_stream( - timestamped_stream() + _wake_word_audio_stream( + audio_stream=stream, + stt_audio_buffer=stt_audio_buffer, + wake_word_vad=wake_word_vad, + ) ) - if audio_ring_buffer: + if stt_audio_buffer is not None: # All audio kept from right before the wake word was detected as # a single chunk. - audio_buffer.append(audio_ring_buffer) + audio_chunks_for_stt.append(stt_audio_buffer.getvalue()) except WakeWordTimeoutError: _LOGGER.debug("Timeout during wake word detection") raise @@ -540,9 +523,14 @@ class PipelineRun: wake_word_output: dict[str, Any] = {} else: if result.queued_audio: - # Add audio that was pending at detection + # Add audio that was pending at detection. + # + # Because detection occurs *after* the wake word was actually + # spoken, we need to make sure pending audio is forwarded to + # speech-to-text so the user does not have to pause before + # speaking the voice command. for chunk_ts in result.queued_audio: - audio_buffer.append(chunk_ts[0]) + audio_chunks_for_stt.append(chunk_ts[0]) wake_word_output = asdict(result) @@ -608,41 +596,12 @@ class PipelineRun: ) try: - segmenter = VoiceCommandSegmenter() - - async def segment_stream( - stream: AsyncIterable[bytes], - ) -> AsyncGenerator[bytes, None]: - """Stop stream when voice command is finished.""" - sent_vad_start = False - timestamp_ms = 0 - async for chunk in stream: - if not segmenter.process(chunk): - # Silence detected at the end of voice command - self.process_event( - PipelineEvent( - PipelineEventType.STT_VAD_END, - {"timestamp": timestamp_ms}, - ) - ) - break - - if segmenter.in_command and (not sent_vad_start): - # Speech detected at start of voice command - self.process_event( - PipelineEvent( - PipelineEventType.STT_VAD_START, - {"timestamp": timestamp_ms}, - ) - ) - sent_vad_start = True - - yield chunk - timestamp_ms += (len(chunk) // 2) // 16 # milliseconds @ 16Khz - # Transcribe audio stream result = await self.stt_provider.async_process_audio_stream( - metadata, segment_stream(stream) + metadata, + self._speech_to_text_stream( + audio_stream=stream, stt_vad=VoiceCommandSegmenter() + ), ) except Exception as src_error: _LOGGER.exception("Unexpected error during speech-to-text") @@ -677,6 +636,42 @@ class PipelineRun: return result.text + async def _speech_to_text_stream( + self, + audio_stream: AsyncIterable[bytes], + stt_vad: VoiceCommandSegmenter | None, + sample_rate: int = 16000, + sample_width: int = 2, + ) -> AsyncGenerator[bytes, None]: + """Yield audio chunks until VAD detects silence or speech-to-text completes.""" + ms_per_sample = sample_rate // 1000 + sent_vad_start = False + timestamp_ms = 0 + async for chunk in audio_stream: + if stt_vad is not None: + if not stt_vad.process(chunk): + # Silence detected at the end of voice command + self.process_event( + PipelineEvent( + PipelineEventType.STT_VAD_END, + {"timestamp": timestamp_ms}, + ) + ) + break + + if stt_vad.in_command and (not sent_vad_start): + # Speech detected at start of voice command + self.process_event( + PipelineEvent( + PipelineEventType.STT_VAD_START, + {"timestamp": timestamp_ms}, + ) + ) + sent_vad_start = True + + yield chunk + timestamp_ms += (len(chunk) // sample_width) // ms_per_sample + async def prepare_recognize_intent(self) -> None: """Prepare recognizing an intent.""" agent_info = conversation.async_get_agent_info( @@ -861,13 +856,14 @@ class PipelineInput: """Run pipeline.""" self.run.start() current_stage: PipelineStage | None = self.run.start_stage - audio_buffer: list[bytes] = [] + stt_audio_buffer: list[bytes] = [] try: if current_stage == PipelineStage.WAKE_WORD: + # wake-word-detection assert self.stt_stream is not None detect_result = await self.run.wake_word_detection( - self.stt_stream, audio_buffer + self.stt_stream, stt_audio_buffer ) if detect_result is None: # No wake word. Abort the rest of the pipeline. @@ -882,19 +878,22 @@ class PipelineInput: assert self.stt_metadata is not None assert self.stt_stream is not None - if audio_buffer: + stt_stream = self.stt_stream - async def buffered_stream() -> AsyncGenerator[bytes, None]: - for chunk in audio_buffer: + if stt_audio_buffer: + # Send audio in the buffer first to speech-to-text, then move on to stt_stream. + # This is basically an async itertools.chain. + async def buffer_then_audio_stream() -> AsyncGenerator[bytes, None]: + # Buffered audio + for chunk in stt_audio_buffer: yield chunk + # Streamed audio 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 + stt_stream = buffer_then_audio_stream() intent_input = await self.run.speech_to_text( self.stt_metadata, @@ -906,6 +905,7 @@ class PipelineInput: tts_input = self.tts_input if current_stage == PipelineStage.INTENT: + # intent-recognition assert intent_input is not None tts_input = await self.run.recognize_intent( intent_input, @@ -915,6 +915,7 @@ class PipelineInput: current_stage = PipelineStage.TTS if self.run.end_stage != PipelineStage.INTENT: + # text-to-speech if current_stage == PipelineStage.TTS: assert tts_input is not None await self.run.text_to_speech(tts_input) @@ -999,6 +1000,36 @@ class PipelineInput: await asyncio.gather(*prepare_tasks) +async def _wake_word_audio_stream( + audio_stream: AsyncIterable[bytes], + stt_audio_buffer: RingBuffer | None, + wake_word_vad: VoiceActivityTimeout | None, + sample_rate: int = 16000, + sample_width: int = 2, +) -> AsyncIterable[tuple[bytes, int]]: + """Yield audio chunks with timestamps (milliseconds since start of stream). + + Adds audio to a ring buffer that will be forwarded to speech-to-text after + detection. Times out if VAD detects enough silence. + """ + ms_per_sample = sample_rate // 1000 + timestamp_ms = 0 + async for chunk in audio_stream: + yield chunk, timestamp_ms + timestamp_ms += (len(chunk) // sample_width) // ms_per_sample + + # Wake-word-detection occurs *after* the wake word was actually + # spoken. Keeping audio right before detection allows the voice + # command to be spoken immediately after the wake word. + if stt_audio_buffer is not None: + stt_audio_buffer.put(chunk) + + 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" + ) + + class PipelinePreferred(CollectionError): """Raised when attempting to delete the preferred pipelen.""" diff --git a/homeassistant/components/assist_pipeline/ring_buffer.py b/homeassistant/components/assist_pipeline/ring_buffer.py new file mode 100644 index 00000000000..d134389216c --- /dev/null +++ b/homeassistant/components/assist_pipeline/ring_buffer.py @@ -0,0 +1,57 @@ +"""Implementation of a ring buffer using bytearray.""" + + +class RingBuffer: + """Basic ring buffer using a bytearray. + + Not threadsafe. + """ + + def __init__(self, maxlen: int) -> None: + """Initialize empty buffer.""" + self._buffer = bytearray(maxlen) + self._pos = 0 + self._length = 0 + self._maxlen = maxlen + + @property + def maxlen(self) -> int: + """Return the maximum size of the buffer.""" + return self._maxlen + + @property + def pos(self) -> int: + """Return the current put position.""" + return self._pos + + def __len__(self) -> int: + """Return the length of data stored in the buffer.""" + return self._length + + def put(self, data: bytes) -> None: + """Put a chunk of data into the buffer, possibly wrapping around.""" + data_len = len(data) + new_pos = self._pos + data_len + if new_pos >= self._maxlen: + # Split into two chunks + num_bytes_1 = self._maxlen - self._pos + num_bytes_2 = new_pos - self._maxlen + + self._buffer[self._pos : self._maxlen] = data[:num_bytes_1] + self._buffer[:num_bytes_2] = data[num_bytes_1:] + new_pos = new_pos - self._maxlen + else: + # Entire chunk fits at current position + self._buffer[self._pos : self._pos + data_len] = data + + self._pos = new_pos + self._length = min(self._maxlen, self._length + data_len) + + def getvalue(self) -> bytes: + """Get bytes written to the buffer.""" + if (self._pos + self._length) <= self._maxlen: + # Single chunk + return bytes(self._buffer[: self._length]) + + # Two chunks + return bytes(self._buffer[self._pos :] + self._buffer[: self._pos]) diff --git a/homeassistant/components/assist_pipeline/vad.py b/homeassistant/components/assist_pipeline/vad.py index cae31671a3c..20a048d5621 100644 --- a/homeassistant/components/assist_pipeline/vad.py +++ b/homeassistant/components/assist_pipeline/vad.py @@ -1,12 +1,15 @@ """Voice activity detection.""" from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass, field from enum import StrEnum +from typing import Final import webrtcvad -_SAMPLE_RATE = 16000 +_SAMPLE_RATE: Final = 16000 # Hz +_SAMPLE_WIDTH: Final = 2 # bytes class VadSensitivity(StrEnum): @@ -29,6 +32,45 @@ class VadSensitivity(StrEnum): return 1.0 +class AudioBuffer: + """Fixed-sized audio buffer with variable internal length.""" + + def __init__(self, maxlen: int) -> None: + """Initialize buffer.""" + self._buffer = bytearray(maxlen) + self._length = 0 + + @property + def length(self) -> int: + """Get number of bytes currently in the buffer.""" + return self._length + + def clear(self) -> None: + """Clear the buffer.""" + self._length = 0 + + def append(self, data: bytes) -> None: + """Append bytes to the buffer, increasing the internal length.""" + data_len = len(data) + if (self._length + data_len) > len(self._buffer): + raise ValueError("Length cannot be greater than buffer size") + + self._buffer[self._length : self._length + data_len] = data + self._length += data_len + + def bytes(self) -> bytes: + """Convert written portion of buffer to bytes.""" + return bytes(self._buffer[: self._length]) + + def __len__(self) -> int: + """Get the number of bytes currently in the buffer.""" + return self._length + + def __bool__(self) -> bool: + """Return True if there are bytes in the buffer.""" + return self._length > 0 + + @dataclass class VoiceCommandSegmenter: """Segments an audio stream into voice commands using webrtcvad.""" @@ -36,7 +78,7 @@ class VoiceCommandSegmenter: vad_mode: int = 3 """Aggressiveness in filtering out non-speech. 3 is the most aggressive.""" - vad_frames: int = 480 # 30 ms + vad_samples_per_chunk: int = 480 # 30 ms """Must be 10, 20, or 30 ms at 16Khz.""" speech_seconds: float = 0.3 @@ -67,20 +109,23 @@ class VoiceCommandSegmenter: """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 + _leftover_chunk_buffer: AudioBuffer = field(init=False) + _bytes_per_chunk: int = field(init=False) + _seconds_per_chunk: float = field(init=False) 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._bytes_per_chunk = self.vad_samples_per_chunk * _SAMPLE_WIDTH + self._seconds_per_chunk = self.vad_samples_per_chunk / _SAMPLE_RATE + self._leftover_chunk_buffer = AudioBuffer( + self.vad_samples_per_chunk * _SAMPLE_WIDTH + ) self.reset() def reset(self) -> None: """Reset all counters and state.""" - self._audio_buffer = b"" + self._leftover_chunk_buffer.clear() self._speech_seconds_left = self.speech_seconds self._silence_seconds_left = self.silence_seconds self._timeout_seconds_left = self.timeout_seconds @@ -92,27 +137,20 @@ class VoiceCommandSegmenter: Returns False when command is done. """ - 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 - ] + for chunk in chunk_samples( + samples, self._bytes_per_chunk, self._leftover_chunk_buffer + ): if not self._process_chunk(chunk): self.reset() return False - if num_chunks > 0: - # Remove from buffer - self._audio_buffer = self._audio_buffer[ - num_chunks * self._bytes_per_chunk : - ] - return True + @property + def audio_buffer(self) -> bytes: + """Get partial chunk in the audio buffer.""" + return self._leftover_chunk_buffer.bytes() + def _process_chunk(self, chunk: bytes) -> bool: """Process a single chunk of 16-bit 16Khz mono audio. @@ -163,7 +201,7 @@ class VoiceActivityTimeout: vad_mode: int = 3 """Aggressiveness in filtering out non-speech. 3 is the most aggressive.""" - vad_frames: int = 480 # 30 ms + vad_samples_per_chunk: int = 480 # 30 ms """Must be 10, 20, or 30 ms at 16Khz.""" _silence_seconds_left: float = 0.0 @@ -173,20 +211,23 @@ class VoiceActivityTimeout: """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 + _leftover_chunk_buffer: AudioBuffer = field(init=False) + _bytes_per_chunk: int = field(init=False) + _seconds_per_chunk: float = field(init=False) 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._bytes_per_chunk = self.vad_samples_per_chunk * _SAMPLE_WIDTH + self._seconds_per_chunk = self.vad_samples_per_chunk / _SAMPLE_RATE + self._leftover_chunk_buffer = AudioBuffer( + self.vad_samples_per_chunk * _SAMPLE_WIDTH + ) self.reset() def reset(self) -> None: """Reset all counters and state.""" - self._audio_buffer = b"" + self._leftover_chunk_buffer.clear() self._silence_seconds_left = self.silence_seconds self._reset_seconds_left = self.reset_seconds @@ -195,24 +236,12 @@ class VoiceActivityTimeout: 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 - ] + for chunk in chunk_samples( + samples, self._bytes_per_chunk, self._leftover_chunk_buffer + ): 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: @@ -239,3 +268,37 @@ class VoiceActivityTimeout: ) return True + + +def chunk_samples( + samples: bytes, + bytes_per_chunk: int, + leftover_chunk_buffer: AudioBuffer, +) -> Iterable[bytes]: + """Yield fixed-sized chunks from samples, keeping leftover bytes from previous call(s).""" + + if (len(leftover_chunk_buffer) + len(samples)) < bytes_per_chunk: + # Extend leftover chunk, but not enough samples to complete it + leftover_chunk_buffer.append(samples) + return + + next_chunk_idx = 0 + + if leftover_chunk_buffer: + # Add to leftover chunk from previous call(s). + bytes_to_copy = bytes_per_chunk - len(leftover_chunk_buffer) + leftover_chunk_buffer.append(samples[:bytes_to_copy]) + next_chunk_idx = bytes_to_copy + + # Process full chunk in buffer + yield leftover_chunk_buffer.bytes() + leftover_chunk_buffer.clear() + + while next_chunk_idx < len(samples) - bytes_per_chunk + 1: + # Process full chunk + yield samples[next_chunk_idx : next_chunk_idx + bytes_per_chunk] + next_chunk_idx += bytes_per_chunk + + # Capture leftover chunks + if rest_samples := samples[next_chunk_idx:]: + leftover_chunk_buffer.append(rest_samples) diff --git a/homeassistant/components/wake_word/__init__.py b/homeassistant/components/wake_word/__init__.py index 0a751b7eea2..b308cf98912 100644 --- a/homeassistant/components/wake_word/__init__.py +++ b/homeassistant/components/wake_word/__init__.py @@ -79,8 +79,6 @@ class WakeWordDetectionEntity(RestoreEntity): @final def state(self) -> str | None: """Return the state of the entity.""" - if self.__last_detected is None: - return None return self.__last_detected @property diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index 58835e37973..7c1cf0e2b2d 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -317,6 +317,12 @@ }), 'type': , }), + dict({ + 'data': dict({ + 'timestamp': 1500, + }), + 'type': , + }), dict({ 'data': dict({ 'stt_output': dict({ diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 184f479f830..aba9862614b 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -1,7 +1,7 @@ """Test Voice Assistant init.""" from dataclasses import asdict import itertools as it -from unittest.mock import ANY +from unittest.mock import ANY, patch import pytest from syrupy.assertion import SnapshotAssertion @@ -49,9 +49,9 @@ async def test_pipeline_from_audio_stream_auto( await assist_pipeline.async_pipeline_from_audio_stream( hass, - Context(), - events.append, - stt.SpeechMetadata( + context=Context(), + event_callback=events.append, + stt_metadata=stt.SpeechMetadata( language="", format=stt.AudioFormats.WAV, codec=stt.AudioCodecs.PCM, @@ -59,7 +59,7 @@ async def test_pipeline_from_audio_stream_auto( sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, channel=stt.AudioChannels.CHANNEL_MONO, ), - audio_data(), + stt_stream=audio_data(), ) assert process_events(events) == snapshot @@ -108,9 +108,9 @@ async def test_pipeline_from_audio_stream_legacy( # Use the created pipeline await assist_pipeline.async_pipeline_from_audio_stream( hass, - Context(), - events.append, - stt.SpeechMetadata( + context=Context(), + event_callback=events.append, + stt_metadata=stt.SpeechMetadata( language="en-UK", format=stt.AudioFormats.WAV, codec=stt.AudioCodecs.PCM, @@ -118,7 +118,7 @@ async def test_pipeline_from_audio_stream_legacy( sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, channel=stt.AudioChannels.CHANNEL_MONO, ), - audio_data(), + stt_stream=audio_data(), pipeline_id=pipeline_id, ) @@ -168,9 +168,9 @@ async def test_pipeline_from_audio_stream_entity( # Use the created pipeline await assist_pipeline.async_pipeline_from_audio_stream( hass, - Context(), - events.append, - stt.SpeechMetadata( + context=Context(), + event_callback=events.append, + stt_metadata=stt.SpeechMetadata( language="en-UK", format=stt.AudioFormats.WAV, codec=stt.AudioCodecs.PCM, @@ -178,7 +178,7 @@ async def test_pipeline_from_audio_stream_entity( sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, channel=stt.AudioChannels.CHANNEL_MONO, ), - audio_data(), + stt_stream=audio_data(), pipeline_id=pipeline_id, ) @@ -229,9 +229,9 @@ async def test_pipeline_from_audio_stream_no_stt( with pytest.raises(assist_pipeline.pipeline.PipelineRunValidationError): await assist_pipeline.async_pipeline_from_audio_stream( hass, - Context(), - events.append, - stt.SpeechMetadata( + context=Context(), + event_callback=events.append, + stt_metadata=stt.SpeechMetadata( language="en-UK", format=stt.AudioFormats.WAV, codec=stt.AudioCodecs.PCM, @@ -239,7 +239,7 @@ async def test_pipeline_from_audio_stream_no_stt( sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, channel=stt.AudioChannels.CHANNEL_MONO, ), - audio_data(), + stt_stream=audio_data(), pipeline_id=pipeline_id, ) @@ -268,9 +268,9 @@ async def test_pipeline_from_audio_stream_unknown_pipeline( with pytest.raises(assist_pipeline.PipelineNotFound): await assist_pipeline.async_pipeline_from_audio_stream( hass, - Context(), - events.append, - stt.SpeechMetadata( + context=Context(), + event_callback=events.append, + stt_metadata=stt.SpeechMetadata( language="en-UK", format=stt.AudioFormats.WAV, codec=stt.AudioCodecs.PCM, @@ -278,7 +278,7 @@ async def test_pipeline_from_audio_stream_unknown_pipeline( sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, channel=stt.AudioChannels.CHANNEL_MONO, ), - audio_data(), + stt_stream=audio_data(), pipeline_id="blah", ) @@ -308,26 +308,38 @@ async def test_pipeline_from_audio_stream_wake_word( yield b"wake word" yield b"part1" yield b"part2" + yield b"end" 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 - ), - ) + def continue_stt(self, chunk): + # Ensure stt_vad_start event is triggered + self.in_command = True + + # Stop on fake end chunk to trigger stt_vad_end + return chunk != b"end" + + with patch( + "homeassistant.components.assist_pipeline.pipeline.VoiceCommandSegmenter.process", + continue_stt, + ): + await assist_pipeline.async_pipeline_from_audio_stream( + hass, + context=Context(), + event_callback=events.append, + stt_metadata=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, + ), + stt_stream=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 diff --git a/tests/components/assist_pipeline/test_ring_buffer.py b/tests/components/assist_pipeline/test_ring_buffer.py new file mode 100644 index 00000000000..22185c3ad5b --- /dev/null +++ b/tests/components/assist_pipeline/test_ring_buffer.py @@ -0,0 +1,38 @@ +"""Tests for audio ring buffer.""" +from homeassistant.components.assist_pipeline.ring_buffer import RingBuffer + + +def test_ring_buffer_empty() -> None: + """Test empty ring buffer.""" + rb = RingBuffer(10) + assert rb.maxlen == 10 + assert rb.pos == 0 + assert rb.getvalue() == b"" + + +def test_ring_buffer_put_1() -> None: + """Test putting some data smaller than the maximum length.""" + rb = RingBuffer(10) + rb.put(bytes([1, 2, 3, 4, 5])) + assert len(rb) == 5 + assert rb.pos == 5 + assert rb.getvalue() == bytes([1, 2, 3, 4, 5]) + + +def test_ring_buffer_put_2() -> None: + """Test putting some data past the end of the buffer.""" + rb = RingBuffer(10) + rb.put(bytes([1, 2, 3, 4, 5])) + rb.put(bytes([6, 7, 8, 9, 10, 11, 12])) + assert len(rb) == 10 + assert rb.pos == 2 + assert rb.getvalue() == bytes([3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) + + +def test_ring_buffer_put_too_large() -> None: + """Test putting data too large for the buffer.""" + rb = RingBuffer(10) + rb.put(bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])) + assert len(rb) == 10 + assert rb.pos == 2 + assert rb.getvalue() == bytes([3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) diff --git a/tests/components/assist_pipeline/test_vad.py b/tests/components/assist_pipeline/test_vad.py index 3a5c763ee5c..4dc8c8f6197 100644 --- a/tests/components/assist_pipeline/test_vad.py +++ b/tests/components/assist_pipeline/test_vad.py @@ -1,7 +1,12 @@ """Tests for webrtcvad voice command segmenter.""" +import itertools as it from unittest.mock import patch -from homeassistant.components.assist_pipeline.vad import VoiceCommandSegmenter +from homeassistant.components.assist_pipeline.vad import ( + AudioBuffer, + VoiceCommandSegmenter, + chunk_samples, +) _ONE_SECOND = 16000 * 2 # 16Khz 16-bit @@ -36,3 +41,87 @@ def test_speech() -> None: # silence # False return value indicates voice command is finished assert not segmenter.process(bytes(_ONE_SECOND)) + + +def test_audio_buffer() -> None: + """Test audio buffer wrapping.""" + + def is_speech(self, chunk, sample_rate): + """Disable VAD.""" + return False + + with patch( + "webrtcvad.Vad.is_speech", + new=is_speech, + ): + segmenter = VoiceCommandSegmenter() + bytes_per_chunk = segmenter.vad_samples_per_chunk * 2 + + with patch.object( + segmenter, "_process_chunk", return_value=True + ) as mock_process: + # Partially fill audio buffer + half_chunk = bytes(it.islice(it.cycle(range(256)), bytes_per_chunk // 2)) + segmenter.process(half_chunk) + + assert not mock_process.called + assert segmenter.audio_buffer == half_chunk + + # Fill and wrap with 1/4 chunk left over + three_quarters_chunk = bytes( + it.islice(it.cycle(range(256)), int(0.75 * bytes_per_chunk)) + ) + segmenter.process(three_quarters_chunk) + + assert mock_process.call_count == 1 + assert ( + segmenter.audio_buffer + == three_quarters_chunk[ + len(three_quarters_chunk) - (bytes_per_chunk // 4) : + ] + ) + assert ( + mock_process.call_args[0][0] + == half_chunk + three_quarters_chunk[: bytes_per_chunk // 2] + ) + + # Run 2 chunks through + segmenter.reset() + assert len(segmenter.audio_buffer) == 0 + + mock_process.reset_mock() + two_chunks = bytes(it.islice(it.cycle(range(256)), bytes_per_chunk * 2)) + segmenter.process(two_chunks) + + assert mock_process.call_count == 2 + assert len(segmenter.audio_buffer) == 0 + assert mock_process.call_args_list[0][0][0] == two_chunks[:bytes_per_chunk] + assert mock_process.call_args_list[1][0][0] == two_chunks[bytes_per_chunk:] + + +def test_partial_chunk() -> None: + """Test that chunk_samples returns when given a partial chunk.""" + bytes_per_chunk = 5 + samples = bytes([1, 2, 3]) + leftover_chunk_buffer = AudioBuffer(bytes_per_chunk) + chunks = list(chunk_samples(samples, bytes_per_chunk, leftover_chunk_buffer)) + + assert len(chunks) == 0 + assert leftover_chunk_buffer.bytes() == samples + + +def test_chunk_samples_leftover() -> None: + """Test that chunk_samples property keeps left over bytes across calls.""" + bytes_per_chunk = 5 + samples = bytes([1, 2, 3, 4, 5, 6]) + leftover_chunk_buffer = AudioBuffer(bytes_per_chunk) + chunks = list(chunk_samples(samples, bytes_per_chunk, leftover_chunk_buffer)) + + assert len(chunks) == 1 + assert leftover_chunk_buffer.bytes() == bytes([6]) + + # Add some more to the chunk + chunks = list(chunk_samples(samples, bytes_per_chunk, leftover_chunk_buffer)) + + assert len(chunks) == 1 + assert leftover_chunk_buffer.bytes() == bytes([5, 6]) From 3a71e21d6a6551877eb529b0d8205242216146dd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 25 Aug 2023 19:47:13 +0200 Subject: [PATCH 0894/1151] Add and improve comments about staggering of event listeners (#99058) --- homeassistant/helpers/event.py | 6 +++++- tests/common.py | 12 +++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index e615a6422f0..daad994bbd4 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -68,6 +68,10 @@ _ENTITIES_LISTENER = "entities" _LOGGER = logging.getLogger(__name__) +# Used to spread async_track_utc_time_change listeners and DataUpdateCoordinator +# refresh cycles between RANDOM_MICROSECOND_MIN..RANDOM_MICROSECOND_MAX. +# The values have been determined experimentally in production testing, background +# in PR https://github.com/home-assistant/core/pull/82233 RANDOM_MICROSECOND_MIN = 50000 RANDOM_MICROSECOND_MAX = 500000 @@ -1640,7 +1644,7 @@ def async_track_utc_time_change( matching_seconds = dt_util.parse_time_expression(second, 0, 59) matching_minutes = dt_util.parse_time_expression(minute, 0, 59) matching_hours = dt_util.parse_time_expression(hour, 0, 23) - # Avoid aligning all time trackers to the same second + # Avoid aligning all time trackers to the same fraction of a second # since it can create a thundering herd problem # https://github.com/home-assistant/core/issues/82231 microsecond = randint(RANDOM_MICROSECOND_MIN, RANDOM_MICROSECOND_MAX) diff --git a/tests/common.py b/tests/common.py index 0b63a9a2ef6..6ee38b72532 100644 --- a/tests/common.py +++ b/tests/common.py @@ -59,6 +59,7 @@ from homeassistant.helpers import ( entity, entity_platform, entity_registry as er, + event, intent, issue_registry as ir, recorder as recorder_helper, @@ -397,9 +398,10 @@ def async_fire_time_changed( ) -> None: """Fire a time changed event. - This function will add up to 0.5 seconds to the time to ensure that - it accounts for the accidental synchronization avoidance code in repeating - listeners. + If called within the first 500 ms of a second, time will be bumped to exactly + 500 ms to match the async_track_utc_time_change event listeners and + DataUpdateCoordinator which spreads all updates between 0.05..0.50. + Background in PR https://github.com/home-assistant/core/pull/82233 As asyncio is cooperative, we can't guarantee that the event loop will run an event at the exact time we want. If you need to fire time changed @@ -410,12 +412,12 @@ def async_fire_time_changed( else: utc_datetime = dt_util.as_utc(datetime_) - if utc_datetime.microsecond < 500000: + if utc_datetime.microsecond < event.RANDOM_MICROSECOND_MAX: # Allow up to 500000 microseconds to be added to the time # to handle update_coordinator's and # async_track_time_interval's # staggering to avoid thundering herd. - utc_datetime = utc_datetime.replace(microsecond=500000) + utc_datetime = utc_datetime.replace(microsecond=event.RANDOM_MICROSECOND_MAX) _async_fire_time_changed(hass, utc_datetime, fire_all) From 57144a6064db0ff97cdc536580a329381bc3b1d3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 25 Aug 2023 20:12:21 +0200 Subject: [PATCH 0895/1151] Use entity descriptions in Switcher (#98958) --- .../components/switcher_kis/sensor.py | 75 +++++++------------ 1 file changed, 29 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index 2c74f14cb5c..0b6263a6b2e 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -1,19 +1,19 @@ """Switcher integration Sensor platform.""" from __future__ import annotations -from dataclasses import dataclass - from aioswitcher.device import DeviceCategory from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfElectricCurrent, UnitOfPower 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_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -22,48 +22,36 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import SwitcherDataUpdateCoordinator from .const import SIGNAL_DEVICE_ADD - -@dataclass -class AttributeDescription: - """Class to describe a sensor.""" - - name: str - icon: str | None = None - unit: str | None = None - device_class: SensorDeviceClass | None = None - state_class: SensorStateClass | None = None - default_enabled: bool = True - - -POWER_SENSORS = { - "power_consumption": AttributeDescription( +POWER_SENSORS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="power_consumption", name="Power Consumption", - unit=UnitOfPower.WATT, + native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), - "electric_current": AttributeDescription( + SensorEntityDescription( + key="electric_current", name="Electric Current", - unit=UnitOfElectricCurrent.AMPERE, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), -} - -TIME_SENSORS = { - "remaining_time": AttributeDescription( - name="Remaining Time", - icon="mdi:av-timer", +] +TIME_SENSORS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="remaining_time", name="Remaining Time", icon="mdi:av-timer" ), - "auto_off_set": AttributeDescription( + SensorEntityDescription( + key="auto_off_set", name="Auto Shutdown", icon="mdi:progress-clock", - default_enabled=False, + entity_registry_enabled_default=False, ), -} +] POWER_PLUG_SENSORS = POWER_SENSORS -WATER_HEATER_SENSORS = {**POWER_SENSORS, **TIME_SENSORS} +WATER_HEATER_SENSORS = [*POWER_SENSORS, *TIME_SENSORS] async def async_setup_entry( @@ -78,13 +66,13 @@ async def async_setup_entry( """Add sensors from Switcher device.""" if coordinator.data.device_type.category == DeviceCategory.POWER_PLUG: async_add_entities( - SwitcherSensorEntity(coordinator, attribute, info) - for attribute, info in POWER_PLUG_SENSORS.items() + SwitcherSensorEntity(coordinator, description) + for description in POWER_PLUG_SENSORS ) elif coordinator.data.device_type.category == DeviceCategory.WATER_HEATER: async_add_entities( - SwitcherSensorEntity(coordinator, attribute, info) - for attribute, info in WATER_HEATER_SENSORS.items() + SwitcherSensorEntity(coordinator, description) + for description in WATER_HEATER_SENSORS ) config_entry.async_on_unload( @@ -100,28 +88,23 @@ class SwitcherSensorEntity( def __init__( self, coordinator: SwitcherDataUpdateCoordinator, - attribute: str, - description: AttributeDescription, + description: SensorEntityDescription, ) -> None: """Initialize the entity.""" super().__init__(coordinator) - self.attribute = attribute + self.entity_description = description # Entity class attributes self._attr_name = f"{coordinator.name} {description.name}" - self._attr_icon = description.icon - self._attr_native_unit_of_measurement = description.unit - self._attr_device_class = description.device_class - self._attr_entity_registry_enabled_default = description.default_enabled self._attr_unique_id = ( - f"{coordinator.device_id}-{coordinator.mac_address}-{attribute}" + f"{coordinator.device_id}-{coordinator.mac_address}-{description.key}" + ) + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)} ) - self._attr_device_info = { - "connections": {(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)} - } @property def native_value(self) -> StateType: """Return value of sensor.""" - return getattr(self.coordinator.data, self.attribute) # type: ignore[no-any-return] + return getattr(self.coordinator.data, self.entity_description.key) # type: ignore[no-any-return] From 544d6b05a5fe289c099840efd9f0f5d2fcc42c0a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 Aug 2023 22:54:55 +0200 Subject: [PATCH 0896/1151] Replace mock_coro with AsyncMock (#99014) * Replace mock_coro with AsyncMock * Remove mock_coro test helper function * Remove redundant AsyncMock --- tests/common.py | 10 ---------- tests/components/ecobee/test_config_flow.py | 8 +++----- tests/components/ios/test_init.py | 10 ++++++---- tests/components/logi_circle/test_config_flow.py | 6 ++++-- tests/components/mill/test_init.py | 6 ++++-- tests/components/mobile_app/test_http_api.py | 3 +-- tests/components/onboarding/test_init.py | 3 +-- tests/components/owntracks/test_device_tracker.py | 8 ++++---- tests/components/spaceapi/test_init.py | 4 +--- tests/components/spc/test_init.py | 8 +++----- tests/components/syncthru/test_config_flow.py | 4 ++-- tests/components/websocket_api/test_auth.py | 2 -- tests/components/zha/test_button.py | 6 ++---- tests/components/zha/test_device_action.py | 4 ++-- tests/components/zha/test_number.py | 4 +--- tests/components/zha/test_siren.py | 8 ++++---- tests/test_bootstrap.py | 3 +-- tests/test_config_entries.py | 6 ++++-- 18 files changed, 43 insertions(+), 60 deletions(-) diff --git a/tests/common.py b/tests/common.py index 6ee38b72532..6ccb804ee73 100644 --- a/tests/common.py +++ b/tests/common.py @@ -965,16 +965,6 @@ def patch_yaml_files(files_dict, endswith=True): return patch.object(yaml_loader, "open", mock_open_f, create=True) -def mock_coro(return_value=None, exception=None): - """Return a coro that returns a value or raise an exception.""" - fut = asyncio.Future() - if exception is not None: - fut.set_exception(exception) - else: - fut.set_result(return_value) - return fut - - @contextmanager def assert_setup_component(count, domain=None): """Collect valid configuration from setup_component. diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index a4185313f5f..7d79a10e912 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -14,7 +14,7 @@ from homeassistant.components.ecobee.const import ( from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry async def test_abort_if_already_setup(hass: HomeAssistant) -> None: @@ -175,9 +175,7 @@ async def test_import_flow_triggered_with_ecobee_conf_and_invalid_data( with patch( "homeassistant.components.ecobee.config_flow.load_json_object", return_value=MOCK_ECOBEE_CONF, - ), patch.object( - flow, "async_step_user", return_value=mock_coro() - ) as mock_async_step_user: + ), patch.object(flow, "async_step_user") as mock_async_step_user: await flow.async_step_import(import_data=None) mock_async_step_user.assert_called_once_with( @@ -201,7 +199,7 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_stale_t ), patch( "homeassistant.components.ecobee.config_flow.Ecobee" ) as mock_ecobee, patch.object( - flow, "async_step_user", return_value=mock_coro() + flow, "async_step_user" ) as mock_async_step_user: mock_ecobee = mock_ecobee.return_value mock_ecobee.refresh_tokens.return_value = False diff --git a/tests/components/ios/test_init.py b/tests/components/ios/test_init.py index 67c8bbde2cc..9586bd3c011 100644 --- a/tests/components/ios/test_init.py +++ b/tests/components/ios/test_init.py @@ -7,7 +7,7 @@ from homeassistant.components import ios from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import mock_component, mock_coro +from tests.common import mock_component @pytest.fixture(autouse=True) @@ -28,7 +28,7 @@ async def test_creating_entry_sets_up_sensor(hass: HomeAssistant) -> None: """Test setting up iOS loads the sensor component.""" with patch( "homeassistant.components.ios.sensor.async_setup_entry", - return_value=mock_coro(True), + return_value=True, ) as mock_setup: assert await async_setup_component(hass, ios.DOMAIN, {ios.DOMAIN: {}}) await hass.async_block_till_done() @@ -39,7 +39,8 @@ async def test_creating_entry_sets_up_sensor(hass: HomeAssistant) -> None: async def test_configuring_ios_creates_entry(hass: HomeAssistant) -> None: """Test that specifying config will create an entry.""" with patch( - "homeassistant.components.ios.async_setup_entry", return_value=mock_coro(True) + "homeassistant.components.ios.async_setup_entry", + return_value=True, ) as mock_setup: await async_setup_component(hass, ios.DOMAIN, {"ios": {"push": {}}}) await hass.async_block_till_done() @@ -50,7 +51,8 @@ async def test_configuring_ios_creates_entry(hass: HomeAssistant) -> None: async def test_not_configuring_ios_not_creates_entry(hass: HomeAssistant) -> None: """Test that no config will not create an entry.""" with patch( - "homeassistant.components.ios.async_setup_entry", return_value=mock_coro(True) + "homeassistant.components.ios.async_setup_entry", + return_value=True, ) as mock_setup: await async_setup_component(hass, ios.DOMAIN, {"foo": "bar"}) await hass.async_block_till_done() diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 885459a5df2..de4a9bd4da4 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant.components.logi_circle.config_flow import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry class MockRequest: @@ -50,10 +50,12 @@ def mock_logi_circle(): with patch( "homeassistant.components.logi_circle.config_flow.LogiCircle" ) as logi_circle: + future = asyncio.Future() + future.set_result({"accountId": "testId"}) LogiCircle = logi_circle() LogiCircle.authorize = AsyncMock(return_value=True) LogiCircle.close = AsyncMock(return_value=True) - LogiCircle.account = mock_coro(return_value={"accountId": "testId"}) + LogiCircle.account = future LogiCircle.authorize_url = "http://authorize.url" yield LogiCircle diff --git a/tests/components/mill/test_init.py b/tests/components/mill/test_init.py index 2c17a2d7550..694e9537a8c 100644 --- a/tests/components/mill/test_init.py +++ b/tests/components/mill/test_init.py @@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry async def test_setup_with_cloud_config(hass: HomeAssistant) -> None: @@ -109,7 +109,9 @@ async def test_unload_entry(hass: HomeAssistant) -> None: entry.add_to_hass(hass) with patch.object( - hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) + hass.config_entries, + "async_forward_entry_unload", + return_value=True, ) as unload_entry, patch( "mill.Mill.fetch_heater_and_sensor_data", return_value={} ), patch( diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 4b9169b48db..28a8a26657a 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -13,7 +13,7 @@ from homeassistant.setup import async_setup_component from .const import REGISTER, REGISTER_CLEARTEXT, RENDER_TEMPLATE -from tests.common import MockUser, mock_coro +from tests.common import MockUser from tests.typing import ClientSessionGenerator @@ -28,7 +28,6 @@ async def test_registration( with patch( "homeassistant.components.person.async_add_user_device_tracker", spec=True, - return_value=mock_coro(), ) as add_user_dev_track: resp = await api_client.post( "/api/mobile_app/registrations", json=REGISTER_CLEARTEXT diff --git a/tests/components/onboarding/test_init.py b/tests/components/onboarding/test_init.py index 0f7dc8d242b..bcaa9ad611f 100644 --- a/tests/components/onboarding/test_init.py +++ b/tests/components/onboarding/test_init.py @@ -8,7 +8,7 @@ from homeassistant.setup import async_setup_component from . import mock_storage -from tests.common import MockUser, mock_coro +from tests.common import MockUser # Temporarily: if auth not active, always set onboarded=True @@ -31,7 +31,6 @@ async def test_setup_views_if_not_onboarded(hass: HomeAssistant) -> None: """Test if onboarding is not done, we setup views.""" with patch( "homeassistant.components.onboarding.views.async_setup", - return_value=mock_coro(), ) as mock_setup: assert await async_setup_component(hass, "onboarding", {}) diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 41c3b7f058d..1be21e8b1b2 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -9,7 +9,7 @@ from homeassistant.const import STATE_NOT_HOME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, async_fire_mqtt_message, mock_coro +from tests.common import MockConfigEntry, async_fire_mqtt_message from tests.typing import ClientSessionGenerator USER = "greg" @@ -1303,7 +1303,7 @@ async def test_not_implemented_message(hass: HomeAssistant, context) -> None: """Handle not implemented message type.""" patch_handler = patch( "homeassistant.components.owntracks.messages.async_handle_not_impl_msg", - return_value=mock_coro(False), + return_value=False, ) patch_handler.start() assert not await send_message(hass, LWT_TOPIC, LWT_MESSAGE) @@ -1314,7 +1314,7 @@ async def test_unsupported_message(hass: HomeAssistant, context) -> None: """Handle not implemented message type.""" patch_handler = patch( "homeassistant.components.owntracks.messages.async_handle_unsupported_msg", - return_value=mock_coro(False), + return_value=False, ) patch_handler.start() assert not await send_message(hass, BAD_TOPIC, BAD_MESSAGE) @@ -1393,7 +1393,7 @@ def config_context(hass, setup_comp): """Set up the mocked context.""" patch_load = patch( "homeassistant.components.device_tracker.async_load_config", - return_value=mock_coro([]), + return_value=[], ) patch_load.start() diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index d2f81ac18dc..ac892eeb2d8 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -9,8 +9,6 @@ from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, UnitOfTemp from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import mock_coro - CONFIG = { DOMAIN: { "space": "Home", @@ -83,7 +81,7 @@ SENSOR_OUTPUT = { @pytest.fixture def mock_client(hass, hass_client): """Start the Home Assistant HTTP component.""" - with patch("homeassistant.components.spaceapi", return_value=mock_coro(True)): + with patch("homeassistant.components.spaceapi", return_value=True): hass.loop.run_until_complete(async_setup_component(hass, "spaceapi", CONFIG)) hass.states.async_set( diff --git a/tests/components/spc/test_init.py b/tests/components/spc/test_init.py index 7e4faa68e00..1972b7af5c8 100644 --- a/tests/components/spc/test_init.py +++ b/tests/components/spc/test_init.py @@ -6,8 +6,6 @@ from homeassistant.components.spc import DATA_API from homeassistant.const import STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED from homeassistant.core import HomeAssistant -from tests.common import mock_coro - async def test_valid_device_config(hass: HomeAssistant, monkeypatch) -> None: """Test valid device config.""" @@ -15,7 +13,7 @@ async def test_valid_device_config(hass: HomeAssistant, monkeypatch) -> None: with patch( "homeassistant.components.spc.SpcWebGateway.async_load_parameters", - return_value=mock_coro(True), + return_value=True, ): assert await async_setup_component(hass, "spc", config) is True @@ -26,7 +24,7 @@ async def test_invalid_device_config(hass: HomeAssistant, monkeypatch) -> None: with patch( "homeassistant.components.spc.SpcWebGateway.async_load_parameters", - return_value=mock_coro(True), + return_value=True, ): assert await async_setup_component(hass, "spc", config) is False @@ -53,7 +51,7 @@ async def test_update_alarm_device(hass: HomeAssistant) -> None: mock_areas.return_value = {"1": area_mock} with patch( "homeassistant.components.spc.SpcWebGateway.async_load_parameters", - return_value=mock_coro(True), + return_value=True, ): assert await async_setup_component(hass, "spc", config) is True diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index ae6172af6d8..948e55649fc 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -11,7 +11,7 @@ from homeassistant.components.syncthru.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker FIXTURE_USER_INPUT = { @@ -90,7 +90,7 @@ async def test_syncthru_not_supported(hass: HomeAssistant) -> None: async def test_unknown_state(hass: HomeAssistant) -> None: """Test we show user form on unsupported device.""" - with patch.object(SyncThru, "update", return_value=mock_coro()), patch.object( + with patch.object(SyncThru, "update"), patch.object( SyncThru, "is_unknown_state", return_value=True ): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index aba34aeb44b..d5ff879de78 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -23,7 +23,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component -from tests.common import mock_coro from tests.typing import ClientSessionGenerator @@ -72,7 +71,6 @@ async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client) -> None: """Test authenticating.""" with patch( "homeassistant.components.websocket_api.auth.process_wrong_login", - return_value=mock_coro(), ) as mock_process_wrong_login: await no_auth_websocket_client.send_json( {"type": TYPE_AUTH, "api_password": "wrong"} diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index 2a2fbc92ace..461e592ef85 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -35,8 +35,6 @@ from homeassistant.helpers import entity_registry as er from .common import find_entity_id from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE -from tests.common import mock_coro - @pytest.fixture(autouse=True) def button_platform_only(): @@ -151,7 +149,7 @@ async def test_button(hass: HomeAssistant, contact_sensor) -> None: with patch( "zigpy.zcl.Cluster.request", - return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + return_value=[0x00, zcl_f.Status.SUCCESS], ): await hass.services.async_call( DOMAIN, @@ -191,7 +189,7 @@ async def test_frost_unlock(hass: HomeAssistant, tuya_water_valve) -> None: with patch( "zigpy.zcl.Cluster.request", - return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + return_value=[0x00, zcl_f.Status.SUCCESS], ): await hass.services.async_call( DOMAIN, diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 9c44a0d08b5..31ffe9449e2 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -19,7 +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 @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -274,7 +274,7 @@ async def test_action(hass: HomeAssistant, device_ias, device_inovelli) -> None: with patch( "zigpy.zcl.Cluster.request", - return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + return_value=[0x00, zcl_f.Status.SUCCESS], ): assert await async_setup_component( hass, diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 60aa355af5f..67770efd591 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -23,8 +23,6 @@ from .common import ( ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -from tests.common import mock_coro - @pytest.fixture(autouse=True) def number_platform_only(): @@ -153,7 +151,7 @@ async def test_number( # change value from HA with patch( "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), + return_value=[zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS], ): # set value via UI await hass.services.async_call( diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index 2df6c2be5db..b953d833330 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -27,7 +27,7 @@ import homeassistant.util.dt as dt_util from .common import async_enable_traffic, find_entity_id from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE -from tests.common import async_fire_time_changed, mock_coro +from tests.common import async_fire_time_changed @pytest.fixture(autouse=True) @@ -87,7 +87,7 @@ async def test_siren(hass: HomeAssistant, siren) -> None: # turn on from HA with patch( "zigpy.device.Device.request", - return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + return_value=[0x00, zcl_f.Status.SUCCESS], ), patch( "zigpy.zcl.Cluster.request", side_effect=zigpy.zcl.Cluster.request, @@ -119,7 +119,7 @@ async def test_siren(hass: HomeAssistant, siren) -> None: # turn off from HA with patch( "zigpy.device.Device.request", - return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), + return_value=[0x01, zcl_f.Status.SUCCESS], ), patch( "zigpy.zcl.Cluster.request", side_effect=zigpy.zcl.Cluster.request, @@ -151,7 +151,7 @@ async def test_siren(hass: HomeAssistant, siren) -> None: # turn on from HA with patch( "zigpy.device.Device.request", - return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + return_value=[0x00, zcl_f.Status.SUCCESS], ), patch( "zigpy.zcl.Cluster.request", side_effect=zigpy.zcl.Cluster.request, diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 26eef47273f..ea9e04ac993 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -23,7 +23,6 @@ from .common import ( MockModule, MockPlatform, get_test_config_dir, - mock_coro, mock_entity_platform, mock_integration, ) @@ -110,7 +109,7 @@ async def test_core_failure_loads_safe_mode( """Test failing core setup aborts further setup.""" with patch( "homeassistant.components.homeassistant.async_setup", - return_value=mock_coro(False), + return_value=False, ): await bootstrap.async_from_config_dict({"group": {}}, hass) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 75b6377973b..760c7138c88 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -40,7 +40,6 @@ from .common import ( MockPlatform, async_fire_time_changed, mock_config_flow, - mock_coro, mock_entity_platform, mock_integration, ) @@ -605,7 +604,10 @@ async def test_domains_gets_domains_excludes_ignore_and_disabled( async def test_saving_and_loading(hass: HomeAssistant) -> None: """Test that we're saving and loading correctly.""" mock_integration( - hass, MockModule("test", async_setup_entry=lambda *args: mock_coro(True)) + hass, + MockModule( + "test", async_setup_entry=lambda *args: AsyncMock(return_value=True) + ), ) mock_entity_platform(hass, "config_flow.test", None) From 8d9c5a61ec09a458bdccc8dc37a4648ad918f1d8 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 25 Aug 2023 18:32:20 -0700 Subject: [PATCH 0897/1151] Update calendar handle state updates at start/end of active/upcoming event (#98037) * Update calendar handle state updates at start/end of active/upcoming event * Use async_write_ha_state intercept state updates Remove unrelated changes and whitespace. * Revert unnecessary changes * Move demo calendar to config entries to cleanup event timers * Fix docs on calendars * Move method inside from PR feedback --- homeassistant/components/calendar/__init__.py | 47 +++++++++++++ homeassistant/components/demo/__init__.py | 2 +- homeassistant/components/demo/calendar.py | 15 ++--- homeassistant/components/google/calendar.py | 66 ++++++------------- 4 files changed, 75 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index c85f0d2bff1..e487569453f 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -20,10 +20,12 @@ from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import ( + CALLBACK_TYPE, HomeAssistant, ServiceCall, ServiceResponse, SupportsResponse, + callback, ) from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -34,6 +36,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.event import async_track_point_in_time from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util @@ -478,6 +481,8 @@ def is_offset_reached( class CalendarEntity(Entity): """Base class for calendar event entities.""" + _alarm_unsubs: list[CALLBACK_TYPE] = [] + @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" @@ -513,6 +518,48 @@ class CalendarEntity(Entity): return STATE_OFF + @callback + def async_write_ha_state(self) -> None: + """Write the state to the state machine. + + This sets up listeners to handle state transitions for start or end of + the current or upcoming event. + """ + super().async_write_ha_state() + + for unsub in self._alarm_unsubs: + unsub() + + now = dt_util.now() + event = self.event + if event is None or now >= event.end_datetime_local: + return + + @callback + def update(_: datetime.datetime) -> None: + """Run when the active or upcoming event starts or ends.""" + self._async_write_ha_state() + + if now < event.start_datetime_local: + self._alarm_unsubs.append( + async_track_point_in_time( + self.hass, + update, + event.start_datetime_local, + ) + ) + self._alarm_unsubs.append( + async_track_point_in_time(self.hass, update, event.end_datetime_local) + ) + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass. + + To be extended by integrations. + """ + for unsub in self._alarm_unsubs: + unsub() + async def async_get_events( self, hass: HomeAssistant, diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 04eba5f0586..b40e1ede232 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -26,6 +26,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CAMERA, + Platform.CALENDAR, Platform.CLIMATE, Platform.COVER, Platform.DATE, @@ -54,7 +55,6 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ Platform.MAILBOX, Platform.NOTIFY, Platform.IMAGE_PROCESSING, - Platform.CALENDAR, Platform.DEVICE_TRACKER, Platform.WEATHER, ] diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 73b45a55640..b4200f1be89 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -1,23 +1,22 @@ -"""Demo platform that has two fake binary sensors.""" +"""Demo platform that has two fake calendars.""" from __future__ import annotations import datetime from homeassistant.components.calendar import CalendarEntity, CalendarEvent +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the Demo Calendar platform.""" - add_entities( + """Set up the Demo Calendar config entry.""" + async_add_entities( [ DemoCalendar(calendar_data_future(), "Calendar 1"), DemoCalendar(calendar_data_current(), "Calendar 2"), diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 347e8444946..9559a06d49c 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -36,7 +36,7 @@ from homeassistant.components.calendar import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import entity_platform, entity_registry as er from homeassistant.helpers.entity import generate_entity_id @@ -383,7 +383,6 @@ class GoogleCalendarEntity( self._event: CalendarEvent | None = None self._attr_name = data[CONF_NAME].capitalize() self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) - self._offset_value: timedelta | None = None self.entity_id = entity_id self._attr_unique_id = unique_id self._attr_entity_registry_enabled_default = entity_enabled @@ -392,17 +391,6 @@ class GoogleCalendarEntity( CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT ) - @property - def should_poll(self) -> bool: - """Enable polling for the entity. - - The coordinator is not used by multiple entities, but instead - is used to poll the calendar API at a separate interval from the - entity state updates itself which happen more frequently (e.g. to - fire an alarm when the next event starts). - """ - return True - @property def extra_state_attributes(self) -> dict[str, bool]: """Return the device state attributes.""" @@ -411,16 +399,16 @@ class GoogleCalendarEntity( @property def offset_reached(self) -> bool: """Return whether or not the event offset was reached.""" - if self._event and self._offset_value: - return is_offset_reached( - self._event.start_datetime_local, self._offset_value - ) + (event, offset_value) = self._event_with_offset() + if event is not None and offset_value is not None: + return is_offset_reached(event.start_datetime_local, offset_value) return False @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" - return self._event + (event, _) = self._event_with_offset() + return event def _event_filter(self, event: Event) -> bool: """Return True if the event is visible.""" @@ -435,12 +423,10 @@ class GoogleCalendarEntity( # We do not ask for an update with async_add_entities() # because it will update disabled entities. This is started as a # task to let if sync in the background without blocking startup - async def refresh() -> None: - await self.coordinator.async_request_refresh() - self._apply_coordinator_update() - self.coordinator.config_entry.async_create_background_task( - self.hass, refresh(), "google.calendar-refresh" + self.hass, + self.coordinator.async_request_refresh(), + "google.calendar-refresh", ) async def async_get_events( @@ -453,8 +439,10 @@ class GoogleCalendarEntity( for event in filter(self._event_filter, result_items) ] - def _apply_coordinator_update(self) -> None: - """Copy state from the coordinator to this entity.""" + def _event_with_offset( + self, + ) -> tuple[CalendarEvent | None, timedelta | None]: + """Get the calendar event and offset if any.""" if api_event := next( filter( self._event_filter, @@ -462,27 +450,13 @@ class GoogleCalendarEntity( ), None, ): - self._event = _get_calendar_event(api_event) - (self._event.summary, self._offset_value) = extract_offset( - self._event.summary, self._offset - ) - else: - self._event = None - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._apply_coordinator_update() - super()._handle_coordinator_update() - - async def async_update(self) -> None: - """Disable update behavior. - - This relies on the coordinator callback update to write home assistant - state with the next calendar event. This update is a no-op as no new data - fetch is needed to evaluate the state to determine if the next event has - started, handled by CalendarEntity parent class. - """ + event = _get_calendar_event(api_event) + if self._offset: + (event.summary, offset_value) = extract_offset( + event.summary, self._offset + ) + return event, offset_value + return None, None async def async_create_event(self, **kwargs: Any) -> None: """Add a new event to calendar.""" From 6f43dd1c140682915ce9ada107d129d650e702bc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 26 Aug 2023 07:35:10 +0200 Subject: [PATCH 0898/1151] Adjust netatmo test (#99071) --- tests/components/netatmo/fixtures/getpublicdata.json | 2 +- tests/components/netatmo/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/netatmo/fixtures/getpublicdata.json b/tests/components/netatmo/fixtures/getpublicdata.json index cf2ec3c66cb..622e7f962f1 100644 --- a/tests/components/netatmo/fixtures/getpublicdata.json +++ b/tests/components/netatmo/fixtures/getpublicdata.json @@ -91,7 +91,7 @@ }, "70:ee:50:27:25:b0": { "res": { - "1560247907": [1012.8] + "1560247907": [1012.9] }, "type": ["pressure"] }, diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index 5c04f0d2fc7..00cec6f8aa0 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -46,7 +46,7 @@ async def test_public_weather_sensor( assert hass.states.get(f"{prefix}temperature").state == "22.7" assert hass.states.get(f"{prefix}humidity").state == "63.2" - assert hass.states.get(f"{prefix}pressure").state == "1010.3" + assert hass.states.get(f"{prefix}pressure").state == "1010.4" entities_before_change = len(hass.states.async_all()) From d74a0fd6dd2eac147e35da28aa561fc6eeebee4a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Aug 2023 09:11:42 +0200 Subject: [PATCH 0899/1151] Use freezegun in additional fronius tests (#99066) --- tests/components/fronius/__init__.py | 6 +- tests/components/fronius/test_sensor.py | 87 +++++++++++++++++-------- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index 4d11291508b..5a757da1e9c 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -6,7 +6,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -85,7 +84,7 @@ def mock_responses( ) -async def enable_all_entities(hass, config_entry_id, time_till_next_update): +async def enable_all_entities(hass, freezer, config_entry_id, time_till_next_update): """Enable all entities for a config entry and fast forward time to receive data.""" registry = er.async_get(hass) entities = er.async_entries_for_config_entry(registry, config_entry_id) @@ -96,5 +95,6 @@ async def enable_all_entities(hass, config_entry_id, time_till_next_update): ]: registry.async_update_entity(entry.entity_id, **{"disabled_by": None}) await hass.async_block_till_done() - async_fire_time_changed(hass, dt_util.utcnow() + time_till_next_update) + freezer.tick(time_till_next_update) + async_fire_time_changed(hass) await hass.async_block_till_done() diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index 47b6410a146..c2e0c4ad969 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -1,4 +1,7 @@ """Tests for the Fronius sensor platform.""" + +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.fronius.const import DOMAIN from homeassistant.components.fronius.coordinator import ( FroniusInverterUpdateCoordinator, @@ -8,7 +11,6 @@ from homeassistant.components.fronius.coordinator import ( from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.util import dt as dt_util from . import enable_all_entities, mock_responses, setup_fronius_integration @@ -17,7 +19,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_symo_inverter( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, ) -> None: """Test Fronius Symo inverter entities.""" @@ -31,7 +35,10 @@ async def test_symo_inverter( assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 20 await enable_all_entities( - hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval + hass, + freezer, + config_entry.entry_id, + FroniusInverterUpdateCoordinator.default_interval, ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 assert_state("sensor.symo_20_dc_current", 0) @@ -42,13 +49,15 @@ async def test_symo_inverter( # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) - async_fire_time_changed( - hass, dt_util.utcnow() + FroniusInverterUpdateCoordinator.default_interval - ) + freezer.tick(FroniusInverterUpdateCoordinator.default_interval) + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 56 await enable_all_entities( - hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval + hass, + freezer, + config_entry.entry_id, + FroniusInverterUpdateCoordinator.default_interval, ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # 4 additional AC entities @@ -64,9 +73,8 @@ async def test_symo_inverter( # Third test at nighttime - additional AC entities default to 0 mock_responses(aioclient_mock, night=True) - async_fire_time_changed( - hass, dt_util.utcnow() + FroniusInverterUpdateCoordinator.default_interval - ) + freezer.tick(FroniusInverterUpdateCoordinator.default_interval) + async_fire_time_changed(hass) await hass.async_block_till_done() assert_state("sensor.symo_20_ac_current", 0) assert_state("sensor.symo_20_frequency", 0) @@ -94,7 +102,9 @@ async def test_symo_logger( async def test_symo_meter( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, ) -> None: """Test Fronius Symo meter entities.""" @@ -108,7 +118,10 @@ async def test_symo_meter( assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 24 await enable_all_entities( - hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval + hass, + freezer, + config_entry.entry_id, + FroniusMeterUpdateCoordinator.default_interval, ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # states are rounded to 4 decimals @@ -147,10 +160,12 @@ async def test_symo_meter( async def test_symo_power_flow( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, ) -> None: """Test Fronius Symo power flow entities.""" - async_fire_time_changed(hass, dt_util.utcnow()) + async_fire_time_changed(hass) def assert_state(entity_id, expected_state): state = hass.states.get(entity_id) @@ -162,7 +177,10 @@ async def test_symo_power_flow( assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 20 await enable_all_entities( - hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval + hass, + freezer, + config_entry.entry_id, + FroniusInverterUpdateCoordinator.default_interval, ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 # states are rounded to 4 decimals @@ -175,9 +193,8 @@ async def test_symo_power_flow( # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) - async_fire_time_changed( - hass, dt_util.utcnow() + FroniusPowerFlowUpdateCoordinator.default_interval - ) + freezer.tick(FroniusPowerFlowUpdateCoordinator.default_interval) + async_fire_time_changed(hass) await hass.async_block_till_done() # 54 because power_flow `rel_SelfConsumption` and `P_PV` is not `null` anymore assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 @@ -192,9 +209,8 @@ async def test_symo_power_flow( # Third test at nighttime - default values are used mock_responses(aioclient_mock, night=True) - async_fire_time_changed( - hass, dt_util.utcnow() + FroniusPowerFlowUpdateCoordinator.default_interval - ) + freezer.tick(FroniusPowerFlowUpdateCoordinator.default_interval) + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 assert_state("sensor.solarnet_energy_day", 10828) @@ -207,7 +223,11 @@ async def test_symo_power_flow( assert_state("sensor.solarnet_relative_self_consumption", 0) -async def test_gen24(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: +async def test_gen24( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, +) -> None: """Test Fronius Gen24 inverter entities.""" def assert_state(entity_id, expected_state): @@ -220,7 +240,10 @@ async def test_gen24(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) - assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 22 await enable_all_entities( - hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval + hass, + freezer, + config_entry.entry_id, + FroniusMeterUpdateCoordinator.default_interval, ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 # inverter 1 @@ -281,7 +304,9 @@ async def test_gen24(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) - async def test_gen24_storage( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, ) -> None: """Test Fronius Gen24 inverter with BYD battery and Ohmpilot entities.""" @@ -297,7 +322,10 @@ async def test_gen24_storage( assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 34 await enable_all_entities( - hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval + hass, + freezer, + config_entry.entry_id, + FroniusMeterUpdateCoordinator.default_interval, ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 64 # inverter 1 @@ -405,7 +433,9 @@ async def test_gen24_storage( async def test_primo_s0( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, ) -> None: """Test Fronius Primo dual inverter with S0 meter entities.""" @@ -419,7 +449,10 @@ async def test_primo_s0( assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 29 await enable_all_entities( - hass, config_entry.entry_id, FroniusMeterUpdateCoordinator.default_interval + hass, + freezer, + config_entry.entry_id, + FroniusMeterUpdateCoordinator.default_interval, ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 40 # logger From a25a7ebbeb85ba6a9675a3d5d64fc6e57025adcd Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 26 Aug 2023 06:39:48 -0700 Subject: [PATCH 0900/1151] Bump opower to 0.0.32 (#99079) --- 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 aff1ad2f599..fb4ff5153ec 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.0.31"] + "requirements": ["opower==0.0.32"] } diff --git a/requirements_all.txt b/requirements_all.txt index b915a38368f..29f5527746c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1368,7 +1368,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.31 +opower==0.0.32 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a292cfb78ac..dcc4abbce29 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.31 +opower==0.0.32 # homeassistant.components.oralb oralb-ble==0.17.6 From c287bd1a3be85678dddd14dcded5b97a9e212108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 26 Aug 2023 17:46:03 +0300 Subject: [PATCH 0901/1151] Remove pylint configs flagged by useless-suppression (#99081) --- homeassistant/components/azure_service_bus/notify.py | 5 ----- homeassistant/components/discovergy/__init__.py | 2 +- homeassistant/components/http/__init__.py | 2 +- homeassistant/components/huawei_lte/utils.py | 2 +- homeassistant/components/integration/sensor.py | 1 - homeassistant/components/mqtt/models.py | 4 ++-- homeassistant/components/recorder/filters.py | 2 -- homeassistant/components/samsungtv/bridge.py | 1 - homeassistant/components/supla/__init__.py | 1 - 9 files changed, 5 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index 23235a23dff..4005460ecae 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -4,13 +4,8 @@ from __future__ import annotations import json import logging -# pylint: disable-next=no-name-in-module from azure.servicebus import ServiceBusMessage - -# pylint: disable-next=no-name-in-module from azure.servicebus.aio import ServiceBusClient, ServiceBusSender - -# pylint: disable-next=no-name-in-module from azure.servicebus.exceptions import ( MessagingEntityNotFoundError, ServiceBusConnectionError, diff --git a/homeassistant/components/discovergy/__init__.py b/homeassistant/components/discovergy/__init__.py index fe1045203d8..ab892cd9324 100644 --- a/homeassistant/components/discovergy/__init__.py +++ b/homeassistant/components/discovergy/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: discovergy_data.meters = await discovergy_data.api_client.meters() except discovergyError.InvalidLogin as err: raise ConfigEntryAuthFailed("Invalid email or password") from err - except Exception as err: # pylint: disable=broad-except + except Exception as err: raise ConfigEntryNotReady( "Unexpected error while while getting meters" ) from err diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 68f68d7f558..409b78fb16a 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -318,7 +318,7 @@ class HomeAssistantHTTP: # By default aiohttp does a linear search for routing rules, # we have a lot of routes, so use a dict lookup with a fallback # to the linear search. - self.app._router = FastUrlDispatcher() # pylint: disable=protected-access + self.app._router = FastUrlDispatcher() self.hass = hass self.ssl_certificate = ssl_certificate self.ssl_peer_certificate = ssl_peer_certificate diff --git a/homeassistant/components/huawei_lte/utils.py b/homeassistant/components/huawei_lte/utils.py index ab787a97ea9..172e8658928 100644 --- a/homeassistant/components/huawei_lte/utils.py +++ b/homeassistant/components/huawei_lte/utils.py @@ -21,7 +21,7 @@ def get_device_macs( for x in ("MacAddress1", "MacAddress2", "WifiMacAddrWl0", "WifiMacAddrWl1") ] # Assume not supported when exception is thrown - with suppress(Exception): # pylint: disable=broad-except + with suppress(Exception): macs.extend(x.get("WifiMac") for x in wlan_settings["Ssids"]["Ssid"]) return sorted({format_mac(str(x)) for x in macs if x}) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index ba17a448477..66a99b63681 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -207,7 +207,6 @@ async def async_setup_platform( async_add_entities([integral]) -# pylint: disable-next=hass-invalid-inheritance # needs fixing class IntegrationSensor(RestoreSensor): """Representation of an integration sensor.""" diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index d553274ab3e..8c599469ff2 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -247,7 +247,7 @@ class MqttValueTemplate: payload, variables=values ) ) - except Exception as ex: # pylint: disable=broad-except + except Exception as ex: _LOGGER.error( "%s: %s rendering template for entity '%s', template: '%s'", type(ex).__name__, @@ -274,7 +274,7 @@ class MqttValueTemplate: payload, default, variables=values ) ) - except Exception as ex: # pylint: disable=broad-except + except Exception as ex: _LOGGER.error( "%s: %s rendering template for entity '%s', template: " "'%s', default value: %s and payload: %s", diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 24d22704a89..bf76c7264d5 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -187,8 +187,6 @@ class Filters: if self._included_domains or self._included_entity_globs: return or_( i_entities, - # https://github.com/sqlalchemy/sqlalchemy/issues/9190 - # pylint: disable-next=invalid-unary-operand-type (~e_entities & (i_entity_globs | (~e_entity_globs & i_domains))), ).self_group() diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 0cc4dd556d5..03a9c35c9ba 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -548,7 +548,6 @@ class SamsungTVWSBridge( return RESULT_AUTH_MISSING except (ConnectionFailure, OSError, AsyncioTimeoutError) as err: LOGGER.debug("Failing config: %s, %s error: %s", config, type(err), err) - # pylint: disable-next=useless-else-on-loop else: # noqa: PLW0120 if result: return result diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 14d617ba88e..9652cae4aa4 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -102,7 +102,6 @@ async def discover_devices(hass, hass_config): async with asyncio.timeout(SCAN_INTERVAL.total_seconds()): channels = { channel["id"]: channel - # pylint: disable-next=cell-var-from-loop for channel in await server.get_channels( # noqa: B023 include=["iodevice", "state", "connected"] ) From e003903bc5655a580c80ec6f290add701300e12b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 26 Aug 2023 12:26:12 -0500 Subject: [PATCH 0902/1151] Bump zeroconf to 0.83.0 (#99091) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 6b04b6c7c4a..2f75e4008fd 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.82.1"] + "requirements": ["zeroconf==0.83.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 71f8a37ff40..b032ff6c148 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.82.1 +zeroconf==0.83.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 29f5527746c..85c76129b25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2759,7 +2759,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.82.1 +zeroconf==0.83.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dcc4abbce29..5e5e8373075 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2029,7 +2029,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.82.1 +zeroconf==0.83.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 407aa31adc326d5a6fccd10ee429abec44218379 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 27 Aug 2023 01:39:40 +0800 Subject: [PATCH 0903/1151] Generate Stream snapshots using next keyframe (#96991) * Add wait_for_next_keyframe option to stream images Add STREAM_SNAPSHOT to CameraEntityFeature Use wait_for_next_keyframe option for snapshots using stream * Update stream test comments * Add generic camera snapshot test * Get stream still images directly in camera Remove getting stream images from generic, nest, and ONVIF Refactor camera preferences Add use_stream_for_stills setting to camera Update tests * Only attempt to get stream image if integration supports stream * Use property instead of entity registry setting * Split out getting stream prerequisites from stream_source in nest * Use cached_property for rtsp live stream trait * Make rtsp live stream trait NestCamera attribute * Update homeassistant/components/nest/camera.py Co-authored-by: Allen Porter * Change usage of async_timeout * Change import formatting in generic/test_camera * Simplify Nest camera property initialization --------- Co-authored-by: Allen Porter --- homeassistant/components/camera/__init__.py | 39 +++++++++++-- homeassistant/components/generic/camera.py | 9 +-- homeassistant/components/nest/camera.py | 42 +++++++------- homeassistant/components/onvif/camera.py | 8 ++- homeassistant/components/stream/__init__.py | 5 +- homeassistant/components/stream/core.py | 21 +++++-- homeassistant/components/stream/worker.py | 2 +- tests/components/camera/test_init.py | 58 ++++++++++++++++++++ tests/components/generic/test_camera.py | 47 +--------------- tests/components/generic/test_config_flow.py | 2 +- tests/components/nest/test_camera.py | 6 -- tests/components/stream/test_worker.py | 30 ++++++++-- 12 files changed, 174 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index af64b2f1953..07394ca75b2 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -168,9 +168,14 @@ async def _async_get_image( """ with suppress(asyncio.CancelledError, asyncio.TimeoutError): async with asyncio.timeout(timeout): - if image_bytes := await camera.async_camera_image( - width=width, height=height - ): + image_bytes = ( + await _async_get_stream_image( + camera, width=width, height=height, wait_for_next_keyframe=False + ) + if camera.use_stream_for_stills + else await camera.async_camera_image(width=width, height=height) + ) + if image_bytes: content_type = camera.content_type image = Image(content_type, image_bytes) if ( @@ -205,6 +210,21 @@ async def async_get_image( return await _async_get_image(camera, timeout, width, height) +async def _async_get_stream_image( + camera: Camera, + width: int | None = None, + height: int | None = None, + wait_for_next_keyframe: bool = False, +) -> bytes | None: + if not camera.stream and camera.supported_features & SUPPORT_STREAM: + camera.stream = await camera.async_create_stream() + if camera.stream: + return await camera.stream.async_get_image( + width=width, height=height, wait_for_next_keyframe=wait_for_next_keyframe + ) + return None + + @bind_hass async def async_get_stream_source(hass: HomeAssistant, entity_id: str) -> str | None: """Fetch the stream source for a camera entity.""" @@ -360,6 +380,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) async def preload_stream(_event: Event) -> None: + """Load stream prefs and start stream if preload_stream is True.""" for camera in list(component.entities): stream_prefs = await prefs.get_dynamic_stream_settings(camera.entity_id) if not stream_prefs.preload_stream: @@ -459,6 +480,11 @@ class Camera(Entity): return self._attr_entity_picture return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1]) + @property + def use_stream_for_stills(self) -> bool: + """Whether or not to use stream to generate stills.""" + return False + @property def supported_features(self) -> CameraEntityFeature: """Flag supported features.""" @@ -926,7 +952,12 @@ async def async_handle_snapshot_service( f"Cannot write `{snapshot_file}`, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`" ) - image = await camera.async_camera_image() + async with asyncio.timeout(CAMERA_IMAGE_TIMEOUT): + image = ( + await _async_get_stream_image(camera, wait_for_next_keyframe=True) + if camera.use_stream_for_stills + else await camera.async_camera_image() + ) if image is None: return diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index c171c95e659..621566a70f5 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -172,15 +172,16 @@ class GenericCamera(Camera): self._last_url = None self._last_image = None + @property + def use_stream_for_stills(self) -> bool: + """Whether or not to use stream to generate stills.""" + return not self._still_image_url + async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" if not self._still_image_url: - if not self.stream: - await self.async_create_stream() - if self.stream: - return await self.stream.async_get_image(width, height) return None try: url = self._still_image_url.async_render(parse_result=False) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 721af504fd8..90c4056161e 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -7,6 +7,7 @@ import datetime import functools import logging from pathlib import Path +from typing import cast from google_nest_sdm.camera_traits import ( CameraImageTrait, @@ -71,9 +72,24 @@ class NestCamera(Camera): self._stream: RtspStream | None = None self._create_stream_url_lock = asyncio.Lock() self._stream_refresh_unsub: Callable[[], None] | None = None - self._attr_is_streaming = CameraLiveStreamTrait.NAME in self._device.traits + self._attr_is_streaming = False + self._attr_supported_features = CameraEntityFeature(0) + self._rtsp_live_stream_trait: CameraLiveStreamTrait | None = None + if CameraLiveStreamTrait.NAME in self._device.traits: + self._attr_is_streaming = True + self._attr_supported_features |= CameraEntityFeature.STREAM + trait = cast( + CameraLiveStreamTrait, self._device.traits[CameraLiveStreamTrait.NAME] + ) + if StreamingProtocol.RTSP in trait.supported_protocols: + self._rtsp_live_stream_trait = trait self.stream_options[CONF_EXTRA_PART_WAIT_TIME] = 3 + @property + def use_stream_for_stills(self) -> bool: + """Whether or not to use stream to generate stills.""" + return self._rtsp_live_stream_trait is not None + @property def unique_id(self) -> str: """Return a unique ID.""" @@ -95,14 +111,6 @@ class NestCamera(Camera): """Return the camera model.""" return self._device_info.device_model - @property - def supported_features(self) -> CameraEntityFeature: - """Flag supported features.""" - supported_features = CameraEntityFeature(0) - if CameraLiveStreamTrait.NAME in self._device.traits: - supported_features |= CameraEntityFeature.STREAM - return supported_features - @property def frontend_stream_type(self) -> StreamType | None: """Return the type of stream supported by this camera.""" @@ -125,18 +133,15 @@ class NestCamera(Camera): async def stream_source(self) -> str | None: """Return the source of the stream.""" - if not self.supported_features & CameraEntityFeature.STREAM: - return None - if CameraLiveStreamTrait.NAME not in self._device.traits: - return None - trait = self._device.traits[CameraLiveStreamTrait.NAME] - if StreamingProtocol.RTSP not in trait.supported_protocols: + if not self._rtsp_live_stream_trait: return None async with self._create_stream_url_lock: if not self._stream: _LOGGER.debug("Fetching stream url") try: - self._stream = await trait.generate_rtsp_stream() + self._stream = ( + await self._rtsp_live_stream_trait.generate_rtsp_stream() + ) except ApiException as err: raise HomeAssistantError(f"Nest API error: {err}") from err self._schedule_stream_refresh() @@ -204,10 +209,7 @@ class NestCamera(Camera): ) -> bytes | None: """Return bytes of camera image.""" # Use the thumbnail from RTSP stream, or a placeholder if stream is - # not supported (e.g. WebRTC) - stream = await self.async_create_stream() - if stream: - return await stream.async_get_image(width, height) + # not supported (e.g. WebRTC) as a fallback when 'use_stream_for_stills' if False return await self.hass.async_add_executor_job(self.placeholder_image) @classmethod diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 7a87ec66c83..96ce70344fd 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -114,6 +114,11 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): self._stream_uri: str | None = None self._stream_uri_future: asyncio.Future[str] | None = None + @property + def use_stream_for_stills(self) -> bool: + """Whether or not to use stream to generate stills.""" + return bool(self.stream and self.stream.dynamic_stream_settings.preload_stream) + @property def name(self) -> str: """Return the name of this camera.""" @@ -140,9 +145,6 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): ) -> bytes | None: """Return a still image response from the camera.""" - if self.stream and self.stream.dynamic_stream_settings.preload_stream: - return await self.stream.async_get_image(width, height) - if self.device.capabilities.snapshot: try: if image := await self.device.device.get_snapshot( diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 63269401a40..691ba262ee2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -537,6 +537,7 @@ class Stream: self, width: int | None = None, height: int | None = None, + wait_for_next_keyframe: bool = False, ) -> bytes | None: """Fetch an image from the Stream and return it as a jpeg in bytes. @@ -548,7 +549,9 @@ class Stream: self.add_provider(HLS_PROVIDER) await self.start() return await self._keyframe_converter.async_get_image( - width=width, height=height + width=width, + height=height, + wait_for_next_keyframe=wait_for_next_keyframe, ) def get_diagnostics(self) -> dict[str, Any]: diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index f3591e7e5d7..6b8e6c44a1c 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -441,7 +441,8 @@ class KeyFrameConverter: # pylint: disable-next=import-outside-toplevel from homeassistant.components.camera.img_util import TurboJPEGSingleton - self.packet: Packet = None + self._packet: Packet = None + self._event: asyncio.Event = asyncio.Event() self._hass = hass self._image: bytes | None = None self._turbojpeg = TurboJPEGSingleton.instance() @@ -450,6 +451,14 @@ class KeyFrameConverter: self._stream_settings = stream_settings self._dynamic_stream_settings = dynamic_stream_settings + def stash_keyframe_packet(self, packet: Packet) -> None: + """Store the keyframe and set the asyncio.Event from the event loop. + + This is called from the worker thread. + """ + self._packet = packet + self._hass.loop.call_soon_threadsafe(self._event.set) + def create_codec_context(self, codec_context: CodecContext) -> None: """Create a codec context to be used for decoding the keyframes. @@ -482,10 +491,10 @@ class KeyFrameConverter: at a time per instance. """ - if not (self._turbojpeg and self.packet and self._codec_context): + if not (self._turbojpeg and self._packet and self._codec_context): return - packet = self.packet - self.packet = None + packet = self._packet + self._packet = None for _ in range(2): # Retry once if codec context needs to be flushed try: # decode packet (flush afterwards) @@ -519,10 +528,14 @@ class KeyFrameConverter: self, width: int | None = None, height: int | None = None, + wait_for_next_keyframe: bool = False, ) -> bytes | None: """Fetch an image from the Stream and return it as a jpeg in bytes.""" # Use a lock to ensure only one thread is working on the keyframe at a time + if wait_for_next_keyframe: + self._event.clear() + await self._event.wait() async with self._lock: await self._hass.async_add_executor_job(self._generate_image, width, height) return self._image diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 07d274e655c..cc4970c8a5e 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -624,4 +624,4 @@ def stream_worker( muxer.mux_packet(packet) if packet.is_keyframe and is_video(packet): - keyframe_converter.packet = packet + keyframe_converter.stash_keyframe_packet(packet) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 8d37eba219a..2a91a375a13 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -909,3 +909,61 @@ async def test_rtsp_to_web_rtc_offer_not_accepted( assert mock_provider.called unsub() + + +async def test_use_stream_for_stills( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_camera, +) -> None: + """Test that the component can grab images from stream.""" + + client = await hass_client() + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value=None, + ) as mock_stream_source, patch( + "homeassistant.components.demo.camera.DemoCamera.use_stream_for_stills", + return_value=True, + ): + # First test when the integration does not support stream should fail + resp = await client.get("/api/camera_proxy/camera.demo_camera") + await hass.async_block_till_done() + mock_stream_source.assert_not_called() + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + # Test when the integration does not provide a stream_source should fail + with patch( + "homeassistant.components.demo.camera.DemoCamera.supported_features", + return_value=camera.SUPPORT_STREAM, + ): + resp = await client.get("/api/camera_proxy/camera.demo_camera") + await hass.async_block_till_done() + mock_stream_source.assert_called_once() + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://some_source", + ) as mock_stream_source, patch( + "homeassistant.components.camera.create_stream" + ) as mock_create_stream, patch( + "homeassistant.components.demo.camera.DemoCamera.supported_features", + return_value=camera.SUPPORT_STREAM, + ), patch( + "homeassistant.components.demo.camera.DemoCamera.use_stream_for_stills", + return_value=True, + ): + # Now test when creating the stream succeeds + mock_stream = Mock() + mock_stream.async_get_image = AsyncMock() + mock_stream.async_get_image.return_value = b"stream_keyframe_image" + mock_create_stream.return_value = mock_stream + + # should start the stream and get the image + resp = await client.get("/api/camera_proxy/camera.demo_camera") + await hass.async_block_till_done() + mock_create_stream.assert_called_once() + mock_stream.async_get_image.assert_called_once() + assert resp.status == HTTPStatus.OK + assert await resp.read() == b"stream_keyframe_image" diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index e83966c0912..f7f7c390e0d 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -27,7 +27,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import AsyncMock, Mock, MockConfigEntry +from tests.common import Mock, MockConfigEntry from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -503,51 +503,6 @@ async def test_timeout_cancelled( assert await resp.read() == fakeimgbytes_png -async def test_no_still_image_url( - hass: HomeAssistant, hass_client: ClientSessionGenerator -) -> None: - """Test that the component can grab images from stream with no still_image_url.""" - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "stream_source": "rtsp://example.com:554/rtsp/", - }, - }, - ) - await hass.async_block_till_done() - - client = await hass_client() - - with patch( - "homeassistant.components.generic.camera.GenericCamera.stream_source", - return_value=None, - ) as mock_stream_source: - # First test when there is no stream_source should fail - resp = await client.get("/api/camera_proxy/camera.config_test") - await hass.async_block_till_done() - mock_stream_source.assert_called_once() - assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR - - with patch("homeassistant.components.camera.create_stream") as mock_create_stream: - # Now test when creating the stream succeeds - mock_stream = Mock() - mock_stream.async_get_image = AsyncMock() - mock_stream.async_get_image.return_value = b"stream_keyframe_image" - mock_create_stream.return_value = mock_stream - - # should start the stream and get the image - resp = await client.get("/api/camera_proxy/camera.config_test") - await hass.async_block_till_done() - mock_create_stream.assert_called_once() - mock_stream.async_get_image.assert_called_once() - assert resp.status == HTTPStatus.OK - assert await resp.read() == b"stream_keyframe_image" - - async def test_frame_interval_property(hass: HomeAssistant) -> None: """Test that the frame interval is calculated and returned correctly.""" diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index db9787fb283..c4d11d4af22 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -423,7 +423,7 @@ async def test_form_only_stream( await hass.async_block_till_done() with patch( - "homeassistant.components.generic.camera.GenericCamera.async_camera_image", + "homeassistant.components.camera._async_get_stream_image", return_value=fakeimgbytes_jpg, ): image_obj = await async_get_image(hass, "camera.127_0_0_1") diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index 9e082dc1b05..56c5bedaf0d 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -244,8 +244,6 @@ async def test_camera_stream( stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" - assert await async_get_image(hass) == IMAGE_BYTES_FROM_STREAM - async def test_camera_ws_stream( hass: HomeAssistant, @@ -280,8 +278,6 @@ async def test_camera_ws_stream( assert msg["success"] assert msg["result"]["url"] == "http://home.assistant/playlist.m3u8" - assert await async_get_image(hass) == IMAGE_BYTES_FROM_STREAM - async def test_camera_ws_stream_failure( hass: HomeAssistant, @@ -746,8 +742,6 @@ async def test_camera_multiple_streams( stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" - assert await async_get_image(hass) == IMAGE_BYTES_FROM_STREAM - # WebRTC stream client = await hass_ws_client(hass) await client.send_json( diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index e0152190d90..bd998b008be 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -643,7 +643,7 @@ async def test_pts_out_of_order(hass: HomeAssistant) -> None: async def test_stream_stopped_while_decoding(hass: HomeAssistant) -> None: - """Tests that worker quits when stop() is called while decodign.""" + """Tests that worker quits when stop() is called while decoding.""" # Add some synchronization so that the test can pause the background # worker. When the worker is stopped, the test invokes stop() which # will cause the worker thread to exit once it enters the decode @@ -966,7 +966,7 @@ async def test_h265_video_is_hvc1(hass: HomeAssistant, worker_finished_stream) - async def test_get_image(hass: HomeAssistant, h264_video, filename) -> None: - """Test that the has_keyframe metadata matches the media.""" + """Test getting an image from the stream.""" await async_setup_component(hass, "stream", {"stream": {}}) # Since libjpeg-turbo is not installed on the CI runner, we use a mock @@ -976,10 +976,30 @@ async def test_get_image(hass: HomeAssistant, h264_video, filename) -> None: mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg() stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) - with patch.object(hass.config, "is_allowed_path", return_value=True): + worker_wake = threading.Event() + + temp_av_open = av.open + + def blocking_open(stream_source, *args, **kwargs): + # Block worker thread until test wakes up + worker_wake.wait() + return temp_av_open(stream_source, *args, **kwargs) + + with patch.object(hass.config, "is_allowed_path", return_value=True), patch( + "av.open", new=blocking_open + ): make_recording = hass.async_create_task(stream.async_record(filename)) + assert stream._keyframe_converter._image is None + # async_get_image should not work because there is no keyframe yet + assert not await stream.async_get_image() + # async_get_image should work if called with wait_for_next_keyframe=True + next_keyframe_request = hass.async_create_task( + stream.async_get_image(wait_for_next_keyframe=True) + ) + worker_wake.set() await make_recording - assert stream._keyframe_converter._image is None + + assert await next_keyframe_request == EMPTY_8_6_JPEG assert await stream.async_get_image() == EMPTY_8_6_JPEG @@ -1008,7 +1028,7 @@ async def test_worker_disable_ll_hls(hass: HomeAssistant) -> None: async def test_get_image_rotated(hass: HomeAssistant, h264_video, filename) -> None: - """Test that the has_keyframe metadata matches the media.""" + """Test getting a rotated image.""" await async_setup_component(hass, "stream", {"stream": {}}) # Since libjpeg-turbo is not installed on the CI runner, we use a mock From 2fc728db420b604dcf3d9cad4bb134cc14bd9a1b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 26 Aug 2023 19:42:49 +0200 Subject: [PATCH 0904/1151] Remove unused variable from Airthings BLE (#99085) * Remove unused variable from Airthings BLE * Remove unused variable from Airthings BLE --- homeassistant/components/airthings_ble/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 7f44d71a9fa..4783f3e3b35 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -166,7 +166,6 @@ class AirthingsSensor( name += f" ({identifier})" self._attr_unique_id = f"{name}_{entity_description.key}" - self._id = airthings_device.address self._attr_device_info = DeviceInfo( connections={ ( From a81e6d5811356b966c6444dd6a8a162e36a9cb0a Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Sat, 26 Aug 2023 21:13:25 +0200 Subject: [PATCH 0905/1151] Bump python bsblan 0.5.14 (#99089) * update python-bsblan to 0.5.14 * fix test diagnostics --- homeassistant/components/bsblan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../bsblan/snapshots/test_diagnostics.ambr | 18 +++++++++--------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index 5abb888513d..d5866bf8b42 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["bsblan"], - "requirements": ["python-bsblan==0.5.11"] + "requirements": ["python-bsblan==0.5.14"] } diff --git a/requirements_all.txt b/requirements_all.txt index 85c76129b25..d8d3890bbc6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2071,7 +2071,7 @@ python-awair==0.2.4 python-blockchain-api==0.0.2 # homeassistant.components.bsblan -python-bsblan==0.5.11 +python-bsblan==0.5.14 # homeassistant.components.clementine python-clementine-remote==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e5e8373075..dc7358f8811 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1539,7 +1539,7 @@ pytautulli==23.1.1 python-awair==0.2.4 # homeassistant.components.bsblan -python-bsblan==0.5.11 +python-bsblan==0.5.14 # homeassistant.components.ecobee python-ecobee-api==0.2.14 diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr index 2fff33de046..b172d26c249 100644 --- a/tests/components/bsblan/snapshots/test_diagnostics.ambr +++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr @@ -9,21 +9,21 @@ }), 'info': dict({ 'controller_family': dict({ - 'dataType': 0, + 'data_type': 0, 'desc': '', 'name': 'Device family', 'unit': '', 'value': '211', }), 'controller_variant': dict({ - 'dataType': 0, + 'data_type': 0, 'desc': '', 'name': 'Device variant', 'unit': '', 'value': '127', }), 'device_identification': dict({ - 'dataType': 7, + 'data_type': 7, 'desc': '', 'name': 'Gerte-Identifikation', 'unit': '', @@ -32,42 +32,42 @@ }), 'state': dict({ 'current_temperature': dict({ - 'dataType': 0, + 'data_type': 0, 'desc': '', 'name': 'Room temp 1 actual value', 'unit': '°C', 'value': '18.6', }), 'hvac_action': dict({ - 'dataType': 1, + 'data_type': 1, 'desc': 'Raumtemp’begrenzung', 'name': 'Status heating circuit 1', 'unit': '', 'value': '122', }), 'hvac_mode': dict({ - 'dataType': 1, + 'data_type': 1, 'desc': 'Komfort', 'name': 'Operating mode', 'unit': '', 'value': 'heat', }), 'hvac_mode2': dict({ - 'dataType': 1, + 'data_type': 1, 'desc': 'Reduziert', 'name': 'Operating mode', 'unit': '', 'value': '2', }), 'room1_thermostat_mode': dict({ - 'dataType': 1, + 'data_type': 1, 'desc': 'Kein Bedarf', 'name': 'Raumthermostat 1', 'unit': '', 'value': '0', }), 'target_temperature': dict({ - 'dataType': 0, + 'data_type': 0, 'desc': '', 'name': 'Room temperature Comfort setpoint', 'unit': '°C', From 45efe292625f13f4581872a1e78d849ff0257b95 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 27 Aug 2023 01:27:45 +0200 Subject: [PATCH 0906/1151] Bump aiounifi to v58 (#99103) --- homeassistant/components/unifi/controller.py | 18 ++++++++++-------- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_controller.py | 18 ++++++------------ 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index c1ffa0aa57d..59cbbb5b7fd 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -10,6 +10,7 @@ from typing import Any from aiohttp import CookieJar import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent +from aiounifi.models.configuration import Configuration from aiounifi.websocket import WebsocketState from homeassistant.config_entries import ConfigEntry @@ -409,18 +410,19 @@ async def get_unifi_controller( ) controller = aiounifi.Controller( - host=config[CONF_HOST], - username=config[CONF_USERNAME], - password=config[CONF_PASSWORD], - port=config[CONF_PORT], - site=config[CONF_SITE_ID], - websession=session, - ssl_context=ssl_context, + Configuration( + session, + host=config[CONF_HOST], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + port=config[CONF_PORT], + site=config[CONF_SITE_ID], + ssl_context=ssl_context, + ) ) try: async with asyncio.timeout(10): - await controller.check_unifi_os() await controller.login() return controller diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 579e64c5862..363313bf878 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==55"], + "requirements": ["aiounifi==58"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index d8d3890bbc6..ce37e1f72f1 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==55 +aiounifi==58 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc7358f8811..9c319ef31f0 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==55 +aiounifi==58 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index a2be388af4c..18fe92e6d64 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -395,9 +395,7 @@ async def test_reconnect_mechanism( await setup_unifi_integration(hass, aioclient_mock) aioclient_mock.clear_requests() - aioclient_mock.post( - f"https://{DEFAULT_HOST}:1234/api/login", status=HTTPStatus.BAD_GATEWAY - ) + aioclient_mock.get(f"https://{DEFAULT_HOST}:1234/", status=HTTPStatus.BAD_GATEWAY) mock_unifi_websocket(state=WebsocketState.DISCONNECTED) await hass.async_block_till_done() @@ -448,9 +446,7 @@ async def test_reconnect_mechanism_exceptions( async def test_get_unifi_controller(hass: HomeAssistant) -> None: """Successful call.""" - with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( - "aiounifi.Controller.login", return_value=True - ): + with patch("aiounifi.Controller.login", return_value=True): assert await get_unifi_controller(hass, ENTRY_CONFIG) @@ -458,9 +454,7 @@ async def test_get_unifi_controller_verify_ssl_false(hass: HomeAssistant) -> Non """Successful call with verify ssl set to false.""" controller_data = dict(ENTRY_CONFIG) controller_data[CONF_VERIFY_SSL] = False - with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( - "aiounifi.Controller.login", return_value=True - ): + with patch("aiounifi.Controller.login", return_value=True): assert await get_unifi_controller(hass, controller_data) @@ -481,7 +475,7 @@ async def test_get_unifi_controller_fails_to_connect( hass: HomeAssistant, side_effect, raised_exception ) -> None: """Check that get_unifi_controller can handle controller being unavailable.""" - with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( - "aiounifi.Controller.login", side_effect=side_effect - ), pytest.raises(raised_exception): + with patch("aiounifi.Controller.login", side_effect=side_effect), pytest.raises( + raised_exception + ): await get_unifi_controller(hass, ENTRY_CONFIG) From 0362ce92b5d118ceac2b74afa6b875d0aeb49ca7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 02:37:27 -0500 Subject: [PATCH 0907/1151] Drop switchbot codeowner (#99108) I picked up working on this integration because I wanted to make sure the new Bluetooth stack had a good test case to work out issues and did not generate unexpected breaking changes. Since I do not use switchbot in production, I usually cannot help solve problems beyond the Bluetooth stack that is visible to HA. While I am still happy to do code reviews here, the Bluetooth stack has matured to the point where watching for issues here is no longer helpful to maintaining the stack as the signal to noise ratio is too high --- CODEOWNERS | 4 ++-- homeassistant/components/switchbot/manifest.json | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index dd52cb196a6..c5c55876054 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1228,8 +1228,8 @@ build.json @home-assistant/supervisor /tests/components/switch_as_x/ @home-assistant/core /homeassistant/components/switchbee/ @jafar-atili /tests/components/switchbee/ @jafar-atili -/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski -/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski +/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski +/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /homeassistant/components/switcher_kis/ @thecode /tests/components/switcher_kis/ @thecode /homeassistant/components/switchmate/ @danielhiversen @qiz-li diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index e45ea1f893e..237ba98c19d 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -28,7 +28,6 @@ } ], "codeowners": [ - "@bdraco", "@danielhiversen", "@RenierM26", "@murtas", From 54cd0e81838bf750c344e36500edc01651b9e33a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 05:33:00 -0500 Subject: [PATCH 0908/1151] Add some missing typing to isy994 (#99110) --- homeassistant/components/isy994/binary_sensor.py | 3 ++- homeassistant/components/isy994/button.py | 3 ++- homeassistant/components/isy994/climate.py | 3 ++- homeassistant/components/isy994/cover.py | 3 ++- homeassistant/components/isy994/fan.py | 3 ++- homeassistant/components/isy994/light.py | 3 ++- homeassistant/components/isy994/lock.py | 3 ++- homeassistant/components/isy994/number.py | 3 ++- homeassistant/components/isy994/sensor.py | 3 ++- 9 files changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 32fa72e5565..69db4afd1be 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -44,6 +44,7 @@ from .const import ( TYPE_INSTEON_MOTION, ) from .entity import ISYNodeEntity, ISYProgramEntity +from .models import IsyData DEVICE_PARENT_REQUIRED = [ BinarySensorDeviceClass.OPENING, @@ -79,7 +80,7 @@ async def async_setup_entry( | ISYBinarySensorProgramEntity ) - isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] devices: dict[str, DeviceInfo] = isy_data.devices for node in isy_data.nodes[Platform.BINARY_SENSOR]: assert isinstance(node, Node) diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index 1ccc3acf659..6e00e1934f2 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -21,6 +21,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_NETWORK, DOMAIN +from .models import IsyData async def async_setup_entry( @@ -29,7 +30,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up ISY/IoX button from config entry.""" - isy_data = hass.data[DOMAIN][config_entry.entry_id] + isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id] isy: ISY = isy_data.root device_info = isy_data.devices entities: list[ diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 8b244004da3..4ddbbd86060 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -56,6 +56,7 @@ from .const import ( ) from .entity import ISYNodeEntity from .helpers import convert_isy_value_to_hass +from .models import IsyData async def async_setup_entry( @@ -64,7 +65,7 @@ async def async_setup_entry( """Set up the ISY thermostat platform.""" entities = [] - isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] devices: dict[str, DeviceInfo] = isy_data.devices for node in isy_data.nodes[Platform.CLIMATE]: entities.append(ISYThermostatEntity(node, devices.get(node.primary_node))) diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 1b1b5e226f7..2ada6339295 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -18,13 +18,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import _LOGGER, DOMAIN, UOM_8_BIT_RANGE from .entity import ISYNodeEntity, ISYProgramEntity +from .models import IsyData async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY cover platform.""" - isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [] devices: dict[str, DeviceInfo] = isy_data.devices for node in isy_data.nodes[Platform.COVER]: diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 99e359b27b9..e451ef882b4 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -20,6 +20,7 @@ from homeassistant.util.percentage import ( from .const import _LOGGER, DOMAIN from .entity import ISYNodeEntity, ISYProgramEntity +from .models import IsyData SPEED_RANGE = (1, 255) # off is not included @@ -28,7 +29,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY fan platform.""" - isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] devices: dict[str, DeviceInfo] = isy_data.devices entities: list[ISYFanEntity | ISYFanProgramEntity] = [] diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index a1fa8975e79..b16b4ca5a83 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -17,6 +17,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE from .entity import ISYNodeEntity +from .models import IsyData ATTR_LAST_BRIGHTNESS = "last_brightness" @@ -25,7 +26,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY light platform.""" - isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] devices: dict[str, DeviceInfo] = isy_data.devices isy_options = entry.options restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 81c7e925af2..67c2587a238 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -18,6 +18,7 @@ from homeassistant.helpers.entity_platform import ( from .const import DOMAIN from .entity import ISYNodeEntity, ISYProgramEntity +from .models import IsyData from .services import ( SERVICE_DELETE_USER_CODE_SCHEMA, SERVICE_DELETE_ZWAVE_LOCK_USER_CODE, @@ -49,7 +50,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY lock platform.""" - isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] devices: dict[str, DeviceInfo] = isy_data.devices entities: list[ISYLockEntity | ISYLockProgramEntity] = [] for node in isy_data.nodes[Platform.LOCK]: diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py index 7448ba7ff27..baadf3b2dc7 100644 --- a/homeassistant/components/isy994/number.py +++ b/homeassistant/components/isy994/number.py @@ -51,6 +51,7 @@ from .const import ( ) from .entity import ISYAuxControlEntity from .helpers import convert_isy_value_to_hass +from .models import IsyData ISY_MAX_SIZE = (2**32) / 2 ON_RANGE = (1, 255) # Off is not included @@ -81,7 +82,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up ISY/IoX number entities from config entry.""" - isy_data = hass.data[DOMAIN][config_entry.entry_id] + isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id] device_info = isy_data.devices entities: list[ ISYVariableNumberEntity | ISYAuxControlNumberEntity | ISYBacklightNumberEntity diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index a994e1dadef..b1899100dd4 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -45,6 +45,7 @@ from .const import ( ) from .entity import ISYNodeEntity from .helpers import convert_isy_value_to_hass +from .models import IsyData # Disable general purpose and redundant sensors by default AUX_DISABLED_BY_DEFAULT_MATCH = ["GV", "DO"] @@ -109,7 +110,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY sensor platform.""" - isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] entities: list[ISYSensorEntity] = [] devices: dict[str, DeviceInfo] = isy_data.devices From 2dd6b26fbc5eb28cace2e6ea52992b7bd6ad8b1f Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Sun, 27 Aug 2023 13:43:30 +0300 Subject: [PATCH 0909/1151] Add type hints to transmission (#99117) * Add type hints * Apply suggestions --- .../components/transmission/__init__.py | 29 ++++++++++++------- .../components/transmission/config_flow.py | 8 +++-- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 43a37179b03..c3c364e229b 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -1,7 +1,8 @@ """Support for the Transmission BitTorrent client API.""" from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta from functools import partial import logging import re @@ -27,7 +28,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import ( config_validation as cv, entity_registry as er, @@ -159,7 +164,9 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -async def get_api(hass, entry): +async def get_api( + hass: HomeAssistant, entry: dict[str, Any] +) -> transmission_rpc.Client: """Get Transmission client.""" host = entry[CONF_HOST] port = entry[CONF_PORT] @@ -205,24 +212,26 @@ def _get_client(hass: HomeAssistant, data: dict[str, Any]) -> TransmissionClient class TransmissionClient: """Transmission Client Object.""" - def __init__(self, hass, config_entry): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the Transmission RPC API.""" self.hass = hass self.config_entry = config_entry self.tm_api: transmission_rpc.Client = None - self._tm_data: TransmissionData = None - self.unsub_timer = None + self._tm_data: TransmissionData | None = None + self.unsub_timer: Callable[[], None] | None = None @property def api(self) -> TransmissionData: """Return the TransmissionData object.""" + if self._tm_data is None: + raise HomeAssistantError("data not initialized") return self._tm_data async def async_setup(self) -> None: """Set up the Transmission client.""" try: - self.tm_api = await get_api(self.hass, self.config_entry.data) + self.tm_api = await get_api(self.hass, dict(self.config_entry.data)) except CannotConnect as error: raise ConfigEntryNotReady from error except (AuthenticationError, UnknownError) as error: @@ -328,12 +337,12 @@ class TransmissionClient: self.config_entry, options=options ) - def set_scan_interval(self, scan_interval): + def set_scan_interval(self, scan_interval: float) -> None: """Update scan interval.""" - def refresh(event_time): + def refresh(event_time: datetime): """Get the latest data from Transmission.""" - self._tm_data.update() + self.api.update() if self.unsub_timer is not None: self.unsub_timer() diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index b7784fbe4a9..d1005f5e84c 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -57,7 +57,9 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return TransmissionOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -141,7 +143,9 @@ class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): """Initialize Transmission options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Transmission options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) From 7070302001744a1d3b179c69161fdb57aa5d7215 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 27 Aug 2023 04:05:17 -0700 Subject: [PATCH 0910/1151] Use climate entity built in attrs for nest climate (#99093) * Use climate entity built in attrs for nest climate * Update homeassistant/components/nest/climate.py Co-authored-by: Shay Levy --------- Co-authored-by: Shay Levy --- homeassistant/components/nest/climate.py | 45 ++++++------------------ 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 0dcdec1cac1..03fb79eb78e 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -32,7 +32,6 @@ 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.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_DEVICE_MANAGER, DOMAIN @@ -106,17 +105,18 @@ class ThermostatEntity(ClimateEntity): """Initialize ThermostatEntity.""" self._device = device self._device_info = NestDeviceInfo(device) - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" # The API "name" field is a unique device identifier. - return self._device.name - - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes.""" - return self._device_info.device_info + self._attr_unique_id = device.name + self._attr_device_info = self._device_info.device_info + self._attr_temperature_unit = UnitOfTemperature.CELSIUS + if mode_trait := device.traits.get(ThermostatModeTrait.NAME): + self._attr_hvac_modes = [ + THERMOSTAT_MODE_MAP[mode] + for mode in mode_trait.available_modes + if mode in THERMOSTAT_MODE_MAP + ] + else: + self._attr_hvac_modes = [] @property def available(self) -> bool: @@ -130,11 +130,6 @@ class ThermostatEntity(ClimateEntity): self._device.add_update_listener(self.async_write_ha_state) ) - @property - def temperature_unit(self) -> str: - """Return the unit of temperature measurement for the system.""" - return UnitOfTemperature.CELSIUS - @property def current_temperature(self) -> float | None: """Return the current temperature.""" @@ -201,24 +196,6 @@ class ThermostatEntity(ClimateEntity): hvac_mode = THERMOSTAT_MODE_MAP[trait.mode] return hvac_mode - @property - def hvac_modes(self) -> list[HVACMode]: - """List of available operation modes.""" - supported_modes = [] - for mode in self._get_device_hvac_modes: - if mode in THERMOSTAT_MODE_MAP: - supported_modes.append(THERMOSTAT_MODE_MAP[mode]) - return supported_modes - - @property - def _get_device_hvac_modes(self) -> set[str]: - """Return the set of SDM API hvac modes supported by the device.""" - modes = [] - if ThermostatModeTrait.NAME in self._device.traits: - trait = self._device.traits[ThermostatModeTrait.NAME] - modes.extend(trait.available_modes) - return set(modes) - @property def hvac_action(self) -> HVACAction | None: """Return the current HVAC action (heating, cooling).""" From c087e6eab61444cc17852fd131bd84218462f20a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 08:59:28 -0500 Subject: [PATCH 0911/1151] Revert "Bump python bsblan 0.5.14" (#99130) --- homeassistant/components/bsblan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../bsblan/snapshots/test_diagnostics.ambr | 18 +++++++++--------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index d5866bf8b42..5abb888513d 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["bsblan"], - "requirements": ["python-bsblan==0.5.14"] + "requirements": ["python-bsblan==0.5.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index ce37e1f72f1..1200b7c7352 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2071,7 +2071,7 @@ python-awair==0.2.4 python-blockchain-api==0.0.2 # homeassistant.components.bsblan -python-bsblan==0.5.14 +python-bsblan==0.5.11 # homeassistant.components.clementine python-clementine-remote==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c319ef31f0..2aacc289bd1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1539,7 +1539,7 @@ pytautulli==23.1.1 python-awair==0.2.4 # homeassistant.components.bsblan -python-bsblan==0.5.14 +python-bsblan==0.5.11 # homeassistant.components.ecobee python-ecobee-api==0.2.14 diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr index b172d26c249..2fff33de046 100644 --- a/tests/components/bsblan/snapshots/test_diagnostics.ambr +++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr @@ -9,21 +9,21 @@ }), 'info': dict({ 'controller_family': dict({ - 'data_type': 0, + 'dataType': 0, 'desc': '', 'name': 'Device family', 'unit': '', 'value': '211', }), 'controller_variant': dict({ - 'data_type': 0, + 'dataType': 0, 'desc': '', 'name': 'Device variant', 'unit': '', 'value': '127', }), 'device_identification': dict({ - 'data_type': 7, + 'dataType': 7, 'desc': '', 'name': 'Gerte-Identifikation', 'unit': '', @@ -32,42 +32,42 @@ }), 'state': dict({ 'current_temperature': dict({ - 'data_type': 0, + 'dataType': 0, 'desc': '', 'name': 'Room temp 1 actual value', 'unit': '°C', 'value': '18.6', }), 'hvac_action': dict({ - 'data_type': 1, + 'dataType': 1, 'desc': 'Raumtemp’begrenzung', 'name': 'Status heating circuit 1', 'unit': '', 'value': '122', }), 'hvac_mode': dict({ - 'data_type': 1, + 'dataType': 1, 'desc': 'Komfort', 'name': 'Operating mode', 'unit': '', 'value': 'heat', }), 'hvac_mode2': dict({ - 'data_type': 1, + 'dataType': 1, 'desc': 'Reduziert', 'name': 'Operating mode', 'unit': '', 'value': '2', }), 'room1_thermostat_mode': dict({ - 'data_type': 1, + 'dataType': 1, 'desc': 'Kein Bedarf', 'name': 'Raumthermostat 1', 'unit': '', 'value': '0', }), 'target_temperature': dict({ - 'data_type': 0, + 'dataType': 0, 'desc': '', 'name': 'Room temperature Comfort setpoint', 'unit': '°C', From a73f214eadadd7530353db958d3754994a7d0bc0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 16:03:36 +0200 Subject: [PATCH 0912/1151] Add typing to Venstar Config flow (#99016) --- .../components/venstar/config_flow.py | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/venstar/config_flow.py b/homeassistant/components/venstar/config_flow.py index d97c5ada9e6..66ce22cb00b 100644 --- a/homeassistant/components/venstar/config_flow.py +++ b/homeassistant/components/venstar/config_flow.py @@ -1,8 +1,10 @@ """Config flow to configure the Venstar integration.""" +from typing import Any + from venstarcolortouch import VenstarColorTouch import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -10,21 +12,15 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import ConfigType from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): str, - vol.Optional(CONF_USERNAME): str, - vol.Optional(CONF_PASSWORD): str, - vol.Optional(CONF_PIN): str, - vol.Optional(CONF_SSL, default=False): bool, - } -) - -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> str: """Validate the user input allows us to connect.""" username = data.get(CONF_USERNAME) password = data.get(CONF_PASSWORD) @@ -48,37 +44,48 @@ async def validate_input(hass: core.HomeAssistant, data): if not info_success: raise CannotConnect - return {"title": client.name} + return client.name -class VenstarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class VenstarConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a venstar config flow.""" 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: """Create config entry. Show the setup form to the user.""" errors = {} - info = {} if user_input is not None: self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) try: - info = await validate_input(self.hass, user_input) + title = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - return self.async_create_entry(title=info["title"], data=user_input) + return self.async_create_entry(title=title, data=user_input) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Optional(CONF_PIN): str, + vol.Optional(CONF_SSL, default=False): bool, + } + ), + errors=errors, ) - async def async_step_import(self, import_data): + async def async_step_import(self, import_data: ConfigType) -> FlowResult: """Import entry from configuration.yaml.""" self._async_abort_entries_match({CONF_HOST: import_data[CONF_HOST]}) return await self.async_step_user( @@ -92,5 +99,5 @@ class VenstarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" From 6e157fef18b3658f416907dcd8164fb240ad41e0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 16:06:08 +0200 Subject: [PATCH 0913/1151] Add device info to Withings (#99052) --- homeassistant/components/withings/common.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index ef3b6456d20..17e3c551bcc 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -38,12 +38,13 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( AbstractOAuth2Implementation, OAuth2Session, ) +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt as dt_util from . import const -from .const import Measurement +from .const import DOMAIN, Measurement _LOGGER = logging.getLogger(const.LOG_NAMESPACE) _RETRY_COEFFICIENT = 0.5 @@ -561,6 +562,10 @@ class BaseWithingsSensor(Entity): description, data_manager.user_id ) self._state_data: Any | None = None + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, str(data_manager.user_id))}, + name=data_manager.profile, + ) @property def available(self) -> bool: From 045c514c1835b32c107d51d0b04f1768638513a8 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 27 Aug 2023 16:08:58 +0200 Subject: [PATCH 0914/1151] Bump async-upnp-client to 0.35.0 (#99129) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 350ea692338..23c45b73ec5 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.34.1", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.35.0", "getmac==0.8.2"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 9aabc3cea5e..2adb2e76347 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.34.1"], + "requirements": ["async-upnp-client==0.35.0"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index d32e71c71c0..9461eb86af6 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -39,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.6.0", "wakeonlan==2.1.0", - "async-upnp-client==0.34.1" + "async-upnp-client==0.35.0" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 61b6b05d9d6..a6eb95933b4 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.34.1"] + "requirements": ["async-upnp-client==0.35.0"] } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 4b4f0358bb9..95bb3e77966 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.34.1", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.35.0", "getmac==0.8.2"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 766ac0700e5..993cc6ca4fa 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.13", "async-upnp-client==0.34.1"], + "requirements": ["yeelight==0.7.13", "async-upnp-client==0.35.0"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b032ff6c148..3189bd2c56a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -3,7 +3,7 @@ aiohttp-cors==0.7.0 aiohttp==3.8.5 astral==2.2 async-timeout==4.0.3 -async-upnp-client==0.34.1 +async-upnp-client==0.35.0 atomicwrites-homeassistant==1.4.1 attrs==23.1.0 awesomeversion==22.9.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1200b7c7352..7a5ae79a54c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -446,7 +446,7 @@ asterisk-mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.34.1 +async-upnp-client==0.35.0 # homeassistant.components.esphome async_interrupt==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2aacc289bd1..e1e73fdce39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -397,7 +397,7 @@ arcam-fmj==1.4.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.34.1 +async-upnp-client==0.35.0 # homeassistant.components.esphome async_interrupt==1.1.1 From faed58c01b2ed6b87e8c6404950a235e03d9532e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 16:09:15 +0200 Subject: [PATCH 0915/1151] Migrate Somfy mylink to has entity name (#98947) --- homeassistant/components/somfy_mylink/cover.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index c4c506401d9..38f5fdc12f8 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -65,6 +65,8 @@ class SomfyShade(RestoreEntity, CoverEntity): _attr_should_poll = False _attr_assumed_state = True + _attr_has_entity_name = True + _attr_name = None def __init__( self, @@ -78,7 +80,6 @@ class SomfyShade(RestoreEntity, CoverEntity): self.somfy_mylink = somfy_mylink self._target_id = target_id self._attr_unique_id = target_id - self._attr_name = name self._reverse = reverse self._attr_is_closed = None self._attr_device_class = device_class From 11cecc3f0a11e019315b8c2e35260065dd0c8556 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 16:11:45 +0200 Subject: [PATCH 0916/1151] Use shorthand attributes for airtouch4 (#99086) --- homeassistant/components/airtouch4/climate.py | 48 +++++++------------ 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index 882cc1de068..bd1c481ce65 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -98,28 +98,20 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): self._ac_number = ac_number self._airtouch = coordinator.airtouch self._info = info - self._unit = self._airtouch.GetAcs()[self._ac_number] + self._unit = self._airtouch.GetAcs()[ac_number] + self._attr_unique_id = f"ac_{ac_number}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"ac_{ac_number}")}, + name=f"AC {ac_number}", + manufacturer="Airtouch", + model="Airtouch 4", + ) @callback def _handle_coordinator_update(self): self._unit = self._airtouch.GetAcs()[self._ac_number] return super()._handle_coordinator_update() - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, - name=f"AC {self._ac_number}", - manufacturer="Airtouch", - model="Airtouch 4", - ) - - @property - def unique_id(self): - """Return unique ID for this device.""" - return f"ac_{self._ac_number}" - @property def current_temperature(self): """Return the current temperature.""" @@ -208,29 +200,21 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): """Initialize the climate device.""" super().__init__(coordinator) self._group_number = group_number + self._attr_unique_id = group_number self._airtouch = coordinator.airtouch self._info = info - self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number) - - @callback - def _handle_coordinator_update(self): - self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number) - return super()._handle_coordinator_update() - - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, + self._unit = self._airtouch.GetGroupByGroupNumber(group_number) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, group_number)}, manufacturer="Airtouch", model="Airtouch 4", name=self._unit.GroupName, ) - @property - def unique_id(self): - """Return unique ID for this device.""" - return self._group_number + @callback + def _handle_coordinator_update(self): + self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number) + return super()._handle_coordinator_update() @property def min_temp(self): From b266096ae10575c64467c56342af367cce62d2f2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 16:19:15 +0200 Subject: [PATCH 0917/1151] Use snapshot assertion for Watttime diagnostics test (#99023) --- .../watttime/snapshots/test_diagnostics.ambr | 32 ++++++++++++++++ tests/components/watttime/test_diagnostics.py | 37 ++++--------------- 2 files changed, 39 insertions(+), 30 deletions(-) create mode 100644 tests/components/watttime/snapshots/test_diagnostics.ambr diff --git a/tests/components/watttime/snapshots/test_diagnostics.ambr b/tests/components/watttime/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..e1cf4a8a42f --- /dev/null +++ b/tests/components/watttime/snapshots/test_diagnostics.ambr @@ -0,0 +1,32 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'ba': 'CAISO_NORTH', + 'freq': '300', + 'moer': '850.743982', + 'percent': '53', + 'point_time': '2019-01-29T14:55:00.00Z', + }), + 'entry': dict({ + 'data': dict({ + 'balancing_authority': '**REDACTED**', + 'balancing_authority_abbreviation': '**REDACTED**', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'password': '**REDACTED**', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'watttime', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/watttime/test_diagnostics.py b/tests/components/watttime/test_diagnostics.py index 8f40e8dbcd2..1f45ba870fc 100644 --- a/tests/components/watttime/test_diagnostics.py +++ b/tests/components/watttime/test_diagnostics.py @@ -1,5 +1,7 @@ """Test WattTime diagnostics.""" -from homeassistant.components.diagnostics import REDACTED +from syrupy import SnapshotAssertion +from syrupy.filters import props + from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -11,34 +13,9 @@ async def test_entry_diagnostics( config_entry, hass_client: ClientSessionGenerator, setup_watttime, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "entry_id": config_entry.entry_id, - "version": 1, - "domain": "watttime", - "title": REDACTED, - "data": { - "username": REDACTED, - "password": REDACTED, - "latitude": REDACTED, - "longitude": REDACTED, - "balancing_authority": REDACTED, - "balancing_authority_abbreviation": REDACTED, - }, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - }, - "data": { - "freq": "300", - "ba": "CAISO_NORTH", - "percent": "53", - "moer": "850.743982", - "point_time": "2019-01-29T14:55:00.00Z", - }, - } + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("entry_id")) From 5cc49f6dd61e856c69c91f196d0982afdddd5ff4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 09:27:30 -0500 Subject: [PATCH 0918/1151] Bump dbus-fast to 1.94.1 (#99132) --- 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 e6916d00881..090531c0fea 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.9.0", - "dbus-fast==1.94.0" + "dbus-fast==1.94.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3189bd2c56a..513ee3d205a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-data-tools==1.9.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 -dbus-fast==1.94.0 +dbus-fast==1.94.1 fnv-hash-fast==0.4.0 ha-av==10.1.1 hass-nabucasa==0.70.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7a5ae79a54c..6f4179fc87e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -635,7 +635,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.94.0 +dbus-fast==1.94.1 # homeassistant.components.debugpy debugpy==1.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1e73fdce39..e2c3395274a 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.94.0 +dbus-fast==1.94.1 # homeassistant.components.debugpy debugpy==1.6.7 From 842a56f5c7f7e77bf69944149449fe5743ce7047 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 09:28:23 -0500 Subject: [PATCH 0919/1151] Bump zeroconf to 0.83.1 (#99134) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 2f75e4008fd..838227bf563 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.83.0"] + "requirements": ["zeroconf==0.83.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 513ee3d205a..2013e6900e2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.83.0 +zeroconf==0.83.1 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 6f4179fc87e..227c5555672 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2759,7 +2759,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.83.0 +zeroconf==0.83.1 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2c3395274a..d4646472f9a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2029,7 +2029,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.83.0 +zeroconf==0.83.1 # homeassistant.components.zeversolar zeversolar==0.3.1 From 20b8c5dd26953bafe00691e4880cfdfb47ad72e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 09:29:17 -0500 Subject: [PATCH 0920/1151] Bump home-assistant-bluetooth to 1.10.3 (#99133) --- 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 2013e6900e2..7a4c9aa4518 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ fnv-hash-fast==0.4.0 ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 -home-assistant-bluetooth==1.10.2 +home-assistant-bluetooth==1.10.3 home-assistant-frontend==20230802.1 home-assistant-intents==2023.8.2 httpx==0.24.1 diff --git a/pyproject.toml b/pyproject.toml index 38501a024f2..eeafdbf6f16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.24.1", - "home-assistant-bluetooth==1.10.2", + "home-assistant-bluetooth==1.10.3", "ifaddr==0.2.0", "Jinja2==3.1.2", "lru-dict==1.2.0", diff --git a/requirements.txt b/requirements.txt index 72fa57f6b1b..2daf8c1718f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ bcrypt==4.0.1 certifi>=2021.5.30 ciso8601==2.3.0 httpx==0.24.1 -home-assistant-bluetooth==1.10.2 +home-assistant-bluetooth==1.10.3 ifaddr==0.2.0 Jinja2==3.1.2 lru-dict==1.2.0 From 71bf782b22addf2179536baf2b0edca86ba128f3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 27 Aug 2023 16:58:48 +0200 Subject: [PATCH 0921/1151] Improve UniFi PoE control by queueing commands together (#99114) * Working draft without timer * Clean up Improve tests * Use async_call_later --- homeassistant/components/unifi/controller.py | 35 ++++++++++++- homeassistant/components/unifi/switch.py | 52 +++++++++----------- tests/components/unifi/test_switch.py | 18 ++++++- 3 files changed, 75 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 59cbbb5b7fd..0b0caf3add6 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -11,6 +11,7 @@ from aiohttp import CookieJar import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.models.configuration import Configuration +from aiounifi.models.device import DeviceSetPoePortModeRequest from aiounifi.websocket import WebsocketState from homeassistant.config_entries import ConfigEntry @@ -35,7 +36,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_entries_for_config_entry -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import async_call_later, async_track_time_interval import homeassistant.util.dt as dt_util from .const import ( @@ -99,6 +100,9 @@ class UniFiController: self.entities: dict[str, str] = {} self.known_objects: set[tuple[str, str]] = set() + self.poe_command_queue: dict[str, dict[int, str]] = {} + self._cancel_poe_command: CALLBACK_TYPE | None = None + def load_config_entry_options(self) -> None: """Store attributes to avoid property call overhead since they are called frequently.""" options = self.config_entry.options @@ -312,6 +316,31 @@ class UniFiController: for unique_id in unique_ids_to_remove: del self._heartbeat_time[unique_id] + @callback + def async_queue_poe_port_command( + self, device_id: str, port_idx: int, poe_mode: str + ) -> None: + """Queue commands to execute them together per device.""" + if self._cancel_poe_command: + self._cancel_poe_command() + self._cancel_poe_command = None + + device_queue = self.poe_command_queue.setdefault(device_id, {}) + device_queue[port_idx] = poe_mode + + async def async_execute_command(now: datetime) -> None: + """Execute previously queued commands.""" + queue = self.poe_command_queue.copy() + self.poe_command_queue.clear() + for device_id, device_commands in queue.items(): + device = self.api.devices[device_id] + commands = [(idx, mode) for idx, mode in device_commands.items()] + await self.api.request( + DeviceSetPoePortModeRequest.create(device, targets=commands) + ) + + self._cancel_poe_command = async_call_later(self.hass, 5, async_execute_command) + async def async_update_device_registry(self) -> None: """Update device registry.""" if self.mac is None: @@ -390,6 +419,10 @@ class UniFiController: self._cancel_heartbeat_check() self._cancel_heartbeat_check = None + if self._cancel_poe_command: + self._cancel_poe_command() + self._cancel_poe_command = None + return True diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 046aa3a1abd..560e150e63c 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -22,10 +22,7 @@ from aiounifi.interfaces.ports import Ports from aiounifi.interfaces.wlans import Wlans from aiounifi.models.api import ApiItemT from aiounifi.models.client import Client, ClientBlockRequest -from aiounifi.models.device import ( - DeviceSetOutletRelayRequest, - DeviceSetPoePortModeRequest, -) +from aiounifi.models.device import DeviceSetOutletRelayRequest from aiounifi.models.dpi_restriction_app import DPIRestrictionAppEnableRequest from aiounifi.models.dpi_restriction_group import DPIRestrictionGroup from aiounifi.models.event import Event, EventKey @@ -107,20 +104,22 @@ def async_port_forward_device_info_fn( async def async_block_client_control_fn( - api: aiounifi.Controller, obj_id: str, target: bool + controller: UniFiController, obj_id: str, target: bool ) -> None: """Control network access of client.""" - await api.request(ClientBlockRequest.create(obj_id, not target)) + await controller.api.request(ClientBlockRequest.create(obj_id, not target)) async def async_dpi_group_control_fn( - api: aiounifi.Controller, obj_id: str, target: bool + controller: UniFiController, obj_id: str, target: bool ) -> None: """Enable or disable DPI group.""" - dpi_group = api.dpi_groups[obj_id] + dpi_group = controller.api.dpi_groups[obj_id] await asyncio.gather( *[ - api.request(DPIRestrictionAppEnableRequest.create(app_id, target)) + controller.api.request( + DPIRestrictionAppEnableRequest.create(app_id, target) + ) for app_id in dpi_group.dpiapp_ids or [] ] ) @@ -136,46 +135,47 @@ def async_outlet_supports_switching_fn( async def async_outlet_control_fn( - api: aiounifi.Controller, obj_id: str, target: bool + controller: UniFiController, obj_id: str, target: bool ) -> None: """Control outlet relay.""" mac, _, index = obj_id.partition("_") - device = api.devices[mac] - await api.request(DeviceSetOutletRelayRequest.create(device, int(index), target)) + device = controller.api.devices[mac] + await controller.api.request( + DeviceSetOutletRelayRequest.create(device, int(index), target) + ) async def async_poe_port_control_fn( - api: aiounifi.Controller, obj_id: str, target: bool + controller: UniFiController, obj_id: str, target: bool ) -> None: """Control poe state.""" mac, _, index = obj_id.partition("_") - device = api.devices[mac] - port = api.ports[obj_id] + port = controller.api.ports[obj_id] on_state = "auto" if port.raw["poe_caps"] != 8 else "passthrough" state = on_state if target else "off" - await api.request(DeviceSetPoePortModeRequest.create(device, int(index), state)) + controller.async_queue_poe_port_command(mac, int(index), state) async def async_port_forward_control_fn( - api: aiounifi.Controller, obj_id: str, target: bool + controller: UniFiController, obj_id: str, target: bool ) -> None: """Control port forward state.""" - port_forward = api.port_forwarding[obj_id] - await api.request(PortForwardEnableRequest.create(port_forward, target)) + port_forward = controller.api.port_forwarding[obj_id] + await controller.api.request(PortForwardEnableRequest.create(port_forward, target)) async def async_wlan_control_fn( - api: aiounifi.Controller, obj_id: str, target: bool + controller: UniFiController, obj_id: str, target: bool ) -> None: """Control outlet relay.""" - await api.request(WlanEnableRequest.create(obj_id, target)) + await controller.api.request(WlanEnableRequest.create(obj_id, target)) @dataclass class UnifiSwitchEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): """Validate and load entities from different UniFi handlers.""" - control_fn: Callable[[aiounifi.Controller, str, bool], Coroutine[Any, Any, None]] + control_fn: Callable[[UniFiController, str, bool], Coroutine[Any, Any, None]] is_on_fn: Callable[[UniFiController, ApiItemT], bool] @@ -352,15 +352,11 @@ class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" - await self.entity_description.control_fn( - self.controller.api, self._obj_id, True - ) + await self.entity_description.control_fn(self.controller, self._obj_id, True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" - await self.entity_description.control_fn( - self.controller.api, self._obj_id, False - ) + await self.entity_description.control_fn(self.controller, self._obj_id, False) @callback def async_update_state(self, event: ItemEvent, obj_id: str) -> None: diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 8e3e215e717..d376cab8add 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1345,6 +1345,9 @@ async def test_poe_port_switches( ent_reg.async_update_entity( entity_id="switch.mock_name_port_1_poe", disabled_by=None ) + ent_reg.async_update_entity( + entity_id="switch.mock_name_port_2_poe", disabled_by=None + ) await hass.async_block_till_done() async_fire_time_changed( @@ -1378,6 +1381,8 @@ async def test_poe_port_switches( {"entity_id": "switch.mock_name_port_1_poe"}, blocking=True, ) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() assert aioclient_mock.call_count == 1 assert aioclient_mock.mock_calls[0][2] == { "port_overrides": [{"poe_mode": "off", "port_idx": 1, "portconf_id": "1a1"}] @@ -1390,9 +1395,20 @@ async def test_poe_port_switches( {"entity_id": "switch.mock_name_port_1_poe"}, blocking=True, ) + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_off", + {"entity_id": "switch.mock_name_port_2_poe"}, + blocking=True, + ) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() assert aioclient_mock.call_count == 2 assert aioclient_mock.mock_calls[1][2] == { - "port_overrides": [{"poe_mode": "auto", "port_idx": 1, "portconf_id": "1a1"}] + "port_overrides": [ + {"poe_mode": "auto", "port_idx": 1, "portconf_id": "1a1"}, + {"poe_mode": "off", "port_idx": 2, "portconf_id": "1a2"}, + ] } # Availability signalling From 5e5193eeb5142015b794880381b032e0d2e140f6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 27 Aug 2023 17:07:38 +0200 Subject: [PATCH 0922/1151] Rework UniFi Network Controller device and add software version (#99136) Rework Network Controller device and add software version --- homeassistant/components/unifi/__init__.py | 2 +- homeassistant/components/unifi/controller.py | 47 +++++++++++--------- tests/components/unifi/test_controller.py | 44 ++++++++++-------- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 4d11a690f35..10959b8965c 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) - await controller.async_update_device_registry() + controller.async_update_device_registry() if len(hass.data[UNIFI_DOMAIN]) == 1: async_setup_services(hass) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 0b0caf3add6..ba188f80135 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -29,7 +29,11 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.device_registry import ( + DeviceEntry, + DeviceEntryType, + DeviceInfo, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -158,14 +162,6 @@ class UniFiController: host: str = self.config_entry.data[CONF_HOST] return host - @property - def mac(self) -> str | None: - """Return the mac address of this controller.""" - for client in self.api.clients.values(): - if self.host == client.ip: - return client.mac - return None - @callback def register_platform_add_entities( self, @@ -341,19 +337,30 @@ class UniFiController: self._cancel_poe_command = async_call_later(self.hass, 5, async_execute_command) - async def async_update_device_registry(self) -> None: + @property + def device_info(self) -> DeviceInfo: + """UniFi controller device info.""" + assert self.config_entry.unique_id is not None + + version: str | None = None + if sysinfo := next(iter(self.api.system_information.values()), None): + version = sysinfo.version + + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(UNIFI_DOMAIN, self.config_entry.unique_id)}, + manufacturer=ATTR_MANUFACTURER, + model="UniFi Network Application", + name="UniFi Network", + sw_version=version, + ) + + @callback + def async_update_device_registry(self) -> DeviceEntry: """Update device registry.""" - if self.mac is None: - return - device_registry = dr.async_get(self.hass) - - device_registry.async_get_or_create( - config_entry_id=self.config_entry.entry_id, - connections={(CONNECTION_NETWORK_MAC, self.mac)}, - default_manufacturer=ATTR_MANUFACTURER, - default_model="UniFi Network", - default_name="UniFi Network", + return device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, **self.device_info ) @staticmethod diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 18fe92e6d64..f4738862aef 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -40,7 +40,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -81,6 +80,24 @@ CONFIGURATION = [] SITE = [{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}] +SYSTEM_INFORMATION = [ + { + "anonymous_controller_id": "24f81231-a456-4c32-abcd-f5612345385f", + "build": "atag_7.4.162_21057", + "console_display_version": "3.1.15", + "hostname": "UDMP", + "name": "UDMP", + "previous_version": "7.4.156", + "timezone": "Europe/Stockholm", + "ubnt_device_type": "UDMPRO", + "udm_version": "3.0.20.9281", + "update_available": False, + "update_downloaded": False, + "uptime": 1196290, + "version": "7.4.162", + } +] + def mock_default_unifi_requests( aioclient_mock, @@ -224,7 +241,9 @@ async def test_controller_setup( "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", return_value=True, ) as forward_entry_setup: - config_entry = await setup_unifi_integration(hass, aioclient_mock) + config_entry = await setup_unifi_integration( + hass, aioclient_mock, system_information_response=SYSTEM_INFORMATION + ) controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] entry = controller.config_entry @@ -247,29 +266,16 @@ async def test_controller_setup( assert controller.option_detection_time == timedelta(seconds=DEFAULT_DETECTION_TIME) assert isinstance(controller.option_ssid_filter, set) - assert controller.mac is None - assert controller.signal_reachable == "unifi-reachable-1" assert controller.signal_options_update == "unifi-options-1" assert controller.signal_heartbeat_missed == "unifi-heartbeat-missed" - -async def test_controller_mac( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that it is possible to identify controller mac.""" - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=[CONTROLLER_HOST] - ) - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - assert controller.mac == CONTROLLER_HOST["mac"] - - device_registry = dr.async_get(hass) - device_entry = device_registry.async_get_or_create( + device_entry = dr.async_get(hass).async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(CONNECTION_NETWORK_MAC, controller.mac)}, + identifiers={(UNIFI_DOMAIN, config_entry.unique_id)}, ) - assert device_entry + + assert device_entry.sw_version == "7.4.162" async def test_controller_not_accessible(hass: HomeAssistant) -> None: From 6992ea9af0ec340089a8f3e0f5924af21634811b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 10:08:21 -0500 Subject: [PATCH 0923/1151] Bump fnv-hash-fast to 0.4.1 (#99135) --- homeassistant/components/homekit/manifest.json | 2 +- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 04ba4cc1a6a..6f3067d7a78 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -10,7 +10,7 @@ "loggers": ["pyhap"], "requirements": [ "HAP-python==4.7.1", - "fnv-hash-fast==0.4.0", + "fnv-hash-fast==0.4.1", "PyQRCode==1.2.1", "base36==0.1.1" ], diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 6f919ee50da..63b19cdb3bf 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -8,7 +8,7 @@ "quality_scale": "internal", "requirements": [ "SQLAlchemy==2.0.15", - "fnv-hash-fast==0.4.0", + "fnv-hash-fast==0.4.1", "psutil-home-assistant==0.0.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7a4c9aa4518..16cf9815734 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 dbus-fast==1.94.1 -fnv-hash-fast==0.4.0 +fnv-hash-fast==0.4.1 ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 diff --git a/requirements_all.txt b/requirements_all.txt index 227c5555672..1508c7a96e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -804,7 +804,7 @@ flux-led==1.0.2 # homeassistant.components.homekit # homeassistant.components.recorder -fnv-hash-fast==0.4.0 +fnv-hash-fast==0.4.1 # homeassistant.components.foobot foobot-async==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d4646472f9a..cc312cb208a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -629,7 +629,7 @@ flux-led==1.0.2 # homeassistant.components.homekit # homeassistant.components.recorder -fnv-hash-fast==0.4.0 +fnv-hash-fast==0.4.1 # homeassistant.components.foobot foobot-async==1.0.0 From 6cd28b64e8b9f6af833ea5eb267c4d29fed6cccb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 10:10:00 -0500 Subject: [PATCH 0924/1151] Bump bluetooth-data-tools 1.9.1 (#99131) --- homeassistant/components/bluetooth/manifest.json | 2 +- 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 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 090531c0fea..59a87f4dfbb 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.9.0", + "bluetooth-data-tools==1.9.1", "dbus-fast==1.94.1" ] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 0bcae814b75..dca1bce0d24 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "requirements": [ "async_interrupt==1.1.1", "aioesphomeapi==16.0.1", - "bluetooth-data-tools==1.9.0", + "bluetooth-data-tools==1.9.1", "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 6065009760e..0c77e0e2ef5 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.9.0", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.9.1", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index b9bdf31f066..36e3b7355ff 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.9.0", "led-ble==1.0.0"] + "requirements": ["bluetooth-data-tools==1.9.1", "led-ble==1.0.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 16cf9815734..04aab76f5b7 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.9.0 +bluetooth-data-tools==1.9.1 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 1508c7a96e5..76631991621 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.9.0 +bluetooth-data-tools==1.9.1 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc312cb208a..1b088fb4c9c 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.9.0 +bluetooth-data-tools==1.9.1 # homeassistant.components.bond bond-async==0.2.1 From f42b8e217b84947219793aef858757d4dca3e877 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 11:32:41 -0500 Subject: [PATCH 0925/1151] Bump ulid-transform to 0.8.1 (#99139) --- 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 04aab76f5b7..34847cbf353 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -48,7 +48,7 @@ requests==2.31.0 scapy==2.5.0 SQLAlchemy==2.0.15 typing-extensions>=4.7.0,<5.0 -ulid-transform==0.8.0 +ulid-transform==0.8.1 voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 diff --git a/pyproject.toml b/pyproject.toml index eeafdbf6f16..375aa7e5088 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ dependencies = [ "PyYAML==6.0.1", "requests==2.31.0", "typing-extensions>=4.7.0,<5.0", - "ulid-transform==0.8.0", + "ulid-transform==0.8.1", "voluptuous==0.13.1", "voluptuous-serialize==2.6.0", "yarl==1.9.2", diff --git a/requirements.txt b/requirements.txt index 2daf8c1718f..10220697390 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ python-slugify==4.0.1 PyYAML==6.0.1 requests==2.31.0 typing-extensions>=4.7.0,<5.0 -ulid-transform==0.8.0 +ulid-transform==0.8.1 voluptuous==0.13.1 voluptuous-serialize==2.6.0 yarl==1.9.2 From d21ee30ddfd060dfb5c9264e733eabe2b2884145 Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 27 Aug 2023 18:51:31 +0200 Subject: [PATCH 0926/1151] Extend Nextcloud integration (#94066) --- .../components/nextcloud/__init__.py | 19 +- .../components/nextcloud/binary_sensor.py | 41 ++- .../components/nextcloud/coordinator.py | 4 +- homeassistant/components/nextcloud/entity.py | 19 +- homeassistant/components/nextcloud/sensor.py | 296 +++++++++++++++--- 5 files changed, 311 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 866fe3befa9..27c9b8b6078 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -1,5 +1,7 @@ """The Nextcloud integration.""" +import logging + from nextcloudmonitor import ( NextcloudMonitor, NextcloudMonitorAuthorizationError, @@ -17,7 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator @@ -26,10 +28,25 @@ PLATFORMS = (Platform.SENSOR, Platform.BINARY_SENSOR) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Nextcloud integration.""" + # migrate old entity unique ids + entity_reg = er.async_get(hass) + entities: list[er.RegistryEntry] = er.async_entries_for_config_entry( + entity_reg, entry.entry_id + ) + for entity in entities: + old_uid_start = f"{entry.data[CONF_URL]}#nextcloud_" + new_uid_start = f"{entry.data[CONF_URL]}#" + if entity.unique_id.startswith(old_uid_start): + new_uid = entity.unique_id.replace(old_uid_start, new_uid_start) + _LOGGER.debug("migrate unique id '%s' to '%s'", entity.unique_id, new_uid) + entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_uid) + def _connect_nc(): return NextcloudMonitor( entry.data[CONF_URL], diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py index 3cf3cc3ae2a..5281342dc14 100644 --- a/homeassistant/components/nextcloud/binary_sensor.py +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -1,8 +1,14 @@ """Summary binary data from Nextcoud.""" from __future__ import annotations -from homeassistant.components.binary_sensor import BinarySensorEntity +from typing import Final + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -10,12 +16,28 @@ from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator from .entity import NextcloudEntity -BINARY_SENSORS = ( - "nextcloud_system_enable_avatars", - "nextcloud_system_enable_previews", - "nextcloud_system_filelocking.enabled", - "nextcloud_system_debug", -) +BINARY_SENSORS: Final[dict[str, BinarySensorEntityDescription]] = { + "system_debug": BinarySensorEntityDescription( + key="system_debug", + translation_key="nextcloud_system_debug", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "system_enable_avatars": BinarySensorEntityDescription( + key="system_enable_avatars", + translation_key="nextcloud_system_enable_avatars", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "system_enable_previews": BinarySensorEntityDescription( + key="system_enable_previews", + translation_key="nextcloud_system_enable_previews", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "system_filelocking.enabled": BinarySensorEntityDescription( + key="system_filelocking.enabled", + translation_key="nextcloud_system_filelocking_enabled", + entity_category=EntityCategory.DIAGNOSTIC, + ), +} async def async_setup_entry( @@ -25,7 +47,7 @@ async def async_setup_entry( coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - NextcloudBinarySensor(coordinator, name, entry) + NextcloudBinarySensor(coordinator, name, entry, BINARY_SENSORS[name]) for name in coordinator.data if name in BINARY_SENSORS ] @@ -38,4 +60,5 @@ class NextcloudBinarySensor(NextcloudEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.coordinator.data.get(self.item) == "yes" + val = self.coordinator.data.get(self.item) + return val is True or val == "yes" diff --git a/homeassistant/components/nextcloud/coordinator.py b/homeassistant/components/nextcloud/coordinator.py index 73a07a77e23..c721168e848 100644 --- a/homeassistant/components/nextcloud/coordinator.py +++ b/homeassistant/components/nextcloud/coordinator.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import DEFAULT_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -55,7 +55,7 @@ class NextcloudDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): leaf = True result.update(self._get_data_points(value, key_path, leaf)) else: - result[f"{DOMAIN}_{key_path}{key}"] = value + result[f"{key_path}{key}"] = value leaf = False return result diff --git a/homeassistant/components/nextcloud/entity.py b/homeassistant/components/nextcloud/entity.py index 17d59fe6b29..8f3ec55beec 100644 --- a/homeassistant/components/nextcloud/entity.py +++ b/homeassistant/components/nextcloud/entity.py @@ -1,8 +1,10 @@ """Base entity for the Nextcloud integration.""" +from urllib.parse import urlparse + from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import slugify from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator @@ -12,19 +14,22 @@ class NextcloudEntity(CoordinatorEntity[NextcloudDataUpdateCoordinator]): """Base Nextcloud entity.""" _attr_has_entity_name = True - _attr_icon = "mdi:cloud" def __init__( - self, coordinator: NextcloudDataUpdateCoordinator, item: str, entry: ConfigEntry + self, + coordinator: NextcloudDataUpdateCoordinator, + item: str, + entry: ConfigEntry, + desc: EntityDescription, ) -> None: """Initialize the Nextcloud sensor.""" super().__init__(coordinator) self.item = item - self._attr_translation_key = slugify(item) self._attr_unique_id = f"{coordinator.url}#{item}" self._attr_device_info = DeviceInfo( - name="Nextcloud", - identifiers={(DOMAIN, entry.entry_id)}, - sw_version=coordinator.data.get("nextcloud_system_version"), configuration_url=coordinator.url, + identifiers={(DOMAIN, entry.entry_id)}, + name=urlparse(coordinator.url).netloc, + sw_version=coordinator.data.get("system_version"), ) + self.entity_description = desc diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index a5df872e084..a91efee2284 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -1,8 +1,16 @@ """Summary data from Nextcoud.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from datetime import UTC, datetime +from typing import Final, cast + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory, UnitOfInformation, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -11,51 +19,235 @@ from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator from .entity import NextcloudEntity -SENSORS = ( - "nextcloud_system_version", - "nextcloud_system_theme", - "nextcloud_system_memcache.local", - "nextcloud_system_memcache.distributed", - "nextcloud_system_memcache.locking", - "nextcloud_system_freespace", - "nextcloud_system_cpuload", - "nextcloud_system_mem_total", - "nextcloud_system_mem_free", - "nextcloud_system_swap_total", - "nextcloud_system_swap_free", - "nextcloud_system_apps_num_installed", - "nextcloud_system_apps_num_updates_available", - "nextcloud_system_apps_app_updates_calendar", - "nextcloud_system_apps_app_updates_contacts", - "nextcloud_system_apps_app_updates_tasks", - "nextcloud_system_apps_app_updates_twofactor_totp", - "nextcloud_storage_num_users", - "nextcloud_storage_num_files", - "nextcloud_storage_num_storages", - "nextcloud_storage_num_storages_local", - "nextcloud_storage_num_storages_home", - "nextcloud_storage_num_storages_other", - "nextcloud_shares_num_shares", - "nextcloud_shares_num_shares_user", - "nextcloud_shares_num_shares_groups", - "nextcloud_shares_num_shares_link", - "nextcloud_shares_num_shares_mail", - "nextcloud_shares_num_shares_room", - "nextcloud_shares_num_shares_link_no_password", - "nextcloud_shares_num_fed_shares_sent", - "nextcloud_shares_num_fed_shares_received", - "nextcloud_shares_permissions_3_1", - "nextcloud_server_webserver", - "nextcloud_server_php_version", - "nextcloud_server_php_memory_limit", - "nextcloud_server_php_max_execution_time", - "nextcloud_server_php_upload_max_filesize", - "nextcloud_database_type", - "nextcloud_database_version", - "nextcloud_activeUsers_last5minutes", - "nextcloud_activeUsers_last1hour", - "nextcloud_activeUsers_last24hours", -) +UNIT_OF_LOAD: Final[str] = "load" + +SENSORS: Final[dict[str, SensorEntityDescription]] = { + "activeUsers_last1hour": SensorEntityDescription( + key="activeUsers_last1hour", + translation_key="nextcloud_activeusers_last1hour", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:account-multiple", + ), + "activeUsers_last24hours": SensorEntityDescription( + key="activeUsers_last24hours", + translation_key="nextcloud_activeusers_last24hours", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:account-multiple", + ), + "activeUsers_last5minutes": SensorEntityDescription( + key="activeUsers_last5minutes", + translation_key="nextcloud_activeusers_last5minutes", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:account-multiple", + ), + "database_type": SensorEntityDescription( + key="database_type", + translation_key="nextcloud_database_type", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:database", + ), + "database_version": SensorEntityDescription( + key="database_version", + translation_key="nextcloud_database_version", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:database", + ), + "server_php_max_execution_time": SensorEntityDescription( + key="server_php_max_execution_time", + translation_key="nextcloud_server_php_max_execution_time", + device_class=SensorDeviceClass.DURATION, + entity_category=EntityCategory.CONFIG, + icon="mdi:language-php", + native_unit_of_measurement=UnitOfTime.SECONDS, + ), + "server_php_memory_limit": SensorEntityDescription( + key="server_php_memory_limit", + translation_key="nextcloud_server_php_memory_limit", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.CONFIG, + icon="mdi:language-php", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + "server_php_upload_max_filesize": SensorEntityDescription( + key="server_php_upload_max_filesize", + translation_key="nextcloud_server_php_upload_max_filesize", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.CONFIG, + icon="mdi:language-php", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + "server_php_version": SensorEntityDescription( + key="server_php_version", + translation_key="nextcloud_server_php_version", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:language-php", + ), + "server_webserver": SensorEntityDescription( + key="server_webserver", + translation_key="nextcloud_server_webserver", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_fed_shares_sent": SensorEntityDescription( + key="shares_num_fed_shares_sent", + translation_key="nextcloud_shares_num_fed_shares_sent", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_fed_shares_received": SensorEntityDescription( + key="shares_num_fed_shares_received", + translation_key="nextcloud_shares_num_fed_shares_received", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares": SensorEntityDescription( + key="shares_num_shares", + translation_key="nextcloud_shares_num_shares", + ), + "shares_num_shares_groups": SensorEntityDescription( + key="shares_num_shares_groups", + translation_key="nextcloud_shares_num_shares_groups", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares_link": SensorEntityDescription( + key="shares_num_shares_link", + translation_key="nextcloud_shares_num_shares_link", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares_link_no_password": SensorEntityDescription( + key="shares_num_shares_link_no_password", + translation_key="nextcloud_shares_num_shares_link_no_password", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares_mail": SensorEntityDescription( + key="shares_num_shares_mail", + translation_key="nextcloud_shares_num_shares_mail", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares_room": SensorEntityDescription( + key="shares_num_shares_room", + translation_key="nextcloud_shares_num_shares_room", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "shares_num_shares_user": SensorEntityDescription( + key="server_num_shares_user", + translation_key="nextcloud_shares_num_shares_user", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "storage_num_files": SensorEntityDescription( + key="storage_num_files", + translation_key="nextcloud_storage_num_files", + ), + "storage_num_storages": SensorEntityDescription( + key="storage_num_storages", + translation_key="nextcloud_storage_num_storages", + ), + "storage_num_storages_home": SensorEntityDescription( + key="storage_num_storages_home", + translation_key="nextcloud_storage_num_storages_home", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "storage_num_storages_local": SensorEntityDescription( + key="storage_num_storages_local", + translation_key="nextcloud_storage_num_storages_local", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "storage_num_storages_other": SensorEntityDescription( + key="storage_num_storages_other", + translation_key="nextcloud_storage_num_storages_other", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "storage_num_users": SensorEntityDescription( + key="storage_num_users", + translation_key="nextcloud_storage_num_users", + ), + "system_apps_num_installed": SensorEntityDescription( + key="system_apps_num_installed", + translation_key="nextcloud_system_apps_num_installed", + ), + "system_apps_num_updates_available": SensorEntityDescription( + key="system_apps_num_updates_available", + translation_key="nextcloud_system_apps_num_updates_available", + icon="mdi:update", + ), + "system_cpuload": SensorEntityDescription( + key="system_cpuload", + translation_key="nextcloud_system_cpuload", + icon="mdi:chip", + ), + "system_freespace": SensorEntityDescription( + key="system_freespace", + translation_key="nextcloud_system_freespace", + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:harddisk", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + ), + "system_mem_free": SensorEntityDescription( + key="system_mem_free", + translation_key="nextcloud_system_mem_free", + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:memory", + native_unit_of_measurement=UnitOfInformation.KILOBYTES, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + ), + "system_mem_total": SensorEntityDescription( + key="system_mem_total", + translation_key="nextcloud_system_mem_total", + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:memory", + native_unit_of_measurement=UnitOfInformation.KILOBYTES, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + ), + "system_memcache.distributed": SensorEntityDescription( + key="system_memcache.distributed", + translation_key="nextcloud_system_memcache_distributed", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "system_memcache.local": SensorEntityDescription( + key="system_memcache.local", + translation_key="nextcloud_system_memcache_local", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "system_memcache.locking": SensorEntityDescription( + key="system_memcache.locking", + translation_key="nextcloud_system_memcache_locking", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "system_swap_total": SensorEntityDescription( + key="system_swap_total", + translation_key="nextcloud_system_swap_total", + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:memory", + native_unit_of_measurement=UnitOfInformation.KILOBYTES, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + ), + "system_swap_free": SensorEntityDescription( + key="system_swap_free", + translation_key="nextcloud_system_swap_free", + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:memory", + native_unit_of_measurement=UnitOfInformation.KILOBYTES, + suggested_display_precision=2, + suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, + ), + "system_theme": SensorEntityDescription( + key="system_theme", + translation_key="nextcloud_system_theme", + ), + "system_version": SensorEntityDescription( + key="system_version", + translation_key="nextcloud_system_version", + ), +} async def async_setup_entry( @@ -65,7 +257,7 @@ async def async_setup_entry( coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - NextcloudSensor(coordinator, name, entry) + NextcloudSensor(coordinator, name, entry, SENSORS[name]) for name in coordinator.data if name in SENSORS ] @@ -76,6 +268,12 @@ class NextcloudSensor(NextcloudEntity, SensorEntity): """Represents a Nextcloud sensor.""" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state for this sensor.""" - return self.coordinator.data.get(self.item) + val = self.coordinator.data.get(self.item) + if ( + getattr(self.entity_description, "device_class", None) + == SensorDeviceClass.TIMESTAMP + ): + return datetime.fromtimestamp(cast(int, val), tz=UTC) + return val From d17ffff3e3524e75fa29ff2efbf26bae05fb7443 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 12:00:39 -0500 Subject: [PATCH 0927/1151] Retry tplink setup later if device has an unexpected mac address (#98784) Retry tplink setup later if device has an unexpected serial If the DHCP reservation changed and there is now a different tplink device at the saved IP address, retry setup later to avoid cross linking devices --- homeassistant/components/tplink/__init__.py | 14 ++++++++++++- tests/components/tplink/test_init.py | 22 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 2edea30835f..d8285cbed70 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -85,11 +85,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up TPLink from a config entry.""" + host = entry.data[CONF_HOST] try: - device: SmartDevice = await Discover.discover_single(entry.data[CONF_HOST]) + device: SmartDevice = await Discover.discover_single(host) except SmartDeviceException as ex: raise ConfigEntryNotReady from ex + found_mac = dr.format_mac(device.mac) + if found_mac != entry.unique_id: + # If the mac address of the device does not match the unique_id + # of the config entry, it likely means the DHCP lease has expired + # and the device has been assigned a new IP address. We need to + # wait for the next discovery to find the device at its new address + # and update the config entry so we do not mix up devices. + raise ConfigEntryNotReady( + f"Unexpected device found at {host}; expected {entry.unique_id}, found {found_mac}" + ) + hass.data[DOMAIN][entry.entry_id] = TPLinkDataUpdateCoordinator(hass, device) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 0354549d451..4206c0de6ad 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -4,6 +4,8 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import MagicMock, patch +import pytest + from homeassistant import setup from homeassistant.components import tplink from homeassistant.components.tplink.const import DOMAIN @@ -111,3 +113,23 @@ async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( ) assert migrated_dimmer_entity_reg.entity_id == original_dimmer_entity_reg.entity_id assert migrated_dimmer_entity_reg.entity_id != rollout_dimmer_entity_reg.entity_id + + +async def test_config_entry_wrong_mac_Address( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test config entry enters setup retry when mac address mismatches.""" + mismatched_mac = f"{MAC_ADDRESS[:-1]}0" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=mismatched_mac + ) + already_migrated_config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_single_discovery(): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + + assert ( + "Unexpected device found at 127.0.0.1; expected aa:bb:cc:dd:ee:f0, found aa:bb:cc:dd:ee:ff" + in caplog.text + ) From cc103ddbaa18339b279434b94fe6532cd3d7f0aa Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 27 Aug 2023 19:34:58 +0200 Subject: [PATCH 0928/1151] Split Owncloud CPU load in separate sensors (#99141) * split cpu load values into own sensors * apply suggestion --- .../components/nextcloud/coordinator.py | 5 +++++ homeassistant/components/nextcloud/sensor.py | 22 ++++++++++++++++--- .../components/nextcloud/strings.json | 10 +++++++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nextcloud/coordinator.py b/homeassistant/components/nextcloud/coordinator.py index c721168e848..b5dc5e29507 100644 --- a/homeassistant/components/nextcloud/coordinator.py +++ b/homeassistant/components/nextcloud/coordinator.py @@ -54,6 +54,11 @@ class NextcloudDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): key_path += f"{key}_" leaf = True result.update(self._get_data_points(value, key_path, leaf)) + elif key == "cpuload" and isinstance(value, list): + result[f"{key_path}{key}_1"] = value[0] + result[f"{key_path}{key}_5"] = value[1] + result[f"{key_path}{key}_15"] = value[2] + leaf = False else: result[f"{key_path}{key}"] = value leaf = False diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index a91efee2284..daac259a3b9 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -171,10 +171,26 @@ SENSORS: Final[dict[str, SensorEntityDescription]] = { translation_key="nextcloud_system_apps_num_updates_available", icon="mdi:update", ), - "system_cpuload": SensorEntityDescription( - key="system_cpuload", - translation_key="nextcloud_system_cpuload", + "system_cpuload_1": SensorEntityDescription( + key="system_cpuload_1", + translation_key="nextcloud_system_cpuload_1", + native_unit_of_measurement=UNIT_OF_LOAD, icon="mdi:chip", + suggested_display_precision=2, + ), + "system_cpuload_5": SensorEntityDescription( + key="system_cpuload_5", + translation_key="nextcloud_system_cpuload_5", + native_unit_of_measurement=UNIT_OF_LOAD, + icon="mdi:chip", + suggested_display_precision=2, + ), + "system_cpuload_15": SensorEntityDescription( + key="system_cpuload_15", + translation_key="nextcloud_system_cpuload_15", + native_unit_of_measurement=UNIT_OF_LOAD, + icon="mdi:chip", + suggested_display_precision=2, ), "system_freespace": SensorEntityDescription( key="system_freespace", diff --git a/homeassistant/components/nextcloud/strings.json b/homeassistant/components/nextcloud/strings.json index 6c70421bf93..7be15a6e62b 100644 --- a/homeassistant/components/nextcloud/strings.json +++ b/homeassistant/components/nextcloud/strings.json @@ -63,8 +63,14 @@ "nextcloud_system_freespace": { "name": "Free space" }, - "nextcloud_system_cpuload": { - "name": "CPU Load" + "nextcloud_system_cpuload_1": { + "name": "CPU Load last minute" + }, + "nextcloud_system_cpuload_15": { + "name": "CPU Load last 15 minutes" + }, + "nextcloud_system_cpuload_5": { + "name": "CPU Load last 5 minutes" }, "nextcloud_system_mem_total": { "name": "Total memory" From 4394fc2897c8e480cff24f27107ae813d52f4d62 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 19:38:12 +0200 Subject: [PATCH 0929/1151] Fix typo in AnthemAV const (#99149) --- homeassistant/components/anthemav/__init__.py | 4 ++-- homeassistant/components/anthemav/const.py | 2 +- homeassistant/components/anthemav/media_player.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index fe7fe072785..0a7e36d8a95 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ANTHEMAV_UDATE_SIGNAL, DEVICE_TIMEOUT_SECONDS, DOMAIN +from .const import ANTHEMAV_UPDATE_SIGNAL, DEVICE_TIMEOUT_SECONDS, DOMAIN PLATFORMS = [Platform.MEDIA_PLAYER] @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def async_anthemav_update_callback(message: str) -> None: """Receive notification from transport that new data exists.""" _LOGGER.debug("Received update callback from AVR: %s", message) - async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.entry_id}") + async_dispatcher_send(hass, f"{ANTHEMAV_UPDATE_SIGNAL}_{entry.entry_id}") try: avr = await anthemav.Connection.create( diff --git a/homeassistant/components/anthemav/const.py b/homeassistant/components/anthemav/const.py index 02f56aed5c4..2b1ff753fba 100644 --- a/homeassistant/components/anthemav/const.py +++ b/homeassistant/components/anthemav/const.py @@ -1,5 +1,5 @@ """Constants for the Anthem A/V Receivers integration.""" -ANTHEMAV_UDATE_SIGNAL = "anthemav_update" +ANTHEMAV_UPDATE_SIGNAL = "anthemav_update" CONF_MODEL = "model" DEFAULT_NAME = "Anthem AV" DEFAULT_PORT = 14999 diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index a28a428a550..4056a34995a 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -19,7 +19,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ANTHEMAV_UDATE_SIGNAL, CONF_MODEL, DOMAIN, MANUFACTURER +from .const import ANTHEMAV_UPDATE_SIGNAL, CONF_MODEL, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -96,7 +96,7 @@ class AnthemAVR(MediaPlayerEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{ANTHEMAV_UDATE_SIGNAL}_{self._entry_id}", + f"{ANTHEMAV_UPDATE_SIGNAL}_{self._entry_id}", self.update_states, ) ) From e1dc133fa16c4d3a51d9f65de27e8d05a0463c44 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 19:50:36 +0200 Subject: [PATCH 0930/1151] Add device info to Watttime (#99022) --- homeassistant/components/watttime/sensor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index c8e9a376fdc..636e73af8f2 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, PERCENTAGE, UnitOfMass from homeassistant.core import HomeAssistant +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 ( @@ -77,13 +78,14 @@ class RealtimeEmissionsSensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - - self._attr_name = ( - f"{description.name} ({entry.data[CONF_BALANCING_AUTHORITY_ABBREV]})" - ) self._attr_unique_id = f"{entry.entry_id}_{description.key}" self._entry = entry self.entity_description = description + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + name=entry.data[CONF_BALANCING_AUTHORITY_ABBREV], + entry_type=DeviceEntryType.SERVICE, + ) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: From 17fd5381985f67a975f6b4fab8bf2b1a0a828d4c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 13:00:58 -0500 Subject: [PATCH 0931/1151] Bump zeroconf to 0.84.0 (#99138) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 838227bf563..c557c671237 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.83.1"] + "requirements": ["zeroconf==0.84.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 34847cbf353..9bc11dc0520 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.83.1 +zeroconf==0.84.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 76631991621..8f1a9321ec6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2759,7 +2759,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.83.1 +zeroconf==0.84.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b088fb4c9c..30686d0445a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2029,7 +2029,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.83.1 +zeroconf==0.84.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From a48c7f67b494870b61a136f5b5ad14dff7342cb2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 20:04:49 +0200 Subject: [PATCH 0932/1151] Remove codeowner from airtouch4 (#99145) --- CODEOWNERS | 2 -- homeassistant/components/airtouch4/manifest.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c5c55876054..df3ebbe1760 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -49,8 +49,6 @@ build.json @home-assistant/supervisor /tests/components/airthings/ @danielhiversen /homeassistant/components/airthings_ble/ @vincegio /tests/components/airthings_ble/ @vincegio -/homeassistant/components/airtouch4/ @LonePurpleWolf -/tests/components/airtouch4/ @LonePurpleWolf /homeassistant/components/airvisual/ @bachya /tests/components/airvisual/ @bachya /homeassistant/components/airvisual_pro/ @bachya diff --git a/homeassistant/components/airtouch4/manifest.json b/homeassistant/components/airtouch4/manifest.json index 1e03a88da6c..e845c278a54 100644 --- a/homeassistant/components/airtouch4/manifest.json +++ b/homeassistant/components/airtouch4/manifest.json @@ -1,7 +1,7 @@ { "domain": "airtouch4", "name": "AirTouch 4", - "codeowners": ["@LonePurpleWolf"], + "codeowners": [], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airtouch4", "iot_class": "local_polling", From 65103d4515409c231d819c427becabfcc8a2b365 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 20:06:19 +0200 Subject: [PATCH 0933/1151] Improve Anova typing (#99146) --- homeassistant/components/anova/__init__.py | 6 +++--- homeassistant/components/anova/config_flow.py | 12 ++++++------ homeassistant/components/anova/entity.py | 9 +++++---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/anova/__init__.py b/homeassistant/components/anova/__init__.py index 2fee8a6beeb..6181d02025d 100644 --- a/homeassistant/components/anova/__init__.py +++ b/homeassistant/components/anova/__init__.py @@ -6,7 +6,7 @@ import logging from anova_wifi import AnovaApi, AnovaPrecisionCooker, InvalidLogin, NoDevicesFound from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device[1], api.jwt, ) - for device in entry.data["devices"] + for device in entry.data[CONF_DEVICES] ] try: new_devices = await api.get_devices() @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, data={ **entry.data, - **{"devices": serialize_device_list(devices)}, + **{CONF_DEVICES: serialize_device_list(devices)}, }, ) coordinators = [AnovaCoordinator(hass, device) for device in devices] diff --git a/homeassistant/components/anova/config_flow.py b/homeassistant/components/anova/config_flow.py index 5d0d2dbf628..d0846fbffc7 100644 --- a/homeassistant/components/anova/config_flow.py +++ b/homeassistant/components/anova/config_flow.py @@ -4,16 +4,16 @@ from __future__ import annotations from anova_wifi import AnovaApi, InvalidLogin, NoDevicesFound import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN from .util import serialize_device_list -class AnovaConfligFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AnovaConfligFlow(ConfigFlow, domain=DOMAIN): """Sets up a config flow for Anova.""" VERSION = 1 @@ -25,7 +25,7 @@ class AnovaConfligFlow(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: api = AnovaApi( - aiohttp_client.async_get_clientsession(self.hass), + async_get_clientsession(self.hass), user_input[CONF_USERNAME], user_input[CONF_PASSWORD], ) @@ -48,7 +48,7 @@ class AnovaConfligFlow(config_entries.ConfigFlow, domain=DOMAIN): data={ CONF_USERNAME: api.username, CONF_PASSWORD: api.password, - "devices": device_list, + CONF_DEVICES: device_list, }, ) diff --git a/homeassistant/components/anova/entity.py b/homeassistant/components/anova/entity.py index fd104e194f1..c4d4ff2e2b2 100644 --- a/homeassistant/components/anova/entity.py +++ b/homeassistant/components/anova/entity.py @@ -8,18 +8,19 @@ from .coordinator import AnovaCoordinator class AnovaEntity(CoordinatorEntity[AnovaCoordinator], Entity): - """Defines a Anova entity.""" + """Defines an Anova entity.""" + + _attr_has_entity_name = True def __init__(self, coordinator: AnovaCoordinator) -> None: """Initialize the Anova entity.""" super().__init__(coordinator) self.device = coordinator.anova_device self._attr_device_info = coordinator.device_info - self._attr_has_entity_name = True -class AnovaDescriptionEntity(AnovaEntity, Entity): - """Defines a Anova entity that uses a description.""" +class AnovaDescriptionEntity(AnovaEntity): + """Defines an Anova entity that uses a description.""" def __init__( self, coordinator: AnovaCoordinator, description: EntityDescription From fbe2228c3fa45336c2ee98b53a3d87159fddf232 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 20:09:10 +0200 Subject: [PATCH 0934/1151] Extract Ambient Station base entity to separate file (#99142) * Extract Ambient Station entity to separate file * Add to coveragerc --- .coveragerc | 1 + .../components/ambient_station/__init__.py | 66 +---------------- .../ambient_station/binary_sensor.py | 2 +- .../components/ambient_station/entity.py | 70 +++++++++++++++++++ .../components/ambient_station/sensor.py | 3 +- 5 files changed, 75 insertions(+), 67 deletions(-) create mode 100644 homeassistant/components/ambient_station/entity.py diff --git a/.coveragerc b/.coveragerc index 5753bc13195..5adb465509a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -57,6 +57,7 @@ omit = homeassistant/components/ambiclimate/climate.py homeassistant/components/ambient_station/__init__.py homeassistant/components/ambient_station/binary_sensor.py + homeassistant/components/ambient_station/entity.py homeassistant/components/ambient_station/sensor.py homeassistant/components/amcrest/* homeassistant/components/ampio/* diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index e8c71fcad7a..1718b559fde 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -5,7 +5,6 @@ from typing import Any from aioambient import Websocket from aioambient.errors import WebsocketError -from aioambient.util import get_public_device_id from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -19,12 +18,7 @@ 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 Entity, EntityDescription +from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.entity_registry as er from .const import ( @@ -198,61 +192,3 @@ class AmbientStation: async def ws_disconnect(self) -> None: """Disconnect from the websocket.""" await self.websocket.disconnect() - - -class AmbientWeatherEntity(Entity): - """Define a base Ambient PWS entity.""" - - _attr_has_entity_name = True - _attr_should_poll = False - - def __init__( - self, - ambient: AmbientStation, - mac_address: str, - station_name: str, - description: EntityDescription, - ) -> None: - """Initialize the entity.""" - self._ambient = ambient - - public_device_id = get_public_device_id(mac_address) - self._attr_device_info = DeviceInfo( - configuration_url=( - f"https://ambientweather.net/dashboard/{public_device_id}" - ), - identifiers={(DOMAIN, mac_address)}, - manufacturer="Ambient Weather", - name=station_name.capitalize(), - ) - - self._attr_unique_id = f"{mac_address}_{description.key}" - self._mac_address = mac_address - self.entity_description = description - - @callback - def _async_update(self) -> None: - """Update the state.""" - last_data = self._ambient.stations[self._mac_address][ATTR_LAST_DATA] - key = self.entity_description.key - available_key = TYPE_SOLARRADIATION if key == TYPE_SOLARRADIATION_LX else key - self._attr_available = last_data[available_key] is not None - self.update_from_latest_data() - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"ambient_station_data_update_{self._mac_address}", - self._async_update, - ) - ) - - self.update_from_latest_data() - - @callback - def update_from_latest_data(self) -> None: - """Update the entity from the latest data.""" - raise NotImplementedError diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index a58a0ec6f85..49ff43bcc7e 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -14,8 +14,8 @@ from homeassistant.const import ATTR_NAME, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AmbientWeatherEntity from .const import ATTR_LAST_DATA, DOMAIN +from .entity import AmbientWeatherEntity TYPE_BATT1 = "batt1" TYPE_BATT10 = "batt10" diff --git a/homeassistant/components/ambient_station/entity.py b/homeassistant/components/ambient_station/entity.py new file mode 100644 index 00000000000..277b69e8f68 --- /dev/null +++ b/homeassistant/components/ambient_station/entity.py @@ -0,0 +1,70 @@ +"""Base entity Ambient Weather Station Service.""" +from __future__ import annotations + +from aioambient.util import get_public_device_id + +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 Entity, EntityDescription + +from . import AmbientStation +from .const import ATTR_LAST_DATA, DOMAIN, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX + + +class AmbientWeatherEntity(Entity): + """Define a base Ambient PWS entity.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + ambient: AmbientStation, + mac_address: str, + station_name: str, + description: EntityDescription, + ) -> None: + """Initialize the entity.""" + self._ambient = ambient + + public_device_id = get_public_device_id(mac_address) + self._attr_device_info = DeviceInfo( + configuration_url=( + f"https://ambientweather.net/dashboard/{public_device_id}" + ), + identifiers={(DOMAIN, mac_address)}, + manufacturer="Ambient Weather", + name=station_name.capitalize(), + ) + + self._attr_unique_id = f"{mac_address}_{description.key}" + self._mac_address = mac_address + self.entity_description = description + + @callback + def _async_update(self) -> None: + """Update the state.""" + last_data = self._ambient.stations[self._mac_address][ATTR_LAST_DATA] + key = self.entity_description.key + available_key = TYPE_SOLARRADIATION if key == TYPE_SOLARRADIATION_LX else key + self._attr_available = last_data[available_key] is not None + self.update_from_latest_data() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"ambient_station_data_update_{self._mac_address}", + self._async_update, + ) + ) + + self.update_from_latest_data() + + @callback + def update_from_latest_data(self) -> None: + """Update the entity from the latest data.""" + raise NotImplementedError diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index e1f624da52f..4873da566b5 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -28,8 +28,9 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AmbientStation, AmbientWeatherEntity +from . import AmbientStation from .const import ATTR_LAST_DATA, DOMAIN, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX +from .entity import AmbientWeatherEntity TYPE_24HOURRAININ = "24hourrainin" TYPE_AQI_PM25 = "aqi_pm25" From c88672c352096d6cb4eba4fcba0383fddeca022a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 20:10:08 +0200 Subject: [PATCH 0935/1151] Make Anova device unique id public (#99147) --- homeassistant/components/anova/coordinator.py | 4 ++-- homeassistant/components/anova/entity.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anova/coordinator.py b/homeassistant/components/anova/coordinator.py index 94bd9bec9aa..83dc2c295c3 100644 --- a/homeassistant/components/anova/coordinator.py +++ b/homeassistant/components/anova/coordinator.py @@ -30,7 +30,7 @@ class AnovaCoordinator(DataUpdateCoordinator[APCUpdate]): update_interval=timedelta(seconds=30), ) assert self.config_entry is not None - self._device_unique_id = anova_device.device_key + self.device_unique_id = anova_device.device_key self.anova_device = anova_device self.device_info: DeviceInfo | None = None @@ -38,7 +38,7 @@ class AnovaCoordinator(DataUpdateCoordinator[APCUpdate]): def async_setup(self, firmware_version: str) -> None: """Set the firmware version info.""" self.device_info = DeviceInfo( - identifiers={(DOMAIN, self._device_unique_id)}, + identifiers={(DOMAIN, self.device_unique_id)}, name="Anova Precision Cooker", manufacturer="Anova", model="Precision Cooker", diff --git a/homeassistant/components/anova/entity.py b/homeassistant/components/anova/entity.py index c4d4ff2e2b2..d3ed2eb2667 100644 --- a/homeassistant/components/anova/entity.py +++ b/homeassistant/components/anova/entity.py @@ -28,4 +28,4 @@ class AnovaDescriptionEntity(AnovaEntity): """Initialize the entity and declare unique id based on description key.""" super().__init__(coordinator) self.entity_description = description - self._attr_unique_id = f"{coordinator._device_unique_id}_{description.key}" + self._attr_unique_id = f"{coordinator.device_unique_id}_{description.key}" From 0ce9d21bea3ca3584e7b88a2468e2e3d89a23b60 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 27 Aug 2023 20:18:55 +0200 Subject: [PATCH 0936/1151] Rework to use list of entity descriptions in Nextcloud integration (#99150) --- .../components/nextcloud/binary_sensor.py | 20 ++-- homeassistant/components/nextcloud/entity.py | 8 +- homeassistant/components/nextcloud/sensor.py | 92 +++++++++---------- 3 files changed, 59 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py index 5281342dc14..fa733332763 100644 --- a/homeassistant/components/nextcloud/binary_sensor.py +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -16,28 +16,28 @@ from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator from .entity import NextcloudEntity -BINARY_SENSORS: Final[dict[str, BinarySensorEntityDescription]] = { - "system_debug": BinarySensorEntityDescription( +BINARY_SENSORS: Final[list[BinarySensorEntityDescription]] = [ + BinarySensorEntityDescription( key="system_debug", translation_key="nextcloud_system_debug", entity_category=EntityCategory.DIAGNOSTIC, ), - "system_enable_avatars": BinarySensorEntityDescription( + BinarySensorEntityDescription( key="system_enable_avatars", translation_key="nextcloud_system_enable_avatars", entity_category=EntityCategory.DIAGNOSTIC, ), - "system_enable_previews": BinarySensorEntityDescription( + BinarySensorEntityDescription( key="system_enable_previews", translation_key="nextcloud_system_enable_previews", entity_category=EntityCategory.DIAGNOSTIC, ), - "system_filelocking.enabled": BinarySensorEntityDescription( + BinarySensorEntityDescription( key="system_filelocking.enabled", translation_key="nextcloud_system_filelocking_enabled", entity_category=EntityCategory.DIAGNOSTIC, ), -} +] async def async_setup_entry( @@ -47,9 +47,9 @@ async def async_setup_entry( coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - NextcloudBinarySensor(coordinator, name, entry, BINARY_SENSORS[name]) - for name in coordinator.data - if name in BINARY_SENSORS + NextcloudBinarySensor(coordinator, entry, sensor) + for sensor in BINARY_SENSORS + if sensor.key in coordinator.data ] ) @@ -60,5 +60,5 @@ class NextcloudBinarySensor(NextcloudEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - val = self.coordinator.data.get(self.item) + val = self.coordinator.data.get(self.entity_description.key) return val is True or val == "yes" diff --git a/homeassistant/components/nextcloud/entity.py b/homeassistant/components/nextcloud/entity.py index 8f3ec55beec..92ba65a134b 100644 --- a/homeassistant/components/nextcloud/entity.py +++ b/homeassistant/components/nextcloud/entity.py @@ -18,18 +18,16 @@ class NextcloudEntity(CoordinatorEntity[NextcloudDataUpdateCoordinator]): def __init__( self, coordinator: NextcloudDataUpdateCoordinator, - item: str, entry: ConfigEntry, - desc: EntityDescription, + description: EntityDescription, ) -> None: """Initialize the Nextcloud sensor.""" super().__init__(coordinator) - self.item = item - self._attr_unique_id = f"{coordinator.url}#{item}" + self._attr_unique_id = f"{coordinator.url}#{description.key}" self._attr_device_info = DeviceInfo( configuration_url=coordinator.url, identifiers={(DOMAIN, entry.entry_id)}, name=urlparse(coordinator.url).netloc, sw_version=coordinator.data.get("system_version"), ) - self.entity_description = desc + self.entity_description = description diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index daac259a3b9..a0d657e4880 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -21,38 +21,38 @@ from .entity import NextcloudEntity UNIT_OF_LOAD: Final[str] = "load" -SENSORS: Final[dict[str, SensorEntityDescription]] = { - "activeUsers_last1hour": SensorEntityDescription( +SENSORS: Final[list[SensorEntityDescription]] = [ + SensorEntityDescription( key="activeUsers_last1hour", translation_key="nextcloud_activeusers_last1hour", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:account-multiple", ), - "activeUsers_last24hours": SensorEntityDescription( + SensorEntityDescription( key="activeUsers_last24hours", translation_key="nextcloud_activeusers_last24hours", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:account-multiple", ), - "activeUsers_last5minutes": SensorEntityDescription( + SensorEntityDescription( key="activeUsers_last5minutes", translation_key="nextcloud_activeusers_last5minutes", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:account-multiple", ), - "database_type": SensorEntityDescription( + SensorEntityDescription( key="database_type", translation_key="nextcloud_database_type", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:database", ), - "database_version": SensorEntityDescription( + SensorEntityDescription( key="database_version", translation_key="nextcloud_database_version", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:database", ), - "server_php_max_execution_time": SensorEntityDescription( + SensorEntityDescription( key="server_php_max_execution_time", translation_key="nextcloud_server_php_max_execution_time", device_class=SensorDeviceClass.DURATION, @@ -60,7 +60,7 @@ SENSORS: Final[dict[str, SensorEntityDescription]] = { icon="mdi:language-php", native_unit_of_measurement=UnitOfTime.SECONDS, ), - "server_php_memory_limit": SensorEntityDescription( + SensorEntityDescription( key="server_php_memory_limit", translation_key="nextcloud_server_php_memory_limit", device_class=SensorDeviceClass.DATA_SIZE, @@ -70,7 +70,7 @@ SENSORS: Final[dict[str, SensorEntityDescription]] = { suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - "server_php_upload_max_filesize": SensorEntityDescription( + SensorEntityDescription( key="server_php_upload_max_filesize", translation_key="nextcloud_server_php_upload_max_filesize", device_class=SensorDeviceClass.DATA_SIZE, @@ -80,119 +80,119 @@ SENSORS: Final[dict[str, SensorEntityDescription]] = { suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - "server_php_version": SensorEntityDescription( + SensorEntityDescription( key="server_php_version", translation_key="nextcloud_server_php_version", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:language-php", ), - "server_webserver": SensorEntityDescription( + SensorEntityDescription( key="server_webserver", translation_key="nextcloud_server_webserver", entity_category=EntityCategory.DIAGNOSTIC, ), - "shares_num_fed_shares_sent": SensorEntityDescription( + SensorEntityDescription( key="shares_num_fed_shares_sent", translation_key="nextcloud_shares_num_fed_shares_sent", entity_category=EntityCategory.DIAGNOSTIC, ), - "shares_num_fed_shares_received": SensorEntityDescription( + SensorEntityDescription( key="shares_num_fed_shares_received", translation_key="nextcloud_shares_num_fed_shares_received", entity_category=EntityCategory.DIAGNOSTIC, ), - "shares_num_shares": SensorEntityDescription( + SensorEntityDescription( key="shares_num_shares", translation_key="nextcloud_shares_num_shares", ), - "shares_num_shares_groups": SensorEntityDescription( + SensorEntityDescription( key="shares_num_shares_groups", translation_key="nextcloud_shares_num_shares_groups", entity_category=EntityCategory.DIAGNOSTIC, ), - "shares_num_shares_link": SensorEntityDescription( + SensorEntityDescription( key="shares_num_shares_link", translation_key="nextcloud_shares_num_shares_link", entity_category=EntityCategory.DIAGNOSTIC, ), - "shares_num_shares_link_no_password": SensorEntityDescription( + SensorEntityDescription( key="shares_num_shares_link_no_password", translation_key="nextcloud_shares_num_shares_link_no_password", entity_category=EntityCategory.DIAGNOSTIC, ), - "shares_num_shares_mail": SensorEntityDescription( + SensorEntityDescription( key="shares_num_shares_mail", translation_key="nextcloud_shares_num_shares_mail", entity_category=EntityCategory.DIAGNOSTIC, ), - "shares_num_shares_room": SensorEntityDescription( + SensorEntityDescription( key="shares_num_shares_room", translation_key="nextcloud_shares_num_shares_room", entity_category=EntityCategory.DIAGNOSTIC, ), - "shares_num_shares_user": SensorEntityDescription( + SensorEntityDescription( key="server_num_shares_user", translation_key="nextcloud_shares_num_shares_user", entity_category=EntityCategory.DIAGNOSTIC, ), - "storage_num_files": SensorEntityDescription( + SensorEntityDescription( key="storage_num_files", translation_key="nextcloud_storage_num_files", ), - "storage_num_storages": SensorEntityDescription( + SensorEntityDescription( key="storage_num_storages", translation_key="nextcloud_storage_num_storages", ), - "storage_num_storages_home": SensorEntityDescription( + SensorEntityDescription( key="storage_num_storages_home", translation_key="nextcloud_storage_num_storages_home", entity_category=EntityCategory.DIAGNOSTIC, ), - "storage_num_storages_local": SensorEntityDescription( + SensorEntityDescription( key="storage_num_storages_local", translation_key="nextcloud_storage_num_storages_local", entity_category=EntityCategory.DIAGNOSTIC, ), - "storage_num_storages_other": SensorEntityDescription( + SensorEntityDescription( key="storage_num_storages_other", translation_key="nextcloud_storage_num_storages_other", entity_category=EntityCategory.DIAGNOSTIC, ), - "storage_num_users": SensorEntityDescription( + SensorEntityDescription( key="storage_num_users", translation_key="nextcloud_storage_num_users", ), - "system_apps_num_installed": SensorEntityDescription( + SensorEntityDescription( key="system_apps_num_installed", translation_key="nextcloud_system_apps_num_installed", ), - "system_apps_num_updates_available": SensorEntityDescription( + SensorEntityDescription( key="system_apps_num_updates_available", translation_key="nextcloud_system_apps_num_updates_available", icon="mdi:update", ), - "system_cpuload_1": SensorEntityDescription( + SensorEntityDescription( key="system_cpuload_1", translation_key="nextcloud_system_cpuload_1", native_unit_of_measurement=UNIT_OF_LOAD, icon="mdi:chip", suggested_display_precision=2, ), - "system_cpuload_5": SensorEntityDescription( + SensorEntityDescription( key="system_cpuload_5", translation_key="nextcloud_system_cpuload_5", native_unit_of_measurement=UNIT_OF_LOAD, icon="mdi:chip", suggested_display_precision=2, ), - "system_cpuload_15": SensorEntityDescription( + SensorEntityDescription( key="system_cpuload_15", translation_key="nextcloud_system_cpuload_15", native_unit_of_measurement=UNIT_OF_LOAD, icon="mdi:chip", suggested_display_precision=2, ), - "system_freespace": SensorEntityDescription( + SensorEntityDescription( key="system_freespace", translation_key="nextcloud_system_freespace", device_class=SensorDeviceClass.DATA_SIZE, @@ -201,7 +201,7 @@ SENSORS: Final[dict[str, SensorEntityDescription]] = { suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - "system_mem_free": SensorEntityDescription( + SensorEntityDescription( key="system_mem_free", translation_key="nextcloud_system_mem_free", device_class=SensorDeviceClass.DATA_SIZE, @@ -210,7 +210,7 @@ SENSORS: Final[dict[str, SensorEntityDescription]] = { suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - "system_mem_total": SensorEntityDescription( + SensorEntityDescription( key="system_mem_total", translation_key="nextcloud_system_mem_total", device_class=SensorDeviceClass.DATA_SIZE, @@ -219,25 +219,25 @@ SENSORS: Final[dict[str, SensorEntityDescription]] = { suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - "system_memcache.distributed": SensorEntityDescription( + SensorEntityDescription( key="system_memcache.distributed", translation_key="nextcloud_system_memcache_distributed", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - "system_memcache.local": SensorEntityDescription( + SensorEntityDescription( key="system_memcache.local", translation_key="nextcloud_system_memcache_local", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - "system_memcache.locking": SensorEntityDescription( + SensorEntityDescription( key="system_memcache.locking", translation_key="nextcloud_system_memcache_locking", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - "system_swap_total": SensorEntityDescription( + SensorEntityDescription( key="system_swap_total", translation_key="nextcloud_system_swap_total", device_class=SensorDeviceClass.DATA_SIZE, @@ -246,7 +246,7 @@ SENSORS: Final[dict[str, SensorEntityDescription]] = { suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - "system_swap_free": SensorEntityDescription( + SensorEntityDescription( key="system_swap_free", translation_key="nextcloud_system_swap_free", device_class=SensorDeviceClass.DATA_SIZE, @@ -255,15 +255,15 @@ SENSORS: Final[dict[str, SensorEntityDescription]] = { suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - "system_theme": SensorEntityDescription( + SensorEntityDescription( key="system_theme", translation_key="nextcloud_system_theme", ), - "system_version": SensorEntityDescription( + SensorEntityDescription( key="system_version", translation_key="nextcloud_system_version", ), -} +] async def async_setup_entry( @@ -273,9 +273,9 @@ async def async_setup_entry( coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - NextcloudSensor(coordinator, name, entry, SENSORS[name]) - for name in coordinator.data - if name in SENSORS + NextcloudSensor(coordinator, entry, sensor) + for sensor in SENSORS + if sensor.key in coordinator.data ] ) @@ -286,7 +286,7 @@ class NextcloudSensor(NextcloudEntity, SensorEntity): @property def native_value(self) -> StateType | datetime: """Return the state for this sensor.""" - val = self.coordinator.data.get(self.item) + val = self.coordinator.data.get(self.entity_description.key) if ( getattr(self.entity_description, "device_class", None) == SensorDeviceClass.TIMESTAMP From 1bd37612af8f7dca0a88921e5259cd33c46e85af Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 27 Aug 2023 20:50:27 +0200 Subject: [PATCH 0937/1151] Introduce more sensors to Nextcloud (#99155) --- .../components/nextcloud/binary_sensor.py | 12 + homeassistant/components/nextcloud/sensor.py | 305 ++++++++++++++++- .../components/nextcloud/strings.json | 318 ++++++++++++------ 3 files changed, 538 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py index fa733332763..313d555a3d7 100644 --- a/homeassistant/components/nextcloud/binary_sensor.py +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -17,6 +17,18 @@ from .coordinator import NextcloudDataUpdateCoordinator from .entity import NextcloudEntity BINARY_SENSORS: Final[list[BinarySensorEntityDescription]] = [ + BinarySensorEntityDescription( + key="jit_enabled", + translation_key="nextcloud_jit_enabled", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + BinarySensorEntityDescription( + key="jit_on", + translation_key="nextcloud_jit_on", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), BinarySensorEntityDescription( key="system_debug", translation_key="nextcloud_system_debug", diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index a0d657e4880..0cf30cee000 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -10,7 +10,12 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory, UnitOfInformation, UnitOfTime +from homeassistant.const import ( + PERCENTAGE, + EntityCategory, + UnitOfInformation, + UnitOfTime, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -40,6 +45,79 @@ SENSORS: Final[list[SensorEntityDescription]] = [ entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:account-multiple", ), + SensorEntityDescription( + key="cache_expunges", + translation_key="nextcloud_cache_expunges", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cache_mem_size", + translation_key="nextcloud_cache_mem_size", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + SensorEntityDescription( + key="cache_memory_type", + translation_key="nextcloud_cache_memory_type", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cache_num_entries", + translation_key="nextcloud_cache_num_entries", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cache_num_hits", + translation_key="nextcloud_cache_num_hits", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cache_num_inserts", + translation_key="nextcloud_cache_num_inserts", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cache_num_misses", + translation_key="nextcloud_cache_num_misses", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cache_num_slots", + translation_key="nextcloud_cache_num_slots", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cache_start_time", + translation_key="nextcloud_cache_start_time", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cache_ttl", + translation_key="nextcloud_cache_ttl", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="database_size", + translation_key="nextcloud_database_size", + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:database", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), SensorEntityDescription( key="database_type", translation_key="nextcloud_database_type", @@ -52,6 +130,205 @@ SENSORS: Final[list[SensorEntityDescription]] = [ entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:database", ), + SensorEntityDescription( + key="interned_strings_usage_buffer_size", + translation_key="nextcloud_interned_strings_usage_buffer_size", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + SensorEntityDescription( + key="interned_strings_usage_free_memory", + translation_key="nextcloud_interned_strings_usage_free_memory", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + SensorEntityDescription( + key="interned_strings_usage_number_of_strings", + translation_key="nextcloud_interned_strings_usage_number_of_strings", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="interned_strings_usage_used_memory", + translation_key="nextcloud_interned_strings_usage_used_memory", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + SensorEntityDescription( + key="jit_buffer_free", + translation_key="nextcloud_jit_buffer_free", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + SensorEntityDescription( + key="jit_buffer_size", + translation_key="nextcloud_jit_buffer_size", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + SensorEntityDescription( + key="jit_kind", + translation_key="nextcloud_jit_kind", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="jit_opt_flags", + translation_key="nextcloud_jit_opt_flags", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="jit_opt_level", + translation_key="nextcloud_jit_opt_level", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="opcache_statistics_blacklist_miss_ratio", + translation_key="nextcloud_opcache_statistics_blacklist_miss_ratio", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="opcache_statistics_blacklist_misses", + translation_key="nextcloud_opcache_statistics_blacklist_misses", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="opcache_statistics_hash_restarts", + translation_key="nextcloud_opcache_statistics_hash_restarts", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="opcache_statistics_hits", + translation_key="nextcloud_opcache_statistics_hits", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="opcache_statistics_last_restart_time", + translation_key="nextcloud_opcache_statistics_last_restart_time", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="opcache_statistics_manual_restarts", + translation_key="nextcloud_opcache_statistics_manual_restarts", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="opcache_statistics_max_cached_keys", + translation_key="nextcloud_opcache_statistics_max_cached_keys", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="opcache_statistics_misses", + translation_key="nextcloud_opcache_statistics_misses", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="opcache_statistics_num_cached_keys", + translation_key="nextcloud_opcache_statistics_num_cached_keys", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="opcache_statistics_num_cached_scripts", + translation_key="nextcloud_opcache_statistics_num_cached_scripts", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="opcache_statistics_oom_restarts", + translation_key="nextcloud_opcache_statistics_oom_restarts", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="opcache_statistics_opcache_hit_rate", + translation_key="nextcloud_opcache_statistics_opcache_hit_rate", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=1, + ), + SensorEntityDescription( + key="opcache_statistics_start_time", + translation_key="nextcloud_opcache_statistics_start_time", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="server_php_opcache_memory_usage_current_wasted_percentage", + translation_key="nextcloud_server_php_opcache_memory_usage_current_wasted_percentage", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:language-php", + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=1, + ), + SensorEntityDescription( + key="server_php_opcache_memory_usage_free_memory", + translation_key="nextcloud_server_php_opcache_memory_usage_free_memory", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:language-php", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + SensorEntityDescription( + key="server_php_opcache_memory_usage_used_memory", + translation_key="nextcloud_server_php_opcache_memory_usage_used_memory", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:language-php", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + SensorEntityDescription( + key="server_php_opcache_memory_usage_wasted_memory", + translation_key="nextcloud_server_php_opcache_memory_usage_wasted_memory", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:language-php", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), SensorEntityDescription( key="server_php_max_execution_time", translation_key="nextcloud_server_php_max_execution_time", @@ -135,6 +412,32 @@ SENSORS: Final[list[SensorEntityDescription]] = [ translation_key="nextcloud_shares_num_shares_user", entity_category=EntityCategory.DIAGNOSTIC, ), + SensorEntityDescription( + key="sma_avail_mem", + translation_key="nextcloud_sma_avail_mem", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), + SensorEntityDescription( + key="sma_num_seg", + translation_key="nextcloud_sma_num_seg", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="sma_seg_size", + translation_key="nextcloud_sma_seg_size", + device_class=SensorDeviceClass.DATA_SIZE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_display_precision=1, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + ), SensorEntityDescription( key="storage_num_files", translation_key="nextcloud_storage_num_files", diff --git a/homeassistant/components/nextcloud/strings.json b/homeassistant/components/nextcloud/strings.json index 7be15a6e62b..cfe57f201ca 100644 --- a/homeassistant/components/nextcloud/strings.json +++ b/homeassistant/components/nextcloud/strings.json @@ -31,6 +31,15 @@ }, "entity": { "binary_sensor": { + "nextcloud_jit_enabled": { + "name": "JIT enabled" + }, + "nextcloud_jit_on": { + "name": "JIT active" + }, + "nextcloud_system_debug": { + "name": "Debug enabled" + }, "nextcloud_system_enable_avatars": { "name": "Avatars enabled" }, @@ -39,56 +48,206 @@ }, "nextcloud_system_filelocking_enabled": { "name": "Filelocking enabled" - }, - "nextcloud_system_debug": { - "name": "Debug enabled" } }, "sensor": { - "nextcloud_system_version": { - "name": "System version" + "nextcloud_activeusers_last1hour": { + "name": "Amount of active users last hour" }, - "nextcloud_system_theme": { - "name": "System theme" + "nextcloud_activeusers_last24hours": { + "name": "Amount of active users last day" }, - "nextcloud_system_memcache_local": { - "name": "System memcache local" + "nextcloud_activeusers_last5minutes": { + "name": "Amount of active users last 5 minutes" }, - "nextcloud_system_memcache_distributed": { - "name": "System memcache distributed" + "nextcloud_cache_expunges": { + "name": "Cache expunges" }, - "nextcloud_system_memcache_locking": { - "name": "System memcache locking" + "nextcloud_cache_mem_size": { + "name": "Cache memory size" }, - "nextcloud_system_freespace": { - "name": "Free space" + "nextcloud_cache_memory_type": { + "name": "Cache memory" }, - "nextcloud_system_cpuload_1": { - "name": "CPU Load last minute" + "nextcloud_cache_num_entries": { + "name": "Cache number of entires" }, - "nextcloud_system_cpuload_15": { - "name": "CPU Load last 15 minutes" + "nextcloud_cache_num_hits": { + "name": "Cache number of hits" }, - "nextcloud_system_cpuload_5": { - "name": "CPU Load last 5 minutes" + "nextcloud_cache_num_inserts": { + "name": "Cache number of inserts" }, - "nextcloud_system_mem_total": { - "name": "Total memory" + "nextcloud_cache_num_misses": { + "name": "Cache number of misses" }, - "nextcloud_system_mem_free": { - "name": "Free memory" + "nextcloud_cache_num_slots": { + "name": "Cache number of slots" }, - "nextcloud_system_swap_total": { - "name": "Total swap memory" + "nextcloud_cache_start_time": { + "name": "Cache start time" }, - "nextcloud_system_swap_free": { - "name": "Free swap memory" + "nextcloud_cache_ttl": { + "name": "Cache ttl" }, - "nextcloud_system_apps_num_installed": { - "name": "Apps installed" + "nextcloud_database_size": { + "name": "Databse size" }, - "nextcloud_system_apps_num_updates_available": { - "name": "Updates available" + "nextcloud_database_type": { + "name": "Database type" + }, + "nextcloud_database_version": { + "name": "Database version" + }, + "nextcloud_interned_strings_usage_buffer_size": { + "name": "Interned buffer size" + }, + "nextcloud_interned_strings_usage_free_memory": { + "name": "Interned free memory" + }, + "nextcloud_interned_strings_usage_number_of_strings": { + "name": "Interned number of strings" + }, + "nextcloud_interned_strings_usage_used_memory": { + "name": "Interned used memory" + }, + "nextcloud_jit_buffer_free": { + "name": "JIT buffer free" + }, + "nextcloud_jit_buffer_size": { + "name": "JIT buffer size" + }, + "nextcloud_jit_kind": { + "name": "JIT kind" + }, + "nextcloud_jit_opt_flags": { + "name": "JIT opt flags" + }, + "nextcloud_jit_opt_level": { + "name": "JIT opt level" + }, + "nextcloud_opcache_statistics_blacklist_miss_ratio": { + "name": "Opcache blacklist miss ratio" + }, + "nextcloud_opcache_statistics_blacklist_misses": { + "name": "Opcache blacklist misses" + }, + "nextcloud_opcache_statistics_hash_restarts": { + "name": "Opcache hash restarts" + }, + "nextcloud_opcache_statistics_hits": { + "name": "Opcache hits" + }, + "nextcloud_opcache_statistics_last_restart_time": { + "name": "Opcache last restart time" + }, + "nextcloud_opcache_statistics_manual_restarts": { + "name": "Opcache manual restarts" + }, + "nextcloud_opcache_statistics_max_cached_keys": { + "name": "Opcache max cached keys" + }, + "nextcloud_opcache_statistics_misses": { + "name": "Opcache misses" + }, + "nextcloud_opcache_statistics_num_cached_keys": { + "name": "Opcache cached keys" + }, + "nextcloud_opcache_statistics_num_cached_scripts": { + "name": "Opcache cached scripts" + }, + "nextcloud_opcache_statistics_oom_restarts": { + "name": "Opcache out of memory restarts" + }, + "nextcloud_opcache_statistics_opcache_hit_rate": { + "name": "Opcache hit rate" + }, + "nextcloud_opcache_statistics_start_time": { + "name": "Opcache start time" + }, + "nextcloud_server_php_max_execution_time": { + "name": "PHP max execution time" + }, + "nextcloud_server_php_memory_limit": { + "name": "PHP memory limit" + }, + "nextcloud_server_php_opcache_memory_usage_current_wasted_percentage": { + "name": "Opcache current wasted percentage" + }, + "nextcloud_server_php_opcache_memory_usage_free_memory": { + "name": "Opcache free memory" + }, + "nextcloud_server_php_opcache_memory_usage_used_memory": { + "name": "Opcache used memory" + }, + "nextcloud_server_php_opcache_memory_usage_wasted_memory": { + "name": "Opcache wasted memory" + }, + "nextcloud_server_php_upload_max_filesize": { + "name": "PHP upload maximum filesize" + }, + "nextcloud_server_php_version": { + "name": "PHP version" + }, + "nextcloud_server_webserver": { + "name": "Webserver" + }, + "nextcloud_shares_num_fed_shares_received": { + "name": "Amount of shares received" + }, + "nextcloud_shares_num_fed_shares_sent": { + "name": "Amount of shares sent" + }, + "nextcloud_shares_num_shares": { + "name": "Amount of shares" + }, + "nextcloud_shares_num_shares_groups": { + "name": "Amount of group shares" + }, + "nextcloud_shares_num_shares_link": { + "name": "Amount of link shares" + }, + "nextcloud_shares_num_shares_link_no_password": { + "name": "Amount of passwordless link shares" + }, + "nextcloud_shares_num_shares_mail": { + "name": "Amount of mail shares" + }, + "nextcloud_shares_num_shares_room": { + "name": "Amount of room shares" + }, + "nextcloud_shares_num_shares_user": { + "name": "Amount of user shares" + }, + "nextcloud_shares_permissions_3_1": { + "name": "Permissions 3.1" + }, + "nextcloud_sma_avail_mem": { + "name": "SMA available memory" + }, + "nextcloud_sma_num_seg": { + "name": "SMA number of segments" + }, + "nextcloud_sma_seg_size": { + "name": "SMA segment size" + }, + "nextcloud_storage_num_files": { + "name": "Amount of files" + }, + "nextcloud_storage_num_storages": { + "name": "Amount of storages" + }, + "nextcloud_storage_num_storages_home": { + "name": "Amount of storages at home" + }, + "nextcloud_storage_num_storages_local": { + "name": "Amount of local storages" + }, + "nextcloud_storage_num_storages_other": { + "name": "Amount of other storages" + }, + "nextcloud_storage_num_users": { + "name": "Amount of user" }, "nextcloud_system_apps_app_updates_calendar": { "name": "Calendar updates" @@ -102,83 +261,50 @@ "nextcloud_system_apps_app_updates_twofactor_totp": { "name": "Two factor authentication updates" }, - "nextcloud_storage_num_users": { - "name": "Amount of user" + "nextcloud_system_apps_num_installed": { + "name": "Apps installed" }, - "nextcloud_storage_num_files": { - "name": "Amount of files" + "nextcloud_system_apps_num_updates_available": { + "name": "Updates available" }, - "nextcloud_storage_num_storages": { - "name": "Amount of storages" + "nextcloud_system_cpuload_1": { + "name": "CPU Load last 1 minute" }, - "nextcloud_storage_num_storages_local": { - "name": "Amount of local storages" + "nextcloud_system_cpuload_15": { + "name": "CPU Load last 15 minutes" }, - "nextcloud_storage_num_storages_home": { - "name": "Amount of storages at home" + "nextcloud_system_cpuload_5": { + "name": "CPU Load last 5 minutes" }, - "nextcloud_storage_num_storages_other": { - "name": "Amount of other storages" + "nextcloud_system_freespace": { + "name": "Free space" }, - "nextcloud_shares_num_shares": { - "name": "Amount of shares" + "nextcloud_system_mem_free": { + "name": "Free memory" }, - "nextcloud_shares_num_shares_user": { - "name": "Amount of user shares" + "nextcloud_system_mem_total": { + "name": "Total memory" }, - "nextcloud_shares_num_shares_groups": { - "name": "Amount of group shares" + "nextcloud_system_memcache_distributed": { + "name": "System memcache distributed" }, - "nextcloud_shares_num_shares_link": { - "name": "Amount of link shares" + "nextcloud_system_memcache_local": { + "name": "System memcache local" }, - "nextcloud_shares_num_shares_mail": { - "name": "Amount of mail shares" + "nextcloud_system_memcache_locking": { + "name": "System memcache locking" }, - "nextcloud_shares_num_shares_room": { - "name": "Amount of room shares" + "nextcloud_system_swap_free": { + "name": "Free swap memory" }, - "nextcloud_shares_num_shares_link_no_password": { - "name": "Amount of passwordless link shares" + "nextcloud_system_swap_total": { + "name": "Total swap memory" }, - "nextcloud_shares_num_fed_shares_sent": { - "name": "Amount of shares sent" + "nextcloud_system_theme": { + "name": "System theme" }, - "nextcloud_shares_num_fed_shares_received": { - "name": "Amount of shares received" - }, - "nextcloud_shares_permissions_3_1": { - "name": "Permissions 3.1" - }, - "nextcloud_server_webserver": { - "name": "Webserver" - }, - "nextcloud_server_php_version": { - "name": "PHP version" - }, - "nextcloud_server_php_memory_limit": { - "name": "PHP memory limit" - }, - "nextcloud_server_php_max_execution_time": { - "name": "PHP max execution time" - }, - "nextcloud_server_php_upload_max_filesize": { - "name": "PHP upload maximum filesize" - }, - "nextcloud_database_type": { - "name": "Database type" - }, - "nextcloud_database_version": { - "name": "Database version" - }, - "nextcloud_activeusers_last5minutes": { - "name": "Amount of active users last 5 minutes" - }, - "nextcloud_activeusers_last1hour": { - "name": "Amount of active users last hour" - }, - "nextcloud_activeusers_last24hours": { - "name": "Amount of active users last day" + "nextcloud_system_version": { + "name": "System version" } } } From 16041e51278f89e4ca58ef5a331e1cc31294da63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Aug 2023 16:00:19 -0500 Subject: [PATCH 0938/1151] Bump zeroconf to 0.85.0 (#99165) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index c557c671237..18b8bb4e64c 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.84.0"] + "requirements": ["zeroconf==0.85.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9bc11dc0520..e66bad9f599 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.84.0 +zeroconf==0.85.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 8f1a9321ec6..65bf257a7eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2759,7 +2759,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.84.0 +zeroconf==0.85.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30686d0445a..a46028263af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2029,7 +2029,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.84.0 +zeroconf==0.85.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From c9905fda6dc32a110db3c4d10d5f7bd16407d34e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 27 Aug 2023 23:54:49 +0200 Subject: [PATCH 0939/1151] Add entity translations to Watttime (#99151) --- homeassistant/components/watttime/sensor.py | 4 ++-- homeassistant/components/watttime/strings.json | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index 636e73af8f2..2a0e21ecf4c 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -36,14 +36,14 @@ SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT = "percent" REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_MOER, - name="Marginal operating emissions rate", + translation_key="marginal_operating_emissions_rate", icon="mdi:blur", native_unit_of_measurement=f"{UnitOfMass.POUNDS} CO2/MWh", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT, - name="Relative marginal emissions intensity", + translation_key="relative_marginal_emissions_intensity", icon="mdi:blur", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/watttime/strings.json b/homeassistant/components/watttime/strings.json index 1650856a669..b62a9961131 100644 --- a/homeassistant/components/watttime/strings.json +++ b/homeassistant/components/watttime/strings.json @@ -48,5 +48,15 @@ } } } + }, + "entity": { + "sensor": { + "marginal_operating_emissions_rate": { + "name": "Marginal operating emissions rate" + }, + "relative_marginal_emissions_intensity": { + "name": "Relative marginal emissions intensity" + } + } } } From dbb00b1725c6108011028d35da6093d9a76d1dce Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 00:05:41 +0200 Subject: [PATCH 0940/1151] Add code owner for Media Extractor (#99153) --- CODEOWNERS | 1 + homeassistant/components/media_extractor/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index df3ebbe1760..b6241669796 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -729,6 +729,7 @@ build.json @home-assistant/supervisor /tests/components/mazda/ @bdr99 /homeassistant/components/meater/ @Sotolotl @emontnemery /tests/components/meater/ @Sotolotl @emontnemery +/homeassistant/components/media_extractor/ @joostlek /homeassistant/components/media_player/ @home-assistant/core /tests/components/media_player/ @home-assistant/core /homeassistant/components/media_source/ @hunterjm diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 0e5d9ead0f8..707cbdf9e8b 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -1,7 +1,7 @@ { "domain": "media_extractor", "name": "Media Extractor", - "codeowners": [], + "codeowners": ["@joostlek"], "dependencies": ["media_player"], "documentation": "https://www.home-assistant.io/integrations/media_extractor", "iot_class": "calculated", From c686f962b5e6118fe4add02a070607c74b488a67 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Mon, 28 Aug 2023 03:37:08 +0200 Subject: [PATCH 0941/1151] Bump bimmer_connected to 0.14.0 (#99161) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index ff2804a8c04..0a9e9cac5af 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected==0.13.9"] + "requirements": ["bimmer-connected==0.14.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 65bf257a7eb..10d2e7eab02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -506,7 +506,7 @@ beautifulsoup4==4.12.2 bellows==0.35.9 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.13.9 +bimmer-connected==0.14.0 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a46028263af..d9534a96f97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -427,7 +427,7 @@ beautifulsoup4==4.12.2 bellows==0.35.9 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.13.9 +bimmer-connected==0.14.0 # homeassistant.components.bluetooth bleak-retry-connector==3.1.1 From 579c760f533d6f898ac336f0a07b5418cc70cfe2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 28 Aug 2023 06:23:26 +0000 Subject: [PATCH 0942/1151] Add missing `low` state for `ENUM` Tractive sensors (#99057) * Add missing "low" option * Use existing translations --- homeassistant/components/tractive/sensor.py | 6 ++++-- homeassistant/components/tractive/strings.json | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 0d486606802..49eda4f8d09 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -176,8 +176,9 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( value_fn=lambda state: state.lower() if isinstance(state, str) else state, device_class=SensorDeviceClass.ENUM, options=[ - "ok", "good", + "low", + "ok", ], ), TractiveSensorEntityDescription( @@ -188,8 +189,9 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( value_fn=lambda state: state.lower() if isinstance(state, str) else state, device_class=SensorDeviceClass.ENUM, options=[ - "ok", "good", + "low", + "ok", ], ), ) diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index e315a8e6013..82b7ecc295c 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -33,8 +33,9 @@ "activity": { "name": "Activity", "state": { - "ok": "OK", - "good": "Good" + "good": "Good", + "low": "Low", + "ok": "OK" } }, "activity_time": { @@ -58,8 +59,9 @@ "sleep": { "name": "Sleep", "state": { - "ok": "OK", - "good": "Good" + "good": "[%key:component::tractive::entity::sensor::activity::state::good%]", + "low": "[%key:component::tractive::entity::sensor::activity::state::low%]", + "ok": "[%key:component::tractive::entity::sensor::activity::state::ok%]" } }, "tracker_battery_level": { From 4b50c95d1dbd44f18566c95d64bd2814f7291a0e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Aug 2023 09:05:09 +0200 Subject: [PATCH 0943/1151] Fix trafikverket_camera recorder platform setup (#99080) --- .../components/trafikverket_camera/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/trafikverket_camera/__init__.py b/homeassistant/components/trafikverket_camera/__init__.py index 0ee4fd5010e..dfac8416c49 100644 --- a/homeassistant/components/trafikverket_camera/__init__.py +++ b/homeassistant/components/trafikverket_camera/__init__.py @@ -3,10 +3,25 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.integration_platform import ( + async_process_integration_platform_for_component, +) +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, PLATFORMS from .coordinator import TVDataUpdateCoordinator +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up trafikverket_camera.""" + # Process integration platforms right away since + # we will create entities before firing EVENT_COMPONENT_LOADED + await async_process_integration_platform_for_component(hass, DOMAIN) + return True + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Trafikverket Camera from a config entry.""" From a3b526eef649bc01b66eb1857cdf74648197d372 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 28 Aug 2023 09:07:31 +0200 Subject: [PATCH 0944/1151] Address late modbus review (#99123) Post review comments. --- homeassistant/components/modbus/base_platform.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 9cf582a5dda..7c3fcd78b05 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -22,7 +22,6 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_UNIQUE_ID, STATE_ON, - STATE_UNAVAILABLE, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -188,10 +187,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): registers.reverse() return registers - def __process_raw_value(self, entry: float | int | str) -> float | int | str: + def __process_raw_value(self, entry: float | int | str) -> float | int | str | None: """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" if self._nan_value and entry in (self._nan_value, -self._nan_value): - return STATE_UNAVAILABLE + return None val: float | int = self._scale * entry + self._offset if self._min_value is not None and val < self._min_value: return self._min_value @@ -231,6 +230,8 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): # the conversion only when it's absolutely necessary. if isinstance(v_temp, int) and self._precision == 0: v_result.append(str(v_temp)) + elif v_temp is None: + v_result.append("") # pragma: no cover elif v_temp != v_temp: # noqa: PLR0124 # NaN float detection replace with None v_result.append("nan") # pragma: no cover @@ -245,6 +246,8 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): # we lose some precision, and unit tests will fail. Therefore, we do # the conversion only when it's absolutely necessary. + if val_result is None: + return None # NaN float detection replace with None if val_result != val_result: # noqa: PLR0124 return None # pragma: no cover @@ -252,7 +255,7 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): return str(val_result) if isinstance(val_result, str): if val_result == "nan": - val_result = STATE_UNAVAILABLE # pragma: no cover + val_result = None # pragma: no cover return val_result return f"{float(val_result):.{self._precision}f}" From e97e9ae55ac6eb5856796a56331819439c628237 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Aug 2023 09:23:32 +0200 Subject: [PATCH 0945/1151] Use freezegun in trafikverket_camera tests (#99067) --- .../components/trafikverket_camera/test_camera.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/components/trafikverket_camera/test_camera.py b/tests/components/trafikverket_camera/test_camera.py index 57451ae93a9..b3df7cfcdcb 100644 --- a/tests/components/trafikverket_camera/test_camera.py +++ b/tests/components/trafikverket_camera/test_camera.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from pytrafikverket.trafikverket_camera import CameraInfo @@ -11,7 +12,6 @@ from homeassistant.components.camera import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker @@ -20,6 +20,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_camera( hass: HomeAssistant, load_int: ConfigEntry, + freezer: FrozenDateTimeFactory, monkeypatch: pytest.MonkeyPatch, aioclient_mock: AiohttpClientMocker, get_camera: CameraInfo, @@ -39,10 +40,8 @@ async def test_camera( "https://www.testurl.com/test_photo.jpg?type=fullsize", content=b"9876543210", ) - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=6), - ) + freezer.tick(timedelta(minutes=6)) + async_fire_time_changed(hass) await hass.async_block_till_done() state1 = hass.states.get("camera.test_location") @@ -65,10 +64,8 @@ async def test_camera( "homeassistant.components.trafikverket_camera.coordinator.TrafikverketCamera.async_get_camera", return_value=get_camera, ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=6), - ) + freezer.tick(timedelta(minutes=6)) + async_fire_time_changed(hass) await hass.async_block_till_done() with pytest.raises(HomeAssistantError): From 01d29512ffa56048be2315bc629bfcf004315a31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Aug 2023 02:33:08 -0500 Subject: [PATCH 0946/1151] Bump zeroconf to 0.86.0 (#99177) changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.85.0...0.86.0 --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 18b8bb4e64c..e1f1b4ebe69 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.85.0"] + "requirements": ["zeroconf==0.86.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e66bad9f599..e430a25b248 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.85.0 +zeroconf==0.86.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 10d2e7eab02..0ee2a253020 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2759,7 +2759,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.85.0 +zeroconf==0.86.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9534a96f97..d3caed511b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2029,7 +2029,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.85.0 +zeroconf==0.86.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 30b815bfb89d47bd3e563c32ebf518b11a7aaba3 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 28 Aug 2023 00:34:30 -0700 Subject: [PATCH 0947/1151] Bump pywemo to 1.3.0 (#99172) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index cb189116eeb..c0428e62b71 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -9,7 +9,7 @@ }, "iot_class": "local_push", "loggers": ["pywemo"], - "requirements": ["pywemo==1.2.1"], + "requirements": ["pywemo==1.3.0"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 0ee2a253020..c39d698881a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2234,7 +2234,7 @@ pywaze==0.3.0 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==1.2.1 +pywemo==1.3.0 # homeassistant.components.wilight pywilight==0.0.74 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3caed511b9..92afd3319d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1642,7 +1642,7 @@ pywaze==0.3.0 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==1.2.1 +pywemo==1.3.0 # homeassistant.components.wilight pywilight==0.0.74 From 1683ffb83054eb1413310114262bf546625ff221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 28 Aug 2023 09:35:29 +0200 Subject: [PATCH 0948/1151] Update aioqsw to v0.3.4 (#99183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index 1b9ba097b36..28e1ba7b8e4 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", "iot_class": "local_polling", "loggers": ["aioqsw"], - "requirements": ["aioqsw==0.3.3"] + "requirements": ["aioqsw==0.3.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index c39d698881a..6be8bbd1c31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -324,7 +324,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.3.3 +aioqsw==0.3.4 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92afd3319d3..7bdc29d3690 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -299,7 +299,7 @@ aiopvpc==4.2.2 aiopyarr==23.4.0 # homeassistant.components.qnap_qsw -aioqsw==0.3.3 +aioqsw==0.3.4 # homeassistant.components.recollect_waste aiorecollect==1.0.8 From bb545b1c4d44b74c4e88af0b9d772a26ff228be0 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 28 Aug 2023 10:15:14 +0200 Subject: [PATCH 0949/1151] Fix typos in home_plus_controls (#99188) --- .../components/home_plus_control/__init__.py | 16 ++++++++-------- .../components/home_plus_control/strings.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index 0accf53970d..2ed37480705 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -54,7 +54,7 @@ PLATFORMS = [Platform.SWITCH] _LOGGER = logging.getLogger(__name__) -_ISSUE_MOTE_TO_NETAMO = "move_to_netamo" +_ISSUE_MOVE_TO_NETATMO = "move_to_netatmo" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -67,12 +67,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_create_issue( hass, DOMAIN, - _ISSUE_MOTE_TO_NETAMO, + _ISSUE_MOVE_TO_NETATMO, is_fixable=False, is_persistent=False, - breaks_in_ha_version="2023.12.0", # Netamo decided to shutdown the api in december + breaks_in_ha_version="2023.12.0", # Netatmo decided to shutdown the api in december severity=IssueSeverity.WARNING, - translation_key=_ISSUE_MOTE_TO_NETAMO, + translation_key=_ISSUE_MOVE_TO_NETATMO, translation_placeholders={ "url": "https://www.home-assistant.io/integrations/netatmo/" }, @@ -94,12 +94,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_create_issue( hass, DOMAIN, - _ISSUE_MOTE_TO_NETAMO, + _ISSUE_MOVE_TO_NETATMO, is_fixable=False, is_persistent=False, - breaks_in_ha_version="2023.12.0", # Netamo decided to shutdown the api in december + breaks_in_ha_version="2023.12.0", # Netatmo decided to shutdown the api in december severity=IssueSeverity.WARNING, - translation_key=_ISSUE_MOTE_TO_NETAMO, + translation_key=_ISSUE_MOVE_TO_NETATMO, translation_placeholders={ "url": "https://www.home-assistant.io/integrations/netatmo/" }, @@ -203,6 +203,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # And finally unload the domain config entry data hass.data[DOMAIN].pop(config_entry.entry_id) - async_delete_issue(hass, DOMAIN, _ISSUE_MOTE_TO_NETAMO) + async_delete_issue(hass, DOMAIN, _ISSUE_MOVE_TO_NETATMO) return unload_ok diff --git a/homeassistant/components/home_plus_control/strings.json b/homeassistant/components/home_plus_control/strings.json index d795323586d..280a92055bd 100644 --- a/homeassistant/components/home_plus_control/strings.json +++ b/homeassistant/components/home_plus_control/strings.json @@ -18,7 +18,7 @@ } }, "issues": { - "move_to_netamo": { + "move_to_netatmo": { "title": "Legrand Home+ Control deprecation", "description": "Home Assistant has been informed that the platform the Legrand Home+ Control integration is using, will be shutting down upcoming December.\n\nOnce that happens, it means this integration is no longer functional. We advise you to remove this integration and switch to the [Netatmo]({url}) integration, which provides a replacement for controlling your Legrand Home+ Control devices." } From b0f3b7bb760b30d65a76a562dd781bb957921d3a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 28 Aug 2023 11:42:24 +0300 Subject: [PATCH 0950/1151] Revert "Change naming of Shelly entities to correspond with HA guidelines" (#99059) --- homeassistant/components/shelly/button.py | 2 +- homeassistant/components/shelly/climate.py | 8 +++-- homeassistant/components/shelly/entity.py | 5 --- homeassistant/components/shelly/logbook.py | 3 +- homeassistant/components/shelly/utils.py | 33 ++++++++----------- tests/components/shelly/test_binary_sensor.py | 2 +- tests/components/shelly/test_coordinator.py | 8 ++--- tests/components/shelly/test_cover.py | 24 +++++++------- tests/components/shelly/test_init.py | 8 ++--- tests/components/shelly/test_light.py | 10 +++--- tests/components/shelly/test_sensor.py | 2 +- tests/components/shelly/test_switch.py | 14 ++++---- tests/components/shelly/test_utils.py | 6 ++-- 13 files changed, 59 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index a505867b3e8..edc33c9a8a0 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -154,7 +154,6 @@ class ShellyButton( entity_description: ShellyButtonDescription[ ShellyRpcCoordinator | ShellyBlockCoordinator ] - _attr_has_entity_name = True def __init__( self, @@ -167,6 +166,7 @@ class ShellyButton( super().__init__(coordinator) self.entity_description = description + self._attr_name = f"{coordinator.device.name} {description.name}" self._attr_unique_id = f"{coordinator.mac}_{description.key}" self._attr_device_info = DeviceInfo( connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index d77a491661c..a9712e62d25 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -130,7 +130,6 @@ class BlockSleepingClimate( ) _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] _attr_temperature_unit = UnitOfTemperature.CELSIUS - _attr_has_entity_name = True def __init__( self, @@ -174,6 +173,11 @@ class BlockSleepingClimate( """Set unique id of entity.""" return self._unique_id + @property + def name(self) -> str: + """Name of entity.""" + return self.coordinator.name + @property def target_temperature(self) -> float | None: """Set target temperature.""" @@ -350,7 +354,7 @@ class BlockSleepingClimate( severity=ir.IssueSeverity.ERROR, translation_key="device_not_calibrated", translation_placeholders={ - "device_name": self.coordinator.name, + "device_name": self.name, "ip_address": self.coordinator.device.ip_address, }, ) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index ac06624c750..1dc7573b738 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -321,8 +321,6 @@ class RestEntityDescription(EntityDescription): class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Helper class to represent a block entity.""" - _attr_has_entity_name = True - def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: """Initialize Shelly entity.""" super().__init__(coordinator) @@ -361,8 +359,6 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): """Helper class to represent a rpc entity.""" - _attr_has_entity_name = True - def __init__(self, coordinator: ShellyRpcCoordinator, key: str) -> None: """Initialize Shelly entity.""" super().__init__(coordinator) @@ -466,7 +462,6 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Class to load info from REST.""" entity_description: RestEntityDescription - _attr_has_entity_name = True def __init__( self, diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index b8f0c8e1744..d55ffe0fd28 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -42,8 +42,7 @@ def async_describe_events( rpc_coordinator = get_rpc_coordinator_by_device_id(hass, device_id) if rpc_coordinator and rpc_coordinator.device.initialized: key = f"input:{channel-1}" - if iname := get_rpc_entity_name(rpc_coordinator.device, key): - input_name = iname + input_name = get_rpc_entity_name(rpc_coordinator.device, key) elif click_type in BLOCK_INPUTS_EVENTS_TYPES: block_coordinator = get_block_coordinator_by_device_id(hass, device_id) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 1faa36ce118..a66b77ed94b 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -72,26 +72,26 @@ def get_block_entity_name( device: BlockDevice, block: Block | None, description: str | None = None, -) -> str | None: +) -> str: """Naming for block based switch and sensors.""" channel_name = get_block_channel_name(device, block) - if description and channel_name: - return f"{channel_name} {uncapitalize(description)}" if description: - return description + return f"{channel_name} {description.lower()}" return channel_name -def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | None: +def get_block_channel_name(device: BlockDevice, block: Block | None) -> str: """Get name based on device and channel name.""" + entity_name = device.name + if ( not block or block.type == "device" or get_number_of_channels(device, block) == 1 ): - return None + return entity_name assert block.channel @@ -108,7 +108,7 @@ def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | No else: base = ord("1") - return f"Channel {chr(int(block.channel)+base)}" + return f"{entity_name} channel {chr(int(block.channel)+base)}" def is_block_momentary_input( @@ -285,32 +285,32 @@ def get_model_name(info: dict[str, Any]) -> str: return cast(str, MODEL_NAMES.get(info["type"], info["type"])) -def get_rpc_channel_name(device: RpcDevice, key: str) -> str | None: +def get_rpc_channel_name(device: RpcDevice, key: str) -> str: """Get name based on device and channel name.""" key = key.replace("emdata", "em") if device.config.get("switch:0"): key = key.replace("input", "switch") + device_name = device.name entity_name: str | None = None if key in device.config: - entity_name = device.config[key].get("name") + entity_name = device.config[key].get("name", device_name) if entity_name is None: if key.startswith(("input:", "light:", "switch:")): - return key.replace(":", " ").capitalize() + return f"{device_name} {key.replace(':', '_')}" + return device_name return entity_name def get_rpc_entity_name( device: RpcDevice, key: str, description: str | None = None -) -> str | None: +) -> str: """Naming for RPC based switch and sensors.""" channel_name = get_rpc_channel_name(device, key) - if description and channel_name: - return f"{channel_name} {uncapitalize(description)}" if description: - return description + return f"{channel_name} {description.lower()}" return channel_name @@ -405,8 +405,3 @@ def mac_address_from_name(name: str) -> str | None: """Convert a name to a mac address.""" mac = name.partition(".")[0].partition("-")[-1] return mac.upper() if len(mac) == 12 else None - - -def uncapitalize(description: str) -> str: - """Uncapitalize the first letter of a description.""" - return description[:1].lower() + description[1:] diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index a54b5398b11..8905ff5c3e8 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -167,7 +167,7 @@ async def test_rpc_binary_sensor( hass: HomeAssistant, mock_rpc_device, monkeypatch ) -> None: """Test RPC binary sensor.""" - entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_test_cover_0_overpowering" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_cover_0_overpowering" await init_integration(hass, 2) assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index a7fa64962e9..3872f6f5a1a 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -347,14 +347,14 @@ async def test_rpc_reload_on_cfg_change( ) await hass.async_block_till_done() - assert hass.states.get("switch.test_name_test_switch_0") is not None + assert hass.states.get("switch.test_switch_0") is not None # Wait for debouncer freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("switch.test_name_test_switch_0") is None + assert hass.states.get("switch.test_switch_0") is None async def test_rpc_reload_with_invalid_auth( @@ -577,7 +577,7 @@ async def test_rpc_reconnect_error( """Test RPC reconnect error.""" await init_integration(hass, 2) - assert hass.states.get("switch.test_name_test_switch_0").state == STATE_ON + assert hass.states.get("switch.test_switch_0").state == STATE_ON monkeypatch.setattr(mock_rpc_device, "connected", False) monkeypatch.setattr( @@ -593,7 +593,7 @@ async def test_rpc_reconnect_error( async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("switch.test_name_test_switch_0").state == STATE_UNAVAILABLE + assert hass.states.get("switch.test_switch_0").state == STATE_UNAVAILABLE async def test_rpc_polling_connection_error( diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index 56740981fc5..08c0c76d35e 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -97,10 +97,10 @@ async def test_rpc_device_services( await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.test_name_test_cover_0", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.test_cover_0", ATTR_POSITION: 50}, blocking=True, ) - state = hass.states.get("cover.test_name_test_cover_0") + state = hass.states.get("cover.test_cover_0") assert state.attributes[ATTR_CURRENT_POSITION] == 50 mutate_rpc_device_status( @@ -109,11 +109,11 @@ async def test_rpc_device_services( await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.test_name_test_cover_0"}, + {ATTR_ENTITY_ID: "cover.test_cover_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPENING + assert hass.states.get("cover.test_cover_0").state == STATE_OPENING mutate_rpc_device_status( monkeypatch, mock_rpc_device, "cover:0", "state", "closing" @@ -121,21 +121,21 @@ async def test_rpc_device_services( await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.test_name_test_cover_0"}, + {ATTR_ENTITY_ID: "cover.test_cover_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSING + assert hass.states.get("cover.test_cover_0").state == STATE_CLOSING mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") await hass.services.async_call( COVER_DOMAIN, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.test_name_test_cover_0"}, + {ATTR_ENTITY_ID: "cover.test_cover_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSED + assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED async def test_rpc_device_no_cover_keys( @@ -144,7 +144,7 @@ async def test_rpc_device_no_cover_keys( """Test RPC device without cover keys.""" monkeypatch.delitem(mock_rpc_device.status, "cover:0") await init_integration(hass, 2) - assert hass.states.get("cover.test_name_test_cover_0") is None + assert hass.states.get("cover.test_cover_0") is None async def test_rpc_device_update( @@ -153,11 +153,11 @@ async def test_rpc_device_update( """Test RPC device update.""" mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") await init_integration(hass, 2) - assert hass.states.get("cover.test_name_test_cover_0").state == STATE_CLOSED + assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "open") mock_rpc_device.mock_update() - assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPEN + assert hass.states.get("cover.test_cover_0").state == STATE_OPEN async def test_rpc_device_no_position_control( @@ -168,4 +168,4 @@ async def test_rpc_device_no_position_control( monkeypatch, mock_rpc_device, "cover:0", "pos_control", False ) await init_integration(hass, 2) - assert hass.states.get("cover.test_name_test_cover_0").state == STATE_OPEN + assert hass.states.get("cover.test_cover_0").state == STATE_OPEN diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 1fdfc9d4304..2ead9cba198 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -195,7 +195,7 @@ async def test_sleeping_rpc_device_online_new_firmware( ("gen", "entity_id"), [ (1, "switch.test_name_channel_1"), - (2, "switch.test_name_test_switch_0"), + (2, "switch.test_switch_0"), ], ) async def test_entry_unload( @@ -218,7 +218,7 @@ async def test_entry_unload( ("gen", "entity_id"), [ (1, "switch.test_name_channel_1"), - (2, "switch.test_name_test_switch_0"), + (2, "switch.test_switch_0"), ], ) async def test_entry_unload_device_not_ready( @@ -246,7 +246,7 @@ async def test_entry_unload_not_connected( entry = await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} ) - entity_id = "switch.test_name_test_switch_0" + entity_id = "switch.test_switch_0" assert entry.state is ConfigEntryState.LOADED assert hass.states.get(entity_id).state is STATE_ON @@ -272,7 +272,7 @@ async def test_entry_unload_not_connected_but_we_think_we_are( entry = await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} ) - entity_id = "switch.test_name_test_switch_0" + entity_id = "switch.test_switch_0" assert entry.state is ConfigEntryState.LOADED assert hass.states.get(entity_id).state is STATE_ON diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index ab631516ec2..69d0fccf421 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -385,25 +385,25 @@ async def test_rpc_device_switch_type_lights_mode( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_name_test_switch_0"}, + {ATTR_ENTITY_ID: "light.test_switch_0"}, blocking=True, ) - assert hass.states.get("light.test_name_test_switch_0").state == STATE_ON + assert hass.states.get("light.test_switch_0").state == STATE_ON mutate_rpc_device_status(monkeypatch, mock_rpc_device, "switch:0", "output", False) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.test_name_test_switch_0"}, + {ATTR_ENTITY_ID: "light.test_switch_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("light.test_name_test_switch_0").state == STATE_OFF + assert hass.states.get("light.test_switch_0").state == STATE_OFF async def test_rpc_light(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None: """Test RPC light.""" - entity_id = f"{LIGHT_DOMAIN}.test_name_test_light_0" + entity_id = f"{LIGHT_DOMAIN}.test_light_0" monkeypatch.delitem(mock_rpc_device.status, "switch:0") await init_integration(hass, 2) diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 630ee551e89..892d06ad626 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -264,7 +264,7 @@ async def test_block_sensor_unknown_value( async def test_rpc_sensor(hass: HomeAssistant, mock_rpc_device, monkeypatch) -> None: """Test RPC sensor.""" - entity_id = f"{SENSOR_DOMAIN}.test_name_test_cover_0_power" + entity_id = f"{SENSOR_DOMAIN}.test_cover_0_power" await init_integration(hass, 2) assert hass.states.get(entity_id).state == "85.3" diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index a53c5dc185b..115ad5edabb 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -152,20 +152,20 @@ async def test_rpc_device_services( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_switch_0"}, blocking=True, ) - assert hass.states.get("switch.test_name_test_switch_0").state == STATE_ON + assert hass.states.get("switch.test_switch_0").state == STATE_ON monkeypatch.setitem(mock_rpc_device.status["switch:0"], "output", False) await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_switch_0"}, blocking=True, ) mock_rpc_device.mock_update() - assert hass.states.get("switch.test_name_test_switch_0").state == STATE_OFF + assert hass.states.get("switch.test_switch_0").state == STATE_OFF async def test_rpc_device_switch_type_lights_mode( @@ -176,7 +176,7 @@ async def test_rpc_device_switch_type_lights_mode( mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"] ) await init_integration(hass, 2) - assert hass.states.get("switch.test_name_test_switch_0") is None + assert hass.states.get("switch.test_switch_0") is None @pytest.mark.parametrize("exc", [DeviceConnectionError, RpcCallError(-1, "error")]) @@ -191,7 +191,7 @@ async def test_rpc_set_state_errors( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_switch_0"}, blocking=True, ) @@ -212,7 +212,7 @@ async def test_rpc_auth_error( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_switch_0"}, blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/shelly/test_utils.py b/tests/components/shelly/test_utils.py index 53fc77ed6ef..1bf660deb2a 100644 --- a/tests/components/shelly/test_utils.py +++ b/tests/components/shelly/test_utils.py @@ -58,7 +58,7 @@ async def test_block_get_block_channel_name(mock_block_device, monkeypatch) -> N mock_block_device, mock_block_device.blocks[DEVICE_BLOCK_ID], ) - == "Channel 1" + == "Test name channel 1" ) monkeypatch.setitem(mock_block_device.settings["device"], "type", "SHEM-3") @@ -68,7 +68,7 @@ async def test_block_get_block_channel_name(mock_block_device, monkeypatch) -> N mock_block_device, mock_block_device.blocks[DEVICE_BLOCK_ID], ) - == "Channel A" + == "Test name channel A" ) monkeypatch.setitem( @@ -207,7 +207,7 @@ async def test_get_block_input_triggers(mock_block_device, monkeypatch) -> None: async def test_get_rpc_channel_name(mock_rpc_device) -> None: """Test get RPC channel name.""" assert get_rpc_channel_name(mock_rpc_device, "input:0") == "test switch_0" - assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Switch 3" + assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Test name switch_3" async def test_get_rpc_input_triggers(mock_rpc_device, monkeypatch) -> None: From 8edae3708243db87898ef20a5155815d7fca0027 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 28 Aug 2023 12:20:18 +0300 Subject: [PATCH 0951/1151] Add more type hints to Transmission (#99190) * More type hints of transmssion * More type hints --- .../components/transmission/__init__.py | 33 ++++++++++--------- .../components/transmission/sensor.py | 32 ++++++++++++------ .../components/transmission/switch.py | 22 +++++++++---- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index c3c364e229b..5276a60e5fb 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -14,6 +14,7 @@ from transmission_rpc.error import ( TransmissionConnectError, TransmissionError, ) +from transmission_rpc.session import SessionStats import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -340,7 +341,7 @@ class TransmissionClient: def set_scan_interval(self, scan_interval: float) -> None: """Update scan interval.""" - def refresh(event_time: datetime): + def refresh(event_time: datetime) -> None: """Get the latest data from Transmission.""" self.api.update() @@ -367,22 +368,22 @@ class TransmissionData: """Initialize the Transmission RPC API.""" self.hass = hass self.config = config - self.data: transmission_rpc.Session = None - self.available: bool = True - self._all_torrents: list[transmission_rpc.Torrent] = [] self._api: transmission_rpc.Client = api + self.data: SessionStats | None = None + self.available: bool = True + self._session: transmission_rpc.Session | None = None + self._all_torrents: list[transmission_rpc.Torrent] = [] self._completed_torrents: list[transmission_rpc.Torrent] = [] - self._session: transmission_rpc.Session = None self._started_torrents: list[transmission_rpc.Torrent] = [] self._torrents: list[transmission_rpc.Torrent] = [] @property - def host(self): + def host(self) -> str: """Return the host name.""" return self.config.data[CONF_HOST] @property - def signal_update(self): + def signal_update(self) -> str: """Update signal per transmission entry.""" return f"{DATA_UPDATED}-{self.host}" @@ -391,7 +392,7 @@ class TransmissionData: """Get the list of torrents.""" return self._torrents - def update(self): + def update(self) -> None: """Get the latest data from Transmission instance.""" try: self.data = self._api.session_stats() @@ -409,7 +410,7 @@ class TransmissionData: _LOGGER.error("Unable to connect to Transmission client %s", self.host) dispatcher_send(self.hass, self.signal_update) - def init_torrent_list(self): + def init_torrent_list(self) -> None: """Initialize torrent lists.""" self._torrents = self._api.get_torrents() self._completed_torrents = [ @@ -419,7 +420,7 @@ class TransmissionData: torrent for torrent in self._torrents if torrent.status == "downloading" ] - def check_completed_torrent(self): + def check_completed_torrent(self) -> None: """Get completed torrent functionality.""" old_completed_torrent_names = { torrent.name for torrent in self._completed_torrents @@ -437,7 +438,7 @@ class TransmissionData: self._completed_torrents = current_completed_torrents - def check_started_torrent(self): + def check_started_torrent(self) -> None: """Get started torrent functionality.""" old_started_torrent_names = {torrent.name for torrent in self._started_torrents} @@ -453,7 +454,7 @@ class TransmissionData: self._started_torrents = current_started_torrents - def check_removed_torrent(self): + def check_removed_torrent(self) -> None: """Get removed torrent functionality.""" current_torrent_names = {torrent.name for torrent in self._torrents} @@ -465,24 +466,24 @@ class TransmissionData: self._all_torrents = self._torrents.copy() - def start_torrents(self): + def start_torrents(self) -> None: """Start all torrents.""" if not self._torrents: return self._api.start_all() - def stop_torrents(self): + def stop_torrents(self) -> None: """Stop all active torrents.""" if not self._torrents: return torrent_ids = [torrent.id for torrent in self._torrents] self._api.stop_torrent(torrent_ids) - def set_alt_speed_enabled(self, is_enabled): + def set_alt_speed_enabled(self, is_enabled: bool) -> None: """Set the alternative speed flag.""" self._api.set_session(alt_speed_enabled=is_enabled) - def get_alt_speed_enabled(self): + def get_alt_speed_enabled(self) -> bool | None: """Get the alternative speed flag.""" if self._session is None: return None diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 5c5e530ccbf..93bea8a25c9 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from contextlib import suppress +from typing import Any from transmission_rpc.torrent import Torrent @@ -12,6 +13,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from . import TransmissionClient from .const import ( @@ -33,8 +35,8 @@ async def async_setup_entry( ) -> None: """Set up the Transmission sensors.""" - tm_client = hass.data[DOMAIN][config_entry.entry_id] - name = config_entry.data[CONF_NAME] + tm_client: TransmissionClient = hass.data[DOMAIN][config_entry.entry_id] + name: str = config_entry.data[CONF_NAME] dev = [ TransmissionSpeedSensor( @@ -96,12 +98,18 @@ class TransmissionSensor(SensorEntity): _attr_has_entity_name = True _attr_should_poll = False - def __init__(self, tm_client, client_name, sensor_translation_key, key): + def __init__( + self, + tm_client: TransmissionClient, + client_name: str, + sensor_translation_key: str, + key: str, + ) -> None: """Initialize the sensor.""" - self._tm_client: TransmissionClient = tm_client + self._tm_client = tm_client self._attr_translation_key = sensor_translation_key self._key = key - self._state = None + self._state: StateType = None self._attr_unique_id = f"{tm_client.config_entry.entry_id}-{key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -111,7 +119,7 @@ class TransmissionSensor(SensorEntity): ) @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the sensor.""" return self._state @@ -192,12 +200,12 @@ class TransmissionTorrentsSensor(TransmissionSensor): } @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return "Torrents" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes, if any.""" info = _torrents_info( torrents=self._tm_client.api.torrents, @@ -217,7 +225,9 @@ class TransmissionTorrentsSensor(TransmissionSensor): self._state = len(torrents) -def _filter_torrents(torrents: list[Torrent], statuses=None) -> list[Torrent]: +def _filter_torrents( + torrents: list[Torrent], statuses: list[str] | None = None +) -> list[Torrent]: return [ torrent for torrent in torrents @@ -225,7 +235,9 @@ def _filter_torrents(torrents: list[Torrent], statuses=None) -> list[Torrent]: ] -def _torrents_info(torrents, order, limit, statuses=None): +def _torrents_info( + torrents: list[Torrent], order: str, limit: int, statuses: list[str] | None = None +) -> dict[str, Any]: infos = {} torrents = _filter_torrents(torrents, statuses) torrents = SUPPORTED_ORDER_MODES[order](torrents) diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 34d8de5d620..fad099fc5b9 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -1,4 +1,5 @@ """Support for setting the Transmission BitTorrent client Turtle Mode.""" +from collections.abc import Callable import logging from typing import Any @@ -10,6 +11,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import TransmissionClient from .const import DOMAIN, SWITCH_TYPES _LOGGING = logging.getLogger(__name__) @@ -22,8 +24,8 @@ async def async_setup_entry( ) -> None: """Set up the Transmission switch.""" - tm_client = hass.data[DOMAIN][config_entry.entry_id] - name = config_entry.data[CONF_NAME] + tm_client: TransmissionClient = hass.data[DOMAIN][config_entry.entry_id] + name: str = config_entry.data[CONF_NAME] dev = [] for switch_type, switch_name in SWITCH_TYPES.items(): @@ -38,14 +40,20 @@ class TransmissionSwitch(SwitchEntity): _attr_has_entity_name = True _attr_should_poll = False - def __init__(self, switch_type, switch_name, tm_client, client_name): + def __init__( + self, + switch_type: str, + switch_name: str, + tm_client: TransmissionClient, + client_name: str, + ) -> None: """Initialize the Transmission switch.""" self._attr_name = switch_name self.type = switch_type self._tm_client = tm_client self._state = STATE_OFF self._data = None - self.unsub_update = None + self.unsub_update: Callable[[], None] | None = None self._attr_unique_id = f"{tm_client.config_entry.entry_id}-{switch_type}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -55,7 +63,7 @@ class TransmissionSwitch(SwitchEntity): ) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._state == STATE_ON @@ -93,10 +101,10 @@ class TransmissionSwitch(SwitchEntity): ) @callback - def _schedule_immediate_update(self): + def _schedule_immediate_update(self) -> None: self.async_schedule_update_ha_state(True) - async def will_remove_from_hass(self): + async def will_remove_from_hass(self) -> None: """Unsubscribe from update dispatcher.""" if self.unsub_update: self.unsub_update() From efcf3ddb577f43ae69ac70926b9d15b4559f7d28 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 12:49:20 +0200 Subject: [PATCH 0952/1151] Remove BleBox switch constructor (#99204) --- homeassistant/components/blebox/switch.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index d7145ebb620..94edc32bc8c 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -32,10 +32,7 @@ async def async_setup_entry( class BleBoxSwitchEntity(BleBoxEntity[blebox_uniapi.switch.Switch], SwitchEntity): """Representation of a BleBox switch feature.""" - def __init__(self, feature: blebox_uniapi.switch.Switch) -> None: - """Initialize a BleBox switch feature.""" - super().__init__(feature) - self._attr_device_class = SwitchDeviceClass.SWITCH + _attr_device_class = SwitchDeviceClass.SWITCH @property def is_on(self): From 1d403a961fba27e29a52470802c0a584de6c65c0 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 28 Aug 2023 14:13:35 +0300 Subject: [PATCH 0953/1151] Reorganize Transmission entry setup (#99195) * simplify integration setup * Update transmission entry setup to avoid None attributes * keep api property in TransmissionData * Apply suggestion * Add __init__.py tp .coveragerc --- .coveragerc | 1 + .../components/transmission/__init__.py | 56 +++++++++---------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/.coveragerc b/.coveragerc index 5adb465509a..46c7c568124 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1357,6 +1357,7 @@ omit = homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/coordinator.py homeassistant/components/trafikverket_weatherstation/sensor.py + homeassistant/components/transmission/__init__.py homeassistant/components/transmission/sensor.py homeassistant/components/transmission/switch.py homeassistant/components/travisci/sensor.py diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 5276a60e5fb..7e02c3d419d 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -29,11 +29,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import ( - ConfigEntryAuthFailed, - ConfigEntryNotReady, - HomeAssistantError, -) +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( config_validation as cv, entity_registry as er, @@ -138,17 +134,25 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id) - client = TransmissionClient(hass, config_entry) + try: + api = await get_api(hass, dict(config_entry.data)) + except CannotConnect as error: + raise ConfigEntryNotReady from error + except (AuthenticationError, UnknownError) as error: + raise ConfigEntryAuthFailed from error + + client = TransmissionClient(hass, config_entry, api) + await client.async_setup() hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client - await client.async_setup() - + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + client.register_services() return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Transmission Entry from config_entry.""" - client = hass.data[DOMAIN].pop(config_entry.entry_id) + client: TransmissionClient = hass.data[DOMAIN].pop(config_entry.entry_id) if client.unsub_timer: client.unsub_timer() @@ -213,41 +217,33 @@ def _get_client(hass: HomeAssistant, data: dict[str, Any]) -> TransmissionClient class TransmissionClient: """Transmission Client Object.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + api: transmission_rpc.Client, + ) -> None: """Initialize the Transmission RPC API.""" self.hass = hass self.config_entry = config_entry - self.tm_api: transmission_rpc.Client = None - self._tm_data: TransmissionData | None = None + self.tm_api = api + self._tm_data = TransmissionData(hass, config_entry, api) self.unsub_timer: Callable[[], None] | None = None @property def api(self) -> TransmissionData: """Return the TransmissionData object.""" - if self._tm_data is None: - raise HomeAssistantError("data not initialized") return self._tm_data async def async_setup(self) -> None: """Set up the Transmission client.""" - - try: - self.tm_api = await get_api(self.hass, dict(self.config_entry.data)) - except CannotConnect as error: - raise ConfigEntryNotReady from error - except (AuthenticationError, UnknownError) as error: - raise ConfigEntryAuthFailed from error - - self._tm_data = TransmissionData(self.hass, self.config_entry, self.tm_api) - - await self.hass.async_add_executor_job(self._tm_data.init_torrent_list) - await self.hass.async_add_executor_job(self._tm_data.update) + await self.hass.async_add_executor_job(self.api.init_torrent_list) + await self.hass.async_add_executor_job(self.api.update) self.add_options() self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - await self.hass.config_entries.async_forward_entry_setups( - self.config_entry, PLATFORMS - ) + def register_services(self) -> None: + """Register integration services.""" def add_torrent(service: ServiceCall) -> None: """Add new torrent to download.""" @@ -354,7 +350,7 @@ class TransmissionClient: @staticmethod async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: """Triggered by config entry options updates.""" - tm_client = hass.data[DOMAIN][entry.entry_id] + tm_client: TransmissionClient = hass.data[DOMAIN][entry.entry_id] tm_client.set_scan_interval(entry.options[CONF_SCAN_INTERVAL]) await hass.async_add_executor_job(tm_client.api.update) From 60844954d203298af08582c2d230245af4347fed Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 14:43:22 +0200 Subject: [PATCH 0954/1151] Add typing to media extractor (#99207) * Add typing to media extractor * Add typing to media extractor * Add typing to media extractor * Add typing to media extractor --- .strict-typing | 1 + .../components/media_extractor/__init__.py | 35 ++++++++++++------- mypy.ini | 10 ++++++ 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/.strict-typing b/.strict-typing index 19cee069b42..b8dc93d6780 100644 --- a/.strict-typing +++ b/.strict-typing @@ -213,6 +213,7 @@ homeassistant.components.luftdaten.* homeassistant.components.mailbox.* homeassistant.components.mastodon.* homeassistant.components.matter.* +homeassistant.components.media_extractor.* homeassistant.components.media_player.* homeassistant.components.media_source.* homeassistant.components.metoffice.* diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index d00f1b33ccc..dae734fc06f 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -1,5 +1,7 @@ """Decorator service for the media_player.play_media service.""" +from collections.abc import Callable import logging +from typing import Any, cast import voluptuous as vol from yt_dlp import YoutubeDL @@ -68,21 +70,26 @@ class MEQueryException(Exception): class MediaExtractor: """Class which encapsulates all extraction logic.""" - def __init__(self, hass, component_config, call_data): + def __init__( + self, + hass: HomeAssistant, + component_config: dict[str, Any], + call_data: dict[str, Any], + ) -> None: """Initialize media extractor.""" self.hass = hass self.config = component_config self.call_data = call_data - def get_media_url(self): + def get_media_url(self) -> str: """Return media content url.""" - return self.call_data.get(ATTR_MEDIA_CONTENT_ID) + return cast(str, self.call_data[ATTR_MEDIA_CONTENT_ID]) - def get_entities(self): + def get_entities(self) -> list[str]: """Return list of entities.""" return self.call_data.get(ATTR_ENTITY_ID, []) - def extract_and_send(self): + def extract_and_send(self) -> None: """Extract exact stream format for each entity_id and play it.""" try: stream_selector = self.get_stream_selector() @@ -97,7 +104,7 @@ class MediaExtractor: for entity_id in entities: self.call_media_player_service(stream_selector, entity_id) - def get_stream_selector(self): + def get_stream_selector(self) -> Callable[[str], str]: """Return format selector for the media URL.""" ydl = YoutubeDL({"quiet": True, "logger": _LOGGER}) @@ -118,7 +125,7 @@ class MediaExtractor: else: selected_media = all_media - def stream_selector(query): + def stream_selector(query: str) -> str: """Find stream URL that matches query.""" try: ydl.params["format"] = query @@ -131,12 +138,14 @@ class MediaExtractor: best_stream = requested_stream["formats"][ len(requested_stream["formats"]) - 1 ] - return best_stream["url"] - return requested_stream["url"] + return str(best_stream["url"]) + return str(requested_stream["url"]) return stream_selector - def call_media_player_service(self, stream_selector, entity_id): + def call_media_player_service( + self, stream_selector: Callable[[str], str], entity_id: str | None + ) -> None: """Call Media player play_media service.""" stream_query = self.get_stream_query_for_entity(entity_id) @@ -156,16 +165,16 @@ class MediaExtractor: self.hass.services.async_call(MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, data) ) - def get_stream_query_for_entity(self, entity_id): + def get_stream_query_for_entity(self, entity_id: str | None) -> str: """Get stream format query for entity.""" - default_stream_query = self.config.get( + default_stream_query: str = self.config.get( CONF_DEFAULT_STREAM_QUERY, DEFAULT_STREAM_QUERY ) if entity_id: media_content_type = self.call_data.get(ATTR_MEDIA_CONTENT_TYPE) - return ( + return str( self.config.get(CONF_CUSTOMIZE_ENTITIES, {}) .get(entity_id, {}) .get(media_content_type, default_stream_query) diff --git a/mypy.ini b/mypy.ini index 644fba0df89..8278a19465c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1892,6 +1892,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.media_extractor.*] +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.media_player.*] check_untyped_defs = true disallow_incomplete_defs = true From 3f0a8b7a56f3c43a7ee40aa378f97f323d6b9717 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 14:43:51 +0200 Subject: [PATCH 0955/1151] Initialize static shorthand attributes outside of constructor for BAF (#99202) Initialize static shorthand attributes outside of init --- homeassistant/components/baf/light.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/baf/light.py b/homeassistant/components/baf/light.py index 9557005e5eb..ed5eea8796f 100644 --- a/homeassistant/components/baf/light.py +++ b/homeassistant/components/baf/light.py @@ -39,6 +39,8 @@ async def async_setup_entry( class BAFLight(BAFEntity, LightEntity): """Representation of a Big Ass Fans light.""" + _attr_name = None + @callback def _async_update_attrs(self) -> None: """Update attrs from device.""" @@ -63,23 +65,19 @@ class BAFLight(BAFEntity, LightEntity): class BAFFanLight(BAFLight): """Representation of a Big Ass Fans light on a fan.""" - _attr_name = None - - def __init__(self, device: Device) -> None: - """Init a fan light.""" - super().__init__(device) - self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} - self._attr_color_mode = ColorMode.BRIGHTNESS + _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + _attr_color_mode = ColorMode.BRIGHTNESS class BAFStandaloneLight(BAFLight): """Representation of a Big Ass Fans light.""" + _attr_supported_color_modes = {ColorMode.COLOR_TEMP} + _attr_color_mode = ColorMode.COLOR_TEMP + def __init__(self, device: Device) -> None: """Init a standalone light.""" super().__init__(device) - self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} - self._attr_color_mode = ColorMode.COLOR_TEMP self._attr_min_mireds = color_temperature_kelvin_to_mired( device.light_warmest_color_temperature ) From 660167cb1b3333f857a6ae462ffe3adacbc72772 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 28 Aug 2023 14:55:49 +0200 Subject: [PATCH 0956/1151] Add image platform to devolo_home_network (#98036) --- .../devolo_home_network/__init__.py | 1 + .../components/devolo_home_network/const.py | 1 + .../components/devolo_home_network/image.py | 100 ++++++++++++++++++ .../devolo_home_network/strings.json | 5 + tests/components/devolo_home_network/const.py | 7 ++ .../snapshots/test_image.ambr | 34 ++++++ .../devolo_home_network/test_image.py | 96 +++++++++++++++++ .../devolo_home_network/test_init.py | 8 +- 8 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/devolo_home_network/image.py create mode 100644 tests/components/devolo_home_network/snapshots/test_image.ambr create mode 100644 tests/components/devolo_home_network/test_image.py diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 181c47aac61..f54fddc9a86 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -213,6 +213,7 @@ def platforms(device: Device) -> set[Platform]: supported_platforms.add(Platform.BINARY_SENSOR) if device.device and "wifi1" in device.device.features: supported_platforms.add(Platform.DEVICE_TRACKER) + supported_platforms.add(Platform.IMAGE) if device.device and "update" in device.device.features: supported_platforms.add(Platform.UPDATE) return supported_platforms diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index 53019e28a23..ba3f5e5b815 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -21,6 +21,7 @@ CONNECTED_PLC_DEVICES = "connected_plc_devices" CONNECTED_TO_ROUTER = "connected_to_router" CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" IDENTIFY = "identify" +IMAGE_GUEST_WIFI = "image_guest_wifi" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" PAIRING = "pairing" REGULAR_FIRMWARE = "regular_firmware" diff --git a/homeassistant/components/devolo_home_network/image.py b/homeassistant/components/devolo_home_network/image.py new file mode 100644 index 00000000000..3670c42bc6b --- /dev/null +++ b/homeassistant/components/devolo_home_network/image.py @@ -0,0 +1,100 @@ +"""Platform for image integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from functools import partial +from typing import Any + +from devolo_plc_api import Device, wifi_qr_code +from devolo_plc_api.device_api import WifiGuestAccessGet + +from homeassistant.components.image import ImageEntity, ImageEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +import homeassistant.util.dt as dt_util + +from .const import DOMAIN, IMAGE_GUEST_WIFI, SWITCH_GUEST_WIFI +from .entity import DevoloCoordinatorEntity + + +@dataclass +class DevoloImageRequiredKeysMixin: + """Mixin for required keys.""" + + image_func: Callable[[WifiGuestAccessGet], bytes] + + +@dataclass +class DevoloImageEntityDescription( + ImageEntityDescription, DevoloImageRequiredKeysMixin +): + """Describes devolo image entity.""" + + +IMAGE_TYPES: dict[str, DevoloImageEntityDescription] = { + IMAGE_GUEST_WIFI: DevoloImageEntityDescription( + key=IMAGE_GUEST_WIFI, + entity_category=EntityCategory.DIAGNOSTIC, + image_func=partial(wifi_qr_code, omitsize=True), + ) +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Get all devices and sensors and setup them via config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + coordinators: dict[str, DataUpdateCoordinator[Any]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinators"] + + entities: list[ImageEntity] = [] + entities.append( + DevoloImageEntity( + entry, + coordinators[SWITCH_GUEST_WIFI], + IMAGE_TYPES[IMAGE_GUEST_WIFI], + device, + ) + ) + async_add_entities(entities) + + +class DevoloImageEntity(DevoloCoordinatorEntity[WifiGuestAccessGet], ImageEntity): + """Representation of a devolo image.""" + + _attr_content_type = "image/svg+xml" + + def __init__( + self, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator[WifiGuestAccessGet], + description: DevoloImageEntityDescription, + device: Device, + ) -> None: + """Initialize entity.""" + self.entity_description: DevoloImageEntityDescription = description + super().__init__(entry, coordinator, device) + ImageEntity.__init__(self, coordinator.hass) + self._attr_image_last_updated = dt_util.utcnow() + self._data = self.coordinator.data + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if ( + self._data.ssid != self.coordinator.data.ssid + or self._data.key != self.coordinator.data.key + ): + self._data = self.coordinator.data + self._attr_image_last_updated = dt_util.utcnow() + super()._handle_coordinator_update() + + async def async_image(self) -> bytes | None: + """Return bytes of image.""" + return self.entity_description.image_func(self.coordinator.data) diff --git a/homeassistant/components/devolo_home_network/strings.json b/homeassistant/components/devolo_home_network/strings.json index e2954c1c7ec..55a7920ab3e 100644 --- a/homeassistant/components/devolo_home_network/strings.json +++ b/homeassistant/components/devolo_home_network/strings.json @@ -48,6 +48,11 @@ "name": "Start WPS" } }, + "image": { + "image_guest_wifi": { + "name": "Guest Wifi credentials as QR code" + } + }, "sensor": { "connected_plc_devices": { "name": "Connected PLC devices" diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index f4cc372660c..bc2ef2d87b2 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -92,6 +92,13 @@ GUEST_WIFI = WifiGuestAccessGet( remaining_duration=0, ) +GUEST_WIFI_CHANGED = WifiGuestAccessGet( + ssid="devolo-guest-930", + key="HMANPGAS", + enabled=False, + remaining_duration=0, +) + NEIGHBOR_ACCESS_POINTS = [ NeighborAPInfo( mac_address="AA:BB:CC:DD:EE:FF", diff --git a/tests/components/devolo_home_network/snapshots/test_image.ambr b/tests/components/devolo_home_network/snapshots/test_image.ambr new file mode 100644 index 00000000000..b00f73ca116 --- /dev/null +++ b/tests/components/devolo_home_network/snapshots/test_image.ambr @@ -0,0 +1,34 @@ +# serializer version: 1 +# name: test_guest_wifi_qr + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'image', + 'entity_category': , + 'entity_id': 'image.mock_title_guest_wifi_credentials_as_qr_code', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Guest Wifi credentials as QR code', + 'platform': 'devolo_home_network', + 'supported_features': 0, + 'translation_key': 'image_guest_wifi', + 'unique_id': '1234567890_image_guest_wifi', + 'unit_of_measurement': None, + }) +# --- +# name: test_guest_wifi_qr.1 + b'\n\n' +# --- diff --git a/tests/components/devolo_home_network/test_image.py b/tests/components/devolo_home_network/test_image.py new file mode 100644 index 00000000000..b8fb491e1ec --- /dev/null +++ b/tests/components/devolo_home_network/test_image.py @@ -0,0 +1,96 @@ +"""Tests for the devolo Home Network images.""" +from http import HTTPStatus +from unittest.mock import AsyncMock + +from devolo_plc_api.exceptions.device import DeviceUnavailable +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.devolo_home_network.const import SHORT_UPDATE_INTERVAL +from homeassistant.components.image import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from . import configure_integration +from .const import GUEST_WIFI_CHANGED +from .mock import MockDevice + +from tests.common import async_fire_time_changed +from tests.typing import ClientSessionGenerator + + +@pytest.mark.usefixtures("mock_device") +async def test_image_setup(hass: HomeAssistant) -> None: + """Test default setup of the image component.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + hass.states.get(f"{DOMAIN}.{device_name}_guest_wifi_credentials_as_qr_code") + is not None + ) + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.freeze_time("2023-01-13 12:00:00+00:00") +async def test_guest_wifi_qr( + hass: HomeAssistant, + mock_device: MockDevice, + entity_registry: er.EntityRegistry, + hass_client: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test showing a QR code of the guest wifi credentials.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_guest_wifi_credentials_as_qr_code" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state.name == "Mock Title Guest Wifi credentials as QR code" + assert state.state == dt_util.utcnow().isoformat() + assert entity_registry.async_get(state_key) == snapshot + + client = await hass_client() + resp = await client.get(f"/api/image_proxy/{state_key}") + assert resp.status == HTTPStatus.OK + body = await resp.read() + assert body == snapshot + + # Emulate device failure + mock_device.device.async_get_wifi_guest_access.side_effect = DeviceUnavailable() + freezer.move_to(dt_util.utcnow() + SHORT_UPDATE_INTERVAL) + async_fire_time_changed(hass, dt_util.utcnow() + SHORT_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # Emulate state change + mock_device.device.async_get_wifi_guest_access = AsyncMock( + return_value=GUEST_WIFI_CHANGED + ) + freezer.move_to(dt_util.utcnow() + SHORT_UPDATE_INTERVAL) + async_fire_time_changed(hass, dt_util.utcnow() + SHORT_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == dt_util.utcnow().isoformat() + + client = await hass_client() + resp = await client.get(f"/api/image_proxy/{state_key}") + assert resp.status == HTTPStatus.OK + assert await resp.read() != body + + await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index ba34eb18490..3c207a1aaef 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -8,6 +8,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.button import DOMAIN as BUTTON from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.devolo_home_network.const import DOMAIN +from homeassistant.components.image import DOMAIN as IMAGE from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.components.update import DOMAIN as UPDATE @@ -87,9 +88,12 @@ async def test_hass_stop(hass: HomeAssistant, mock_device: MockDevice) -> None: [ [ "mock_device", - (BINARY_SENSOR, BUTTON, DEVICE_TRACKER, SENSOR, SWITCH, UPDATE), + (BINARY_SENSOR, BUTTON, DEVICE_TRACKER, IMAGE, SENSOR, SWITCH, UPDATE), + ], + [ + "mock_repeater_device", + (BUTTON, DEVICE_TRACKER, IMAGE, SENSOR, SWITCH, UPDATE), ], - ["mock_repeater_device", (BUTTON, DEVICE_TRACKER, SENSOR, SWITCH, UPDATE)], ["mock_nonwifi_device", (BINARY_SENSOR, BUTTON, SENSOR, SWITCH, UPDATE)], ], ) From 1692d830630ee8d4914ac0fc7319ab4cdd3e0782 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 28 Aug 2023 15:10:23 +0200 Subject: [PATCH 0957/1151] Vodafone Station device tracker (#94032) * New integration for Vodafone Station * coveragerc * Add ConfigFlow,ScannerEntity,DataUpdateCoordinator * Introduce aiovodafone lib * heavy cleanup * bump aiovodafone to v0.0.5 * add config_flow tests (100% coverage) * run pre-comimit scripts again * Remove redundant parameter SSL * rename and cleanup * cleanup and bug fix * cleanup exceptions * constructor comment review * improve test patching * move VodafoneStationDeviceInfo to dataclass * intriduce home field * dispacher cleanup * remove extra attributes (reduces state writes) * attempt to complete test flow * complete flow for test_exception_connection * add comment about unique id --- .coveragerc | 4 + CODEOWNERS | 2 + .../components/vodafone_station/__init__.py | 40 ++++ .../vodafone_station/config_flow.py | 127 +++++++++++ .../components/vodafone_station/const.py | 11 + .../vodafone_station/coordinator.py | 124 ++++++++++ .../vodafone_station/device_tracker.py | 114 ++++++++++ .../components/vodafone_station/manifest.json | 10 + .../components/vodafone_station/strings.json | 33 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/vodafone_station/__init__.py | 1 + tests/components/vodafone_station/const.py | 17 ++ .../vodafone_station/test_config_flow.py | 215 ++++++++++++++++++ 16 files changed, 711 insertions(+) create mode 100644 homeassistant/components/vodafone_station/__init__.py create mode 100644 homeassistant/components/vodafone_station/config_flow.py create mode 100644 homeassistant/components/vodafone_station/const.py create mode 100644 homeassistant/components/vodafone_station/coordinator.py create mode 100644 homeassistant/components/vodafone_station/device_tracker.py create mode 100644 homeassistant/components/vodafone_station/manifest.json create mode 100644 homeassistant/components/vodafone_station/strings.json create mode 100644 tests/components/vodafone_station/__init__.py create mode 100644 tests/components/vodafone_station/const.py create mode 100644 tests/components/vodafone_station/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 46c7c568124..6f26795d1b5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1440,6 +1440,10 @@ omit = homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/__init__.py homeassistant/components/vlc_telnet/media_player.py + homeassistant/components/vodafone_station/__init__.py + homeassistant/components/vodafone_station/const.py + homeassistant/components/vodafone_station/coordinator.py + homeassistant/components/vodafone_station/device_tracker.py homeassistant/components/volkszaehler/sensor.py homeassistant/components/volumio/__init__.py homeassistant/components/volumio/browse_media.py diff --git a/CODEOWNERS b/CODEOWNERS index b6241669796..9e8d297d37f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1368,6 +1368,8 @@ build.json @home-assistant/supervisor /tests/components/vizio/ @raman325 /homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare /tests/components/vlc_telnet/ @rodripf @MartinHjelmare +/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74 +/tests/components/vodafone_station/ @paoloantinori @chemelli74 /homeassistant/components/voip/ @balloob @synesthesiam /tests/components/voip/ @balloob @synesthesiam /homeassistant/components/volumio/ @OnFreund diff --git a/homeassistant/components/vodafone_station/__init__.py b/homeassistant/components/vodafone_station/__init__.py new file mode 100644 index 00000000000..c1cf23d974f --- /dev/null +++ b/homeassistant/components/vodafone_station/__init__.py @@ -0,0 +1,40 @@ +"""Vodafone Station integration.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import VodafoneStationRouter + +PLATFORMS = [Platform.DEVICE_TRACKER] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Vodafone Station platform.""" + coordinator = VodafoneStationRouter( + hass, + entry.data[CONF_HOST], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.unique_id, + ) + + 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) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + coordinator: VodafoneStationRouter = hass.data[DOMAIN][entry.entry_id] + await coordinator.api.logout() + await coordinator.api.close() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/vodafone_station/config_flow.py b/homeassistant/components/vodafone_station/config_flow.py new file mode 100644 index 00000000000..e4a087f6903 --- /dev/null +++ b/homeassistant/components/vodafone_station/config_flow.py @@ -0,0 +1,127 @@ +"""Config flow for Vodafone Station integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from aiovodafone import VodafoneStationApi, exceptions as aiovodafone_exceptions +import voluptuous as vol + +from homeassistant import core +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult + +from .const import _LOGGER, DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN + + +def user_form_schema(user_input: dict[str, Any] | None) -> vol.Schema: + """Return user form schema.""" + user_input = user_input or {} + return vol.Schema( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): str, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ) + + +STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + + +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: + """Validate the user input allows us to connect.""" + + api = VodafoneStationApi(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) + + try: + await api.login() + finally: + await api.logout() + await api.close() + + return {"title": data[CONF_HOST]} + + +class VodafoneStationConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Vodafone Station.""" + + VERSION = 1 + entry: ConfigEntry | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=user_form_schema(user_input) + ) + + # Use host because no serial number or mac is available to use for a unique id + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except aiovodafone_exceptions.CannotConnect: + errors["base"] = "cannot_connect" + except aiovodafone_exceptions.CannotAuthenticate: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=user_form_schema(user_input), errors=errors + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle reauth flow.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert self.entry + self.context["title_placeholders"] = {"host": self.entry.data[CONF_HOST]} + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle reauth confirm.""" + assert self.entry + errors = {} + + if user_input is not None: + try: + await validate_input(self.hass, {**self.entry.data, **user_input}) + except aiovodafone_exceptions.CannotConnect: + errors["base"] = "cannot_connect" + except aiovodafone_exceptions.CannotAuthenticate: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders={CONF_HOST: self.entry.data[CONF_HOST]}, + data_schema=STEP_REAUTH_DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/vodafone_station/const.py b/homeassistant/components/vodafone_station/const.py new file mode 100644 index 00000000000..8d5a60afb60 --- /dev/null +++ b/homeassistant/components/vodafone_station/const.py @@ -0,0 +1,11 @@ +"""Vodafone Station constants.""" +import logging + +_LOGGER = logging.getLogger(__package__) + +DOMAIN = "vodafone_station" + +DEFAULT_DEVICE_NAME = "Unknown device" +DEFAULT_HOST = "192.168.1.1" +DEFAULT_USERNAME = "vodafone" +DEFAULT_SSL = True diff --git a/homeassistant/components/vodafone_station/coordinator.py b/homeassistant/components/vodafone_station/coordinator.py new file mode 100644 index 00000000000..b79acac9ce9 --- /dev/null +++ b/homeassistant/components/vodafone_station/coordinator.py @@ -0,0 +1,124 @@ +"""Support for Vodafone Station.""" +from dataclasses import dataclass +from datetime import datetime, timedelta +from typing import Any + +from aiovodafone import VodafoneStationApi, VodafoneStationDevice, exceptions + +from homeassistant.components.device_tracker import DEFAULT_CONSIDER_HOME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util + +from .const import _LOGGER, DOMAIN + +CONSIDER_HOME_SECONDS = DEFAULT_CONSIDER_HOME.total_seconds() + + +@dataclass(slots=True) +class VodafoneStationDeviceInfo: + """Representation of a device connected to the Vodafone Station.""" + + device: VodafoneStationDevice + update_time: datetime | None + home: bool + + +@dataclass(slots=True) +class UpdateCoordinatorDataType: + """Update coordinator data type.""" + + devices: dict[str, VodafoneStationDeviceInfo] + sensors: dict[str, Any] + + +class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]): + """Queries router running Vodafone Station firmware.""" + + def __init__( + self, + hass: HomeAssistant, + host: str, + username: str, + password: str, + config_entry_unique_id: str | None, + ) -> None: + """Initialize the scanner.""" + + self._host = host + self.api = VodafoneStationApi(host, username, password) + + # Last resort as no MAC or S/N can be retrieved via API + self._id = config_entry_unique_id + + super().__init__( + hass=hass, + logger=_LOGGER, + name=f"{DOMAIN}-{host}-coordinator", + update_interval=timedelta(seconds=30), + ) + + def _calculate_update_time_and_consider_home( + self, device: VodafoneStationDevice, utc_point_in_time: datetime + ) -> tuple[datetime | None, bool]: + """Return update time and consider home. + + If the device is connected, return the current time and True. + + If the device is not connected, return the last update time and + whether the device was considered home at that time. + + If the device is not connected and there is no last update time, + return None and False. + """ + if device.connected: + return utc_point_in_time, True + + if ( + (data := self.data) + and (stored_device := data.devices.get(device.mac)) + and (update_time := stored_device.update_time) + ): + return ( + update_time, + ( + (utc_point_in_time - update_time).total_seconds() + < CONSIDER_HOME_SECONDS + ), + ) + + return None, False + + async def _async_update_data(self) -> UpdateCoordinatorDataType: + """Update router data.""" + _LOGGER.debug("Polling Vodafone Station host: %s", self._host) + try: + logged = await self.api.login() + except exceptions.CannotConnect as err: + _LOGGER.warning("Connection error for %s", self._host) + raise UpdateFailed(f"Error fetching data: {repr(err)}") from err + except exceptions.CannotAuthenticate as err: + raise ConfigEntryAuthFailed from err + + if not logged: + raise ConfigEntryAuthFailed + + utc_point_in_time = dt_util.utcnow() + data_devices = { + dev_info.mac: VodafoneStationDeviceInfo( + dev_info, + *self._calculate_update_time_and_consider_home( + dev_info, utc_point_in_time + ), + ) + for dev_info in (await self.api.get_all_devices()).values() + } + data_sensors = await self.api.get_user_data() + await self.api.logout() + return UpdateCoordinatorDataType(data_devices, data_sensors) + + @property + def signal_device_new(self) -> str: + """Event specific per Vodafone Station entry to signal new device.""" + return f"{DOMAIN}-device-new-{self._id}" diff --git a/homeassistant/components/vodafone_station/device_tracker.py b/homeassistant/components/vodafone_station/device_tracker.py new file mode 100644 index 00000000000..9f98da88d22 --- /dev/null +++ b/homeassistant/components/vodafone_station/device_tracker.py @@ -0,0 +1,114 @@ +"""Support for Vodafone Station routers.""" +from __future__ import annotations + +from aiovodafone import VodafoneStationDevice + +from homeassistant.components.device_tracker import ScannerEntity, SourceType +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import _LOGGER, DOMAIN +from .coordinator import VodafoneStationDeviceInfo, VodafoneStationRouter + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up device tracker for Vodafone Station component.""" + + _LOGGER.debug("Start device trackers setup") + coordinator: VodafoneStationRouter = hass.data[DOMAIN][entry.entry_id] + + tracked: set = set() + + @callback + def async_update_router() -> None: + """Update the values of the router.""" + async_add_new_tracked_entities(coordinator, async_add_entities, tracked) + + entry.async_on_unload( + async_dispatcher_connect( + hass, coordinator.signal_device_new, async_update_router + ) + ) + + async_update_router() + + +@callback +def async_add_new_tracked_entities( + coordinator: VodafoneStationRouter, + async_add_entities: AddEntitiesCallback, + tracked: set[str], +) -> None: + """Add new tracker entities from the router.""" + new_tracked = [] + + _LOGGER.debug("Adding device trackers entities") + for mac, device_info in coordinator.data.devices.items(): + if mac in tracked: + continue + _LOGGER.debug("New device tracker: %s", device_info.device.name) + new_tracked.append(VodafoneStationTracker(coordinator, device_info)) + tracked.add(mac) + + async_add_entities(new_tracked) + + +class VodafoneStationTracker(CoordinatorEntity[VodafoneStationRouter], ScannerEntity): + """Representation of a Vodafone Station device.""" + + def __init__( + self, coordinator: VodafoneStationRouter, device_info: VodafoneStationDeviceInfo + ) -> None: + """Initialize a Vodafone Station device.""" + super().__init__(coordinator) + self._coordinator = coordinator + device = device_info.device + mac = device.mac + self._device_mac = mac + self._attr_unique_id = mac + self._attr_name = device.name or mac.replace(":", "_") + + @property + def _device_info(self) -> VodafoneStationDeviceInfo: + """Return fresh data for the device.""" + return self.coordinator.data.devices[self._device_mac] + + @property + def _device(self) -> VodafoneStationDevice: + """Return fresh data for the device.""" + return self.coordinator.data.devices[self._device_mac].device + + @property + def is_connected(self) -> bool: + """Return true if the device is connected to the network.""" + return self._device_info.home + + @property + def source_type(self) -> SourceType: + """Return the source type.""" + return SourceType.ROUTER + + @property + def hostname(self) -> str | None: + """Return the hostname of device.""" + return self._attr_name + + @property + def icon(self) -> str: + """Return device icon.""" + return "mdi:lan-connect" if self._device.connected else "mdi:lan-disconnect" + + @property + def ip_address(self) -> str | None: + """Return the primary ip address of the device.""" + return self._device.ip_address + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._device_mac diff --git a/homeassistant/components/vodafone_station/manifest.json b/homeassistant/components/vodafone_station/manifest.json new file mode 100644 index 00000000000..7069629ca2e --- /dev/null +++ b/homeassistant/components/vodafone_station/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "vodafone_station", + "name": "Vodafone Station", + "codeowners": ["@paoloantinori", "@chemelli74"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/vodafone_station", + "iot_class": "local_polling", + "loggers": ["aiovodafone"], + "requirements": ["aiovodafone==0.0.6"] +} diff --git a/homeassistant/components/vodafone_station/strings.json b/homeassistant/components/vodafone_station/strings.json new file mode 100644 index 00000000000..3c452133c28 --- /dev/null +++ b/homeassistant/components/vodafone_station/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "description": "Please enter the correct password for host: {host}", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "ssl": "[%key:common::config_flow::data::ssl%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 93d7ec1fbdc..fe23ae9697f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -508,6 +508,7 @@ FLOWS = { "vilfo", "vizio", "vlc_telnet", + "vodafone_station", "voip", "volumio", "volvooncall", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 07960a97fe5..81afb1cecd8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6202,6 +6202,12 @@ } } }, + "vodafone_station": { + "name": "Vodafone Station", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "voicerss": { "name": "VoiceRSS", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 6be8bbd1c31..56455ef39c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -365,6 +365,9 @@ aiounifi==58 # homeassistant.components.vlc_telnet aiovlc==0.1.0 +# homeassistant.components.vodafone_station +aiovodafone==0.0.6 + # homeassistant.components.waqi aiowaqi==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7bdc29d3690..5ad1dd12361 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -340,6 +340,9 @@ aiounifi==58 # homeassistant.components.vlc_telnet aiovlc==0.1.0 +# homeassistant.components.vodafone_station +aiovodafone==0.0.6 + # homeassistant.components.watttime aiowatttime==0.1.1 diff --git a/tests/components/vodafone_station/__init__.py b/tests/components/vodafone_station/__init__.py new file mode 100644 index 00000000000..68f11a27b95 --- /dev/null +++ b/tests/components/vodafone_station/__init__.py @@ -0,0 +1 @@ +"""Tests for the Vodafone Station integration.""" diff --git a/tests/components/vodafone_station/const.py b/tests/components/vodafone_station/const.py new file mode 100644 index 00000000000..40dc305630e --- /dev/null +++ b/tests/components/vodafone_station/const.py @@ -0,0 +1,17 @@ +"""Common stuff for Vodafone Station tests.""" +from homeassistant.components.vodafone_station.const import DOMAIN +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME + +MOCK_CONFIG = { + DOMAIN: { + CONF_DEVICES: [ + { + CONF_HOST: "fake_host", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + } + ] + } +} + +MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] diff --git a/tests/components/vodafone_station/test_config_flow.py b/tests/components/vodafone_station/test_config_flow.py new file mode 100644 index 00000000000..03a1198288d --- /dev/null +++ b/tests/components/vodafone_station/test_config_flow.py @@ -0,0 +1,215 @@ +"""Tests for Vodafone Station config flow.""" +from unittest.mock import patch + +from aiovodafone import exceptions as aiovodafone_exceptions +import pytest + +from homeassistant.components.vodafone_station.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import MOCK_USER_DATA + +from tests.common import MockConfigEntry + + +async def test_user(hass: HomeAssistant) -> None: + """Test starting a flow by user.""" + with patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.login", + ), patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.logout", + ), patch( + "homeassistant.components.vodafone_station.async_setup_entry" + ) as mock_setup_entry, patch( + "requests.get" + ) as mock_request_get: + mock_request_get.return_value.status_code = 200 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_USERNAME] == "fake_username" + assert result["data"][CONF_PASSWORD] == "fake_password" + assert not result["result"].unique_id + await hass.async_block_till_done() + + assert mock_setup_entry.called + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (aiovodafone_exceptions.CannotConnect, "cannot_connect"), + (aiovodafone_exceptions.CannotAuthenticate, "invalid_auth"), + (ConnectionResetError, "unknown"), + ], +) +async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> None: + """Test starting a flow by user with a connection error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + with patch( + "aiovodafone.api.VodafoneStationApi.login", + side_effect=side_effect, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == error + + # Should be recoverable after hits error + with patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.get_all_devices", + return_value={ + "wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G", + "ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;", + }, + ), patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.login", + ), patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.logout", + ), patch( + "homeassistant.components.vodafone_station.async_setup_entry" + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "fake_host", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "fake_host" + assert result2["data"] == { + "host": "fake_host", + "username": "fake_username", + "password": "fake_password", + } + + +async def test_reauth_successful(hass: HomeAssistant) -> None: + """Test starting a reauthentication flow.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.login", + ), patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.logout", + ), patch( + "homeassistant.components.vodafone_station.async_setup_entry" + ), patch( + "requests.get" + ) as mock_request_get: + mock_request_get.return_value.status_code = 200 + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "other_fake_password", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (aiovodafone_exceptions.CannotConnect, "cannot_connect"), + (aiovodafone_exceptions.CannotAuthenticate, "invalid_auth"), + (ConnectionResetError, "unknown"), + ], +) +async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> None: + """Test starting a reauthentication flow but no connection found.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.login", + side_effect=side_effect, + ), patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.logout", + ), patch( + "homeassistant.components.vodafone_station.async_setup_entry" + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "other_fake_password", + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"]["base"] == error + + # Should be recoverable after hits error + with patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.get_all_devices", + return_value={ + "wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G", + "ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;", + }, + ), patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.login", + ), patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationApi.logout", + ), patch( + "homeassistant.components.vodafone_station.async_setup_entry" + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "fake_password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" From 61ff53fcf7e7297658b756bf3ad086f825cd2aa9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 15:14:38 +0200 Subject: [PATCH 0958/1151] Use shorthand attributes in August (#99196) --- homeassistant/components/august/camera.py | 12 ++---------- homeassistant/components/august/sensor.py | 23 ++++------------------- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 4c3c124953a..e618c2d49d5 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -45,22 +45,14 @@ class AugustCamera(AugustEntityMixin, Camera): self._image_url = None self._image_content = None self._attr_unique_id = f"{self._device_id:s}_camera" + self._attr_motion_detection_enabled = True + self._attr_brand = DEFAULT_NAME @property def is_recording(self) -> bool: """Return true if the device is recording.""" return self._device.has_subscription - @property - def motion_detection_enabled(self) -> bool: - """Return the camera motion detection status.""" - return True - - @property - def brand(self): - """Return the camera brand.""" - return DEFAULT_NAME - @property def model(self): """Return the camera model.""" diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 2c688ae7615..12ed3a88558 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -185,7 +185,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): self._operated_keypad = None self._operated_autorelock = None self._operated_time = None - self._entity_picture = None + self._attr_unique_id = f"{self._device_id}_lock_operator" self._update_from_data() @callback @@ -201,7 +201,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): self._operated_remote = lock_activity.operated_remote self._operated_keypad = lock_activity.operated_keypad self._operated_autorelock = lock_activity.operated_autorelock - self._entity_picture = lock_activity.operator_thumbnail_url + self._attr_entity_picture = lock_activity.operator_thumbnail_url @property def extra_state_attributes(self): @@ -236,7 +236,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): self._attr_native_value = last_state.state if ATTR_ENTITY_PICTURE in last_state.attributes: - self._entity_picture = last_state.attributes[ATTR_ENTITY_PICTURE] + self._attr_entity_picture = last_state.attributes[ATTR_ENTITY_PICTURE] if ATTR_OPERATION_REMOTE in last_state.attributes: self._operated_remote = last_state.attributes[ATTR_OPERATION_REMOTE] if ATTR_OPERATION_KEYPAD in last_state.attributes: @@ -244,16 +244,6 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): if ATTR_OPERATION_AUTORELOCK in last_state.attributes: self._operated_autorelock = last_state.attributes[ATTR_OPERATION_AUTORELOCK] - @property - def entity_picture(self): - """Return the entity picture to use in the frontend, if any.""" - return self._entity_picture - - @property - def unique_id(self) -> str: - """Get the unique id of the device sensor.""" - return f"{self._device_id}_lock_operator" - class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[_T]): """Representation of an August sensor.""" @@ -272,8 +262,8 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[_T]): """Initialize the sensor.""" super().__init__(data, device) self.entity_description = description - self._old_device = old_device self._attr_unique_id = f"{self._device_id}_{description.key}" + self.old_unique_id = f"{old_device.device_id}_{description.key}" self._update_from_data() @callback @@ -281,8 +271,3 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[_T]): """Get the latest state of the sensor.""" self._attr_native_value = self.entity_description.value_fn(self._detail) self._attr_available = self._attr_native_value is not None - - @property - def old_unique_id(self) -> str: - """Get the old unique id of the device sensor.""" - return f"{self._old_device.device_id}_{self.entity_description.key}" From a6788208fe6672464fbd36d6525a6a3d8424af84 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 15:36:18 +0200 Subject: [PATCH 0959/1151] Add entity translations to System bridge (#98959) --- .../components/system_bridge/__init__.py | 9 +--- .../components/system_bridge/binary_sensor.py | 3 -- .../components/system_bridge/sensor.py | 40 +++++++-------- .../components/system_bridge/strings.json | 49 +++++++++++++++++++ 4 files changed, 69 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 058d03163ef..b301d0c4b28 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -273,19 +273,19 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]): """Defines a base System Bridge entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SystemBridgeDataUpdateCoordinator, api_port: int, key: str, - name: str | None, ) -> None: """Initialize the System Bridge entity.""" super().__init__(coordinator) self._hostname = coordinator.data.system.hostname self._key = f"{self._hostname}_{key}" - self._name = f"{self._hostname} {name}" self._configuration_url = ( f"http://{self._hostname}:{api_port}/app/settings.html" ) @@ -298,11 +298,6 @@ class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]): """Return the unique ID for this entity.""" return self._key - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - @property def device_info(self) -> DeviceInfo: """Return device information about this System Bridge instance.""" diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index 5c23c3110d8..e3ecc3817a6 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -33,7 +33,6 @@ class SystemBridgeBinarySensorEntityDescription(BinarySensorEntityDescription): BASE_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ...] = ( SystemBridgeBinarySensorEntityDescription( key="version_available", - name="New version available", device_class=BinarySensorDeviceClass.UPDATE, value=lambda data: data.system.version_newer_available, ), @@ -42,7 +41,6 @@ BASE_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ...] BATTERY_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ...] = ( SystemBridgeBinarySensorEntityDescription( key="battery_is_charging", - name="Battery is charging", device_class=BinarySensorDeviceClass.BATTERY_CHARGING, value=lambda data: data.battery.is_charging, ), @@ -92,7 +90,6 @@ class SystemBridgeBinarySensor(SystemBridgeEntity, BinarySensorEntity): coordinator, api_port, description.key, - description.name, ) self.entity_description = description diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index 4e0cbb9d2b9..151a6882e26 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -25,7 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType +from homeassistant.helpers.typing import UNDEFINED, StateType from homeassistant.util.dt import utcnow from . import SystemBridgeEntity @@ -46,10 +46,6 @@ PIXELS: Final = "px" class SystemBridgeSensorEntityDescription(SensorEntityDescription): """Class describing System Bridge sensor entities.""" - # SystemBridgeSensor does not support UNDEFINED or None, - # restrict the type to str. - name: str = "" - value: Callable = round @@ -143,14 +139,14 @@ def memory_used(data: SystemBridgeCoordinatorData) -> float | None: BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( SystemBridgeSensorEntityDescription( key="boot_time", - name="Boot time", + translation_key="boot_time", device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:av-timer", value=lambda data: datetime.fromtimestamp(data.system.boot_time, tz=UTC), ), SystemBridgeSensorEntityDescription( key="cpu_power_package", - name="CPU Package Power", + translation_key="cpu_power_package", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, @@ -159,7 +155,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( ), SystemBridgeSensorEntityDescription( key="cpu_speed", - name="CPU speed", + translation_key="cpu_speed", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfFrequency.GIGAHERTZ, device_class=SensorDeviceClass.FREQUENCY, @@ -168,7 +164,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( ), SystemBridgeSensorEntityDescription( key="cpu_temperature", - name="CPU temperature", + translation_key="cpu_temperature", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -177,7 +173,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( ), SystemBridgeSensorEntityDescription( key="cpu_voltage", - name="CPU voltage", + translation_key="cpu_voltage", entity_registry_enabled_default=False, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -186,13 +182,13 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( ), SystemBridgeSensorEntityDescription( key="kernel", - name="Kernel", + translation_key="kernel", icon="mdi:devices", value=lambda data: data.system.platform, ), SystemBridgeSensorEntityDescription( key="memory_free", - name="Memory free", + translation_key="memory_free", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, @@ -201,7 +197,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( ), SystemBridgeSensorEntityDescription( key="memory_used_percentage", - name="Memory used %", + translation_key="memory_used", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", @@ -209,7 +205,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( ), SystemBridgeSensorEntityDescription( key="memory_used", - name="Memory used", + translation_key="amount_memory_used", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfInformation.GIGABYTES, @@ -219,13 +215,13 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( ), SystemBridgeSensorEntityDescription( key="os", - name="Operating system", + translation_key="os", icon="mdi:devices", value=lambda data: f"{data.system.platform} {data.system.platform_version}", ), SystemBridgeSensorEntityDescription( key="processes_load", - name="Load", + translation_key="load", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", @@ -233,13 +229,13 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( ), SystemBridgeSensorEntityDescription( key="version", - name="Version", + translation_key="version", icon="mdi:counter", value=lambda data: data.system.version, ), SystemBridgeSensorEntityDescription( key="version_latest", - name="Latest version", + translation_key="version_latest", icon="mdi:counter", value=lambda data: data.system.version_latest, ), @@ -248,7 +244,6 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( BATTERY_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( SystemBridgeSensorEntityDescription( key="battery", - name="Battery", device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -256,7 +251,7 @@ BATTERY_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( ), SystemBridgeSensorEntityDescription( key="battery_time_remaining", - name="Battery time remaining", + translation_key="battery_time_remaining", device_class=SensorDeviceClass.TIMESTAMP, value=battery_time_remaining, ), @@ -324,7 +319,7 @@ async def async_setup_entry( coordinator, SystemBridgeSensorEntityDescription( key="displays_connected", - name="Displays connected", + translation_key="displays_connected", state_class=SensorStateClass.MEASUREMENT, icon="mdi:monitor", value=lambda _, count=display_count: count, @@ -578,9 +573,10 @@ class SystemBridgeSensor(SystemBridgeEntity, SensorEntity): coordinator, api_port, description.key, - description.name, ) self.entity_description = description + if description.name != UNDEFINED: + self._attr_has_entity_name = False @property def native_value(self) -> StateType: diff --git a/homeassistant/components/system_bridge/strings.json b/homeassistant/components/system_bridge/strings.json index c3e1f949152..8a31394875e 100644 --- a/homeassistant/components/system_bridge/strings.json +++ b/homeassistant/components/system_bridge/strings.json @@ -28,6 +28,55 @@ "unknown": "[%key:common::config_flow::error::unknown%]" } }, + "entity": { + "sensor": { + "boot_time": { + "name": "Boot time" + }, + "cpu_power_package": { + "name": "CPU package power" + }, + "cpu_speed": { + "name": "CPU speed" + }, + "cpu_temperature": { + "name": "CPU temperature" + }, + "cpu_voltage": { + "name": "CPU voltage" + }, + "kernel": { + "name": "Kernel" + }, + "memory_free": { + "name": "Memory free" + }, + "memory_used": { + "name": "Memory used" + }, + "amount_memory_used": { + "name": "Amount of memory used" + }, + "os": { + "name": "Operating system" + }, + "load": { + "name": "Load" + }, + "version": { + "name": "Version" + }, + "version_latest": { + "name": "Latest version" + }, + "battery_time_remaining": { + "name": "Battery time remaining" + }, + "displays_connected": { + "name": "Displays connected" + } + } + }, "services": { "open_path": { "name": "Open path", From f1378bba8ea9f5575cfd1db98dfae39d0fc7b3c6 Mon Sep 17 00:00:00 2001 From: Jake Colman Date: Mon, 28 Aug 2023 09:45:01 -0400 Subject: [PATCH 0960/1151] Add indoor sensors to Honeywell integration (#98609) Co-authored-by: Robert Resch Co-authored-by: G Johansson --- homeassistant/components/honeywell/const.py | 3 -- homeassistant/components/honeywell/sensor.py | 31 ++++++++++++++---- tests/components/honeywell/conftest.py | 2 +- tests/components/honeywell/test_climate.py | 4 ++- tests/components/honeywell/test_init.py | 12 +++++-- tests/components/honeywell/test_sensor.py | 34 ++++++++++++++++++-- 6 files changed, 70 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/honeywell/const.py b/homeassistant/components/honeywell/const.py index 94455d569cb..d5153a69f65 100644 --- a/homeassistant/components/honeywell/const.py +++ b/homeassistant/components/honeywell/const.py @@ -9,7 +9,4 @@ DEFAULT_COOL_AWAY_TEMPERATURE = 88 DEFAULT_HEAT_AWAY_TEMPERATURE = 61 CONF_DEV_ID = "thermostat" CONF_LOC_ID = "location" -TEMPERATURE_STATUS_KEY = "outdoor_temperature" -HUMIDITY_STATUS_KEY = "outdoor_humidity" - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/honeywell/sensor.py b/homeassistant/components/honeywell/sensor.py index c1f70bbdd1f..9542648b996 100644 --- a/homeassistant/components/honeywell/sensor.py +++ b/homeassistant/components/honeywell/sensor.py @@ -21,7 +21,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from . import HoneywellData -from .const import DOMAIN, HUMIDITY_STATUS_KEY, TEMPERATURE_STATUS_KEY +from .const import DOMAIN + +OUTDOOR_TEMPERATURE_STATUS_KEY = "outdoor_temperature" +OUTDOOR_HUMIDITY_STATUS_KEY = "outdoor_humidity" +CURRENT_TEMPERATURE_STATUS_KEY = "current_temperature" +CURRENT_HUMIDITY_STATUS_KEY = "current_humidity" def _get_temperature_sensor_unit(device: Device) -> str: @@ -48,21 +53,35 @@ class HoneywellSensorEntityDescription( SENSOR_TYPES: tuple[HoneywellSensorEntityDescription, ...] = ( HoneywellSensorEntityDescription( - key=TEMPERATURE_STATUS_KEY, - translation_key="outdoor_temperature", + key=OUTDOOR_TEMPERATURE_STATUS_KEY, + translation_key=OUTDOOR_TEMPERATURE_STATUS_KEY, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda device: device.outdoor_temperature, unit_fn=_get_temperature_sensor_unit, ), HoneywellSensorEntityDescription( - key=HUMIDITY_STATUS_KEY, - translation_key="outdoor_humidity", + key=OUTDOOR_HUMIDITY_STATUS_KEY, + translation_key=OUTDOOR_HUMIDITY_STATUS_KEY, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda device: device.outdoor_humidity, unit_fn=lambda device: PERCENTAGE, ), + HoneywellSensorEntityDescription( + key=CURRENT_TEMPERATURE_STATUS_KEY, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.current_temperature, + unit_fn=_get_temperature_sensor_unit, + ), + HoneywellSensorEntityDescription( + key=CURRENT_HUMIDITY_STATUS_KEY, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.current_humidity, + unit_fn=lambda device: PERCENTAGE, + ), ) @@ -89,7 +108,7 @@ class HoneywellSensor(SensorEntity): entity_description: HoneywellSensorEntityDescription _attr_has_entity_name = True - def __init__(self, device, description): + def __init__(self, device, description) -> None: """Initialize the outdoor temperature sensor.""" self._device = device self.entity_description = description diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py index bedd4290944..8406d76803a 100644 --- a/tests/components/honeywell/conftest.py +++ b/tests/components/honeywell/conftest.py @@ -121,7 +121,7 @@ def device_with_outdoor_sensor(): "hasFan": False, } mock_device.system_mode = "off" - mock_device.name = "device1" + mock_device.name = "device3" mock_device.current_temperature = CURRENTTEMPERATURE mock_device.mac_address = "macaddress1" mock_device.temperature_unit = "C" diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index b8facc54d43..92caa29b71f 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -54,7 +54,9 @@ async def test_no_thermostat_options( """Test the setup of the climate entities when there are no additional options available.""" device._data = {} await init_integration(hass, config_entry) - assert len(hass.states.async_all()) == 1 + assert hass.states.get("climate.device1") + assert hass.states.get("sensor.device1_temperature") + assert hass.states.get("sensor.device1_humidity") async def test_static_attributes( diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 36c94c83f31..f7629fa958e 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -27,7 +27,9 @@ async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry) - await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - assert hass.states.async_entity_ids_count() == 1 + assert ( + hass.states.async_entity_ids_count() == 3 + ) # 1 climate entity; 2 sensor entities async def test_setup_multiple_thermostats( @@ -39,7 +41,9 @@ async def test_setup_multiple_thermostats( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - assert hass.states.async_entity_ids_count() == 2 + assert ( + hass.states.async_entity_ids_count() == 6 + ) # 2 climate entities; 4 sensor entities async def test_setup_multiple_thermostats_with_same_deviceid( @@ -58,7 +62,9 @@ async def test_setup_multiple_thermostats_with_same_deviceid( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - assert hass.states.async_entity_ids_count() == 1 + assert ( + hass.states.async_entity_ids_count() == 3 + ) # 1 climate entity; 2 sensor entities assert "Platform honeywell does not generate unique IDs" not in caplog.text diff --git a/tests/components/honeywell/test_sensor.py b/tests/components/honeywell/test_sensor.py index c40c90131a8..b286132a40f 100644 --- a/tests/components/honeywell/test_sensor.py +++ b/tests/components/honeywell/test_sensor.py @@ -26,8 +26,38 @@ async def test_outdoor_sensor( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - temperature_state = hass.states.get("sensor.device1_outdoor_temperature") - humidity_state = hass.states.get("sensor.device1_outdoor_humidity") + temperature_state = hass.states.get("sensor.device3_outdoor_temperature") + humidity_state = hass.states.get("sensor.device3_outdoor_humidity") + + assert temperature_state + assert humidity_state + assert temperature_state.state == temp + assert humidity_state.state == "25" + + +@pytest.mark.parametrize(("unit", "temp"), [("C", "5"), ("F", "-15")]) +async def test_indoor_sensor( + hass: HomeAssistant, + config_entry: MockConfigEntry, + location: Location, + device: Device, + unit, + temp, +) -> None: + """Test indoor temperature sensor with no outdoor sensors.""" + device.temperature_unit = unit + device.current_temperature = 5 + device.current_humidity = 25 + location.devices_by_id[device.deviceid] = device + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.device1_outdoor_temperature") is None + assert hass.states.get("sensor.device1_outdoor_humidity") is None + + temperature_state = hass.states.get("sensor.device1_temperature") + humidity_state = hass.states.get("sensor.device1_humidity") assert temperature_state assert humidity_state From 4eb71a534f0de289013bda707fabb18a689a160e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Aug 2023 09:57:16 -0500 Subject: [PATCH 0961/1151] Switch async_track_point_in_time to async_call_later in alarmdecoder (#99213) --- homeassistant/components/alarmdecoder/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 7206b24632b..807d12383bc 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -16,8 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import async_track_point_in_time -from homeassistant.util import dt as dt_util +from homeassistant.helpers.event import async_call_later from .const import ( CONF_DEVICE_BAUD, @@ -66,9 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(controller.open, baud) except NoDeviceError: _LOGGER.debug("Failed to connect. Retrying in 5 seconds") - async_track_point_in_time( - hass, open_connection, dt_util.utcnow() + timedelta(seconds=5) - ) + async_call_later(hass, timedelta(seconds=5), open_connection) return _LOGGER.debug("Established a connection with the alarmdecoder") hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = True From 6c16d89c1d860c2737a89281560429d94ec3f1f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Aug 2023 10:00:52 -0500 Subject: [PATCH 0962/1151] Switch w800rf32 to use async_call_later (#99214) --- homeassistant/components/w800rf32/binary_sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index 6d1ce5c61c0..2bc0c0eea75 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -17,7 +17,6 @@ from homeassistant.helpers import config_validation as cv, event as evt from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import dt as dt_util from . import W800RF32_DEVICE @@ -127,8 +126,8 @@ class W800rf32BinarySensor(BinarySensorEntity): self.update_state(is_on) if self.is_on and self._off_delay is not None and self._delay_listener is None: - self._delay_listener = evt.async_track_point_in_time( - self.hass, self._off_delay_listener, dt_util.utcnow() + self._off_delay + self._delay_listener = evt.async_call_later( + self.hass, self._off_delay, self._off_delay_listener ) def update_state(self, state): From d4e72c49faa6f7ac6d3b2c0d1fd4550041f4afe5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Aug 2023 10:02:51 -0500 Subject: [PATCH 0963/1151] Bump aiohomekit to 3.0.1 (#99210) --- 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 5096544ba05..83852f38d52 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.16"], + "requirements": ["aiohomekit==3.0.1"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 56455ef39c6..5ac9acb9db1 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.16 +aiohomekit==3.0.1 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ad1dd12361..5852f0e3089 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.16 +aiohomekit==3.0.1 # homeassistant.components.emulated_hue # homeassistant.components.http From 9dac6a294860d99c1c8105d406d41546c0a4ba08 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Aug 2023 17:16:34 +0200 Subject: [PATCH 0964/1151] Use loop.time in DataUpdateCoordinator (#98937) --- .../components/tomorrowio/__init__.py | 5 ++- homeassistant/helpers/event.py | 31 +++++++++++++++++++ homeassistant/helpers/update_coordinator.py | 31 ++++++++++--------- tests/common.py | 9 ++---- tests/helpers/test_event.py | 10 +++--- tests/helpers/test_update_coordinator.py | 17 +++++++--- 6 files changed, 71 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index 6d1b84ec5d7..41fa8158624 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -221,7 +221,10 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): await self.async_refresh() self.update_interval = async_set_update_interval(self.hass, self._api) - self._schedule_refresh() + self._next_refresh = None + self._async_unsub_refresh() + if self._listeners: + self._schedule_refresh() async def async_unload_entry(self, entry: ConfigEntry) -> bool | None: """Unload a config entry from coordinator. diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index daad994bbd4..11bfe04473a 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1434,6 +1434,37 @@ def async_track_point_in_utc_time( track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_time) +@callback +@bind_hass +def async_call_at( + hass: HomeAssistant, + action: HassJob[[datetime], Coroutine[Any, Any, None] | None] + | Callable[[datetime], Coroutine[Any, Any, None] | None], + loop_time: float, +) -> CALLBACK_TYPE: + """Add a listener that is called at .""" + + @callback + def run_action(job: HassJob[[datetime], Coroutine[Any, Any, None] | None]) -> None: + """Call the action.""" + hass.async_run_hass_job(job, time_tracker_utcnow()) + + job = ( + action + if isinstance(action, HassJob) + else HassJob(action, f"call_at {loop_time}") + ) + cancel_callback = hass.loop.call_at(loop_time, run_action, job) + + @callback + def unsub_call_later_listener() -> None: + """Cancel the call_later.""" + assert cancel_callback is not None + cancel_callback.cancel() + + return unsub_call_later_listener + + @callback @bind_hass def async_call_later( diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index a050c0da9e4..34651fcaf9d 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -81,6 +81,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): self._shutdown_requested = False self.config_entry = config_entries.current_entry.get() self.always_update = always_update + self._next_refresh: float | None = None # It's None before the first successful update. # Components should call async_config_entry_first_refresh @@ -89,10 +90,11 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): # when it was already checked during setup. self.data: _DataT = None # type: ignore[assignment] - # Pick a random microsecond to stagger the refreshes + # Pick a random microsecond in range 0.05..0.50 to stagger the refreshes # and avoid a thundering herd. - self._microsecond = randint( - event.RANDOM_MICROSECOND_MIN, event.RANDOM_MICROSECOND_MAX + self._microsecond = ( + randint(event.RANDOM_MICROSECOND_MIN, event.RANDOM_MICROSECOND_MAX) + / 10**6 ) self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} @@ -182,6 +184,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): """Unschedule any pending refresh since there is no longer any listeners.""" self._async_unsub_refresh() self._debounced_refresh.async_cancel() + self._next_refresh = None def async_contexts(self) -> Generator[Any, None, None]: """Return all registered contexts.""" @@ -214,20 +217,16 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): # than the debouncer cooldown, this would cause the debounce to never be called self._async_unsub_refresh() - # We _floor_ utcnow to create a schedule on a rounded second, - # minimizing the time between the point and the real activation. - # That way we obtain a constant update frequency, - # as long as the update process takes less than 500ms - # - # We do not align everything to happen at microsecond 0 - # since it increases the risk of a thundering herd - # when multiple coordinators are scheduled to update at the same time. - # - # https://github.com/home-assistant/core/issues/82231 - self._unsub_refresh = event.async_track_point_in_utc_time( + # We use event.async_call_at because DataUpdateCoordinator does + # not need an exact update interval. + now = self.hass.loop.time() + if self._next_refresh is None or self._next_refresh <= now: + self._next_refresh = int(now) + self._microsecond + self._next_refresh += self.update_interval.total_seconds() + self._unsub_refresh = event.async_call_at( self.hass, self._job, - utcnow().replace(microsecond=self._microsecond) + self.update_interval, + self._next_refresh, ) async def _handle_refresh_interval(self, _now: datetime) -> None: @@ -266,6 +265,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): async def async_refresh(self) -> None: """Refresh data and log errors.""" + self._next_refresh = None await self._async_refresh(log_failures=True) async def _async_refresh( # noqa: C901 @@ -405,6 +405,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): """Manually update data, notify listeners and reset refresh interval.""" self._async_unsub_refresh() self._debounced_refresh.async_cancel() + self._next_refresh = None self.data = data self.last_update_success = True diff --git a/tests/common.py b/tests/common.py index 6ccb804ee73..48bb38383c7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -412,12 +412,9 @@ def async_fire_time_changed( else: utc_datetime = dt_util.as_utc(datetime_) - if utc_datetime.microsecond < event.RANDOM_MICROSECOND_MAX: - # Allow up to 500000 microseconds to be added to the time - # to handle update_coordinator's and - # async_track_time_interval's - # staggering to avoid thundering herd. - utc_datetime = utc_datetime.replace(microsecond=event.RANDOM_MICROSECOND_MAX) + # Increase the mocked time by 0.5 s to account for up to 0.5 s delay + # added to events scheduled by update_coordinator and async_track_time_interval + utc_datetime += timedelta(microseconds=event.RANDOM_MICROSECOND_MAX) _async_fire_time_changed(hass, utc_datetime, fire_all) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 572a0d22e92..dc06b9d94c8 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -4174,27 +4174,27 @@ async def test_periodic_task_entering_dst_2( ) freezer.move_to(f"{today} 01:59:59.999999+01:00") - async_fire_time_changed(hass) + async_fire_time_changed_exact(hass) await hass.async_block_till_done() assert len(specific_runs) == 0 freezer.move_to(f"{today} 03:00:00.999999+02:00") - async_fire_time_changed(hass) + async_fire_time_changed_exact(hass) await hass.async_block_till_done() assert len(specific_runs) == 1 freezer.move_to(f"{today} 03:00:01.999999+02:00") - async_fire_time_changed(hass) + async_fire_time_changed_exact(hass) await hass.async_block_till_done() assert len(specific_runs) == 2 freezer.move_to(f"{tomorrow} 01:59:59.999999+02:00") - async_fire_time_changed(hass) + async_fire_time_changed_exact(hass) await hass.async_block_till_done() assert len(specific_runs) == 3 freezer.move_to(f"{tomorrow} 02:00:00.999999+02:00") - async_fire_time_changed(hass) + async_fire_time_changed_exact(hass) await hass.async_block_till_done() assert len(specific_runs) == 4 diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 4258a508c34..182ed6c3cb4 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, Mock, patch import urllib.error import aiohttp +from freezegun.api import FrozenDateTimeFactory import pytest import requests @@ -329,11 +330,14 @@ async def test_refresh_no_update_method( async def test_update_interval( - hass: HomeAssistant, crd: update_coordinator.DataUpdateCoordinator[int] + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + crd: update_coordinator.DataUpdateCoordinator[int], ) -> None: """Test update interval works.""" # Test we don't update without subscriber - async_fire_time_changed(hass, utcnow() + crd.update_interval) + freezer.tick(crd.update_interval) + async_fire_time_changed(hass) await hass.async_block_till_done() assert crd.data is None @@ -342,18 +346,21 @@ async def test_update_interval( unsub = crd.async_add_listener(update_callback) # Test twice we update with subscriber - async_fire_time_changed(hass, utcnow() + crd.update_interval) + freezer.tick(crd.update_interval) + async_fire_time_changed(hass) await hass.async_block_till_done() assert crd.data == 1 - async_fire_time_changed(hass, utcnow() + crd.update_interval) + freezer.tick(crd.update_interval) + async_fire_time_changed(hass) await hass.async_block_till_done() assert crd.data == 2 # Test removing listener unsub() - async_fire_time_changed(hass, utcnow() + crd.update_interval) + freezer.tick(crd.update_interval) + async_fire_time_changed(hass) await hass.async_block_till_done() # Test we stop updating after we lose last subscriber From ccb91e367626c4a40ec5b7926248b95869da429b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Aug 2023 10:19:44 -0500 Subject: [PATCH 0965/1151] Switch axis to use async_call_later (#99215) --- homeassistant/components/axis/binary_sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 067014cc81f..4cc81947e27 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -13,8 +13,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util.dt import utcnow +from homeassistant.helpers.event import async_call_later from .const import DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice @@ -95,10 +94,10 @@ class AxisBinarySensor(AxisEventEntity, BinarySensorEntity): self.async_write_ha_state() return - self.cancel_scheduled_update = async_track_point_in_utc_time( + self.cancel_scheduled_update = async_call_later( self.hass, + timedelta(seconds=self.device.option_trigger_time), scheduled_update, - utcnow() + timedelta(seconds=self.device.option_trigger_time), ) @callback From 1bf7b4b2c7d47593407863ddcc75cb0b238491d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Aug 2023 10:20:42 -0500 Subject: [PATCH 0966/1151] Switch lifx to use async_call_later (#99217) --- homeassistant/components/lifx/light.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 0e56155832f..e04e8afb3df 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -8,7 +8,6 @@ from typing import Any import aiolifx_effects as aiolifx_effects_module import voluptuous as vol -from homeassistant import util from homeassistant.components.light import ( ATTR_EFFECT, ATTR_TRANSITION, @@ -24,7 +23,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.event import async_call_later from .const import ( _LOGGER, @@ -187,10 +186,10 @@ class LIFXLight(LIFXEntity, LightEntity): """Refresh the state.""" await self.coordinator.async_refresh() - self.postponed_update = async_track_point_in_utc_time( + self.postponed_update = async_call_later( self.hass, + timedelta(milliseconds=when), _async_refresh, - util.dt.utcnow() + timedelta(milliseconds=when), ) async def async_turn_on(self, **kwargs: Any) -> None: From 739eeeccb0ae20b6717c6623bfb95fc63cdf1973 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Aug 2023 10:21:05 -0500 Subject: [PATCH 0967/1151] Switch hassio to use async_call_later (#99216) --- homeassistant/components/hassio/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 0abc484b798..3451195f3cd 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -42,7 +42,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -535,10 +535,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: except HassioAPIError as err: _LOGGER.warning("Can't read Supervisor data: %s", err) - async_track_point_in_utc_time( + async_call_later( hass, + HASSIO_UPDATE_INTERVAL, HassJob(update_info_data, cancel_on_shutdown=True), - utcnow() + HASSIO_UPDATE_INTERVAL, ) # Fetch data @@ -610,10 +610,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: """Set up hardaware integration for the detected board type.""" if (os_info := get_os_info(hass)) is None: # os info not yet fetched from supervisor, retry later - async_track_point_in_utc_time( + async_call_later( hass, + HASSIO_UPDATE_INTERVAL, async_setup_hardware_integration_job, - utcnow() + HASSIO_UPDATE_INTERVAL, ) return if (board := os_info.get("board")) is None: From ef7a246f09d293e4e25d0bfe830e3c857ce15b7f Mon Sep 17 00:00:00 2001 From: Ian Foster Date: Mon, 28 Aug 2023 17:26:40 +0200 Subject: [PATCH 0968/1151] Fix ruckus_unleashed for python 3.11 (#94835) Co-authored-by: Tony <29752086+ms264556@users.noreply.github.com> --- CODEOWNERS | 4 +- .../components/ruckus_unleashed/__init__.py | 54 +++-- .../ruckus_unleashed/config_flow.py | 87 +++++--- .../components/ruckus_unleashed/const.py | 40 ++-- .../ruckus_unleashed/coordinator.py | 27 ++- .../ruckus_unleashed/device_tracker.py | 44 ++-- .../components/ruckus_unleashed/manifest.json | 7 +- requirements_all.txt | 7 +- requirements_test_all.txt | 7 +- tests/components/ruckus_unleashed/__init__.py | 203 +++++++++++++----- .../ruckus_unleashed/test_config_flow.py | 106 ++++----- .../ruckus_unleashed/test_device_tracker.py | 66 +++--- .../components/ruckus_unleashed/test_init.py | 77 +++---- 13 files changed, 430 insertions(+), 299 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9e8d297d37f..16fb44fd789 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1057,8 +1057,8 @@ build.json @home-assistant/supervisor /tests/components/rss_feed_template/ @home-assistant/core /homeassistant/components/rtsp_to_webrtc/ @allenporter /tests/components/rtsp_to_webrtc/ @allenporter -/homeassistant/components/ruckus_unleashed/ @gabe565 -/tests/components/ruckus_unleashed/ @gabe565 +/homeassistant/components/ruckus_unleashed/ @gabe565 @lanrat +/tests/components/ruckus_unleashed/ @gabe565 @lanrat /homeassistant/components/ruuvi_gateway/ @akx /tests/components/ruuvi_gateway/ @akx /homeassistant/components/ruuvitag_ble/ @akx diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index f276c0f8fc2..e71555598cb 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -1,21 +1,22 @@ """The Ruckus Unleashed integration.""" +import logging -from pyruckus import Ruckus +from aioruckus import AjaxSession +from aioruckus.exceptions import AuthenticationError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from .const import ( - API_AP, - API_DEVICE_NAME, - API_ID, - API_MAC, - API_MODEL, - API_SYSTEM_OVERVIEW, - API_VERSION, + API_AP_DEVNAME, + API_AP_FIRMWAREVERSION, + API_AP_MAC, + API_AP_MODEL, + API_SYS_SYSINFO, + API_SYS_SYSINFO_VERSION, COORDINATOR, DOMAIN, MANUFACTURER, @@ -24,35 +25,45 @@ from .const import ( ) from .coordinator import RuckusUnleashedDataUpdateCoordinator +_LOGGER = logging.getLogger(__package__) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ruckus Unleashed from a config entry.""" + try: - ruckus = await Ruckus.create( + ruckus = AjaxSession.async_create( entry.data[CONF_HOST], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], ) - except ConnectionError as error: - raise ConfigEntryNotReady from error + await ruckus.login() + except (ConnectionRefusedError, ConnectionError) as conerr: + raise ConfigEntryNotReady from conerr + except AuthenticationError as autherr: + raise ConfigEntryAuthFailed from autherr coordinator = RuckusUnleashedDataUpdateCoordinator(hass, ruckus=ruckus) await coordinator.async_config_entry_first_refresh() - system_info = await ruckus.system_info() + system_info = await ruckus.api.get_system_info() registry = dr.async_get(hass) - ap_info = await ruckus.ap_info() - for device in ap_info[API_AP][API_ID].values(): + aps = await ruckus.api.get_aps() + for access_point in aps: + _LOGGER.debug("AP [%s] %s", access_point[API_AP_MAC], entry.entry_id) registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, device[API_MAC])}, - identifiers={(dr.CONNECTION_NETWORK_MAC, device[API_MAC])}, + connections={(dr.CONNECTION_NETWORK_MAC, access_point[API_AP_MAC])}, + identifiers={(DOMAIN, access_point[API_AP_MAC])}, manufacturer=MANUFACTURER, - name=device[API_DEVICE_NAME], - model=device[API_MODEL], - sw_version=system_info[API_SYSTEM_OVERVIEW][API_VERSION], + name=access_point[API_AP_DEVNAME], + model=access_point[API_AP_MODEL], + sw_version=access_point.get( + API_AP_FIRMWAREVERSION, + system_info[API_SYS_SYSINFO][API_SYS_SYSINFO_VERSION], + ), ) hass.data.setdefault(DOMAIN, {}) @@ -68,11 +79,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: for listener in hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENERS]: listener() - + await hass.data[DOMAIN][entry.entry_id][COORDINATOR].ruckus.close() hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py index 4adf245c3de..155eb68f593 100644 --- a/homeassistant/components/ruckus_unleashed/config_flow.py +++ b/homeassistant/components/ruckus_unleashed/config_flow.py @@ -1,22 +1,29 @@ """Config flow for Ruckus Unleashed integration.""" -import logging +from collections.abc import Mapping +from typing import Any -from pyruckus import Ruckus -from pyruckus.exceptions import AuthenticationError +from aioruckus import AjaxSession, SystemStat +from aioruckus.exceptions import AuthenticationError import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult -from .const import API_SERIAL, API_SYSTEM_OVERVIEW, DOMAIN - -_LOGGER = logging.getLogger(__package__) +from .const import ( + API_MESH_NAME, + API_SYS_SYSINFO, + API_SYS_SYSINFO_SERIAL, + DOMAIN, + KEY_SYS_SERIAL, + KEY_SYS_TITLE, +) DATA_SCHEMA = vol.Schema( { - vol.Required("host"): str, - vol.Required("username"): str, - vol.Required("password"): str, + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, } ) @@ -28,26 +35,22 @@ async def validate_input(hass: core.HomeAssistant, data): """ try: - ruckus = await Ruckus.create( + async with AjaxSession.async_create( data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD] - ) - except AuthenticationError as error: - raise InvalidAuth from error - except ConnectionError as error: - raise CannotConnect from error - - mesh_name = await ruckus.mesh_name() - - system_info = await ruckus.system_info() - try: - host_serial = system_info[API_SYSTEM_OVERVIEW][API_SERIAL] - except KeyError as error: - raise CannotConnect from error - - return { - "title": mesh_name, - "serial": host_serial, - } + ) as ruckus: + system_info = await ruckus.api.get_system_info( + SystemStat.SYSINFO, + ) + mesh_name = (await ruckus.api.get_mesh_info())[API_MESH_NAME] + zd_serial = system_info[API_SYS_SYSINFO][API_SYS_SYSINFO_SERIAL] + return { + KEY_SYS_TITLE: mesh_name, + KEY_SYS_SERIAL: zd_serial, + } + except AuthenticationError as autherr: + raise InvalidAuth from autherr + except (ConnectionRefusedError, ConnectionError, KeyError) as connerr: + raise CannotConnect from connerr class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -55,7 +58,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 the initial step.""" errors = {} if user_input is not None: @@ -65,18 +70,32 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" else: - await self.async_set_unique_id(info["serial"]) + await self.async_set_unique_id(info[KEY_SYS_SERIAL]) self._abort_if_unique_id_configured() - return self.async_create_entry(title=info["title"], data=user_input) + return self.async_create_entry( + title=info[KEY_SYS_TITLE], data=user_input + ) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=DATA_SCHEMA, + ) + return await self.async_step_user() + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/ruckus_unleashed/const.py b/homeassistant/components/ruckus_unleashed/const.py index e6087be3fd2..089981348b6 100644 --- a/homeassistant/components/ruckus_unleashed/const.py +++ b/homeassistant/components/ruckus_unleashed/const.py @@ -3,23 +3,35 @@ from homeassistant.const import Platform DOMAIN = "ruckus_unleashed" PLATFORMS = [Platform.DEVICE_TRACKER] -SCAN_INTERVAL = 180 +SCAN_INTERVAL = 30 MANUFACTURER = "Ruckus" COORDINATOR = "coordinator" UNDO_UPDATE_LISTENERS = "undo_update_listeners" -API_CLIENTS = "clients" -API_NAME = "host_name" -API_MAC = "mac_address" -API_IP = "user_ip" -API_SYSTEM_OVERVIEW = "system_overview" -API_SERIAL = "serial_number" -API_DEVICE_NAME = "device_name" -API_MODEL = "model" -API_VERSION = "version" -API_AP = "ap" -API_ID = "id" -API_CURRENT_ACTIVE_CLIENTS = "current_active_clients" -API_ACCESS_POINT = "access_point" +KEY_SYS_CLIENTS = "clients" +KEY_SYS_TITLE = "title" +KEY_SYS_SERIAL = "serial" + +API_MESH_NAME = "name" +API_MESH_PSK = "psk" + +API_CLIENT_HOSTNAME = "hostname" +API_CLIENT_MAC = "mac" +API_CLIENT_IP = "ip" +API_CLIENT_AP_MAC = "ap" + +API_AP_MAC = "mac" +API_AP_SERIALNUMBER = "serial" +API_AP_DEVNAME = "devname" +API_AP_MODEL = "model" +API_AP_FIRMWAREVERSION = "version" + +API_SYS_SYSINFO = "sysinfo" +API_SYS_SYSINFO_VERSION = "version" +API_SYS_SYSINFO_SERIAL = "serial" +API_SYS_IDENTITY = "identity" +API_SYS_IDENTITY_NAME = "name" +API_SYS_UNLEASHEDNETWORK = "unleashed-network" +API_SYS_UNLEASHEDNETWORK_TOKEN = "unleashed-network-token" diff --git a/homeassistant/components/ruckus_unleashed/coordinator.py b/homeassistant/components/ruckus_unleashed/coordinator.py index e84b79ef843..29df676cb76 100644 --- a/homeassistant/components/ruckus_unleashed/coordinator.py +++ b/homeassistant/components/ruckus_unleashed/coordinator.py @@ -2,19 +2,13 @@ from datetime import timedelta import logging -from pyruckus import Ruckus -from pyruckus.exceptions import AuthenticationError +from aioruckus import AjaxSession +from aioruckus.exceptions import AuthenticationError from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - API_CLIENTS, - API_CURRENT_ACTIVE_CLIENTS, - API_MAC, - DOMAIN, - SCAN_INTERVAL, -) +from .const import API_CLIENT_MAC, DOMAIN, KEY_SYS_CLIENTS, SCAN_INTERVAL _LOGGER = logging.getLogger(__package__) @@ -22,7 +16,7 @@ _LOGGER = logging.getLogger(__package__) class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator): """Coordinator to manage data from Ruckus Unleashed client.""" - def __init__(self, hass: HomeAssistant, *, ruckus: Ruckus) -> None: + def __init__(self, hass: HomeAssistant, *, ruckus: AjaxSession) -> None: """Initialize global Ruckus Unleashed data updater.""" self.ruckus = ruckus @@ -37,12 +31,15 @@ class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator): async def _fetch_clients(self) -> dict: """Fetch clients from the API and format them.""" - clients = await self.ruckus.current_active_clients() - return {e[API_MAC]: e for e in clients[API_CURRENT_ACTIVE_CLIENTS][API_CLIENTS]} + clients = await self.ruckus.api.get_active_clients() + _LOGGER.debug("fetched %d active clients", len(clients)) + return {client[API_CLIENT_MAC]: client for client in clients} async def _async_update_data(self) -> dict: """Fetch Ruckus Unleashed data.""" try: - return {API_CLIENTS: await self._fetch_clients()} - except (AuthenticationError, ConnectionError) as error: - raise UpdateFailed(error) from error + return {KEY_SYS_CLIENTS: await self._fetch_clients()} + except AuthenticationError as autherror: + raise UpdateFailed(autherror) from autherror + except (ConnectionRefusedError, ConnectionError) as conerr: + raise UpdateFailed(conerr) from conerr diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index dd6d7fd6764..0e0d2f103c4 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -1,6 +1,8 @@ """Support for Ruckus Unleashed devices.""" from __future__ import annotations +import logging + from homeassistant.components.device_tracker import ScannerEntity, SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -9,14 +11,16 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( - API_CLIENTS, - API_NAME, + API_CLIENT_HOSTNAME, + API_CLIENT_IP, COORDINATOR, DOMAIN, - MANUFACTURER, + KEY_SYS_CLIENTS, UNDO_UPDATE_LISTENERS, ) +_LOGGER = logging.getLogger(__package__) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -46,12 +50,15 @@ def add_new_entities(coordinator, async_add_entities, tracked): """Add new tracker entities from the router.""" new_tracked = [] - for mac in coordinator.data[API_CLIENTS]: + for mac in coordinator.data[KEY_SYS_CLIENTS]: if mac in tracked: continue - device = coordinator.data[API_CLIENTS][mac] - new_tracked.append(RuckusUnleashedDevice(coordinator, mac, device[API_NAME])) + device = coordinator.data[KEY_SYS_CLIENTS][mac] + _LOGGER.debug("adding new device: [%s] %s", mac, device[API_CLIENT_HOSTNAME]) + new_tracked.append( + RuckusUnleashedDevice(coordinator, mac, device[API_CLIENT_HOSTNAME]) + ) tracked.add(mac) async_add_entities(new_tracked) @@ -66,7 +73,7 @@ def restore_entities(registry, coordinator, entry, async_add_entities, tracked): if ( entity.config_entry_id == entry.entry_id and entity.platform == DOMAIN - and entity.unique_id not in coordinator.data[API_CLIENTS] + and entity.unique_id not in coordinator.data[KEY_SYS_CLIENTS] ): missing.append( RuckusUnleashedDevice( @@ -75,6 +82,7 @@ def restore_entities(registry, coordinator, entry, async_add_entities, tracked): ) tracked.add(entity.unique_id) + _LOGGER.debug("added %d missing devices", len(missing)) async_add_entities(missing) @@ -95,17 +103,25 @@ class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity): @property def name(self) -> str: """Return the name.""" - if self.is_connected: - return ( - self.coordinator.data[API_CLIENTS][self._mac][API_NAME] - or f"{MANUFACTURER} {self._mac}" - ) - return self._name + return ( + self._name + if not self.is_connected + else self.coordinator.data[KEY_SYS_CLIENTS][self._mac][API_CLIENT_HOSTNAME] + ) + + @property + def ip_address(self) -> str: + """Return the ip address.""" + return ( + self.coordinator.data[KEY_SYS_CLIENTS][self._mac][API_CLIENT_IP] + if self.is_connected + else None + ) @property def is_connected(self) -> bool: """Return true if the device is connected to the network.""" - return self._mac in self.coordinator.data[API_CLIENTS] + return self._mac in self.coordinator.data[KEY_SYS_CLIENTS] @property def source_type(self) -> SourceType: diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index 124268174b7..8ff69fb1aa9 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -1,10 +1,11 @@ { "domain": "ruckus_unleashed", "name": "Ruckus Unleashed", - "codeowners": ["@gabe565"], + "codeowners": ["@gabe565", "@lanrat"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", + "integration_type": "hub", "iot_class": "local_polling", - "loggers": ["pexpect", "pyruckus"], - "requirements": ["pyruckus==0.16"] + "loggers": ["aioruckus", "xmltodict"], + "requirements": ["aioruckus==0.31", "xmltodict==0.13.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5ac9acb9db1..89df11dc43f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -332,6 +332,9 @@ aiorecollect==1.0.8 # homeassistant.components.ridwell aioridwell==2023.07.0 +# homeassistant.components.ruckus_unleashed +aioruckus==0.31 + # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 @@ -1975,9 +1978,6 @@ pyrituals==0.0.6 # homeassistant.components.thread pyroute2==0.7.5 -# homeassistant.components.ruckus_unleashed -pyruckus==0.16 - # homeassistant.components.rympro pyrympro==0.0.7 @@ -2719,6 +2719,7 @@ xknxproject==3.2.0 # homeassistant.components.bluesound # homeassistant.components.fritz # homeassistant.components.rest +# homeassistant.components.ruckus_unleashed # homeassistant.components.startca # homeassistant.components.ted5000 # homeassistant.components.zestimate diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5852f0e3089..317254f2142 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -307,6 +307,9 @@ aiorecollect==1.0.8 # homeassistant.components.ridwell aioridwell==2023.07.0 +# homeassistant.components.ruckus_unleashed +aioruckus==0.31 + # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 @@ -1467,9 +1470,6 @@ pyrituals==0.0.6 # homeassistant.components.thread pyroute2==0.7.5 -# homeassistant.components.ruckus_unleashed -pyruckus==0.16 - # homeassistant.components.rympro pyrympro==0.0.7 @@ -2001,6 +2001,7 @@ xknxproject==3.2.0 # homeassistant.components.bluesound # homeassistant.components.fritz # homeassistant.components.rest +# homeassistant.components.ruckus_unleashed # homeassistant.components.startca # homeassistant.components.ted5000 # homeassistant.components.zestimate diff --git a/tests/components/ruckus_unleashed/__init__.py b/tests/components/ruckus_unleashed/__init__.py index 1e50ce7dec7..06ad2352988 100644 --- a/tests/components/ruckus_unleashed/__init__.py +++ b/tests/components/ruckus_unleashed/__init__.py @@ -1,45 +1,61 @@ """Tests for the Ruckus Unleashed integration.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch + +from aioruckus import AjaxSession, RuckusAjaxApi -from homeassistant.components.ruckus_unleashed import DOMAIN from homeassistant.components.ruckus_unleashed.const import ( - API_ACCESS_POINT, - API_AP, - API_DEVICE_NAME, - API_ID, - API_IP, - API_MAC, - API_MODEL, - API_NAME, - API_SERIAL, - API_SYSTEM_OVERVIEW, - API_VERSION, + API_AP_DEVNAME, + API_AP_MAC, + API_AP_MODEL, + API_AP_SERIALNUMBER, + API_CLIENT_AP_MAC, + API_CLIENT_HOSTNAME, + API_CLIENT_IP, + API_CLIENT_MAC, + API_MESH_NAME, + API_MESH_PSK, + API_SYS_IDENTITY, + API_SYS_IDENTITY_NAME, + API_SYS_SYSINFO, + API_SYS_SYSINFO_SERIAL, + API_SYS_SYSINFO_VERSION, + API_SYS_UNLEASHEDNETWORK, + API_SYS_UNLEASHEDNETWORK_TOKEN, + DOMAIN, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry -DEFAULT_TITLE = "Ruckus Mesh" -DEFAULT_UNIQUE_ID = "123456789012" DEFAULT_SYSTEM_INFO = { - API_SYSTEM_OVERVIEW: { - API_SERIAL: DEFAULT_UNIQUE_ID, - API_VERSION: "v1.0.0", - } + API_SYS_IDENTITY: {API_SYS_IDENTITY_NAME: "RuckusUnleashed"}, + API_SYS_SYSINFO: { + API_SYS_SYSINFO_SERIAL: "123456789012", + API_SYS_SYSINFO_VERSION: "200.7.10.202 build 141", + }, + API_SYS_UNLEASHEDNETWORK: { + API_SYS_UNLEASHEDNETWORK_TOKEN: "un1234567890121680060227001" + }, } -DEFAULT_AP_INFO = { - API_AP: { - API_ID: { - "1": { - API_MAC: "00:11:22:33:44:55", - API_DEVICE_NAME: "Test Device", - API_MODEL: "r510", - } - } - } + +DEFAULT_MESH_INFO = { + API_MESH_NAME: "Ruckus Mesh", + API_MESH_PSK: "", } +DEFAULT_AP_INFO = [ + { + API_AP_MAC: "00:11:22:33:44:55", + API_AP_DEVNAME: "Test Device", + API_AP_MODEL: "r510", + API_AP_SERIALNUMBER: DEFAULT_SYSTEM_INFO[API_SYS_SYSINFO][ + API_SYS_SYSINFO_SERIAL + ], + } +] + CONFIG = { CONF_HOST: "1.1.1.1", CONF_USERNAME: "test-username", @@ -48,25 +64,28 @@ CONFIG = { TEST_CLIENT_ENTITY_ID = "device_tracker.ruckus_test_device" TEST_CLIENT = { - API_IP: "1.1.1.2", - API_MAC: "AA:BB:CC:DD:EE:FF", - API_NAME: "Ruckus Test Device", - API_ACCESS_POINT: "00:11:22:33:44:55", + API_CLIENT_IP: "1.1.1.2", + API_CLIENT_MAC: "AA:BB:CC:DD:EE:FF", + API_CLIENT_HOSTNAME: "Ruckus Test Device", + API_CLIENT_AP_MAC: DEFAULT_AP_INFO[0][API_AP_MAC], } +DEFAULT_TITLE = DEFAULT_MESH_INFO[API_MESH_NAME] +DEFAULT_UNIQUEID = DEFAULT_SYSTEM_INFO[API_SYS_SYSINFO][API_SYS_SYSINFO_SERIAL] + def mock_config_entry() -> MockConfigEntry: """Return a Ruckus Unleashed mock config entry.""" return MockConfigEntry( domain=DOMAIN, title=DEFAULT_TITLE, - unique_id=DEFAULT_UNIQUE_ID, + unique_id=DEFAULT_UNIQUEID, data=CONFIG, options=None, ) -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the Ruckus Unleashed integration in Home Assistant.""" entry = mock_config_entry() entry.add_to_hass(hass) @@ -76,27 +95,103 @@ async def init_integration(hass) -> MockConfigEntry: dr.async_get(hass).async_get_or_create( name="Device from other integration", config_entry_id=other_config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, TEST_CLIENT[API_MAC])}, + connections={(dr.CONNECTION_NETWORK_MAC, TEST_CLIENT[API_CLIENT_MAC])}, ) - with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.connect", - return_value=None, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.mesh_name", - return_value=DEFAULT_TITLE, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.system_info", - return_value=DEFAULT_SYSTEM_INFO, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.ap_info", - return_value=DEFAULT_AP_INFO, - ), patch( - "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients", - return_value={ - TEST_CLIENT[API_MAC]: TEST_CLIENT, - }, - ): + + with RuckusAjaxApiPatchContext(): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() return entry + + +class RuckusAjaxApiPatchContext: + """Context Manager which mocks the Ruckus AjaxSession and RuckusAjaxApi.""" + + def __init__( + self, + login_mock: AsyncMock = None, + system_info: dict | None = None, + mesh_info: dict | None = None, + active_clients: list[dict] | AsyncMock | None = None, + ) -> None: + """Initialize Ruckus Mock Context Manager.""" + self.login_mock = login_mock + self.system_info = system_info + self.mesh_info = mesh_info + self.active_clients = active_clients + self.patchers = [] + + def __enter__(self): + """Patch RuckusAjaxApi and AjaxSession methods.""" + self.patchers.append( + patch.object(RuckusAjaxApi, "_get_conf", new=AsyncMock(return_value={})) + ) + self.patchers.append( + patch.object( + RuckusAjaxApi, "get_aps", new=AsyncMock(return_value=DEFAULT_AP_INFO) + ) + ) + self.patchers.append( + patch.object( + RuckusAjaxApi, + "get_system_info", + new=AsyncMock( + return_value=DEFAULT_SYSTEM_INFO + if self.system_info is None + else self.system_info + ), + ) + ) + self.patchers.append( + patch.object( + RuckusAjaxApi, + "get_mesh_info", + new=AsyncMock( + return_value=DEFAULT_MESH_INFO + if self.mesh_info is None + else self.mesh_info + ), + ) + ) + self.patchers.append( + patch.object( + RuckusAjaxApi, + "get_active_clients", + new=self.active_clients + if isinstance(self.active_clients, AsyncMock) + else AsyncMock( + return_value=[TEST_CLIENT] + if self.active_clients is None + else self.active_clients + ), + ) + ) + self.patchers.append( + patch.object( + AjaxSession, + "login", + new=self.login_mock or AsyncMock(return_value=self), + ) + ) + self.patchers.append( + patch.object(AjaxSession, "close", new=AsyncMock(return_value=None)) + ) + + def _patched_async_create( + host: str, username: str, password: str + ) -> "AjaxSession": + return AjaxSession(None, host, username, password) + + self.patchers.append( + patch.object(AjaxSession, "async_create", new=_patched_async_create) + ) + + for patcher in self.patchers: + patcher.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Remove RuckusAjaxApi and AjaxSession patches.""" + for patcher in self.patchers: + patcher.stop() diff --git a/tests/components/ruckus_unleashed/test_config_flow.py b/tests/components/ruckus_unleashed/test_config_flow.py index a78c839cf6b..c55d531b0cb 100644 --- a/tests/components/ruckus_unleashed/test_config_flow.py +++ b/tests/components/ruckus_unleashed/test_config_flow.py @@ -1,15 +1,21 @@ """Test the Ruckus Unleashed config flow.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch -from pyruckus.exceptions import AuthenticationError +from aioruckus.const import ( + ERROR_CONNECT_TEMPORARY, + ERROR_CONNECT_TIMEOUT, + ERROR_LOGIN_INCORRECT, +) +from aioruckus.exceptions import AuthenticationError -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.ruckus_unleashed.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.util import utcnow -from . import CONFIG, DEFAULT_SYSTEM_INFO, DEFAULT_TITLE +from . import CONFIG, DEFAULT_TITLE, RuckusAjaxApiPatchContext, mock_config_entry from tests.common import async_fire_time_changed @@ -22,16 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.connect", - return_value=None, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.mesh_name", - return_value=DEFAULT_TITLE, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.system_info", - return_value=DEFAULT_SYSTEM_INFO, - ), patch( + with RuckusAjaxApiPatchContext(), patch( "homeassistant.components.ruckus_unleashed.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -41,10 +38,10 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" - assert result2["title"] == DEFAULT_TITLE - assert result2["data"] == CONFIG - assert len(mock_setup_entry.mock_calls) == 1 + assert result2["type"] == "create_entry" + assert result2["title"] == DEFAULT_TITLE + assert result2["data"] == CONFIG + assert len(mock_setup_entry.mock_calls) == 1 async def test_form_invalid_auth(hass: HomeAssistant) -> None: @@ -53,9 +50,8 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.connect", - side_effect=AuthenticationError, + with RuckusAjaxApiPatchContext( + login_mock=AsyncMock(side_effect=AuthenticationError(ERROR_LOGIN_INCORRECT)) ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -66,15 +62,44 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "invalid_auth"} +async def test_form_user_reauth(hass: HomeAssistant) -> None: + """Test reauth.""" + entry = mock_config_entry() + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH} + ) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert "flow_id" in flows[0] + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + flows[0]["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "new_name", + CONF_PASSWORD: "new_pass", + }, + ) + + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "user" + + async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.connect", - side_effect=ConnectionError, + with RuckusAjaxApiPatchContext( + login_mock=AsyncMock(side_effect=ConnectionError(ERROR_CONNECT_TIMEOUT)) ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -85,15 +110,16 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_unknown_error(hass: HomeAssistant) -> None: +async def test_form_unexpected_response(hass: HomeAssistant) -> None: """Test we handle unknown error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.connect", - side_effect=Exception, + with RuckusAjaxApiPatchContext( + login_mock=AsyncMock( + side_effect=ConnectionRefusedError(ERROR_CONNECT_TEMPORARY) + ) ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -101,7 +127,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "unknown"} + assert result2["errors"] == {"base": "cannot_connect"} async def test_form_cannot_connect_unknown_serial(hass: HomeAssistant) -> None: @@ -112,16 +138,7 @@ async def test_form_cannot_connect_unknown_serial(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.connect", - return_value=None, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.mesh_name", - return_value=DEFAULT_TITLE, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.system_info", - return_value={}, - ): + with RuckusAjaxApiPatchContext(system_info={}): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG, @@ -137,16 +154,7 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.connect", - return_value=None, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.mesh_name", - return_value=DEFAULT_TITLE, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.system_info", - return_value=DEFAULT_SYSTEM_INFO, - ): + with RuckusAjaxApiPatchContext(): await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG, diff --git a/tests/components/ruckus_unleashed/test_device_tracker.py b/tests/components/ruckus_unleashed/test_device_tracker.py index dc57d46715c..403ea7d0ca7 100644 --- a/tests/components/ruckus_unleashed/test_device_tracker.py +++ b/tests/components/ruckus_unleashed/test_device_tracker.py @@ -1,8 +1,11 @@ """The sensor tests for the Ruckus Unleashed platform.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock -from homeassistant.components.ruckus_unleashed import API_MAC, DOMAIN +from aioruckus.const import ERROR_CONNECT_EOF, ERROR_LOGIN_INCORRECT +from aioruckus.exceptions import AuthenticationError + +from homeassistant.components.ruckus_unleashed import DOMAIN from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -10,12 +13,9 @@ from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import utcnow from . import ( - DEFAULT_AP_INFO, - DEFAULT_SYSTEM_INFO, - DEFAULT_TITLE, - DEFAULT_UNIQUE_ID, - TEST_CLIENT, + DEFAULT_UNIQUEID, TEST_CLIENT_ENTITY_ID, + RuckusAjaxApiPatchContext, init_integration, mock_config_entry, ) @@ -28,12 +28,7 @@ async def test_client_connected(hass: HomeAssistant) -> None: await init_integration(hass) future = utcnow() + timedelta(minutes=60) - with patch( - "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients", - return_value={ - TEST_CLIENT[API_MAC]: TEST_CLIENT, - }, - ): + with RuckusAjaxApiPatchContext(): async_fire_time_changed(hass, future) await hass.async_block_till_done() await async_update_entity(hass, TEST_CLIENT_ENTITY_ID) @@ -47,10 +42,7 @@ async def test_client_disconnected(hass: HomeAssistant) -> None: await init_integration(hass) future = utcnow() + timedelta(minutes=60) - with patch( - "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients", - return_value={}, - ): + with RuckusAjaxApiPatchContext(active_clients={}): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -64,9 +56,24 @@ async def test_clients_update_failed(hass: HomeAssistant) -> None: await init_integration(hass) future = utcnow() + timedelta(minutes=60) - with patch( - "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients", - side_effect=ConnectionError, + with RuckusAjaxApiPatchContext( + active_clients=AsyncMock(side_effect=ConnectionError(ERROR_CONNECT_EOF)) + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + await async_update_entity(hass, TEST_CLIENT_ENTITY_ID) + test_client = hass.states.get(TEST_CLIENT_ENTITY_ID) + assert test_client.state == STATE_UNAVAILABLE + + +async def test_clients_update_auth_failed(hass: HomeAssistant) -> None: + """Test failed update with bad auth.""" + await init_integration(hass) + + future = utcnow() + timedelta(minutes=60) + with RuckusAjaxApiPatchContext( + active_clients=AsyncMock(side_effect=AuthenticationError(ERROR_LOGIN_INCORRECT)) ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -85,27 +92,12 @@ async def test_restoring_clients(hass: HomeAssistant) -> None: registry.async_get_or_create( "device_tracker", DOMAIN, - DEFAULT_UNIQUE_ID, + DEFAULT_UNIQUEID, suggested_object_id="ruckus_test_device", config_entry=entry, ) - with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.connect", - return_value=None, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.mesh_name", - return_value=DEFAULT_TITLE, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.system_info", - return_value=DEFAULT_SYSTEM_INFO, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.ap_info", - return_value=DEFAULT_AP_INFO, - ), patch( - "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients", - return_value={}, - ): + with RuckusAjaxApiPatchContext(active_clients={}): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py index 0b32b27517d..c8246a5ac1e 100644 --- a/tests/components/ruckus_unleashed/test_init.py +++ b/tests/components/ruckus_unleashed/test_init.py @@ -1,18 +1,16 @@ """Test the Ruckus Unleashed config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock -from pyruckus.exceptions import AuthenticationError +from aioruckus.const import ERROR_CONNECT_TIMEOUT, ERROR_LOGIN_INCORRECT +from aioruckus.exceptions import AuthenticationError -from homeassistant.components.ruckus_unleashed import ( - API_AP, - API_DEVICE_NAME, - API_ID, - API_MAC, - API_MODEL, - API_SYSTEM_OVERVIEW, - API_VERSION, - DOMAIN, - MANUFACTURER, +from homeassistant.components.ruckus_unleashed import DOMAIN, MANUFACTURER +from homeassistant.components.ruckus_unleashed.const import ( + API_AP_DEVNAME, + API_AP_MAC, + API_AP_MODEL, + API_SYS_SYSINFO, + API_SYS_SYSINFO_VERSION, ) from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -22,7 +20,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from . import ( DEFAULT_AP_INFO, DEFAULT_SYSTEM_INFO, - DEFAULT_TITLE, + RuckusAjaxApiPatchContext, init_integration, mock_config_entry, ) @@ -31,9 +29,8 @@ from . import ( async def test_setup_entry_login_error(hass: HomeAssistant) -> None: """Test entry setup failed due to login error.""" entry = mock_config_entry() - with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.connect", - side_effect=AuthenticationError, + with RuckusAjaxApiPatchContext( + login_mock=AsyncMock(side_effect=AuthenticationError(ERROR_LOGIN_INCORRECT)) ): entry.add_to_hass(hass) result = await hass.config_entries.async_setup(entry.entry_id) @@ -45,9 +42,8 @@ async def test_setup_entry_login_error(hass: HomeAssistant) -> None: async def test_setup_entry_connection_error(hass: HomeAssistant) -> None: """Test entry setup failed due to connection error.""" entry = mock_config_entry() - with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.connect", - side_effect=ConnectionError, + with RuckusAjaxApiPatchContext( + login_mock=AsyncMock(side_effect=ConnectionError(ERROR_CONNECT_TIMEOUT)) ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -60,19 +56,22 @@ async def test_router_device_setup(hass: HomeAssistant) -> None: """Test a router device is created.""" await init_integration(hass) - device_info = DEFAULT_AP_INFO[API_AP][API_ID]["1"] + device_info = DEFAULT_AP_INFO[0] device_registry = dr.async_get(hass) device = device_registry.async_get_device( - identifiers={(CONNECTION_NETWORK_MAC, device_info[API_MAC])}, - connections={(CONNECTION_NETWORK_MAC, device_info[API_MAC])}, + identifiers={(CONNECTION_NETWORK_MAC, device_info[API_AP_MAC])}, + connections={(CONNECTION_NETWORK_MAC, device_info[API_AP_MAC])}, ) assert device assert device.manufacturer == MANUFACTURER - assert device.model == device_info[API_MODEL] - assert device.name == device_info[API_DEVICE_NAME] - assert device.sw_version == DEFAULT_SYSTEM_INFO[API_SYSTEM_OVERVIEW][API_VERSION] + assert device.model == device_info[API_AP_MODEL] + assert device.name == device_info[API_AP_DEVNAME] + assert ( + device.sw_version + == DEFAULT_SYSTEM_INFO[API_SYS_SYSINFO][API_SYS_SYSINFO_VERSION] + ) assert device.via_device_id is None @@ -83,31 +82,9 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state is ConfigEntryState.LOADED - assert await hass.config_entries.async_unload(entry.entry_id) - await hass.async_block_till_done() + with RuckusAjaxApiPatchContext(): + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) - - -async def test_config_not_ready_during_setup(hass: HomeAssistant) -> None: - """Test we throw a ConfigNotReady if Coordinator update fails.""" - entry = mock_config_entry() - with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.connect", - return_value=None, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.mesh_name", - return_value=DEFAULT_TITLE, - ), patch( - "homeassistant.components.ruckus_unleashed.Ruckus.system_info", - return_value=DEFAULT_SYSTEM_INFO, - ), patch( - "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._async_update_data", - side_effect=ConnectionError, - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert entry.state is ConfigEntryState.SETUP_RETRY From 42597f80a39958f28a4e53a00f9689ce9625d805 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 28 Aug 2023 16:44:23 +0100 Subject: [PATCH 0969/1151] Add power service to System Bridge integration (#95719) * Add power service to System Bridge Add missing return types Use in list validator and fix command * Use attr map instead of concatination * Update strings * Update homeassistant/components/system_bridge/strings.json Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- .../components/system_bridge/__init__.py | 36 ++++++++++++++++++- .../components/system_bridge/services.yaml | 19 ++++++++++ .../components/system_bridge/strings.json | 14 ++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index b301d0c4b28..d50540f7b42 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -19,6 +19,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, + CONF_COMMAND, CONF_HOST, CONF_PATH, CONF_PORT, @@ -47,10 +48,20 @@ CONF_KEY = "key" CONF_TEXT = "text" SERVICE_OPEN_PATH = "open_path" +SERVICE_POWER_COMMAND = "power_command" SERVICE_OPEN_URL = "open_url" SERVICE_SEND_KEYPRESS = "send_keypress" SERVICE_SEND_TEXT = "send_text" +POWER_COMMAND_MAP = { + "hibernate": "power_hibernate", + "lock": "power_lock", + "logout": "power_logout", + "restart": "power_restart", + "shutdown": "power_shutdown", + "sleep": "power_sleep", +} + async def async_setup_entry( hass: HomeAssistant, @@ -136,7 +147,7 @@ async def async_setup_entry( if hass.services.has_service(DOMAIN, SERVICE_OPEN_URL): return True - def valid_device(device: str): + def valid_device(device: str) -> str: """Check device is valid.""" device_registry = dr.async_get(hass) device_entry = device_registry.async_get(device) @@ -161,6 +172,17 @@ async def async_setup_entry( OpenPath(path=call.data[CONF_PATH]) ) + async def handle_power_command(call: ServiceCall) -> None: + """Handle the power command service call.""" + _LOGGER.info("Power command: %s", call.data) + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ + call.data[CONF_BRIDGE] + ] + await getattr( + coordinator.websocket_client, + POWER_COMMAND_MAP[call.data[CONF_COMMAND]], + )() + async def handle_open_url(call: ServiceCall) -> None: """Handle the open url service call.""" _LOGGER.info("Open: %s", call.data) @@ -199,6 +221,18 @@ async def async_setup_entry( ), ) + hass.services.async_register( + DOMAIN, + SERVICE_POWER_COMMAND, + handle_power_command, + schema=vol.Schema( + { + vol.Required(CONF_BRIDGE): valid_device, + vol.Required(CONF_COMMAND): vol.In(POWER_COMMAND_MAP), + }, + ), + ) + hass.services.async_register( DOMAIN, SERVICE_OPEN_URL, diff --git a/homeassistant/components/system_bridge/services.yaml b/homeassistant/components/system_bridge/services.yaml index 78d6e87f218..49a7931789e 100644 --- a/homeassistant/components/system_bridge/services.yaml +++ b/homeassistant/components/system_bridge/services.yaml @@ -46,3 +46,22 @@ send_text: example: "Hello world" selector: text: +power_command: + fields: + bridge: + required: true + selector: + device: + integration: system_bridge + command: + required: true + example: "sleep" + selector: + select: + options: + - "hibernate" + - "lock" + - "logout" + - "restart" + - "shutdown" + - "sleep" diff --git a/homeassistant/components/system_bridge/strings.json b/homeassistant/components/system_bridge/strings.json index 8a31394875e..e8565568d20 100644 --- a/homeassistant/components/system_bridge/strings.json +++ b/homeassistant/components/system_bridge/strings.json @@ -133,6 +133,20 @@ "description": "Text to type." } } + }, + "power_command": { + "name": "Power command", + "description": "Sends a power command to the system.", + "fields": { + "bridge": { + "name": "[%key:component::system_bridge::services::open_path::fields::bridge::name%]", + "description": "[%key:component::system_bridge::services::send_keypress::fields::bridge::description%]" + }, + "command": { + "name": "Command", + "description": "Command to call." + } + } } } } From 377f7cba603dc6f0db656c9bb430e4aac419afd1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 17:56:27 +0200 Subject: [PATCH 0970/1151] Improve aurora data schema (#99200) --- .../components/aurora/config_flow.py | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/aurora/config_flow.py b/homeassistant/components/aurora/config_flow.py index 4649a3adc08..bbd0768e74a 100644 --- a/homeassistant/components/aurora/config_flow.py +++ b/homeassistant/components/aurora/config_flow.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, SchemaOptionsFlowHandler, @@ -75,24 +75,19 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", - data_schema=vol.Schema( + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_NAME): str, + vol.Required(CONF_LONGITUDE): cv.longitude, + vol.Required(CONF_LATITUDE): cv.latitude, + } + ), { - vol.Required(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required( - CONF_LONGITUDE, - default=self.hass.config.longitude, - ): vol.All( - vol.Coerce(float), - vol.Range(min=-180, max=180), - ), - vol.Required( - CONF_LATITUDE, - default=self.hass.config.latitude, - ): vol.All( - vol.Coerce(float), - vol.Range(min=-90, max=90), - ), - } + CONF_NAME: DEFAULT_NAME, + CONF_LONGITUDE: self.hass.config.longitude, + CONF_LATITUDE: self.hass.config.latitude, + }, ), errors=errors, ) From 00cc57c4ed102b2d1fd1b081be748970a31cec4e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 17:57:51 +0200 Subject: [PATCH 0971/1151] Use shorthand attribute for Coolmaster (#99211) --- homeassistant/components/coolmaster/climate.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 6ae6613bcca..c9f5cff4339 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -58,12 +58,8 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity): def __init__(self, coordinator, unit_id, info, supported_modes): """Initialize the climate device.""" super().__init__(coordinator, unit_id, info) - self._hvac_modes = supported_modes - - @property - def unique_id(self): - """Return unique ID for this device.""" - return self._unit_id + self._attr_hvac_modes = supported_modes + self._attr_unique_id = unit_id @property def supported_features(self) -> ClimateEntityFeature: @@ -102,11 +98,6 @@ class CoolmasterClimate(CoolmasterEntity, ClimateEntity): return CM_TO_HA_STATE[mode] - @property - def hvac_modes(self): - """Return the list of available operation modes.""" - return self._hvac_modes - @property def fan_mode(self): """Return the fan setting.""" From 1c0d5f86373d1892687257edcfa2e5f5def5cde2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 17:59:53 +0200 Subject: [PATCH 0972/1151] Clean up Balboa entity (#99203) --- homeassistant/components/balboa/entity.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/balboa/entity.py b/homeassistant/components/balboa/entity.py index 2b845b65496..3b4f7d08fff 100644 --- a/homeassistant/components/balboa/entity.py +++ b/homeassistant/components/balboa/entity.py @@ -9,18 +9,18 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN -class BalboaBaseEntity(Entity): +class BalboaEntity(Entity): """Balboa base entity.""" + _attr_should_poll = False + _attr_has_entity_name = True + def __init__(self, client: SpaClient, name: str | None = None) -> None: """Initialize the control.""" mac = client.mac_address model = client.model - - self._attr_should_poll = False self._attr_unique_id = f'{model}-{name}-{mac.replace(":","")[-6:]}' self._attr_name = name - self._attr_has_entity_name = True self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, mac)}, name=model, @@ -36,10 +36,6 @@ class BalboaBaseEntity(Entity): """Return whether the state is based on actual reading from device.""" return not self._client.available - -class BalboaEntity(BalboaBaseEntity): - """Balboa entity.""" - async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" self.async_on_remove(self._client.on(EVENT_UPDATE, self.async_write_ha_state)) From 3db61a99a4b7aadeac7149de21b02b9e96f2d1b2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 18:01:23 +0200 Subject: [PATCH 0973/1151] Remove polling interval property from Aurora (#99198) --- homeassistant/components/aurora/__init__.py | 11 +---------- homeassistant/components/aurora/const.py | 1 - homeassistant/components/aurora/coordinator.py | 3 +-- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index db054910d9a..6ffba5f13da 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -9,14 +9,7 @@ from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, Platfo from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from .const import ( - AURORA_API, - CONF_THRESHOLD, - COORDINATOR, - DEFAULT_POLLING_INTERVAL, - DEFAULT_THRESHOLD, - DOMAIN, -) +from .const import AURORA_API, CONF_THRESHOLD, COORDINATOR, DEFAULT_THRESHOLD, DOMAIN from .coordinator import AuroraDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -35,14 +28,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: longitude = conf[CONF_LONGITUDE] latitude = conf[CONF_LATITUDE] - polling_interval = DEFAULT_POLLING_INTERVAL threshold = options.get(CONF_THRESHOLD, DEFAULT_THRESHOLD) name = conf[CONF_NAME] coordinator = AuroraDataUpdateCoordinator( hass=hass, name=name, - polling_interval=polling_interval, api=api, latitude=latitude, longitude=longitude, diff --git a/homeassistant/components/aurora/const.py b/homeassistant/components/aurora/const.py index d2f91fb1222..419a3c946e6 100644 --- a/homeassistant/components/aurora/const.py +++ b/homeassistant/components/aurora/const.py @@ -3,7 +3,6 @@ DOMAIN = "aurora" COORDINATOR = "coordinator" AURORA_API = "aurora_api" -DEFAULT_POLLING_INTERVAL = 5 CONF_THRESHOLD = "forecast_threshold" DEFAULT_THRESHOLD = 75 ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric Administration" diff --git a/homeassistant/components/aurora/coordinator.py b/homeassistant/components/aurora/coordinator.py index 973e48850a6..0ab1be00902 100644 --- a/homeassistant/components/aurora/coordinator.py +++ b/homeassistant/components/aurora/coordinator.py @@ -19,7 +19,6 @@ class AuroraDataUpdateCoordinator(DataUpdateCoordinator): self, hass: HomeAssistant, name: str, - polling_interval: int, api: AuroraForecast, latitude: float, longitude: float, @@ -31,7 +30,7 @@ class AuroraDataUpdateCoordinator(DataUpdateCoordinator): hass=hass, logger=_LOGGER, name=name, - update_interval=timedelta(minutes=polling_interval), + update_interval=timedelta(minutes=5), ) self.api = api From fc6f48e076e08611a887e33a724bd59c67786944 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 18:03:30 +0200 Subject: [PATCH 0974/1151] Enhance Androidtv remote config flow typing (#99144) --- .../androidtv_remote/config_flow.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/androidtv_remote/config_flow.py b/homeassistant/components/androidtv_remote/config_flow.py index d5c361674bd..03e09c6ecb0 100644 --- a/homeassistant/components/androidtv_remote/config_flow.py +++ b/homeassistant/components/androidtv_remote/config_flow.py @@ -12,8 +12,12 @@ from androidtvremote2 import ( ) import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -35,7 +39,7 @@ STEP_PAIR_DATA_SCHEMA = vol.Schema( ) -class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Android TV Remote.""" VERSION = 1 @@ -43,7 +47,7 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize a new AndroidTVRemoteConfigFlow.""" self.api: AndroidTVRemote | None = None - self.reauth_entry: config_entries.ConfigEntry | None = None + self.reauth_entry: ConfigEntry | None = None self.host: str | None = None self.name: str | None = None self.mac: str | None = None @@ -192,19 +196,15 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> AndroidTVRemoteOptionsFlowHandler: """Create the options flow.""" - return OptionsFlowHandler(config_entry) + return AndroidTVRemoteOptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry): """Android TV Remote options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: From 821d74e904db82e9bce7af559e672d5aead617aa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 28 Aug 2023 19:53:42 +0200 Subject: [PATCH 0975/1151] Add entity translations to Switcher kis (#99223) * Add entity translations to switcher_kis * Add entity translations * Fix tests --- .../components/switcher_kis/button.py | 10 ++++---- .../components/switcher_kis/climate.py | 4 +++- .../components/switcher_kis/cover.py | 3 ++- .../components/switcher_kis/sensor.py | 13 +++++----- .../components/switcher_kis/strings.json | 24 +++++++++++++++++++ .../components/switcher_kis/switch.py | 10 ++++---- tests/components/switcher_kis/test_init.py | 4 ++-- tests/components/switcher_kis/test_sensor.py | 22 ++++++++--------- 8 files changed, 59 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/switcher_kis/button.py b/homeassistant/components/switcher_kis/button.py index d6174920ece..4303c885106 100644 --- a/homeassistant/components/switcher_kis/button.py +++ b/homeassistant/components/switcher_kis/button.py @@ -48,7 +48,7 @@ class SwitcherThermostatButtonEntityDescription( THERMOSTAT_BUTTONS = [ SwitcherThermostatButtonEntityDescription( key="assume_on", - name="Assume on", + translation_key="assume_on", icon="mdi:fan", entity_category=EntityCategory.CONFIG, press_fn=lambda api, remote: api.control_breeze_device( @@ -58,7 +58,7 @@ THERMOSTAT_BUTTONS = [ ), SwitcherThermostatButtonEntityDescription( key="assume_off", - name="Assume off", + translation_key="assume_off", icon="mdi:fan-off", entity_category=EntityCategory.CONFIG, press_fn=lambda api, remote: api.control_breeze_device( @@ -68,7 +68,7 @@ THERMOSTAT_BUTTONS = [ ), SwitcherThermostatButtonEntityDescription( key="vertical_swing_on", - name="Vertical swing on", + translation_key="vertical_swing_on", icon="mdi:autorenew", press_fn=lambda api, remote: api.control_breeze_device( remote, swing=ThermostatSwing.ON @@ -77,7 +77,7 @@ THERMOSTAT_BUTTONS = [ ), SwitcherThermostatButtonEntityDescription( key="vertical_swing_off", - name="Vertical swing off", + translation_key="vertical_swing_off", icon="mdi:autorenew-off", press_fn=lambda api, remote: api.control_breeze_device( remote, swing=ThermostatSwing.OFF @@ -117,6 +117,7 @@ class SwitcherThermostatButtonEntity( """Representation of a Switcher climate entity.""" entity_description: SwitcherThermostatButtonEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -129,7 +130,6 @@ class SwitcherThermostatButtonEntity( self.entity_description = description self._remote = remote - self._attr_name = f"{coordinator.name} {description.name}" self._attr_unique_id = f"{coordinator.mac_address}-{description.key}" self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)} diff --git a/homeassistant/components/switcher_kis/climate.py b/homeassistant/components/switcher_kis/climate.py index 32877f42163..809e3d6a3ad 100644 --- a/homeassistant/components/switcher_kis/climate.py +++ b/homeassistant/components/switcher_kis/climate.py @@ -84,6 +84,9 @@ class SwitcherClimateEntity( ): """Representation of a Switcher climate entity.""" + _attr_has_entity_name = True + _attr_name = None + def __init__( self, coordinator: SwitcherDataUpdateCoordinator, remote: SwitcherBreezeRemote ) -> None: @@ -91,7 +94,6 @@ class SwitcherClimateEntity( super().__init__(coordinator) self._remote = remote - self._attr_name = coordinator.name self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}" self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)} diff --git a/homeassistant/components/switcher_kis/cover.py b/homeassistant/components/switcher_kis/cover.py index 78a722f262c..c627f361d7d 100644 --- a/homeassistant/components/switcher_kis/cover.py +++ b/homeassistant/components/switcher_kis/cover.py @@ -55,6 +55,8 @@ class SwitcherCoverEntity( ): """Representation of a Switcher cover entity.""" + _attr_has_entity_name = True + _attr_name = None _attr_device_class = CoverDeviceClass.SHUTTER _attr_supported_features = ( CoverEntityFeature.OPEN @@ -67,7 +69,6 @@ class SwitcherCoverEntity( """Initialize the entity.""" super().__init__(coordinator) - self._attr_name = coordinator.name self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}" self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)} diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index 0b6263a6b2e..e9fa13fca8a 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -25,14 +25,12 @@ from .const import SIGNAL_DEVICE_ADD POWER_SENSORS: list[SensorEntityDescription] = [ SensorEntityDescription( key="power_consumption", - name="Power Consumption", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electric_current", - name="Electric Current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -40,11 +38,13 @@ POWER_SENSORS: list[SensorEntityDescription] = [ ] TIME_SENSORS: list[SensorEntityDescription] = [ SensorEntityDescription( - key="remaining_time", name="Remaining Time", icon="mdi:av-timer" + key="remaining_time", + translation_key="remaining_time", + icon="mdi:av-timer", ), SensorEntityDescription( key="auto_off_set", - name="Auto Shutdown", + translation_key="auto_shutdown", icon="mdi:progress-clock", entity_registry_enabled_default=False, ), @@ -85,6 +85,8 @@ class SwitcherSensorEntity( ): """Representation of a Switcher sensor entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SwitcherDataUpdateCoordinator, @@ -94,9 +96,6 @@ class SwitcherSensorEntity( super().__init__(coordinator) self.entity_description = description - # Entity class attributes - self._attr_name = f"{coordinator.name} {description.name}" - self._attr_unique_id = ( f"{coordinator.device_id}-{coordinator.mac_address}-{description.key}" ) diff --git a/homeassistant/components/switcher_kis/strings.json b/homeassistant/components/switcher_kis/strings.json index 4c4080a8394..e21bdbcdf7a 100644 --- a/homeassistant/components/switcher_kis/strings.json +++ b/homeassistant/components/switcher_kis/strings.json @@ -10,6 +10,30 @@ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } }, + "entity": { + "button": { + "assume_on": { + "name": "Assume on" + }, + "assume_off": { + "name": "Assume off" + }, + "vertical_swing_on": { + "name": "Vertical swing on" + }, + "vertical_swing_off": { + "name": "Vertical swing off" + } + }, + "sensor": { + "remaining_time": { + "name": "Remaining time" + }, + "auto_shutdown": { + "name": "Auto shutdown" + } + } + }, "services": { "set_auto_off": { "name": "Set auto off", diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 95ea92e62ab..ef8564b3770 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -83,13 +83,15 @@ class SwitcherBaseSwitchEntity( ): """Representation of a Switcher switch entity.""" + _attr_has_entity_name = True + _attr_name = None + def __init__(self, coordinator: SwitcherDataUpdateCoordinator) -> None: """Initialize the entity.""" super().__init__(coordinator) self.control_result: bool | None = None # Entity class attributes - self._attr_name = coordinator.name self._attr_unique_id = f"{coordinator.device_id}-{coordinator.mac_address}" self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)} @@ -118,7 +120,7 @@ class SwitcherBaseSwitchEntity( if error or not response or not response.successful: _LOGGER.error( "Call api for %s failed, api: '%s', args: %s, response/error: %s", - self.name, + self.coordinator.name, api, args, response or error, @@ -150,7 +152,7 @@ class SwitcherBaseSwitchEntity( _LOGGER.warning( "Service '%s' is not supported by %s", SERVICE_SET_AUTO_OFF_NAME, - self.name, + self.coordinator.name, ) async def async_turn_on_with_timer_service(self, timer_minutes: int) -> None: @@ -158,7 +160,7 @@ class SwitcherBaseSwitchEntity( _LOGGER.warning( "Service '%s' is not supported by %s", SERVICE_TURN_ON_WITH_TIMER_NAME, - self.name, + self.coordinator.name, ) diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py index 6b592b25077..f35ff9fbbf2 100644 --- a/tests/components/switcher_kis/test_init.py +++ b/tests/components/switcher_kis/test_init.py @@ -77,7 +77,7 @@ async def test_update_fail( state = hass.states.get(entity_id) assert state.state == STATE_UNAVAILABLE - entity_id = f"sensor.{slugify(device.name)}_power_consumption" + entity_id = f"sensor.{slugify(device.name)}_power" state = hass.states.get(entity_id) assert state.state == STATE_UNAVAILABLE @@ -92,7 +92,7 @@ async def test_update_fail( state = hass.states.get(entity_id) assert state.state != STATE_UNAVAILABLE - entity_id = f"sensor.{slugify(device.name)}_power_consumption" + entity_id = f"sensor.{slugify(device.name)}_power" state = hass.states.get(entity_id) assert state.state != STATE_UNAVAILABLE diff --git a/tests/components/switcher_kis/test_sensor.py b/tests/components/switcher_kis/test_sensor.py index 41f409062ce..03073b21a96 100644 --- a/tests/components/switcher_kis/test_sensor.py +++ b/tests/components/switcher_kis/test_sensor.py @@ -13,16 +13,16 @@ DEVICE_SENSORS_TUPLE = ( ( DUMMY_PLUG_DEVICE, [ - "power_consumption", - "electric_current", + ("power", "power_consumption"), + ("current", "electric_current"), ], ), ( DUMMY_WATER_HEATER_DEVICE, [ - "power_consumption", - "electric_current", - "remaining_time", + ("power", "power_consumption"), + ("current", "electric_current"), + ("remaining_time", "remaining_time"), ], ), ) @@ -39,10 +39,10 @@ async def test_sensor_platform(hass: HomeAssistant, mock_bridge) -> None: assert len(hass.data[DOMAIN][DATA_DEVICE]) == 2 for device, sensors in DEVICE_SENSORS_TUPLE: - for sensor in sensors: + for sensor, field in sensors: entity_id = f"sensor.{slugify(device.name)}_{sensor}" state = hass.states.get(entity_id) - assert state.state == str(getattr(device, sensor)) + assert state.state == str(getattr(device, field)) async def test_sensor_disabled(hass: HomeAssistant, mock_bridge) -> None: @@ -80,13 +80,13 @@ async def test_sensor_update(hass: HomeAssistant, mock_bridge, monkeypatch) -> N assert mock_bridge device = DUMMY_WATER_HEATER_DEVICE - sensor = "power_consumption" - entity_id = f"sensor.{slugify(device.name)}_{sensor}" + field = "power_consumption" + entity_id = f"sensor.{slugify(device.name)}_power" state = hass.states.get(entity_id) - assert state.state == str(getattr(device, sensor)) + assert state.state == str(getattr(device, field)) - monkeypatch.setattr(device, sensor, 1431) + monkeypatch.setattr(device, field, 1431) mock_bridge.mock_callbacks([DUMMY_WATER_HEATER_DEVICE]) await hass.async_block_till_done() From 80d2309896ff096bf75227978119de1e09adb7d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Aug 2023 13:56:22 -0500 Subject: [PATCH 0976/1151] Switch async_track_time_interval to use async_call_later internally (#99220) --- homeassistant/helpers/event.py | 11 ++---- .../components/gardena_bluetooth/conftest.py | 18 +++++----- tests/components/modbus/conftest.py | 34 ++++++++++--------- tests/components/modbus/test_binary_sensor.py | 8 ++--- tests/components/modbus/test_climate.py | 8 ++--- tests/components/modbus/test_cover.py | 8 ++--- tests/components/modbus/test_sensor.py | 8 ++--- tests/components/modbus/test_switch.py | 8 ++--- tests/components/unifi/test_device_tracker.py | 18 +++++----- 9 files changed, 59 insertions(+), 62 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 11bfe04473a..14ba4953694 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1514,24 +1514,19 @@ def async_track_time_interval( """Add a listener that fires repetitively at every timedelta interval.""" remove: CALLBACK_TYPE interval_listener_job: HassJob[[datetime], None] + interval_seconds = interval.total_seconds() job = HassJob( action, f"track time interval {interval}", cancel_on_shutdown=cancel_on_shutdown ) - def next_interval() -> datetime: - """Return the next interval.""" - return dt_util.utcnow() + interval - @callback def interval_listener(now: datetime) -> None: """Handle elapsed intervals.""" nonlocal remove nonlocal interval_listener_job - remove = async_track_point_in_utc_time( - hass, interval_listener_job, next_interval() - ) + remove = async_call_later(hass, interval_seconds, interval_listener_job) hass.async_run_hass_job(job, now) if name: @@ -1542,7 +1537,7 @@ def async_track_time_interval( interval_listener_job = HassJob( interval_listener, job_name, cancel_on_shutdown=cancel_on_shutdown ) - remove = async_track_point_in_utc_time(hass, interval_listener_job, next_interval()) + remove = async_call_later(hass, interval_seconds, interval_listener_job) def remove_listener() -> None: """Remove interval listener.""" diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index 98ae41d195b..9395d8570e6 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Awaitable, Callable, Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch -from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory from gardena_bluetooth.client import Client from gardena_bluetooth.const import DeviceInformation from gardena_bluetooth.exceptions import CharacteristicNotFound @@ -49,19 +49,19 @@ def mock_read_char_raw(): @pytest.fixture async def scan_step( - hass: HomeAssistant, + hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> Generator[None, None, Callable[[], Awaitable[None]]]: """Step system time forward.""" - with freeze_time("2023-01-01", tz_offset=1) as frozen_time: + freezer.move_to("2023-01-01T01:00:00Z") - async def delay(): - """Trigger delay in system.""" - frozen_time.tick(delta=SCAN_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done() + async def delay(): + """Trigger delay in system.""" + freezer.tick(delta=SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() - yield delay + return delay @pytest.fixture(autouse=True) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 21c5f4ddb25..23d3ee522bb 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -5,11 +5,13 @@ from datetime import timedelta import logging from unittest import mock +from freezegun.api import FrozenDateTimeFactory from pymodbus.exceptions import ModbusException import pytest from homeassistant.components.modbus.const import MODBUS_DOMAIN as DOMAIN, TCP from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SLAVE, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -140,26 +142,26 @@ async def mock_pymodbus_return_fixture(hass, register_words, mock_modbus): @pytest.fixture(name="mock_do_cycle") -async def mock_do_cycle_fixture(hass, mock_pymodbus_exception, mock_pymodbus_return): +async def mock_do_cycle_fixture( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_pymodbus_exception, + mock_pymodbus_return, +) -> FrozenDateTimeFactory: """Trigger update call with time_changed event.""" - now = dt_util.utcnow() + timedelta(seconds=90) - with mock.patch( - "homeassistant.helpers.event.dt_util.utcnow", return_value=now, autospec=True - ): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() - return now + freezer.tick(timedelta(seconds=90)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + return freezer -async def do_next_cycle(hass, now, cycle): +async def do_next_cycle( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, cycle: int +) -> None: """Trigger update call with time_changed event.""" - now += timedelta(seconds=cycle) - with mock.patch( - "homeassistant.helpers.event.dt_util.utcnow", return_value=now, autospec=True - ): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() - return now + freezer.tick(timedelta(seconds=cycle)) + async_fire_time_changed(hass) + await hass.async_block_till_done() @pytest.fixture(name="mock_test_state") diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 5c4535f9f29..1e413fcc764 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -1,4 +1,5 @@ """Thetests for the Modbus sensor component.""" +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN @@ -199,14 +200,13 @@ async def test_all_binary_sensor(hass: HomeAssistant, expected, mock_do_cycle) - ], ) async def test_lazy_error_binary_sensor( - hass: HomeAssistant, start_expect, end_expect, mock_do_cycle + hass: HomeAssistant, start_expect, end_expect, mock_do_cycle: FrozenDateTimeFactory ) -> None: """Run test for given config.""" - now = mock_do_cycle assert hass.states.get(ENTITY_ID).state == start_expect - now = await do_next_cycle(hass, now, 11) + await do_next_cycle(hass, mock_do_cycle, 11) assert hass.states.get(ENTITY_ID).state == start_expect - now = await do_next_cycle(hass, now, 11) + await do_next_cycle(hass, mock_do_cycle, 11) assert hass.states.get(ENTITY_ID).state == end_expect diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index ce43cf7c1d2..4ab78df0c81 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -1,4 +1,5 @@ """The tests for the Modbus climate component.""" +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN @@ -597,16 +598,15 @@ async def test_restore_state_climate( ], ) async def test_lazy_error_climate( - hass: HomeAssistant, mock_do_cycle, start_expect, end_expect + hass: HomeAssistant, mock_do_cycle: FrozenDateTimeFactory, start_expect, end_expect ) -> None: """Run test for sensor.""" hass.states.async_set(ENTITY_ID, 17) await hass.async_block_till_done() - now = mock_do_cycle assert hass.states.get(ENTITY_ID).state == start_expect - now = await do_next_cycle(hass, now, 11) + await do_next_cycle(hass, mock_do_cycle, 11) assert hass.states.get(ENTITY_ID).state == start_expect - now = await do_next_cycle(hass, now, 11) + await do_next_cycle(hass, mock_do_cycle, 11) assert hass.states.get(ENTITY_ID).state == end_expect diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 4ec1b9d7bfc..66e4537d67e 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -1,4 +1,5 @@ """The tests for the Modbus cover component.""" +from freezegun.api import FrozenDateTimeFactory from pymodbus.exceptions import ModbusException import pytest @@ -142,14 +143,13 @@ async def test_coil_cover(hass: HomeAssistant, expected, mock_do_cycle) -> None: ], ) async def test_lazy_error_cover( - hass: HomeAssistant, start_expect, end_expect, mock_do_cycle + hass: HomeAssistant, start_expect, end_expect, mock_do_cycle: FrozenDateTimeFactory ) -> None: """Run test for given config.""" - now = mock_do_cycle assert hass.states.get(ENTITY_ID).state == start_expect - now = await do_next_cycle(hass, now, 11) + await do_next_cycle(hass, mock_do_cycle, 11) assert hass.states.get(ENTITY_ID).state == start_expect - now = await do_next_cycle(hass, now, 11) + await do_next_cycle(hass, mock_do_cycle, 11) assert hass.states.get(ENTITY_ID).state == end_expect diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 8481adc0d0f..f72371ed42e 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Modbus sensor component.""" +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.modbus.const import ( @@ -928,16 +929,15 @@ async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None: ], ) async def test_lazy_error_sensor( - hass: HomeAssistant, mock_do_cycle, start_expect, end_expect + hass: HomeAssistant, mock_do_cycle: FrozenDateTimeFactory, start_expect, end_expect ) -> None: """Run test for sensor.""" hass.states.async_set(ENTITY_ID, 17) await hass.async_block_till_done() - now = mock_do_cycle assert hass.states.get(ENTITY_ID).state == start_expect - now = await do_next_cycle(hass, now, 11) + await do_next_cycle(hass, mock_do_cycle, 11) assert hass.states.get(ENTITY_ID).state == start_expect - now = await do_next_cycle(hass, now, 11) + await do_next_cycle(hass, mock_do_cycle, 11) assert hass.states.get(ENTITY_ID).state == end_expect diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 2e2f0081eba..dce4588d606 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -2,6 +2,7 @@ from datetime import timedelta from unittest import mock +from freezegun.api import FrozenDateTimeFactory from pymodbus.exceptions import ModbusException import pytest @@ -237,14 +238,13 @@ async def test_all_switch(hass: HomeAssistant, mock_do_cycle, expected) -> None: ], ) async def test_lazy_error_switch( - hass: HomeAssistant, start_expect, end_expect, mock_do_cycle + hass: HomeAssistant, start_expect, end_expect, mock_do_cycle: FrozenDateTimeFactory ) -> None: """Run test for given config.""" - now = mock_do_cycle assert hass.states.get(ENTITY_ID).state == start_expect - now = await do_next_cycle(hass, now, 11) + await do_next_cycle(hass, mock_do_cycle, 11) assert hass.states.get(ENTITY_ID).state == start_expect - now = await do_next_cycle(hass, now, 11) + await do_next_cycle(hass, mock_do_cycle, 11) assert hass.states.get(ENTITY_ID).state == end_expect diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 16432ff514e..7b939077e48 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -4,6 +4,7 @@ from unittest.mock import patch from aiounifi.models.message import MessageKey from aiounifi.websocket import WebsocketState +from freezegun.api import FrozenDateTimeFactory from homeassistant import config_entries from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN @@ -169,6 +170,7 @@ async def test_tracked_clients( async def test_tracked_wireless_clients_event_source( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, mock_unifi_websocket, mock_device_registry, ) -> None: @@ -234,10 +236,9 @@ async def test_tracked_wireless_clients_event_source( assert hass.states.get("device_tracker.client").state == STATE_HOME # Change time to mark client as away - new_time = dt_util.utcnow() + controller.option_detection_time - with patch("homeassistant.util.dt.utcnow", return_value=new_time): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + freezer.tick(controller.option_detection_time + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME @@ -274,12 +275,11 @@ async def test_tracked_wireless_clients_event_source( assert hass.states.get("device_tracker.client").state == STATE_HOME # Change time to mark client as away - new_time = dt_util.utcnow() + controller.option_detection_time - with patch("homeassistant.util.dt.utcnow", return_value=new_time): - async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + freezer.tick(controller.option_detection_time + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - assert hass.states.get("device_tracker.client").state == STATE_HOME + assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME async def test_tracked_devices( From 9e8d89c4f54127508cdddfae5b11f1eb8f0f8a8c Mon Sep 17 00:00:00 2001 From: jimmyd-be <34766203+jimmyd-be@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:15:18 +0200 Subject: [PATCH 0977/1151] Renson binary sensors (#94490) * Add binary sensors * Add Renson services * Add fan to Renson * Revert "Add fan to Renson" This reverts commit 8e7c09671ebf0a53ce0bb633d5209a3add2856b6. * Revert "Add Renson services" This reverts commit d862976c81404623eccd64275f412dc9cdb2c1c4. * Add binary sensor to coveragerc file * Update homeassistant/components/renson/binary_sensor.py Co-authored-by: Erik Montnemery * Update homeassistant/components/renson/binary_sensor.py Co-authored-by: Erik Montnemery * Changed hard coded names to use translation * Code cleaning * Use super()._handle_coordinator_update() --------- Co-authored-by: Erik Montnemery --- .coveragerc | 1 + homeassistant/components/renson/__init__.py | 1 + .../components/renson/binary_sensor.py | 136 ++++++++++++++++++ homeassistant/components/renson/strings.json | 23 +++ 4 files changed, 161 insertions(+) create mode 100644 homeassistant/components/renson/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 6f26795d1b5..97ed97ef293 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1006,6 +1006,7 @@ omit = homeassistant/components/renson/const.py homeassistant/components/renson/entity.py homeassistant/components/renson/sensor.py + homeassistant/components/renson/binary_sensor.py homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/sensor.py homeassistant/components/recorder/repack.py diff --git a/homeassistant/components/renson/__init__.py b/homeassistant/components/renson/__init__.py index bac9bafa8a5..86dfdc1f18b 100644 --- a/homeassistant/components/renson/__init__.py +++ b/homeassistant/components/renson/__init__.py @@ -20,6 +20,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = [ + Platform.BINARY_SENSOR, Platform.SENSOR, ] diff --git a/homeassistant/components/renson/binary_sensor.py b/homeassistant/components/renson/binary_sensor.py new file mode 100644 index 00000000000..cad8b92c0c3 --- /dev/null +++ b/homeassistant/components/renson/binary_sensor.py @@ -0,0 +1,136 @@ +"""Binary sensors for renson.""" +from __future__ import annotations + +from dataclasses import dataclass + +from renson_endura_delta.field_enum import ( + AIR_QUALITY_CONTROL_FIELD, + BREEZE_ENABLE_FIELD, + BREEZE_MET_FIELD, + CO2_CONTROL_FIELD, + FROST_PROTECTION_FIELD, + HUMIDITY_CONTROL_FIELD, + PREHEATER_FIELD, + DataType, + FieldEnum, +) +from renson_endura_delta.renson import RensonVentilation + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RensonCoordinator +from .const import DOMAIN +from .entity import RensonEntity + + +@dataclass +class RensonBinarySensorEntityDescriptionMixin: + """Mixin for required keys.""" + + field: FieldEnum + + +@dataclass +class RensonBinarySensorEntityDescription( + BinarySensorEntityDescription, RensonBinarySensorEntityDescriptionMixin +): + """Description of binary sensor.""" + + +BINARY_SENSORS: tuple[RensonBinarySensorEntityDescription, ...] = ( + RensonBinarySensorEntityDescription( + translation_key="frost_protection_active", + key="FROST_PROTECTION_FIELD", + field=FROST_PROTECTION_FIELD, + entity_category=EntityCategory.DIAGNOSTIC, + ), + RensonBinarySensorEntityDescription( + key="BREEZE_ENABLE_FIELD", + translation_key="breeze", + field=BREEZE_ENABLE_FIELD, + entity_category=EntityCategory.DIAGNOSTIC, + ), + RensonBinarySensorEntityDescription( + key="BREEZE_MET_FIELD", + translation_key="breeze_conditions_met", + field=BREEZE_MET_FIELD, + ), + RensonBinarySensorEntityDescription( + key="HUMIDITY_CONTROL_FIELD", + translation_key="humidity_control", + field=HUMIDITY_CONTROL_FIELD, + entity_category=EntityCategory.DIAGNOSTIC, + ), + RensonBinarySensorEntityDescription( + key="AIR_QUALITY_CONTROL_FIELD", + translation_key="air_quality_control", + field=AIR_QUALITY_CONTROL_FIELD, + entity_category=EntityCategory.DIAGNOSTIC, + ), + RensonBinarySensorEntityDescription( + key="CO2_CONTROL_FIELD", + translation_key="co2_control", + field=CO2_CONTROL_FIELD, + entity_category=EntityCategory.DIAGNOSTIC, + ), + RensonBinarySensorEntityDescription( + key="PREHEATER_FIELD", + translation_key="preheater", + field=PREHEATER_FIELD, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Call the Renson integration to setup.""" + + api: RensonVentilation = hass.data[DOMAIN][config_entry.entry_id].api + coordinator: RensonCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ].coordinator + + async_add_entities( + RensonBinarySensor(description, api, coordinator) + for description in BINARY_SENSORS + ) + + +class RensonBinarySensor(RensonEntity, BinarySensorEntity): + """Get sensor data from the Renson API and store it in the state of the class.""" + + _attr_has_entity_name = True + + def __init__( + self, + description: RensonBinarySensorEntityDescription, + api: RensonVentilation, + coordinator: RensonCoordinator, + ) -> None: + """Initialize class.""" + super().__init__(description.key, api, coordinator) + + self.field = description.field + self.entity_description = description + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + all_data = self.coordinator.data + + value = self.api.get_field_value(all_data, self.field.name) + + self._attr_is_on = self.api.parse_value(value, DataType.BOOLEAN) + + super()._handle_coordinator_update() diff --git a/homeassistant/components/renson/strings.json b/homeassistant/components/renson/strings.json index 06636c9d503..20db9e788b8 100644 --- a/homeassistant/components/renson/strings.json +++ b/homeassistant/components/renson/strings.json @@ -13,6 +13,29 @@ } }, "entity": { + "binary_sensor": { + "frost_protection_active": { + "name": "Frost protection active" + }, + "breeze": { + "name": "Breeze" + }, + "breeze_conditions_met": { + "name": "Breeze conditions met" + }, + "humidity_control": { + "name": "Humidity control" + }, + "air_quality_control": { + "name": "Air quality control" + }, + "co2_control": { + "name": "CO2 control" + }, + "preheater": { + "name": "Preheater" + } + }, "sensor": { "co2_quality_category": { "name": "CO2 quality category", From 95c03b419287c3eda044ab7b2e9b453c0ee1644a Mon Sep 17 00:00:00 2001 From: "J.P. Krauss" Date: Mon, 28 Aug 2023 12:21:52 -0700 Subject: [PATCH 0978/1151] Add Options Flow to change radius after initial configuration (#97285) * Add Options Flow to change radius after initial configuration * Add tests for Options Flow * Apply suggestions from code review Co-authored-by: G Johansson * Incorporate review suggestions * Fix diagnostics test case * Apply suggestions from code review Co-authored-by: G Johansson * Incorporate review suggestions * Revert "Incorporate review suggestions" This reverts commit 421e140a4fc78da22ea74c95cd1a17f9305ebbf6. * Fix broken review comments * Incorporate rest of review comments * Incorporate rest of review comments * Use Config Entry Migration * Remove old migration code * Update diagnostics snapshot for config entry migration * Incorporate review feedback --------- Co-authored-by: G Johansson --- homeassistant/components/airnow/__init__.py | 31 ++++++++- .../components/airnow/config_flow.py | 46 ++++++++++++- homeassistant/components/airnow/strings.json | 9 +++ tests/components/airnow/conftest.py | 13 +++- .../airnow/snapshots/test_diagnostics.ambr | 4 +- tests/components/airnow/test_config_flow.py | 68 ++++++++++++++++++- 6 files changed, 162 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index 7c26cded4de..c4d52c6ac8e 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -47,7 +47,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_key = entry.data[CONF_API_KEY] latitude = entry.data[CONF_LATITUDE] longitude = entry.data[CONF_LONGITUDE] - distance = entry.data[CONF_RADIUS] + + # Station Radius is a user-configurable option + distance = entry.options[CONF_RADIUS] # Reports are published hourly but update twice per hour update_interval = datetime.timedelta(minutes=30) @@ -65,11 +67,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator + # Listen for option changes + entry.async_on_unload(entry.add_update_listener(update_listener)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", entry.version) + + if entry.version == 1: + new_options = {CONF_RADIUS: entry.data[CONF_RADIUS]} + new_data = entry.data.copy() + del new_data[CONF_RADIUS] + + entry.version = 2 + hass.config_entries.async_update_entry( + entry, data=new_data, options=new_options + ) + + _LOGGER.info("Migration to version %s successful", entry.version) + + return True + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -80,6 +104,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + class AirNowDataUpdateCoordinator(DataUpdateCoordinator): """Define an object to hold Airly data.""" diff --git a/homeassistant/components/airnow/config_flow.py b/homeassistant/components/airnow/config_flow.py index 67bce66e167..d72d145f7de 100644 --- a/homeassistant/components/airnow/config_flow.py +++ b/homeassistant/components/airnow/config_flow.py @@ -1,11 +1,12 @@ """Config flow for AirNow integration.""" import logging +from typing import Any from pyairnow import WebServiceAPI from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, core, data_entry_flow, exceptions from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -48,7 +49,7 @@ async def validate_input(hass: core.HomeAssistant, data): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for AirNow.""" - VERSION = 1 + VERSION = 2 async def async_step_user(self, user_input=None): """Handle the initial step.""" @@ -75,12 +76,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" else: # Create Entry + radius = user_input.pop(CONF_RADIUS) return self.async_create_entry( title=( f"AirNow Sensor at {user_input[CONF_LATITUDE]}," f" {user_input[CONF_LONGITUDE]}" ), data=user_input, + options={CONF_RADIUS: radius}, ) return self.async_show_form( @@ -94,12 +97,49 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): vol.Optional( CONF_LONGITUDE, default=self.hass.config.longitude ): cv.longitude, - vol.Optional(CONF_RADIUS, default=150): int, + vol.Optional(CONF_RADIUS, default=150): vol.All( + int, vol.Range(min=5) + ), } ), errors=errors, ) + @staticmethod + @core.callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Return the options flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): + """Handle an options flow for AirNow.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(data=user_input) + + options_schema = vol.Schema( + { + vol.Optional(CONF_RADIUS): vol.All( + int, + vol.Range(min=5), + ), + } + ) + + return self.async_show_form( + step_id="init", + data_schema=self.add_suggested_values_to_schema( + options_schema, self.config_entry.options + ), + ) + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/airnow/strings.json b/homeassistant/components/airnow/strings.json index 9926a2f78aa..93ca14710b7 100644 --- a/homeassistant/components/airnow/strings.json +++ b/homeassistant/components/airnow/strings.json @@ -21,6 +21,15 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "options": { + "step": { + "init": { + "data": { + "radius": "Station Radius (miles)" + } + } + } + }, "entity": { "sensor": { "o3": { diff --git a/tests/components/airnow/conftest.py b/tests/components/airnow/conftest.py index 15298ef3db0..4e9d1698e8c 100644 --- a/tests/components/airnow/conftest.py +++ b/tests/components/airnow/conftest.py @@ -12,13 +12,15 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config): +def config_entry_fixture(hass, config, options): """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, + version=2, entry_id="3bd2acb0e4f0476d40865546d0d91921", unique_id=f"{config[CONF_LATITUDE]}-{config[CONF_LONGITUDE]}", data=config, + options=options, ) entry.add_to_hass(hass) return entry @@ -31,7 +33,14 @@ def config_fixture(hass): CONF_API_KEY: "abc123", CONF_LATITUDE: 34.053718, CONF_LONGITUDE: -118.244842, - CONF_RADIUS: 75, + } + + +@pytest.fixture(name="options") +def options_fixture(hass): + """Define a config options data fixture.""" + return { + CONF_RADIUS: 150, } diff --git a/tests/components/airnow/snapshots/test_diagnostics.ambr b/tests/components/airnow/snapshots/test_diagnostics.ambr index ca333bbff72..8041cb55692 100644 --- a/tests/components/airnow/snapshots/test_diagnostics.ambr +++ b/tests/components/airnow/snapshots/test_diagnostics.ambr @@ -21,19 +21,19 @@ 'api_key': '**REDACTED**', 'latitude': '**REDACTED**', 'longitude': '**REDACTED**', - 'radius': 75, }), 'disabled_by': None, 'domain': 'airnow', 'entry_id': '3bd2acb0e4f0476d40865546d0d91921', 'options': dict({ + 'radius': 150, }), 'pref_disable_new_entities': False, 'pref_disable_polling': False, 'source': 'user', 'title': '**REDACTED**', 'unique_id': '**REDACTED**', - 'version': 1, + 'version': 2, }), }) # --- diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index 5fda5f532a3..f62fc9aee22 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -6,10 +6,13 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.airnow.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant, config, setup_airnow) -> None: + +async def test_form(hass: HomeAssistant, config, options, setup_airnow) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -20,6 +23,7 @@ async def test_form(hass: HomeAssistant, config, setup_airnow) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == config + assert result2["options"] == options @pytest.mark.parametrize("mock_api_get", [AsyncMock(side_effect=InvalidKeyError)]) @@ -85,3 +89,65 @@ async def test_entry_already_exists(hass: HomeAssistant, config, config_entry) - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], config) assert result2["type"] == "abort" assert result2["reason"] == "already_configured" + + +async def test_config_migration_v2(hass: HomeAssistant, setup_airnow) -> None: + """Test that the config migration from Version 1 to Version 2 works.""" + config_entry = MockConfigEntry( + version=1, + domain=DOMAIN, + title="AirNow", + data={ + CONF_API_KEY: "1234", + CONF_LATITUDE: 33.6, + CONF_LONGITUDE: -118.1, + CONF_RADIUS: 25, + }, + source=config_entries.SOURCE_USER, + options={CONF_RADIUS: 10}, + unique_id="1234", + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.version == 2 + assert not config_entry.data.get(CONF_RADIUS) + assert config_entry.options.get(CONF_RADIUS) == 25 + + +async def test_options_flow(hass: HomeAssistant, setup_airnow) -> None: + """Test that the options flow works.""" + config_entry = MockConfigEntry( + version=2, + domain=DOMAIN, + title="AirNow", + data={ + CONF_API_KEY: "1234", + CONF_LATITUDE: 33.6, + CONF_LONGITUDE: -118.1, + }, + source=config_entries.SOURCE_USER, + options={CONF_RADIUS: 10}, + unique_id="1234", + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_RADIUS: 25}, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert config_entry.options == { + CONF_RADIUS: 25, + } From 0e6b3d658323909d05493cf36b1e0f0d80f3d41a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Aug 2023 15:30:20 -0500 Subject: [PATCH 0979/1151] Switch async_track_same_state to use async_call_later (#99219) * Switch async_track_same_state to use async_call_later There was no need to use async_track_point_in_utc_time here since we only need a delay * update trigger tests * remove some more utcnow patching * remove some more utcnow patching * remove some more utcnow patching --- homeassistant/helpers/event.py | 4 +- .../triggers/test_numeric_state.py | 231 +++++++------- .../homeassistant/triggers/test_state.py | 294 ++++++++---------- 3 files changed, 249 insertions(+), 280 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 14ba4953694..235c1c80534 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1327,9 +1327,7 @@ def async_track_same_state( if not async_check_same_func(entity, from_state, to_state): clear_listener() - async_remove_state_for_listener = async_track_point_in_utc_time( - hass, state_for_listener, dt_util.utcnow() + period - ) + async_remove_state_for_listener = async_call_later(hass, period, state_for_listener) if entity_ids == MATCH_ALL: async_remove_state_for_cancel = hass.bus.async_listen( diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 2098d266f0d..b5bd748a5dc 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol @@ -1147,7 +1148,7 @@ async def test_if_fails_setup_for_without_above_below( ), ) async def test_if_not_fires_on_entity_change_with_for( - hass: HomeAssistant, calls, above, below + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below ) -> None: """Test for not firing on entity change with for.""" assert await async_setup_component( @@ -1171,7 +1172,8 @@ async def test_if_not_fires_on_entity_change_with_for( await hass.async_block_till_done() hass.states.async_set("test.entity", 15) await hass.async_block_till_done() - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + freezer.tick(timedelta(seconds=10)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert len(calls) == 0 @@ -1244,7 +1246,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop( ), ) async def test_if_fires_on_entity_change_with_for_attribute_change( - hass: HomeAssistant, calls, above, below + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below ) -> None: """Test for firing on entity change with for and attribute change.""" hass.states.async_set("test.entity", 0) @@ -1267,20 +1269,17 @@ async def test_if_fires_on_entity_change_with_for_attribute_change( }, ) - utcnow = dt_util.utcnow() - with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow - hass.states.async_set("test.entity", 9) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=4) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity", 9, attributes={"mock_attr": "attr_change"}) - await hass.async_block_till_done() - assert len(calls) == 0 - mock_utcnow.return_value += timedelta(seconds=4) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 + hass.states.async_set("test.entity", 9) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=4)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity", 9, attributes={"mock_attr": "attr_change"}) + await hass.async_block_till_done() + assert len(calls) == 0 + freezer.tick(timedelta(seconds=4)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 @pytest.mark.parametrize( @@ -1374,7 +1373,7 @@ async def test_wait_template_with_trigger(hass: HomeAssistant, calls, above) -> ), ) async def test_if_fires_on_entities_change_no_overlap( - hass: HomeAssistant, calls, above, below + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below ) -> None: """Test for firing on entities change with no overlap.""" hass.states.async_set("test.entity_1", 0) @@ -1402,24 +1401,21 @@ async def test_if_fires_on_entities_change_no_overlap( ) await hass.async_block_till_done() - utcnow = dt_util.utcnow() - with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow - hass.states.async_set("test.entity_1", 9) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=10) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1" + hass.states.async_set("test.entity_1", 9) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" - hass.states.async_set("test.entity_2", 9) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=10) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2" + hass.states.async_set("test.entity_2", 9) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" @pytest.mark.parametrize( @@ -1432,7 +1428,7 @@ async def test_if_fires_on_entities_change_no_overlap( ), ) async def test_if_fires_on_entities_change_overlap( - hass: HomeAssistant, calls, above, below + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below ) -> None: """Test for firing on entities change with overlap.""" hass.states.async_set("test.entity_1", 0) @@ -1460,35 +1456,32 @@ async def test_if_fires_on_entities_change_overlap( ) await hass.async_block_till_done() - utcnow = dt_util.utcnow() - with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow - hass.states.async_set("test.entity_1", 9) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", 9) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", 15) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", 9) - await hass.async_block_till_done() - assert len(calls) == 0 - mock_utcnow.return_value += timedelta(seconds=3) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1" + hass.states.async_set("test.entity_1", 9) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", 9) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", 15) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", 9) + await hass.async_block_till_done() + assert len(calls) == 0 + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" - mock_utcnow.return_value += timedelta(seconds=3) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2" + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" @pytest.mark.parametrize( @@ -1699,7 +1692,7 @@ async def test_invalid_for_template(hass: HomeAssistant, calls, above, below) -> ), ) async def test_if_fires_on_entities_change_overlap_for_template( - hass: HomeAssistant, calls, above, below + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below ) -> None: """Test for firing on entities change with overlap and for template.""" hass.states.async_set("test.entity_1", 0) @@ -1730,39 +1723,36 @@ async def test_if_fires_on_entities_change_overlap_for_template( ) await hass.async_block_till_done() - utcnow = dt_util.utcnow() - with patch("homeassistant.util.dt.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow - hass.states.async_set("test.entity_1", 9) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", 9) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", 15) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", 9) - await hass.async_block_till_done() - assert len(calls) == 0 - mock_utcnow.return_value += timedelta(seconds=3) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1 - 0:00:05" + hass.states.async_set("test.entity_1", 9) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", 9) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", 15) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", 9) + await hass.async_block_till_done() + assert len(calls) == 0 + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1 - 0:00:05" - mock_utcnow.return_value += timedelta(seconds=3) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 - mock_utcnow.return_value += timedelta(seconds=5) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2 - 0:00:10" + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 + freezer.tick(timedelta(seconds=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2 - 0:00:10" async def test_below_above(hass: HomeAssistant) -> None: @@ -1861,7 +1851,9 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( ("above", "below"), ((8, 12),), ) -async def test_variables_priority(hass: HomeAssistant, calls, above, below) -> None: +async def test_variables_priority( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below +) -> None: """Test an externally defined trigger variable is overridden.""" hass.states.async_set("test.entity_1", 0) hass.states.async_set("test.entity_2", 0) @@ -1892,29 +1884,26 @@ async def test_variables_priority(hass: HomeAssistant, calls, above, below) -> N ) await hass.async_block_till_done() - utcnow = dt_util.utcnow() - with patch("homeassistant.util.dt.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow - hass.states.async_set("test.entity_1", 9) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", 9) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", 15) - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", 9) - await hass.async_block_till_done() - assert len(calls) == 0 - mock_utcnow.return_value += timedelta(seconds=3) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1 - 0:00:05" + hass.states.async_set("test.entity_1", 9) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", 9) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", 15) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", 9) + await hass.async_block_till_done() + assert len(calls) == 0 + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1 - 0:00:05" @pytest.mark.parametrize("multiplier", (1, 5)) diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index d58e3dd7c6e..9870beedafc 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -2,6 +2,7 @@ from datetime import timedelta from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest import homeassistant.components.automation as automation @@ -695,7 +696,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop( async def test_if_fires_on_entity_change_with_for_attribute_change( - hass: HomeAssistant, calls + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls ) -> None: """Test for firing on entity change with for and attribute change.""" assert await async_setup_component( @@ -715,26 +716,23 @@ async def test_if_fires_on_entity_change_with_for_attribute_change( ) await hass.async_block_till_done() - utcnow = dt_util.utcnow() - with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow - hass.states.async_set("test.entity", "world") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=4) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set( - "test.entity", "world", attributes={"mock_attr": "attr_change"} - ) - await hass.async_block_till_done() - assert len(calls) == 0 - mock_utcnow.return_value += timedelta(seconds=4) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 + hass.states.async_set("test.entity", "world") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=4)) + async_fire_time_changed(hass) + hass.states.async_set( + "test.entity", "world", attributes={"mock_attr": "attr_change"} + ) + await hass.async_block_till_done() + assert len(calls) == 0 + freezer.tick(timedelta(seconds=4)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_for_multiple_force_update( - hass: HomeAssistant, calls + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls ) -> None: """Test for firing on entity change with for and force update.""" assert await async_setup_component( @@ -754,21 +752,18 @@ async def test_if_fires_on_entity_change_with_for_multiple_force_update( ) await hass.async_block_till_done() - utcnow = dt_util.utcnow() - with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow + hass.states.async_set("test.force_entity", "world", None, True) + await hass.async_block_till_done() + for _ in range(4): + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) hass.states.async_set("test.force_entity", "world", None, True) await hass.async_block_till_done() - for _ in range(4): - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.force_entity", "world", None, True) - await hass.async_block_till_done() - assert len(calls) == 0 - mock_utcnow.return_value += timedelta(seconds=4) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 + assert len(calls) == 0 + freezer.tick(timedelta(seconds=4)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_for(hass: HomeAssistant, calls) -> None: @@ -837,7 +832,7 @@ async def test_if_fires_on_entity_change_with_for_without_to( async def test_if_does_not_fires_on_entity_change_with_for_without_to_2( - hass: HomeAssistant, calls + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls ) -> None: """Test for firing on entity change with for.""" assert await async_setup_component( @@ -856,17 +851,12 @@ async def test_if_does_not_fires_on_entity_change_with_for_without_to_2( ) await hass.async_block_till_done() - utcnow = dt_util.utcnow() - with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow - - for i in range(10): - hass.states.async_set("test.entity", str(i)) - await hass.async_block_till_done() - - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() + for i in range(10): + hass.states.async_set("test.entity", str(i)) + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() assert len(calls) == 0 @@ -1110,7 +1100,7 @@ async def test_wait_template_with_trigger(hass: HomeAssistant, calls) -> None: async def test_if_fires_on_entities_change_no_overlap( - hass: HomeAssistant, calls + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls ) -> None: """Test for firing on entities change with no overlap.""" assert await async_setup_component( @@ -1133,27 +1123,26 @@ async def test_if_fires_on_entities_change_no_overlap( ) await hass.async_block_till_done() - utcnow = dt_util.utcnow() - with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow - hass.states.async_set("test.entity_1", "world") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=10) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1" + hass.states.async_set("test.entity_1", "world") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" - hass.states.async_set("test.entity_2", "world") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=10) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2" + hass.states.async_set("test.entity_2", "world") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" -async def test_if_fires_on_entities_change_overlap(hass: HomeAssistant, calls) -> None: +async def test_if_fires_on_entities_change_overlap( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls +) -> None: """Test for firing on entities change with overlap.""" assert await async_setup_component( hass, @@ -1175,35 +1164,32 @@ async def test_if_fires_on_entities_change_overlap(hass: HomeAssistant, calls) - ) await hass.async_block_till_done() - utcnow = dt_util.utcnow() - with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow - hass.states.async_set("test.entity_1", "world") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", "world") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", "hello") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", "world") - await hass.async_block_till_done() - assert len(calls) == 0 - mock_utcnow.return_value += timedelta(seconds=3) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1" + hass.states.async_set("test.entity_1", "world") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", "world") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", "hello") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", "world") + await hass.async_block_till_done() + assert len(calls) == 0 + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" - mock_utcnow.return_value += timedelta(seconds=3) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2" + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" async def test_if_fires_on_change_with_for_template_1( @@ -1402,7 +1388,7 @@ async def test_invalid_for_template_1(hass: HomeAssistant, calls) -> None: async def test_if_fires_on_entities_change_overlap_for_template( - hass: HomeAssistant, calls + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls ) -> None: """Test for firing on entities change with overlap and for template.""" assert await async_setup_component( @@ -1428,39 +1414,36 @@ async def test_if_fires_on_entities_change_overlap_for_template( ) await hass.async_block_till_done() - utcnow = dt_util.utcnow() - with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow - hass.states.async_set("test.entity_1", "world") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", "world") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", "hello") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", "world") - await hass.async_block_till_done() - assert len(calls) == 0 - mock_utcnow.return_value += timedelta(seconds=3) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1 - 0:00:05" + hass.states.async_set("test.entity_1", "world") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", "world") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", "hello") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", "world") + await hass.async_block_till_done() + assert len(calls) == 0 + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1 - 0:00:05" - mock_utcnow.return_value += timedelta(seconds=3) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 - mock_utcnow.return_value += timedelta(seconds=5) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2 - 0:00:10" + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 + freezer.tick(timedelta(seconds=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2 - 0:00:10" async def test_attribute_if_fires_on_entity_change_with_both_filters( @@ -1702,7 +1685,9 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters_boolean( assert len(calls) == 1 -async def test_variables_priority(hass: HomeAssistant, calls) -> None: +async def test_variables_priority( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls +) -> None: """Test an externally defined trigger variable is overridden.""" assert await async_setup_component( hass, @@ -1728,36 +1713,33 @@ async def test_variables_priority(hass: HomeAssistant, calls) -> None: ) await hass.async_block_till_done() - utcnow = dt_util.utcnow() - with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: - mock_utcnow.return_value = utcnow - hass.states.async_set("test.entity_1", "world") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", "world") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", "hello") - await hass.async_block_till_done() - mock_utcnow.return_value += timedelta(seconds=1) - async_fire_time_changed(hass, mock_utcnow.return_value) - hass.states.async_set("test.entity_2", "world") - await hass.async_block_till_done() - assert len(calls) == 0 - mock_utcnow.return_value += timedelta(seconds=3) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "test.entity_1 - 0:00:05" + hass.states.async_set("test.entity_1", "world") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", "world") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", "hello") + await hass.async_block_till_done() + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + hass.states.async_set("test.entity_2", "world") + await hass.async_block_till_done() + assert len(calls) == 0 + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1 - 0:00:05" - mock_utcnow.return_value += timedelta(seconds=3) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 1 - mock_utcnow.return_value += timedelta(seconds=5) - async_fire_time_changed(hass, mock_utcnow.return_value) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "test.entity_2 - 0:00:10" + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 1 + freezer.tick(timedelta(seconds=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2 - 0:00:10" From 97fd73f9f7cea8e88eb96042244850f24769d250 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 28 Aug 2023 23:14:07 +0200 Subject: [PATCH 0980/1151] Bump syrupy to 4.2.1 (#99156) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index de135e4a997..a2533d0ef2b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -31,7 +31,7 @@ pytest-xdist==3.3.1 pytest==7.3.1 requests_mock==1.11.0 respx==0.20.2 -syrupy==4.0.8 +syrupy==4.2.1 tqdm==4.66.1 types-atomicwrites==1.4.5.1 types-croniter==1.0.6 From 23839a7f10d6d1c1faff6e88d28cb777705f7e70 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:24:12 -0400 Subject: [PATCH 0981/1151] Wrap most ZHA exceptions in `HomeAssistantError` (#98421) * Wrap attribute writes in a helper throwing `HomeAssistantError` * Do not check for `Exception` instances, they are now propagated * Write `cie_addr` synchronously * Fix unnecessary `if` in `async_set_native_value` * Fix unit tests * Use `HomeAssistantError` in cover commands * Revert writing `cie_addr` synchronously * Disallow proxying of some cluster methods to fix unit test warnings * Unit test cover failures to increase coverage * Unit test missing climate device * Unit test remaining cover commands --- homeassistant/components/zha/button.py | 18 +- homeassistant/components/zha/climate.py | 103 +++++------ .../zha/core/cluster_handlers/__init__.py | 66 +++++-- .../zha/core/cluster_handlers/general.py | 37 +--- .../zha/core/cluster_handlers/hvac.py | 66 +------ .../cluster_handlers/manufacturerspecific.py | 8 +- .../zha/core/cluster_handlers/security.py | 19 +- homeassistant/components/zha/cover.py | 47 ++--- homeassistant/components/zha/fan.py | 8 +- homeassistant/components/zha/light.py | 16 +- homeassistant/components/zha/lock.py | 4 +- homeassistant/components/zha/number.py | 23 +-- homeassistant/components/zha/select.py | 2 +- homeassistant/components/zha/switch.py | 28 +-- tests/components/zha/common.py | 5 +- tests/components/zha/conftest.py | 12 ++ tests/components/zha/test_button.py | 28 ++- tests/components/zha/test_climate.py | 143 +++++++++++++-- tests/components/zha/test_cover.py | 172 +++++++++++++++++- tests/components/zha/test_fan.py | 54 ++++-- tests/components/zha/test_number.py | 118 ++++++------ tests/components/zha/test_switch.py | 50 ++--- 22 files changed, 628 insertions(+), 399 deletions(-) diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index b3b6e7f0483..7a4132115b8 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -6,9 +6,6 @@ import functools import logging from typing import TYPE_CHECKING, Any, Self -import zigpy.exceptions -from zigpy.zcl.foundation import Status - from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, Platform @@ -134,17 +131,10 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity): async def async_press(self) -> None: """Write attribute with defined value.""" - try: - result = await self._cluster_handler.cluster.write_attributes( - {self._attribute_name: self._attribute_value} - ) - except zigpy.exceptions.ZigbeeException as ex: - self.error("Could not set value: %s", ex) - return - if not isinstance(result, Exception) and all( - record.status == Status.SUCCESS for record in result[0] - ): - self.async_write_ha_state() + await self._cluster_handler.write_attributes_safe( + {self._attribute_name: self._attribute_value} + ) + self.async_write_ha_state() @CONFIG_DIAGNOSTIC_MATCH( diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 9f999bd52fa..cf868ef8b7b 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -416,15 +416,12 @@ class Thermostat(ZhaEntity, ClimateEntity): if self.preset_mode not in ( preset_mode, PRESET_NONE, - ) and not await self.async_preset_handler(self.preset_mode, enable=False): - self.debug("Couldn't turn off '%s' preset", self.preset_mode) - return - - if preset_mode != PRESET_NONE and not await self.async_preset_handler( - preset_mode, enable=True ): - self.debug("Couldn't turn on '%s' preset", preset_mode) - return + await self.async_preset_handler(self.preset_mode, enable=False) + + if preset_mode != PRESET_NONE: + await self.async_preset_handler(preset_mode, enable=True) + self._preset = preset_mode self.async_write_ha_state() @@ -438,30 +435,29 @@ class Thermostat(ZhaEntity, ClimateEntity): if hvac_mode is not None: await self.async_set_hvac_mode(hvac_mode) - thrm = self._thrm + is_away = self.preset_mode == PRESET_AWAY + if self.hvac_mode == HVACMode.HEAT_COOL: - success = True if low_temp is not None: - low_temp = int(low_temp * ZCL_TEMP) - success = success and await thrm.async_set_heating_setpoint( - low_temp, self.preset_mode == PRESET_AWAY + await self._thrm.async_set_heating_setpoint( + temperature=int(low_temp * ZCL_TEMP), + is_away=is_away, ) - self.debug("Setting heating %s setpoint: %s", low_temp, success) if high_temp is not None: - high_temp = int(high_temp * ZCL_TEMP) - success = success and await thrm.async_set_cooling_setpoint( - high_temp, self.preset_mode == PRESET_AWAY + await self._thrm.async_set_cooling_setpoint( + temperature=int(high_temp * ZCL_TEMP), + is_away=is_away, ) - self.debug("Setting cooling %s setpoint: %s", low_temp, success) elif temp is not None: - temp = int(temp * ZCL_TEMP) if self.hvac_mode == HVACMode.COOL: - success = await thrm.async_set_cooling_setpoint( - temp, self.preset_mode == PRESET_AWAY + await self._thrm.async_set_cooling_setpoint( + temperature=int(temp * ZCL_TEMP), + is_away=is_away, ) elif self.hvac_mode == HVACMode.HEAT: - success = await thrm.async_set_heating_setpoint( - temp, self.preset_mode == PRESET_AWAY + await self._thrm.async_set_heating_setpoint( + temperature=int(temp * ZCL_TEMP), + is_away=is_away, ) else: self.debug("Not setting temperature for '%s' mode", self.hvac_mode) @@ -470,14 +466,13 @@ class Thermostat(ZhaEntity, ClimateEntity): self.debug("incorrect %s setting for '%s' mode", kwargs, self.hvac_mode) return - if success: - self.async_write_ha_state() + self.async_write_ha_state() - async def async_preset_handler(self, preset: str, enable: bool = False) -> bool: + async def async_preset_handler(self, preset: str, enable: bool = False) -> None: """Set the preset mode via handler.""" handler = getattr(self, f"async_preset_handler_{preset}") - return await handler(enable) + await handler(enable) @MULTI_MATCH( @@ -529,7 +524,7 @@ class SinopeTechnologiesThermostat(Thermostat): self.debug("Updating time: %s", secs_2k) self._manufacturer_ch.cluster.create_catching_task( - self._manufacturer_ch.cluster.write_attributes( + self._manufacturer_ch.write_attributes_safe( {"secs_since_2k": secs_2k}, manufacturer=self.manufacturer ) ) @@ -544,16 +539,13 @@ class SinopeTechnologiesThermostat(Thermostat): ) self._async_update_time() - async def async_preset_handler_away(self, is_away: bool = False) -> bool: + async def async_preset_handler_away(self, is_away: bool = False) -> None: """Set occupancy.""" mfg_code = self._zha_device.manufacturer_code - res = await self._thrm.write_attributes( + await self._thrm.write_attributes_safe( {"set_occupancy": 0 if is_away else 1}, manufacturer=mfg_code ) - self.debug("set occupancy to %s. Status: %s", 0 if is_away else 1, res) - return res - @MULTI_MATCH( cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, @@ -635,40 +627,38 @@ class MoesThermostat(Thermostat): self._preset = PRESET_COMPLEX await super().async_attribute_updated(record) - async def async_preset_handler(self, preset: str, enable: bool = False) -> bool: + async def async_preset_handler(self, preset: str, enable: bool = False) -> None: """Set the preset mode.""" mfg_code = self._zha_device.manufacturer_code if not enable: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 2}, manufacturer=mfg_code ) if preset == PRESET_AWAY: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 0}, manufacturer=mfg_code ) if preset == PRESET_SCHEDULE: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 1}, manufacturer=mfg_code ) if preset == PRESET_COMFORT: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 3}, manufacturer=mfg_code ) if preset == PRESET_ECO: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 4}, manufacturer=mfg_code ) if preset == PRESET_BOOST: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 5}, manufacturer=mfg_code ) if preset == PRESET_COMPLEX: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 6}, manufacturer=mfg_code ) - return False - @STRICT_MATCH( cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, @@ -714,36 +704,34 @@ class BecaThermostat(Thermostat): self._preset = PRESET_TEMP_MANUAL await super().async_attribute_updated(record) - async def async_preset_handler(self, preset: str, enable: bool = False) -> bool: + async def async_preset_handler(self, preset: str, enable: bool = False) -> None: """Set the preset mode.""" mfg_code = self._zha_device.manufacturer_code if not enable: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 2}, manufacturer=mfg_code ) if preset == PRESET_AWAY: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 0}, manufacturer=mfg_code ) if preset == PRESET_SCHEDULE: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 1}, manufacturer=mfg_code ) if preset == PRESET_ECO: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 4}, manufacturer=mfg_code ) if preset == PRESET_BOOST: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 5}, manufacturer=mfg_code ) if preset == PRESET_TEMP_MANUAL: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 7}, manufacturer=mfg_code ) - return False - @MULTI_MATCH( cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, @@ -809,23 +797,22 @@ class ZONNSMARTThermostat(Thermostat): self._preset = self.PRESET_FROST await super().async_attribute_updated(record) - async def async_preset_handler(self, preset: str, enable: bool = False) -> bool: + async def async_preset_handler(self, preset: str, enable: bool = False) -> None: """Set the preset mode.""" mfg_code = self._zha_device.manufacturer_code if not enable: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 1}, manufacturer=mfg_code ) if preset == PRESET_SCHEDULE: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 0}, manufacturer=mfg_code ) if preset == self.PRESET_HOLIDAY: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 3}, manufacturer=mfg_code ) if preset == self.PRESET_FROST: - return await self._thrm.write_attributes( + return await self._thrm.write_attributes_safe( {"operation_preset": 4}, manufacturer=mfg_code ) - return False diff --git a/homeassistant/components/zha/core/cluster_handlers/__init__.py b/homeassistant/components/zha/core/cluster_handlers/__init__.py index 6c05ce2fe4f..2b78c90aa19 100644 --- a/homeassistant/components/zha/core/cluster_handlers/__init__.py +++ b/homeassistant/components/zha/core/cluster_handlers/__init__.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Coroutine +from collections.abc import Awaitable, Callable, Coroutine, Iterator +import contextlib from enum import Enum import functools import logging @@ -48,6 +49,7 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) RETRYABLE_REQUEST_DECORATOR = zigpy.util.retryable_request(tries=3) +UNPROXIED_CLUSTER_METHODS = {"general_command"} _P = ParamSpec("_P") @@ -55,24 +57,31 @@ _FuncType = Callable[_P, Awaitable[Any]] _ReturnFuncType = Callable[_P, Coroutine[Any, Any, Any]] +@contextlib.contextmanager +def wrap_zigpy_exceptions() -> Iterator[None]: + """Wrap zigpy exceptions in `HomeAssistantError` exceptions.""" + try: + yield + except asyncio.TimeoutError as exc: + raise HomeAssistantError( + "Failed to send request: device did not respond" + ) from exc + except zigpy.exceptions.ZigbeeException as exc: + message = "Failed to send request" + + if str(exc): + message = f"{message}: {exc}" + + raise HomeAssistantError(message) from exc + + def retry_request(func: _FuncType[_P]) -> _ReturnFuncType[_P]: """Send a request with retries and wrap expected zigpy exceptions.""" @functools.wraps(func) async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> Any: - try: + with wrap_zigpy_exceptions(): return await RETRYABLE_REQUEST_DECORATOR(func)(*args, **kwargs) - except asyncio.TimeoutError as exc: - raise HomeAssistantError( - "Failed to send request: device did not respond" - ) from exc - except zigpy.exceptions.ZigbeeException as exc: - message = "Failed to send request" - - if str(exc): - message = f"{message}: {exc}" - - raise HomeAssistantError(message) from exc return wrapper @@ -501,6 +510,26 @@ class ClusterHandler(LogMixin): get_attributes = functools.partialmethod(_get_attributes, False) + async def write_attributes_safe( + self, attributes: dict[str, Any], manufacturer: int | None = None + ) -> None: + """Wrap `write_attributes` to throw an exception on attribute write failure.""" + + res = await self.write_attributes(attributes, manufacturer=manufacturer) + + for record in res[0]: + if record.status != Status.SUCCESS: + try: + name = self.cluster.attributes[record.attrid].name + value = attributes.get(name, "unknown") + except KeyError: + name = f"0x{record.attrid:04x}" + value = "unknown" + + raise HomeAssistantError( + f"Failed to write attribute {name}={value}: {record.status}", + ) + def log(self, level, msg, *args, **kwargs): """Log a message.""" msg = f"[%s:%s]: {msg}" @@ -509,11 +538,16 @@ class ClusterHandler(LogMixin): def __getattr__(self, name): """Get attribute or a decorated cluster command.""" - if hasattr(self._cluster, name) and callable(getattr(self._cluster, name)): + if ( + hasattr(self._cluster, name) + and callable(getattr(self._cluster, name)) + and name not in UNPROXIED_CLUSTER_METHODS + ): command = getattr(self._cluster, name) - command.__name__ = name + wrapped_command = retry_request(command) + wrapped_command.__name__ = name - return retry_request(command) + return wrapped_command return self.__getattribute__(name) diff --git a/homeassistant/components/zha/core/cluster_handlers/general.py b/homeassistant/components/zha/core/cluster_handlers/general.py index bd66b0f6c63..6ca4e420d5f 100644 --- a/homeassistant/components/zha/core/cluster_handlers/general.py +++ b/homeassistant/components/zha/core/cluster_handlers/general.py @@ -1,7 +1,6 @@ """General cluster handlers module for Zigbee Home Automation.""" from __future__ import annotations -import asyncio from collections.abc import Coroutine from typing import TYPE_CHECKING, Any @@ -12,6 +11,7 @@ from zigpy.zcl.clusters import general from zigpy.zcl.foundation import Status from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_call_later from .. import registries @@ -111,18 +111,9 @@ class AnalogOutput(ClusterHandler): """Return cached value of application_type.""" return self.cluster.get("application_type") - async def async_set_present_value(self, value: float) -> bool: + async def async_set_present_value(self, value: float) -> None: """Update present_value.""" - try: - res = await self.cluster.write_attributes({"present_value": value}) - except zigpy.exceptions.ZigbeeException as ex: - self.error("Could not set value: %s", ex) - return False - if not isinstance(res, Exception) and all( - record.status == Status.SUCCESS for record in res[0] - ): - return True - return False + await self.write_attributes_safe({"present_value": value}) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.AnalogValue.cluster_id) @@ -392,21 +383,19 @@ class OnOffClusterHandler(ClusterHandler): """Return cached value of on/off attribute.""" return self.cluster.get("on_off") - async def turn_on(self) -> bool: + async def turn_on(self) -> None: """Turn the on off cluster on.""" result = await self.on() - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: - return False + if result[1] is not Status.SUCCESS: + raise HomeAssistantError(f"Failed to turn on: {result[1]}") self.cluster.update_attribute(self.ON_OFF, t.Bool.true) - return True - async def turn_off(self) -> bool: + async def turn_off(self) -> None: """Turn the on off cluster off.""" result = await self.off() - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: - return False + if result[1] is not Status.SUCCESS: + raise HomeAssistantError(f"Failed to turn off: {result[1]}") self.cluster.update_attribute(self.ON_OFF, t.Bool.false) - return True @callback def cluster_command(self, tsn, command_id, args): @@ -508,13 +497,7 @@ class PollControl(ClusterHandler): async def async_configure_cluster_handler_specific(self) -> None: """Configure cluster handler: set check-in interval.""" - try: - res = await self.cluster.write_attributes( - {"checkin_interval": self.CHECKIN_INTERVAL} - ) - self.debug("%ss check-in interval set: %s", self.CHECKIN_INTERVAL / 4, res) - except (asyncio.TimeoutError, zigpy.exceptions.ZigbeeException) as ex: - self.debug("Couldn't set check-in interval: %s", ex) + await self.write_attributes_safe({"checkin_interval": self.CHECKIN_INTERVAL}) @callback def cluster_command( diff --git a/homeassistant/components/zha/core/cluster_handlers/hvac.py b/homeassistant/components/zha/core/cluster_handlers/hvac.py index cbc56f5acc5..15050ce67b1 100644 --- a/homeassistant/components/zha/core/cluster_handlers/hvac.py +++ b/homeassistant/components/zha/core/cluster_handlers/hvac.py @@ -8,9 +8,7 @@ from __future__ import annotations from collections import namedtuple from typing import Any -from zigpy.exceptions import ZigbeeException from zigpy.zcl.clusters import hvac -from zigpy.zcl.foundation import Status from homeassistant.core import callback @@ -55,12 +53,7 @@ class FanClusterHandler(ClusterHandler): async def async_set_speed(self, value) -> None: """Set the speed of the fan.""" - - try: - await self.cluster.write_attributes({"fan_mode": value}) - except ZigbeeException as ex: - self.error("Could not set speed: %s", ex) - return + await self.write_attributes_safe({"fan_mode": value}) async def async_update(self) -> None: """Retrieve latest state.""" @@ -247,71 +240,32 @@ class ThermostatClusterHandler(ClusterHandler): async def async_set_operation_mode(self, mode) -> bool: """Set Operation mode.""" - if not await self.write_attributes({"system_mode": mode}): - self.debug("couldn't set '%s' operation mode", mode) - return False - - self.debug("set system to %s", mode) + await self.write_attributes_safe({"system_mode": mode}) return True async def async_set_heating_setpoint( self, temperature: int, is_away: bool = False ) -> bool: """Set heating setpoint.""" - if is_away: - data = {"unoccupied_heating_setpoint": temperature} - else: - data = {"occupied_heating_setpoint": temperature} - if not await self.write_attributes(data): - self.debug("couldn't set heating setpoint") - return False - + attr = "unoccupied_heating_setpoint" if is_away else "occupied_heating_setpoint" + await self.write_attributes_safe({attr: temperature}) return True async def async_set_cooling_setpoint( self, temperature: int, is_away: bool = False ) -> bool: """Set cooling setpoint.""" - if is_away: - data = {"unoccupied_cooling_setpoint": temperature} - else: - data = {"occupied_cooling_setpoint": temperature} - if not await self.write_attributes(data): - self.debug("couldn't set cooling setpoint") - return False - self.debug("set cooling setpoint to %s", temperature) + attr = "unoccupied_cooling_setpoint" if is_away else "occupied_cooling_setpoint" + await self.write_attributes_safe({attr: temperature}) return True async def get_occupancy(self) -> bool | None: """Get unreportable occupancy attribute.""" - try: - res, fail = await self.cluster.read_attributes(["occupancy"]) - self.debug("read 'occupancy' attr, success: %s, fail: %s", res, fail) - if "occupancy" not in res: - return None - return bool(self.occupancy) - except ZigbeeException as ex: - self.debug("Couldn't read 'occupancy' attribute: %s", ex) + res, fail = await self.read_attributes(["occupancy"]) + self.debug("read 'occupancy' attr, success: %s, fail: %s", res, fail) + if "occupancy" not in res: return None - - async def write_attributes(self, data, **kwargs): - """Write attributes helper.""" - try: - res = await self.cluster.write_attributes(data, **kwargs) - except ZigbeeException as exc: - self.debug("couldn't write %s: %s", data, exc) - return False - - self.debug("wrote %s attrs, Status: %s", data, res) - return self.check_result(res) - - @staticmethod - def check_result(res: list) -> bool: - """Normalize the result.""" - if isinstance(res, Exception): - return False - - return all(record.status == Status.SUCCESS for record in res[0]) + return bool(self.occupancy) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(hvac.UserInterface.cluster_id) diff --git a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py b/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py index 450a1aeec97..f2e5dafa099 100644 --- a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py +++ b/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py @@ -5,7 +5,6 @@ import logging from typing import TYPE_CHECKING, Any from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType -from zigpy.exceptions import ZigbeeException import zigpy.zcl from homeassistant.core import callback @@ -351,12 +350,7 @@ class IkeaAirPurifierClusterHandler(ClusterHandler): async def async_set_speed(self, value) -> None: """Set the speed of the fan.""" - - try: - await self.cluster.write_attributes({"fan_mode": value}) - except ZigbeeException as ex: - self.error("Could not set speed: %s", ex) - return + await self.write_attributes_safe({"fan_mode": value}) async def async_update(self) -> None: """Retrieve latest state.""" diff --git a/homeassistant/components/zha/core/cluster_handlers/security.py b/homeassistant/components/zha/core/cluster_handlers/security.py index 28e2d863662..f31830f0bd8 100644 --- a/homeassistant/components/zha/core/cluster_handlers/security.py +++ b/homeassistant/components/zha/core/cluster_handlers/security.py @@ -8,12 +8,12 @@ from __future__ import annotations from collections.abc import Callable from typing import TYPE_CHECKING, Any -from zigpy.exceptions import ZigbeeException import zigpy.zcl from zigpy.zcl.clusters import security from zigpy.zcl.clusters.security import IasAce as AceCluster, IasZone from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from .. import registries from ..const import ( @@ -350,8 +350,11 @@ class IASZoneClusterHandler(ClusterHandler): self.debug("Updated alarm state: %s", zone_status) elif command_id == 1: self.debug("Enroll requested") - res = self._cluster.enroll_response(0, 0) - self._cluster.create_catching_task(res) + self._cluster.create_catching_task( + self.enroll_response( + enroll_response_code=IasZone.EnrollResponse.Success, zone_id=0 + ) + ) async def async_configure(self): """Configure IAS device.""" @@ -366,14 +369,14 @@ class IASZoneClusterHandler(ClusterHandler): ieee = self.cluster.endpoint.device.application.state.node_info.ieee try: - res = await self._cluster.write_attributes({"cie_addr": ieee}) + res = await self.write_attributes_safe({"cie_addr": ieee}) self.debug( "wrote cie_addr: %s to '%s' cluster: %s", str(ieee), self._cluster.ep_attribute, res[0], ) - except ZigbeeException as ex: + except HomeAssistantError as ex: self.debug( "Failed to write cie_addr: %s to '%s' cluster: %s", str(ieee), @@ -382,7 +385,11 @@ class IASZoneClusterHandler(ClusterHandler): ) self.debug("Sending pro-active IAS enroll response") - self._cluster.create_catching_task(self._cluster.enroll_response(0, 0)) + self._cluster.create_catching_task( + self.enroll_response( + enroll_response_code=IasZone.EnrollResponse.Success, zone_id=0 + ) + ) self._status = ClusterHandlerStatus.CONFIGURED self.debug("finished IASZoneClusterHandler configuration") diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 4d76ea27897..0d7062173ca 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -23,6 +23,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -139,30 +140,34 @@ class ZhaCover(ZhaEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the window cover.""" res = await self._cover_cluster_handler.up_open() - if not isinstance(res, Exception) and res[1] is Status.SUCCESS: - self.async_update_state(STATE_OPENING) + if res[1] is not Status.SUCCESS: + raise HomeAssistantError(f"Failed to open cover: {res[1]}") + self.async_update_state(STATE_OPENING) async def async_close_cover(self, **kwargs: Any) -> None: """Close the window cover.""" res = await self._cover_cluster_handler.down_close() - if not isinstance(res, Exception) and res[1] is Status.SUCCESS: - self.async_update_state(STATE_CLOSING) + if res[1] is not Status.SUCCESS: + raise HomeAssistantError(f"Failed to close cover: {res[1]}") + self.async_update_state(STATE_CLOSING) async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the roller shutter to a specific position.""" new_pos = kwargs[ATTR_POSITION] res = await self._cover_cluster_handler.go_to_lift_percentage(100 - new_pos) - if not isinstance(res, Exception) and res[1] is Status.SUCCESS: - self.async_update_state( - STATE_CLOSING if new_pos < self._current_position else STATE_OPENING - ) + if res[1] is not Status.SUCCESS: + raise HomeAssistantError(f"Failed to set cover position: {res[1]}") + self.async_update_state( + STATE_CLOSING if new_pos < self._current_position else STATE_OPENING + ) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the window cover.""" res = await self._cover_cluster_handler.stop() - if not isinstance(res, Exception) and res[1] is Status.SUCCESS: - self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED - self.async_write_ha_state() + if res[1] is not Status.SUCCESS: + raise HomeAssistantError(f"Failed to stop cover: {res[1]}") + self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED + self.async_write_ha_state() async def async_update(self) -> None: """Attempt to retrieve the open/close state of the cover.""" @@ -265,9 +270,8 @@ class Shade(ZhaEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the window cover.""" res = await self._on_off_cluster_handler.on() - if isinstance(res, Exception) or res[1] != Status.SUCCESS: - self.debug("couldn't open cover: %s", res) - return + if res[1] != Status.SUCCESS: + raise HomeAssistantError(f"Failed to open cover: {res[1]}") self._is_open = True self.async_write_ha_state() @@ -275,9 +279,8 @@ class Shade(ZhaEntity, CoverEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Close the window cover.""" res = await self._on_off_cluster_handler.off() - if isinstance(res, Exception) or res[1] != Status.SUCCESS: - self.debug("couldn't open cover: %s", res) - return + if res[1] != Status.SUCCESS: + raise HomeAssistantError(f"Failed to close cover: {res[1]}") self._is_open = False self.async_write_ha_state() @@ -289,9 +292,8 @@ class Shade(ZhaEntity, CoverEntity): new_pos * 255 / 100, 1 ) - if isinstance(res, Exception) or res[1] != Status.SUCCESS: - self.debug("couldn't set cover's position: %s", res) - return + if res[1] != Status.SUCCESS: + raise HomeAssistantError(f"Failed to set cover position: {res[1]}") self._position = new_pos self.async_write_ha_state() @@ -299,9 +301,8 @@ class Shade(ZhaEntity, CoverEntity): async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" res = await self._level_cluster_handler.stop() - if isinstance(res, Exception) or res[1] != Status.SUCCESS: - self.debug("couldn't stop cover: %s", res) - return + if res[1] != Status.SUCCESS: + raise HomeAssistantError(f"Failed to stop cover: {res[1]}") @MULTI_MATCH( diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 82725accfa4..a24272c9a7a 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -6,7 +6,6 @@ import functools import math from typing import Any -from zigpy.exceptions import ZigbeeException from zigpy.zcl.clusters import hvac from homeassistant.components.fan import ( @@ -28,6 +27,7 @@ from homeassistant.util.percentage import ( ) from .core import discovery +from .core.cluster_handlers import wrap_zigpy_exceptions from .core.const import ( CLUSTER_HANDLER_FAN, DATA_ZHA, @@ -207,10 +207,10 @@ class FanGroup(BaseFan, ZhaGroupEntity): async def _async_set_fan_mode(self, fan_mode: int) -> None: """Set the fan mode for the group.""" - try: + + with wrap_zigpy_exceptions(): await self._fan_cluster_handler.write_attributes({"fan_mode": fan_mode}) - except ZigbeeException as ex: - self.error("Could not set fan mode: %s", ex) + self.async_set_state(0, "fan_mode", fan_mode) async def async_update(self) -> None: diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6331b192859..2ec42431498 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -298,7 +298,7 @@ class BaseLight(LogMixin, light.LightEntity): transition_time=int(10 * self._DEFAULT_MIN_TRANSITION_TIME), ) t_log["move_to_level_with_on_off"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + if result[1] is not Status.SUCCESS: # First 'move to level' call failed, so if the transitioning delay # isn't running from a previous call, # the flag can be unset immediately @@ -338,7 +338,7 @@ class BaseLight(LogMixin, light.LightEntity): transition_time=int(10 * duration), ) t_log["move_to_level_with_on_off"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + if result[1] is not Status.SUCCESS: # First 'move to level' call failed, so if the transitioning delay # isn't running from a previous call, the flag can be unset immediately if set_transition_flag and not self._transition_listener: @@ -359,7 +359,7 @@ class BaseLight(LogMixin, light.LightEntity): # if brightness is not 0. result = await self._on_off_cluster_handler.on() t_log["on_off"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + if result[1] is not Status.SUCCESS: # 'On' call failed, but as brightness may still transition # (for FORCE_ON lights), we start the timer to unset the flag after # the transition_time if necessary. @@ -391,7 +391,7 @@ class BaseLight(LogMixin, light.LightEntity): level=level, transition_time=int(10 * duration) ) t_log["move_to_level_if_color"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + if result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._attr_state = bool(level) @@ -474,7 +474,7 @@ class BaseLight(LogMixin, light.LightEntity): if self._zha_config_enable_light_transitioning_flag: self.async_transition_start_timer(transition_time) self.debug("turned off: %s", result) - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + if result[1] is not Status.SUCCESS: return self._attr_state = False @@ -514,7 +514,7 @@ class BaseLight(LogMixin, light.LightEntity): transition_time=int(10 * transition_time), ) t_log["move_to_color_temp"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + if result[1] is not Status.SUCCESS: return False self._attr_color_mode = ColorMode.COLOR_TEMP self._attr_color_temp = temperature @@ -539,7 +539,7 @@ class BaseLight(LogMixin, light.LightEntity): transition_time=int(10 * transition_time), ) t_log["move_to_hue_and_saturation"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + if result[1] is not Status.SUCCESS: return False self._attr_color_mode = ColorMode.HS self._attr_hs_color = hs_color @@ -554,7 +554,7 @@ class BaseLight(LogMixin, light.LightEntity): transition_time=int(10 * transition_time), ) t_log["move_to_color"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + if result[1] is not Status.SUCCESS: return False self._attr_color_mode = ColorMode.XY self._attr_xy_color = xy_color diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 2f6bce0b20e..1e68e95c881 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -132,7 +132,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" result = await self._doorlock_cluster_handler.lock_door() - if isinstance(result, Exception) or result[0] is not Status.SUCCESS: + if result[0] is not Status.SUCCESS: self.error("Error with lock_door: %s", result) return self.async_write_ha_state() @@ -140,7 +140,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" result = await self._doorlock_cluster_handler.unlock_door() - if isinstance(result, Exception) or result[0] is not Status.SUCCESS: + if result[0] is not Status.SUCCESS: self.error("Error with unlock_door: %s", result) return self.async_write_ha_state() diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 807a5e73d00..c12060eb2a8 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -5,9 +5,6 @@ import functools import logging from typing import TYPE_CHECKING, Any, Self -import zigpy.exceptions -from zigpy.zcl.foundation import Status - from homeassistant.components.number import NumberEntity, NumberMode from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, Platform, UnitOfMass, UnitOfTemperature @@ -362,9 +359,8 @@ class ZhaNumber(ZhaEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Update the current value from HA.""" - num_value = float(value) - if await self._analog_output_cluster_handler.async_set_present_value(num_value): - self.async_write_ha_state() + await self._analog_output_cluster_handler.async_set_present_value(float(value)) + self.async_write_ha_state() async def async_update(self) -> None: """Attempt to retrieve the state of the entity.""" @@ -434,17 +430,10 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Update the current value from HA.""" - try: - res = await self._cluster_handler.cluster.write_attributes( - {self._zcl_attribute: int(value / self._attr_multiplier)} - ) - except zigpy.exceptions.ZigbeeException as ex: - self.error("Could not set value: %s", ex) - return - if not isinstance(res, Exception) and all( - record.status == Status.SUCCESS for record in res[0] - ): - self.async_write_ha_state() + await self._cluster_handler.write_attributes_safe( + {self._zcl_attribute: int(value / self._attr_multiplier)} + ) + self.async_write_ha_state() async def async_update(self) -> None: """Attempt to retrieve the state of the entity.""" diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index e6f2f6ab482..018f24675e7 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -210,7 +210,7 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await self._cluster_handler.cluster.write_attributes( + await self._cluster_handler.write_attributes_safe( {self._select_attr: self._enum[option.replace(" ", "_")]} ) self.async_write_ha_state() diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index f975cc5116d..8707dda629f 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -5,7 +5,6 @@ import functools import logging from typing import TYPE_CHECKING, Any, Self -import zigpy.exceptions from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status @@ -85,16 +84,12 @@ class Switch(ZhaEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - result = await self._on_off_cluster_handler.turn_on() - if not result: - return + await self._on_off_cluster_handler.turn_on() self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - result = await self._on_off_cluster_handler.turn_off() - if not result: - return + await self._on_off_cluster_handler.turn_off() self.async_write_ha_state() @callback @@ -145,7 +140,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" result = await self._on_off_cluster_handler.on() - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + if result[1] is not Status.SUCCESS: return self._state = True self.async_write_ha_state() @@ -153,7 +148,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" result = await self._on_off_cluster_handler.off() - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + if result[1] is not Status.SUCCESS: return self._state = False self.async_write_ha_state() @@ -241,17 +236,10 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): async def async_turn_on_off(self, state: bool) -> None: """Turn the entity on or off.""" - try: - result = await self._cluster_handler.cluster.write_attributes( - {self._zcl_attribute: not state if self.inverted else state} - ) - except zigpy.exceptions.ZigbeeException as ex: - self.error("Could not set value: %s", ex) - return - if not isinstance(result, Exception) and all( - record.status == Status.SUCCESS for record in result[0] - ): - self.async_write_ha_state() + await self._cluster_handler.write_attributes_safe( + {self._zcl_attribute: not state if self.inverted else state} + ) + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 79c319398f0..01206c432e6 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -173,9 +173,8 @@ def async_find_group_entity_id(hass, domain, group): entity_ids = hass.states.async_entity_ids(domain) - if entity_id in entity_ids: - return entity_id - return None + assert entity_id in entity_ids + return entity_id async def async_enable_traffic(hass, zha_devices, enabled=True): diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index e3a12703640..dd2c200973c 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -15,6 +15,7 @@ import zigpy.group import zigpy.profiles import zigpy.quirks import zigpy.types +import zigpy.util import zigpy.zdo.types as zdo_t import homeassistant.components.zha.core.const as zha_const @@ -30,6 +31,17 @@ FIXTURE_GRP_ID = 0x1001 FIXTURE_GRP_NAME = "fixture group" +@pytest.fixture(scope="session", autouse=True) +def disable_request_retry_delay(): + """Disable ZHA request retrying delay to speed up failures.""" + + with patch( + "homeassistant.components.zha.core.cluster_handlers.RETRYABLE_REQUEST_DECORATOR", + zigpy.util.retryable_request(tries=3, delay=0), + ): + yield + + @pytest.fixture(scope="session", autouse=True) def globally_load_quirks(): """Load quirks automatically so that ZHA tests run deterministically in isolation. diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index 461e592ef85..cc0b5079fd3 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -30,6 +30,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .common import find_entity_id @@ -198,8 +199,9 @@ async def test_frost_unlock(hass: HomeAssistant, tuya_water_valve) -> None: blocking=True, ) await hass.async_block_till_done() - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"frost_lock_reset": 0}) + assert cluster.write_attributes.mock_calls == [ + call({"frost_lock_reset": 0}, manufacturer=None) + ] state = hass.states.get(entity_id) assert state @@ -208,11 +210,17 @@ async def test_frost_unlock(hass: HomeAssistant, tuya_water_valve) -> None: cluster.write_attributes.reset_mock() cluster.write_attributes.side_effect = ZigbeeException - await hass.services.async_call( - DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"frost_lock_reset": 0}) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + # There are three retries + assert cluster.write_attributes.mock_calls == [ + call({"frost_lock_reset": 0}, manufacturer=None), + call({"frost_lock_reset": 0}, manufacturer=None), + call({"frost_lock_reset": 0}, manufacturer=None), + ] diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index fd8bcaa1085..145aba799ca 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -1,8 +1,10 @@ """Test ZHA climate.""" -from unittest.mock import patch +from typing import Literal +from unittest.mock import call, patch import pytest import zhaquirks.sinope.thermostat +from zhaquirks.sinope.thermostat import SinopeTechnologiesThermostatCluster import zhaquirks.tuya.ts0601_trv import zigpy.profiles import zigpy.types @@ -37,7 +39,12 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.components.zha.climate import HVAC_MODE_2_SYSTEM, SEQ_OF_OPERATION -from homeassistant.components.zha.core.const import PRESET_COMPLEX, PRESET_SCHEDULE +from homeassistant.components.zha.core.const import ( + PRESET_COMPLEX, + PRESET_SCHEDULE, + PRESET_TEMP_MANUAL, +) +from homeassistant.components.zha.core.device import ZHADevice from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -45,6 +52,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .common import async_enable_traffic, find_entity_id, send_attributes_report from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -129,6 +137,23 @@ CLIMATE_MOES = { } } +CLIMATE_BECA = { + 1: { + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SMART_PLUG, + SIG_EP_INPUT: [ + zigpy.zcl.clusters.general.Basic.cluster_id, + zigpy.zcl.clusters.general.Groups.cluster_id, + zigpy.zcl.clusters.general.Scenes.cluster_id, + 61148, + ], + SIG_EP_OUTPUT: [ + zigpy.zcl.clusters.general.Time.cluster_id, + zigpy.zcl.clusters.general.Ota.cluster_id, + ], + } +} + CLIMATE_ZONNSMART = { 1: { SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, @@ -146,6 +171,7 @@ CLIMATE_ZONNSMART = { MANUF_SINOPE = "Sinope Technologies" MANUF_ZEN = "Zen Within" MANUF_MOES = "_TZE200_ckud7u2l" +MANUF_BECA = "_TZE200_b6wax7g0" MANUF_ZONNSMART = "_TZE200_hue3yfsn" ZCL_ATTR_PLUG = { @@ -257,6 +283,17 @@ async def device_climate_moes(device_climate_mock): ) +@pytest.fixture +async def device_climate_beca(device_climate_mock) -> ZHADevice: + """Beca thermostat.""" + + return await device_climate_mock( + CLIMATE_BECA, + manuf=MANUF_BECA, + quirk=zhaquirks.tuya.ts0601_trv.MoesHY368_Type1new, + ) + + @pytest.fixture async def device_climate_zonnsmart(device_climate_mock): """ZONNSMART thermostat.""" @@ -553,7 +590,11 @@ async def test_hvac_modes( ), ) async def test_target_temperature( - hass: HomeAssistant, device_climate_mock, sys_mode, preset, target_temp + hass: HomeAssistant, + device_climate_mock, + sys_mode: Thermostat.SystemMode, + preset: Literal[PRESET_AWAY] | None, + target_temp: int, ) -> None: """Test target temperature property.""" @@ -720,15 +761,23 @@ async def test_preset_setting(hass: HomeAssistant, device_climate_sinope) -> Non # unsuccessful occupancy change thrm_cluster.write_attributes.return_value = [ - zcl_f.WriteAttributesResponse.deserialize(b"\x01\x00\x00")[0] + zcl_f.WriteAttributesResponse( + [ + zcl_f.WriteAttributesStatusRecord( + status=zcl_f.Status.FAILURE, + attrid=SinopeTechnologiesThermostatCluster.AttributeDefs.set_occupancy.id, + ) + ] + ) ] - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, - blocking=True, - ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE @@ -738,7 +787,9 @@ async def test_preset_setting(hass: HomeAssistant, device_climate_sinope) -> Non # successful occupancy change thrm_cluster.write_attributes.reset_mock() thrm_cluster.write_attributes.return_value = [ - zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0] + zcl_f.WriteAttributesResponse( + [zcl_f.WriteAttributesStatusRecord(status=zcl_f.Status.SUCCESS)] + ) ] await hass.services.async_call( CLIMATE_DOMAIN, @@ -755,14 +806,23 @@ async def test_preset_setting(hass: HomeAssistant, device_climate_sinope) -> Non # unsuccessful occupancy change thrm_cluster.write_attributes.reset_mock() thrm_cluster.write_attributes.return_value = [ - zcl_f.WriteAttributesResponse.deserialize(b"\x01\x01\x01")[0] + zcl_f.WriteAttributesResponse( + [ + zcl_f.WriteAttributesStatusRecord( + status=zcl_f.Status.FAILURE, + attrid=SinopeTechnologiesThermostatCluster.AttributeDefs.set_occupancy.id, + ) + ] + ) ] - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, - blocking=True, - ) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY @@ -772,7 +832,9 @@ async def test_preset_setting(hass: HomeAssistant, device_climate_sinope) -> Non # successful occupancy change thrm_cluster.write_attributes.reset_mock() thrm_cluster.write_attributes.return_value = [ - zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0] + zcl_f.WriteAttributesResponse( + [zcl_f.WriteAttributesStatusRecord(status=zcl_f.Status.SUCCESS)] + ) ] await hass.services.async_call( CLIMATE_DOMAIN, @@ -1386,6 +1448,49 @@ async def test_set_moes_operation_mode( assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMPLEX +@pytest.mark.parametrize( + ("preset_attr", "preset_mode"), + [ + (0, PRESET_AWAY), + (1, PRESET_SCHEDULE), + # (2, PRESET_NONE), # TODO: why does this not work? + (4, PRESET_ECO), + (5, PRESET_BOOST), + (7, PRESET_TEMP_MANUAL), + ], +) +async def test_beca_operation_mode_update( + hass: HomeAssistant, + device_climate_beca: ZHADevice, + preset_attr: int, + preset_mode: str, +) -> None: + """Test beca trv operation mode attribute update.""" + + entity_id = find_entity_id(Platform.CLIMATE, device_climate_beca, hass) + thrm_cluster = device_climate_beca.device.endpoints[1].thermostat + + # Test sending an attribute report + await send_attributes_report(hass, thrm_cluster, {"operation_preset": preset_attr}) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == preset_mode + + # Test setting the preset + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset_mode}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.mock_calls == [ + call( + {"operation_preset": preset_attr}, + manufacturer=device_climate_beca.manufacturer_code, + ) + ] + + async def test_set_zonnsmart_preset( hass: HomeAssistant, device_climate_zonnsmart ) -> None: diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 7c4198bd881..08f84613ff3 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -39,6 +39,8 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from tests.common import async_capture_events, mock_restore_cache +Default_Response = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Default_Response].schema + @pytest.fixture(autouse=True) def cover_platform_only(): @@ -206,6 +208,121 @@ async def test_cover( assert hass.states.get(entity_id).state == STATE_OPEN +async def test_cover_failures( + hass: HomeAssistant, zha_device_joined_restored, zigpy_cover_device +) -> None: + """Test ZHA cover platform failure cases.""" + + # load up cover domain + cluster = zigpy_cover_device.endpoints.get(1).window_covering + cluster.PLUGGED_ATTR_READS = {"current_position_lift_percentage": 100} + zha_device = await zha_device_joined_restored(zigpy_cover_device) + + entity_id = find_entity_id(Platform.COVER, zha_device, hass) + assert entity_id is not None + + await async_enable_traffic(hass, [zha_device], enabled=False) + # test that the cover was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + await hass.async_block_till_done() + + # test that the state has changed from unavailable to off + await send_attributes_report(hass, cluster, {0: 0, 8: 100, 1: 1}) + assert hass.states.get(entity_id).state == STATE_CLOSED + + # test to see if it opens + await send_attributes_report(hass, cluster, {0: 1, 8: 0, 1: 100}) + assert hass.states.get(entity_id).state == STATE_OPEN + + # close from UI + with patch( + "zigpy.zcl.Cluster.request", + return_value=Default_Response( + command_id=closures.WindowCovering.ServerCommandDefs.down_close.id, + status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, + ), + ): + with pytest.raises(HomeAssistantError, match=r"Failed to close cover"): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {"entity_id": entity_id}, + blocking=True, + ) + assert cluster.request.call_count == 1 + assert ( + cluster.request.call_args[0][1] + == closures.WindowCovering.ServerCommandDefs.down_close.id + ) + + # open from UI + with patch( + "zigpy.zcl.Cluster.request", + return_value=Default_Response( + command_id=closures.WindowCovering.ServerCommandDefs.up_open.id, + status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, + ), + ): + with pytest.raises(HomeAssistantError, match=r"Failed to open cover"): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {"entity_id": entity_id}, + blocking=True, + ) + assert cluster.request.call_count == 1 + assert ( + cluster.request.call_args[0][1] + == closures.WindowCovering.ServerCommandDefs.up_open.id + ) + + # set position UI + with patch( + "zigpy.zcl.Cluster.request", + return_value=Default_Response( + command_id=closures.WindowCovering.ServerCommandDefs.go_to_lift_percentage.id, + status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, + ), + ): + with pytest.raises(HomeAssistantError, match=r"Failed to set cover position"): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {"entity_id": entity_id, "position": 47}, + blocking=True, + ) + + assert cluster.request.call_count == 1 + assert ( + cluster.request.call_args[0][1] + == closures.WindowCovering.ServerCommandDefs.go_to_lift_percentage.id + ) + + # stop from UI + with patch( + "zigpy.zcl.Cluster.request", + return_value=Default_Response( + command_id=closures.WindowCovering.ServerCommandDefs.stop.id, + status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, + ), + ): + with pytest.raises(HomeAssistantError, match=r"Failed to stop cover"): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {"entity_id": entity_id}, + blocking=True, + ) + assert cluster.request.call_count == 1 + assert ( + cluster.request.call_args[0][1] + == closures.WindowCovering.ServerCommandDefs.stop.id + ) + + async def test_shade( hass: HomeAssistant, zha_device_joined_restored, zigpy_shade_device ) -> None: @@ -236,7 +353,13 @@ async def test_shade( assert hass.states.get(entity_id).state == STATE_OPEN # close from UI command fails - with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): + with patch( + "zigpy.zcl.Cluster.request", + return_value=Default_Response( + command_id=closures.WindowCovering.ServerCommandDefs.down_close.id, + status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, + ), + ): with pytest.raises(HomeAssistantError): await hass.services.async_call( COVER_DOMAIN, @@ -244,7 +367,7 @@ async def test_shade( {"entity_id": entity_id}, blocking=True, ) - assert cluster_on_off.request.call_count == 3 + assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False assert cluster_on_off.request.call_args[0][1] == 0x0000 assert hass.states.get(entity_id).state == STATE_OPEN @@ -261,7 +384,13 @@ async def test_shade( # open from UI command fails assert ATTR_CURRENT_POSITION not in hass.states.get(entity_id).attributes await send_attributes_report(hass, cluster_level, {0: 0}) - with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): + with patch( + "zigpy.zcl.Cluster.request", + return_value=Default_Response( + command_id=closures.WindowCovering.ServerCommandDefs.up_open.id, + status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, + ), + ): with pytest.raises(HomeAssistantError): await hass.services.async_call( COVER_DOMAIN, @@ -269,11 +398,35 @@ async def test_shade( {"entity_id": entity_id}, blocking=True, ) - assert cluster_on_off.request.call_count == 3 + assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False assert cluster_on_off.request.call_args[0][1] == 0x0001 assert hass.states.get(entity_id).state == STATE_CLOSED + # stop from UI command fails + with patch( + "zigpy.zcl.Cluster.request", + return_value=Default_Response( + command_id=general.LevelControl.ServerCommandDefs.stop.id, + status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, + ), + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {"entity_id": entity_id}, + blocking=True, + ) + + assert cluster_level.request.call_count == 1 + assert cluster_level.request.call_args[0][0] is False + assert ( + cluster_level.request.call_args[0][1] + == general.LevelControl.ServerCommandDefs.stop.id + ) + assert hass.states.get(entity_id).state == STATE_CLOSED + # open from UI succeeds with patch("zigpy.zcl.Cluster.request", return_value=[0x0, zcl_f.Status.SUCCESS]): await hass.services.async_call( @@ -285,7 +438,13 @@ async def test_shade( assert hass.states.get(entity_id).state == STATE_OPEN # set position UI command fails - with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): + with patch( + "zigpy.zcl.Cluster.request", + return_value=Default_Response( + command_id=closures.WindowCovering.ServerCommandDefs.go_to_lift_percentage.id, + status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, + ), + ): with pytest.raises(HomeAssistantError): await hass.services.async_call( COVER_DOMAIN, @@ -293,7 +452,8 @@ async def test_shade( {"entity_id": entity_id, "position": 47}, blocking=True, ) - assert cluster_level.request.call_count == 3 + + assert cluster_level.request.call_count == 1 assert cluster_level.request.call_args[0][0] is False assert cluster_level.request.call_args[0][1] == 0x0004 assert int(cluster_level.request.call_args[0][3] * 100 / 255) == 47 diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index f93467ed3e1..3d0b065ab18 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, call, patch import pytest import zhaquirks.ikea.starkvind +from zigpy.device import Device from zigpy.exceptions import ZigbeeException from zigpy.profiles import zha from zigpy.zcl.clusters import general, hvac @@ -17,6 +18,7 @@ from homeassistant.components.fan import ( SERVICE_SET_PRESET_MODE, NotValidPresetModeError, ) +from homeassistant.components.zha.core.device import ZHADevice from homeassistant.components.zha.core.discovery import GROUP_PROBE from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.fan import ( @@ -34,6 +36,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from .common import ( @@ -192,26 +195,30 @@ async def test_fan( # turn on from HA cluster.write_attributes.reset_mock() await async_turn_on(hass, entity_id) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"fan_mode": 2}) + assert cluster.write_attributes.mock_calls == [ + call({"fan_mode": 2}, manufacturer=None) + ] # turn off from HA cluster.write_attributes.reset_mock() await async_turn_off(hass, entity_id) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"fan_mode": 0}) + assert cluster.write_attributes.mock_calls == [ + call({"fan_mode": 0}, manufacturer=None) + ] # change speed from HA cluster.write_attributes.reset_mock() await async_set_percentage(hass, entity_id, percentage=100) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"fan_mode": 3}) + assert cluster.write_attributes.mock_calls == [ + call({"fan_mode": 3}, manufacturer=None) + ] # change preset_mode from HA cluster.write_attributes.reset_mock() await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_ON) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"fan_mode": 4}) + assert cluster.write_attributes.mock_calls == [ + call({"fan_mode": 4}, manufacturer=None) + ] # set invalid preset_mode from HA cluster.write_attributes.reset_mock() @@ -443,13 +450,14 @@ async def test_zha_group_fan_entity_failure_state( # turn on from HA group_fan_cluster.write_attributes.reset_mock() - await async_turn_on(hass, entity_id) + + with pytest.raises(HomeAssistantError): + await async_turn_on(hass, entity_id) + await hass.async_block_till_done() assert len(group_fan_cluster.write_attributes.mock_calls) == 1 assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 2} - assert "Could not set fan mode" in caplog.text - @pytest.mark.parametrize( ("plug_read", "expected_state", "expected_percentage"), @@ -557,7 +565,9 @@ def zigpy_device_ikea(zigpy_device_mock): async def test_fan_ikea( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device_ikea + hass: HomeAssistant, + zha_device_joined_restored: ZHADevice, + zigpy_device_ikea: Device, ) -> None: """Test ZHA fan Ikea platform.""" zha_device = await zha_device_joined_restored(zigpy_device_ikea) @@ -587,26 +597,30 @@ async def test_fan_ikea( # turn on from HA cluster.write_attributes.reset_mock() await async_turn_on(hass, entity_id) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"fan_mode": 1}) + assert cluster.write_attributes.mock_calls == [ + call({"fan_mode": 1}, manufacturer=None) + ] # turn off from HA cluster.write_attributes.reset_mock() await async_turn_off(hass, entity_id) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"fan_mode": 0}) + assert cluster.write_attributes.mock_calls == [ + call({"fan_mode": 0}, manufacturer=None) + ] # change speed from HA cluster.write_attributes.reset_mock() await async_set_percentage(hass, entity_id, percentage=100) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"fan_mode": 10}) + assert cluster.write_attributes.mock_calls == [ + call({"fan_mode": 10}, manufacturer=None) + ] # change preset_mode from HA cluster.write_attributes.reset_mock() await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_AUTO) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"fan_mode": 1}) + assert cluster.write_attributes.mock_calls == [ + call({"fan_mode": 1}, manufacturer=None) + ] # set invalid preset_mode from HA cluster.write_attributes.reset_mock() diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 67770efd591..3d888a57a28 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -9,8 +9,10 @@ import zigpy.zcl.clusters.lighting as lighting import zigpy.zcl.foundation as zcl_f from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN +from homeassistant.components.zha.core.device import ZHADevice from homeassistant.const import STATE_UNAVAILABLE, EntityCategory, Platform 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 @@ -160,8 +162,9 @@ async def test_number( {"entity_id": entity_id, "value": 30.0}, blocking=True, ) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call({"present_value": 30.0}) + assert cluster.write_attributes.mock_calls == [ + call({"present_value": 30.0}, manufacturer=None) + ] cluster.PLUGGED_ATTR_READS["present_value"] = 30.0 # test rejoin @@ -198,7 +201,12 @@ async def test_number( ), ) async def test_level_control_number( - hass: HomeAssistant, light, zha_device_joined, attr, initial_value, new_value + hass: HomeAssistant, + light: ZHADevice, + zha_device_joined, + attr: str, + initial_value: int, + new_value: int, ) -> None: """Test ZHA level control number entities - new join.""" @@ -217,8 +225,7 @@ async def test_level_control_number( ) assert entity_id is not None - assert level_control_cluster.read_attributes.call_count == 3 - assert ( + assert level_control_cluster.read_attributes.mock_calls == [ call( [ "on_off_transition_time", @@ -230,21 +237,13 @@ async def test_level_control_number( allow_cache=True, only_cache=False, manufacturer=None, - ) - in level_control_cluster.read_attributes.call_args_list - ) - - assert ( + ), call( ["start_up_current_level"], allow_cache=True, only_cache=False, manufacturer=None, - ) - in level_control_cluster.read_attributes.call_args_list - ) - - assert ( + ), call( [ "current_level", @@ -252,9 +251,8 @@ async def test_level_control_number( allow_cache=False, only_cache=False, manufacturer=None, - ) - in level_control_cluster.read_attributes.call_args_list - ) + ), + ] state = hass.states.get(entity_id) assert state @@ -275,10 +273,9 @@ async def test_level_control_number( blocking=True, ) - assert level_control_cluster.write_attributes.call_count == 1 - assert level_control_cluster.write_attributes.call_args[0][0] == { - attr: new_value, - } + assert level_control_cluster.write_attributes.mock_calls == [ + call({attr: new_value}, manufacturer=None) + ] state = hass.states.get(entity_id) assert state @@ -293,36 +290,34 @@ async def test_level_control_number( ) # the mocking doesn't update the attr cache so this flips back to initial value assert hass.states.get(entity_id).state == str(initial_value) - assert level_control_cluster.read_attributes.call_count == 1 - assert ( + assert level_control_cluster.read_attributes.mock_calls == [ call( - [ - attr, - ], + [attr], allow_cache=False, only_cache=False, manufacturer=None, ) - in level_control_cluster.read_attributes.call_args_list - ) + ] level_control_cluster.write_attributes.reset_mock() level_control_cluster.write_attributes.side_effect = ZigbeeException - await hass.services.async_call( - "number", - "set_value", - { - "entity_id": entity_id, - "value": new_value, - }, - blocking=True, - ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "number", + "set_value", + { + "entity_id": entity_id, + "value": new_value, + }, + blocking=True, + ) - assert level_control_cluster.write_attributes.call_count == 1 - assert level_control_cluster.write_attributes.call_args[0][0] == { - attr: new_value, - } + assert level_control_cluster.write_attributes.mock_calls == [ + call({attr: new_value}, manufacturer=None), + call({attr: new_value}, manufacturer=None), + call({attr: new_value}, manufacturer=None), + ] assert hass.states.get(entity_id).state == str(initial_value) @@ -331,7 +326,12 @@ async def test_level_control_number( (("start_up_color_temperature", 500, 350),), ) async def test_color_number( - hass: HomeAssistant, light, zha_device_joined, attr, initial_value, new_value + hass: HomeAssistant, + light: ZHADevice, + zha_device_joined, + attr: str, + initial_value: int, + new_value: int, ) -> None: """Test ZHA color number entities - new join.""" @@ -407,9 +407,7 @@ async def test_color_number( assert color_cluster.read_attributes.call_count == 1 assert ( call( - [ - attr, - ], + [attr], allow_cache=False, only_cache=False, manufacturer=None, @@ -420,18 +418,20 @@ async def test_color_number( color_cluster.write_attributes.reset_mock() color_cluster.write_attributes.side_effect = ZigbeeException - await hass.services.async_call( - "number", - "set_value", - { - "entity_id": entity_id, - "value": new_value, - }, - blocking=True, - ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "number", + "set_value", + { + "entity_id": entity_id, + "value": new_value, + }, + blocking=True, + ) - assert color_cluster.write_attributes.call_count == 1 - assert color_cluster.write_attributes.call_args[0][0] == { - attr: new_value, - } + assert color_cluster.write_attributes.mock_calls == [ + call({attr: new_value}, manufacturer=None), + call({attr: new_value}, manufacturer=None), + call({attr: new_value}, manufacturer=None), + ] assert hass.states.get(entity_id).state == str(initial_value) diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index bee7ec409ca..fe7450eff67 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -21,6 +21,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.zha.core.group import GroupMember from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from .common import ( @@ -411,10 +412,11 @@ async def test_switch_configurable( await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call( - {"window_detection_function": True} - ) + assert cluster.write_attributes.mock_calls == [ + call({"window_detection_function": True}, manufacturer=None) + ] + + cluster.write_attributes.reset_mock() # turn off from HA with patch( @@ -425,10 +427,9 @@ async def test_switch_configurable( await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True ) - assert len(cluster.write_attributes.mock_calls) == 2 - assert cluster.write_attributes.call_args == call( - {"window_detection_function": False} - ) + assert cluster.write_attributes.mock_calls == [ + call({"window_detection_function": False}, manufacturer=None) + ] cluster.read_attributes.reset_mock() await async_setup_component(hass, "homeassistant", {}) @@ -461,14 +462,18 @@ async def test_switch_configurable( cluster.write_attributes.reset_mock() cluster.write_attributes.side_effect = ZigbeeException - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True + ) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call( - {"window_detection_function": False} - ) + assert cluster.write_attributes.mock_calls == [ + call({"window_detection_function": False}, manufacturer=None), + call({"window_detection_function": False}, manufacturer=None), + call({"window_detection_function": False}, manufacturer=None), + ] + + cluster.write_attributes.side_effect = None # test inverter cluster.write_attributes.reset_mock() @@ -477,18 +482,17 @@ async def test_switch_configurable( await hass.services.async_call( SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True ) - assert len(cluster.write_attributes.mock_calls) == 1 - assert cluster.write_attributes.call_args == call( - {"window_detection_function": True} - ) + assert cluster.write_attributes.mock_calls == [ + call({"window_detection_function": True}, manufacturer=None) + ] + cluster.write_attributes.reset_mock() await hass.services.async_call( SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True ) - assert len(cluster.write_attributes.mock_calls) == 2 - assert cluster.write_attributes.call_args == call( - {"window_detection_function": False} - ) + assert cluster.write_attributes.mock_calls == [ + call({"window_detection_function": False}, manufacturer=None) + ] # test joining a new switch to the network and HA await async_test_rejoin(hass, zigpy_device_tuya, [cluster], (0,)) From c8ef3f9393c1db4df77cc6f8a674b0e44b691720 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:26:34 -0400 Subject: [PATCH 0982/1151] Automatic migration from multi-PAN back to Zigbee firmware (#93831) * Initial implementation of migration back to Zigbee firmware * Fix typo in `BACKUP_RETRIES` constant name * Name potentially long-running tasks * Add an explicit timeout to `_async_wait_until_addon_state` * Guard against the addon not being installed when uninstalling * Do not launch the progress flow unless the addon is being installed * Use a separate translation key for confirmation before disabling multi-PAN * Disable the bellows UART thread within the ZHA config flow radio manager * Enhance config flow progress keys for flasher addon installation * Allow `zha.async_unload_entry` to succeed when ZHA is not loaded * Do not endlessly spawn task when uninstalling addon synchronously * Include `uninstall_addon.data.*` in SkyConnect and Yellow translations * Make `homeassistant_hardware` unit tests pass * Fix SkyConnect unit test USB mock * Fix unit tests in related integrations * Use a separate constant for connection retrying * Unit test ZHA migration from multi-PAN * Test ZHA multi-PAN migration helper changes * Fix flaky SkyConnect unit test being affected by system USB devices * Unit test the synchronous addon uninstall helper * Test failure when flasher addon is already running * Test failure where flasher addon fails to install * Test ZHA migration failures * Rename `get_addon_manager` to `get_multiprotocol_addon_manager` * Remove stray "addon uninstall" comment * Use better variable names for the two addon managers * Remove extraneous `self.install_task = None` * Use the addon manager's `addon_name` instead of constants * Migrate synchronous addon operations into a new class * Remove wrapper functions with `finally` clause * Use a more descriptive error message when the flasher addon is stalled * Fix existing unit tests * Remove `wait_until_done` * Fully replace all addon name constants with those from managers * Fix OTBR breakage * Simplify `is_hassio` mocking * Add missing tests for `check_multi_pan_addon` * Add missing tests for `multi_pan_addon_using_device` * Use `waiting` instead of `sync` in class name and methods --- .../homeassistant_hardware/const.py | 1 + .../silabs_multiprotocol_addon.py | 423 ++++++-- .../homeassistant_hardware/strings.json | 33 +- .../homeassistant_sky_connect/strings.json | 21 +- .../homeassistant_yellow/strings.json | 21 +- homeassistant/components/otbr/util.py | 8 +- homeassistant/components/zha/__init__.py | 6 +- homeassistant/components/zha/config_flow.py | 6 +- homeassistant/components/zha/radio_manager.py | 27 +- .../homeassistant_hardware/conftest.py | 18 + .../test_silabs_multiprotocol_addon.py | 965 +++++++++++++++--- .../homeassistant_sky_connect/conftest.py | 20 +- .../test_config_flow.py | 12 + .../homeassistant_sky_connect/test_init.py | 7 + .../homeassistant_yellow/test_config_flow.py | 10 + tests/components/zha/test_radio_manager.py | 57 +- 16 files changed, 1383 insertions(+), 252 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/const.py b/homeassistant/components/homeassistant_hardware/const.py index dd3a254d097..e4aa7c80f8d 100644 --- a/homeassistant/components/homeassistant_hardware/const.py +++ b/homeassistant/components/homeassistant_hardware/const.py @@ -5,3 +5,4 @@ import logging LOGGER = logging.getLogger(__package__) SILABS_MULTIPROTOCOL_ADDON_SLUG = "core_silabs_multiprotocol" +SILABS_FLASHER_ADDON_SLUG = "core_silabs_flasher" diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index e4d9902346c..b4723a88742 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -3,10 +3,12 @@ from __future__ import annotations from abc import ABC, abstractmethod import asyncio +from collections.abc import Awaitable import dataclasses import logging from typing import Any, Protocol +import async_timeout import voluptuous as vol import yarl @@ -33,17 +35,19 @@ from homeassistant.helpers.selector import ( from homeassistant.helpers.singleton import singleton from homeassistant.helpers.storage import Store -from .const import LOGGER, SILABS_MULTIPROTOCOL_ADDON_SLUG +from .const import LOGGER, SILABS_FLASHER_ADDON_SLUG, SILABS_MULTIPROTOCOL_ADDON_SLUG _LOGGER = logging.getLogger(__name__) -DATA_ADDON_MANAGER = "silabs_multiprotocol_addon_manager" +DATA_MULTIPROTOCOL_ADDON_MANAGER = "silabs_multiprotocol_addon_manager" +DATA_FLASHER_ADDON_MANAGER = "silabs_flasher" -ADDON_SETUP_TIMEOUT = 5 -ADDON_SETUP_TIMEOUT_ROUNDS = 40 +ADDON_STATE_POLL_INTERVAL = 3 +ADDON_INFO_POLL_TIMEOUT = 15 * 60 CONF_ADDON_AUTOFLASH_FW = "autoflash_firmware" CONF_ADDON_DEVICE = "device" +CONF_DISABLE_MULTI_PAN = "disable_multi_pan" CONF_ENABLE_MULTI_PAN = "enable_multi_pan" DEFAULT_CHANNEL = 15 @@ -55,15 +59,64 @@ STORAGE_VERSION_MINOR = 1 SAVE_DELAY = 10 -@singleton(DATA_ADDON_MANAGER) -async def get_addon_manager(hass: HomeAssistant) -> MultiprotocolAddonManager: +@singleton(DATA_MULTIPROTOCOL_ADDON_MANAGER) +async def get_multiprotocol_addon_manager( + hass: HomeAssistant, +) -> MultiprotocolAddonManager: """Get the add-on manager.""" manager = MultiprotocolAddonManager(hass) await manager.async_setup() return manager -class MultiprotocolAddonManager(AddonManager): +class WaitingAddonManager(AddonManager): + """Addon manager which supports waiting operations for managing an addon.""" + + async def async_wait_until_addon_state(self, *states: AddonState) -> None: + """Poll an addon's info until it is in a specific state.""" + async with async_timeout.timeout(ADDON_INFO_POLL_TIMEOUT): + while True: + try: + info = await self.async_get_addon_info() + except AddonError: + info = None + + _LOGGER.debug("Waiting for addon to be in state %s: %s", states, info) + + if info is not None and info.state in states: + break + + await asyncio.sleep(ADDON_STATE_POLL_INTERVAL) + + async def async_start_addon_waiting(self) -> None: + """Start an add-on.""" + await self.async_schedule_start_addon() + await self.async_wait_until_addon_state(AddonState.RUNNING) + + async def async_install_addon_waiting(self) -> None: + """Install an add-on.""" + await self.async_schedule_install_addon() + await self.async_wait_until_addon_state( + AddonState.RUNNING, + AddonState.NOT_RUNNING, + ) + + async def async_uninstall_addon_waiting(self) -> None: + """Uninstall an add-on.""" + try: + info = await self.async_get_addon_info() + except AddonError: + info = None + + # Do not try to uninstall an addon if it is already uninstalled + if info is not None and info.state == AddonState.NOT_INSTALLED: + return + + await self.async_uninstall_addon() + await self.async_wait_until_addon_state(AddonState.NOT_INSTALLED) + + +class MultiprotocolAddonManager(WaitingAddonManager): """Silicon Labs Multiprotocol add-on manager.""" def __init__(self, hass: HomeAssistant) -> None: @@ -207,6 +260,18 @@ class MultipanProtocol(Protocol): """ +@singleton(DATA_FLASHER_ADDON_MANAGER) +@callback +def get_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager: + """Get the flasher add-on manager.""" + return WaitingAddonManager( + hass, + LOGGER, + "Silicon Labs Flasher", + SILABS_FLASHER_ADDON_SLUG, + ) + + @dataclasses.dataclass class SerialPortSettings: """Serial port settings.""" @@ -242,9 +307,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): ZhaMultiPANMigrationHelper, ) - # If we install the add-on we should uninstall it on entry remove. self.install_task: asyncio.Task | None = None self.start_task: asyncio.Task | None = None + self.stop_task: asyncio.Task | None = None self._zha_migration_mgr: ZhaMultiPANMigrationHelper | None = None self.config_entry = config_entry self.original_addon_config: dict[str, Any] | None = None @@ -275,37 +340,37 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): """Return the correct flow manager.""" return self.hass.config_entries.options - async def _async_get_addon_info(self) -> AddonInfo: + async def _resume_flow_when_done(self, awaitable: Awaitable) -> None: + try: + await awaitable + finally: + self.hass.async_create_task( + self.flow_manager.async_configure(flow_id=self.flow_id) + ) + + async def _async_get_addon_info(self, addon_manager: AddonManager) -> AddonInfo: """Return and cache Silicon Labs Multiprotocol add-on info.""" - addon_manager: AddonManager = await get_addon_manager(self.hass) try: addon_info: AddonInfo = await addon_manager.async_get_addon_info() except AddonError as err: _LOGGER.error(err) - raise AbortFlow("addon_info_failed") from err + raise AbortFlow( + "addon_info_failed", + description_placeholders={"addon_name": addon_manager.addon_name}, + ) from err return addon_info - async def _async_set_addon_config(self, config: dict) -> None: + async def _async_set_addon_config( + self, config: dict, addon_manager: AddonManager + ) -> None: """Set Silicon Labs Multiprotocol add-on config.""" - addon_manager: AddonManager = await get_addon_manager(self.hass) try: await addon_manager.async_set_addon_options(config) except AddonError as err: _LOGGER.error(err) raise AbortFlow("addon_set_config_failed") from err - async def _async_install_addon(self) -> None: - """Install the Silicon Labs Multiprotocol add-on.""" - addon_manager: AddonManager = await get_addon_manager(self.hass) - try: - await addon_manager.async_schedule_install_addon() - finally: - # Continue the flow after show progress when the task is done. - self.hass.async_create_task( - self.flow_manager.async_configure(flow_id=self.flow_id) - ) - async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -319,7 +384,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle logic when on Supervisor host.""" - addon_info = await self._async_get_addon_info() + multipan_manager = await get_multiprotocol_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(multipan_manager) if addon_info.state == AddonState.NOT_INSTALLED: return await self.async_step_addon_not_installed() @@ -347,19 +413,26 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): ) -> FlowResult: """Install Silicon Labs Multiprotocol add-on.""" if not self.install_task: - self.install_task = self.hass.async_create_task(self._async_install_addon()) + multipan_manager = await get_multiprotocol_addon_manager(self.hass) + self.install_task = self.hass.async_create_task( + self._resume_flow_when_done( + multipan_manager.async_install_addon_waiting() + ), + "SiLabs Multiprotocol addon install", + ) return self.async_show_progress( - step_id="install_addon", progress_action="install_addon" + step_id="install_addon", + progress_action="install_addon", + description_placeholders={"addon_name": multipan_manager.addon_name}, ) try: await self.install_task except AddonError as err: - self.install_task = None _LOGGER.error(err) return self.async_show_progress_done(next_step_id="install_failed") - - self.install_task = None + finally: + self.install_task = None return self.async_show_progress_done(next_step_id="configure_addon") @@ -367,7 +440,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Add-on installation failed.""" - return self.async_abort(reason="addon_install_failed") + multipan_manager = await get_multiprotocol_addon_manager(self.hass) + return self.async_abort( + reason="addon_install_failed", + description_placeholders={"addon_name": multipan_manager.addon_name}, + ) async def async_step_configure_addon( self, user_input: dict[str, Any] | None = None @@ -386,7 +463,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async_get_channel as async_get_zha_channel, ) - addon_info = await self._async_get_addon_info() + multipan_manager = await get_multiprotocol_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(multipan_manager) addon_config = addon_info.options @@ -426,14 +504,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): multipan_channel = zha_channel # Initialize the shared channel - multipan_manager = await get_addon_manager(self.hass) + multipan_manager = await get_multiprotocol_addon_manager(self.hass) multipan_manager.async_set_channel(multipan_channel) if new_addon_config != addon_config: # Copy the add-on config to keep the objects separate. self.original_addon_config = dict(addon_config) _LOGGER.debug("Reconfiguring addon with %s", new_addon_config) - await self._async_set_addon_config(new_addon_config) + await self._async_set_addon_config(new_addon_config, multipan_manager) return await self.async_step_start_addon() @@ -442,9 +520,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): ) -> FlowResult: """Start Silicon Labs Multiprotocol add-on.""" if not self.start_task: - self.start_task = self.hass.async_create_task(self._async_start_addon()) + multipan_manager = await get_multiprotocol_addon_manager(self.hass) + self.start_task = self.hass.async_create_task( + self._resume_flow_when_done( + multipan_manager.async_start_addon_waiting() + ) + ) return self.async_show_progress( - step_id="start_addon", progress_action="start_addon" + step_id="start_addon", + progress_action="start_addon", + description_placeholders={"addon_name": multipan_manager.addon_name}, ) try: @@ -461,18 +546,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Add-on start failed.""" - return self.async_abort(reason="addon_start_failed") - - async def _async_start_addon(self) -> None: - """Start Silicon Labs Multiprotocol add-on.""" - addon_manager: AddonManager = await get_addon_manager(self.hass) - try: - await addon_manager.async_schedule_start_addon() - finally: - # Continue the flow after show progress when the task is done. - self.hass.async_create_task( - self.flow_manager.async_configure(flow_id=self.flow_id) - ) + multipan_manager = await get_multiprotocol_addon_manager(self.hass) + return self.async_abort( + reason="addon_start_failed", + description_placeholders={"addon_name": multipan_manager.addon_name}, + ) async def async_step_finish_addon_setup( self, user_input: dict[str, Any] | None = None @@ -493,16 +571,25 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): return self.async_create_entry(title="", data={}) + async def async_step_addon_installed_other_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Show dialog explaining the addon is in use by another device.""" + if user_input is None: + return self.async_show_form(step_id="addon_installed_other_device") + return self.async_create_entry(title="", data={}) + async def async_step_addon_installed( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle logic when the addon is already installed.""" - addon_info = await self._async_get_addon_info() + multipan_manager = await get_multiprotocol_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(multipan_manager) serial_device = (await self._async_serial_port_settings()).device - if addon_info.options.get(CONF_ADDON_DEVICE) == serial_device: - return await self.async_step_show_addon_menu() - return await self.async_step_addon_installed_other_device() + if addon_info.options.get(CONF_ADDON_DEVICE) != serial_device: + return await self.async_step_addon_installed_other_device() + return await self.async_step_show_addon_menu() async def async_step_show_addon_menu( self, user_input: dict[str, Any] | None = None @@ -520,7 +607,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Reconfigure the addon.""" - multipan_manager = await get_addon_manager(self.hass) + multipan_manager = await get_multiprotocol_addon_manager(self.hass) active_platforms = await multipan_manager.async_active_platforms() if set(active_platforms) != {"otbr", "zha"}: return await self.async_step_notify_unknown_multipan_user() @@ -540,7 +627,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Change the channel.""" - multipan_manager = await get_addon_manager(self.hass) + multipan_manager = await get_multiprotocol_addon_manager(self.hass) if user_input is None: channels = [str(x) for x in range(11, 27)] suggested_channel = DEFAULT_CHANNEL @@ -584,23 +671,217 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_uninstall_addon( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Uninstall the addon (not implemented).""" - return await self.async_step_show_revert_guide() + """Uninstall the addon and revert the firmware.""" + if user_input is None: + return self.async_show_form( + step_id="uninstall_addon", + data_schema=vol.Schema( + {vol.Required(CONF_DISABLE_MULTI_PAN, default=False): bool} + ), + description_placeholders={"hardware_name": self._hardware_name()}, + ) + if not user_input[CONF_DISABLE_MULTI_PAN]: + return self.async_create_entry(title="", data={}) - async def async_step_show_revert_guide( + return await self.async_step_firmware_revert() + + async def async_step_firmware_revert( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Link to a guide for reverting to Zigbee firmware.""" - if user_input is None: - return self.async_show_form(step_id="show_revert_guide") - return self.async_create_entry(title="", data={}) + """Install the flasher addon, if necessary.""" - async def async_step_addon_installed_other_device( + flasher_manager = get_flasher_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(flasher_manager) + + if addon_info.state == AddonState.NOT_INSTALLED: + return await self.async_step_install_flasher_addon() + + if addon_info.state == AddonState.NOT_RUNNING: + return await self.async_step_configure_flasher_addon() + + # If the addon is already installed and running, fail + return self.async_abort( + reason="addon_already_running", + description_placeholders={"addon_name": flasher_manager.addon_name}, + ) + + async def async_step_install_flasher_addon( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Show dialog explaining the addon is in use by another device.""" - if user_input is None: - return self.async_show_form(step_id="addon_installed_other_device") + """Show progress dialog for installing flasher addon.""" + flasher_manager = get_flasher_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(flasher_manager) + + _LOGGER.debug("Flasher addon state: %s", addon_info) + + if not self.install_task: + self.install_task = self.hass.async_create_task( + self._resume_flow_when_done( + flasher_manager.async_install_addon_waiting() + ), + "SiLabs Flasher addon install", + ) + return self.async_show_progress( + step_id="install_flasher_addon", + progress_action="install_addon", + description_placeholders={"addon_name": flasher_manager.addon_name}, + ) + + try: + await self.install_task + except AddonError as err: + _LOGGER.error(err) + return self.async_show_progress_done(next_step_id="install_failed") + finally: + self.install_task = None + + return self.async_show_progress_done(next_step_id="configure_flasher_addon") + + async def async_step_configure_flasher_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Perform initial backup and reconfigure ZHA.""" + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN + + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.zha.radio_manager import ( + ZhaMultiPANMigrationHelper, + ) + + zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN) + new_settings = await self._async_serial_port_settings() + + _LOGGER.debug("Using new ZHA settings: %s", new_settings) + + if zha_entries: + zha_migration_mgr = ZhaMultiPANMigrationHelper(self.hass, zha_entries[0]) + migration_data = { + "new_discovery_info": { + "name": self._hardware_name(), + "port": { + "path": new_settings.device, + "baudrate": int(new_settings.baudrate), + "flow_control": ( + "hardware" if new_settings.flow_control else None + ), + }, + "radio_type": "ezsp", + }, + "old_discovery_info": { + "hw": { + "name": self._zha_name(), + "port": {"path": get_zigbee_socket()}, + "radio_type": "ezsp", + } + }, + } + _LOGGER.debug("Starting ZHA migration with: %s", migration_data) + try: + if await zha_migration_mgr.async_initiate_migration(migration_data): + self._zha_migration_mgr = zha_migration_mgr + except Exception as err: + _LOGGER.exception("Unexpected exception during ZHA migration") + raise AbortFlow("zha_migration_failed") from err + + flasher_manager = get_flasher_addon_manager(self.hass) + addon_info = await self._async_get_addon_info(flasher_manager) + new_addon_config = { + **addon_info.options, + "device": new_settings.device, + "flow_control": new_settings.flow_control, + } + + _LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config) + await self._async_set_addon_config(new_addon_config, flasher_manager) + + return await self.async_step_uninstall_multiprotocol_addon() + + async def async_step_uninstall_multiprotocol_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Uninstall Silicon Labs Multiprotocol add-on.""" + + if not self.stop_task: + multipan_manager = await get_multiprotocol_addon_manager(self.hass) + self.stop_task = self.hass.async_create_task( + self._resume_flow_when_done( + multipan_manager.async_uninstall_addon_waiting() + ), + "SiLabs Multiprotocol addon uninstall", + ) + return self.async_show_progress( + step_id="uninstall_multiprotocol_addon", + progress_action="uninstall_multiprotocol_addon", + description_placeholders={"addon_name": multipan_manager.addon_name}, + ) + + try: + await self.stop_task + finally: + self.stop_task = None + + return self.async_show_progress_done(next_step_id="start_flasher_addon") + + async def async_step_start_flasher_addon( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Start Silicon Labs Flasher add-on.""" + + if not self.start_task: + flasher_manager = get_flasher_addon_manager(self.hass) + + async def start_and_wait_until_done() -> None: + await flasher_manager.async_start_addon_waiting() + # Now that the addon is running, wait for it to finish + await flasher_manager.async_wait_until_addon_state( + AddonState.NOT_RUNNING + ) + + self.start_task = self.hass.async_create_task( + self._resume_flow_when_done(start_and_wait_until_done()) + ) + return self.async_show_progress( + step_id="start_flasher_addon", + progress_action="start_flasher_addon", + description_placeholders={"addon_name": flasher_manager.addon_name}, + ) + + try: + await self.start_task + except (AddonError, AbortFlow) as err: + _LOGGER.error(err) + return self.async_show_progress_done(next_step_id="flasher_failed") + finally: + self.start_task = None + + return self.async_show_progress_done(next_step_id="flashing_complete") + + async def async_step_flasher_failed( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Flasher add-on start failed.""" + flasher_manager = get_flasher_addon_manager(self.hass) + return self.async_abort( + reason="addon_start_failed", + description_placeholders={"addon_name": flasher_manager.addon_name}, + ) + + async def async_step_flashing_complete( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Finish flashing and update the config entry.""" + flasher_manager = get_flasher_addon_manager(self.hass) + await flasher_manager.async_uninstall_addon_waiting() + + # Finish ZHA migration if needed + if self._zha_migration_mgr: + try: + await self._zha_migration_mgr.async_finish_migration() + except Exception as err: + _LOGGER.exception("Unexpected exception during ZHA migration") + raise AbortFlow("zha_migration_failed") from err + return self.async_create_entry(title="", data={}) @@ -613,18 +894,18 @@ async def check_multi_pan_addon(hass: HomeAssistant) -> None: if not is_hassio(hass): return - addon_manager: AddonManager = await get_addon_manager(hass) + multipan_manager = await get_multiprotocol_addon_manager(hass) try: - addon_info: AddonInfo = await addon_manager.async_get_addon_info() + addon_info: AddonInfo = await multipan_manager.async_get_addon_info() except AddonError as err: _LOGGER.error(err) raise HomeAssistantError from err # Request the addon to start if it's not started - # addon_manager.async_start_addon returns as soon as the start request has been sent + # `async_start_addon` returns as soon as the start request has been sent # and does not wait for the addon to be started, so we raise below if addon_info.state == AddonState.NOT_RUNNING: - await addon_manager.async_start_addon() + await multipan_manager.async_start_addon() if addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.RUNNING): _LOGGER.debug("Multi pan addon installed and in state %s", addon_info.state) @@ -640,8 +921,8 @@ async def multi_pan_addon_using_device(hass: HomeAssistant, device_path: str) -> if not is_hassio(hass): return False - addon_manager: AddonManager = await get_addon_manager(hass) - addon_info: AddonInfo = await addon_manager.async_get_addon_info() + multipan_manager = await get_multiprotocol_addon_manager(hass) + addon_info: AddonInfo = await multipan_manager.async_get_addon_info() if addon_info.state != AddonState.RUNNING: return False diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index 24cd049668d..a66e4879f68 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -39,31 +39,42 @@ "reconfigure_addon": { "title": "Reconfigure IEEE 802.15.4 radio multiprotocol support" }, - "show_revert_guide": { - "title": "Multiprotocol support is enabled for this device", - "description": "If you want to change to Zigbee only firmware, please complete the following manual steps:\n\n * Remove the Silicon Labs Multiprotocol addon\n\n * Flash the Zigbee only firmware, follow the guide at https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually.\n\n * Reconfigure ZHA to migrate settings to the reflashed radio" - }, "start_addon": { "title": "The Silicon Labs Multiprotocol add-on is starting." }, "uninstall_addon": { - "title": "Remove IEEE 802.15.4 radio multiprotocol support." + "title": "Remove IEEE 802.15.4 radio multiprotocol support", + "description": "Disabling multiprotocol support will revert your {hardware_name}'s radio back to Zigbee-only firmware and will disable Thread support provided by the {hardware_name}. Your Thread devices will continue working only if you have another Thread border router nearby.\n\nIt will take a few minutes to install the Zigbee firmware and restoring a backup.", + "data": { + "disable_multi_pan": "Disable multiprotocol support" + } + }, + "install_flasher_addon": { + "title": "The Silicon Labs Flasher add-on installation has started" + }, + "configure_flasher_addon": { + "title": "The Silicon Labs Flasher add-on installation has started" + }, + "start_flasher_addon": { + "title": "Installing firmware", + "description": "Zigbee firmware is now being installed. This will take a few minutes." } }, "error": { "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "addon_info_failed": "Failed to get Silicon Labs Multiprotocol add-on info.", - "addon_install_failed": "Failed to install the Silicon Labs Multiprotocol add-on.", - "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", - "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", + "addon_info_failed": "Failed to get {addon_name} add-on info.", + "addon_install_failed": "Failed to install the {addon_name} add-on.", + "addon_already_running": "Failed to start the {addon_name} add-on because it is already running.", + "addon_set_config_failed": "Failed to set {addon_name} configuration.", + "addon_start_failed": "Failed to start the {addon_name} add-on.", "not_hassio": "The hardware options can only be configured on HassOS installations.", "zha_migration_failed": "The ZHA migration did not succeed." }, "progress": { - "install_addon": "Please wait while the Silicon Labs Multiprotocol add-on installation finishes. This can take several minutes.", - "start_addon": "Please wait while the Silicon Labs Multiprotocol add-on start completes. This may take some seconds." + "install_addon": "Please wait while the {addon_name} add-on installation finishes. This can take several minutes.", + "start_addon": "Please wait while the {addon_name} add-on start completes. This may take some seconds." } } } diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index 9bc1a49125b..58fc0180743 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -38,15 +38,25 @@ "reconfigure_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]" }, - "show_revert_guide": { - "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]", - "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::description%]" - }, "start_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_addon::title%]" }, "uninstall_addon": { - "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]" + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::description%]", + "data": { + "disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]" + } + }, + "install_flasher_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_flasher_addon::title%]" + }, + "configure_flasher_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::configure_flasher_addon::title%]" + }, + "start_flasher_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::description%]" } }, "error": { @@ -55,6 +65,7 @@ "abort": { "addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]", "addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]", + "addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]", "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json index 644a3c04553..e5250f163ce 100644 --- a/homeassistant/components/homeassistant_yellow/strings.json +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -60,15 +60,25 @@ "reconfigure_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]" }, - "show_revert_guide": { - "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::title%]", - "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::show_revert_guide::description%]" - }, "start_addon": { "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_addon::title%]" }, "uninstall_addon": { - "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]" + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::description%]", + "data": { + "disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]" + } + }, + "install_flasher_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_flasher_addon::title%]" + }, + "configure_flasher_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::configure_flasher_addon::title%]" + }, + "start_flasher_addon": { + "title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::title%]", + "description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::start_flasher_addon::description%]" } }, "error": { @@ -77,6 +87,7 @@ "abort": { "addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]", "addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]", + "addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]", "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", diff --git a/homeassistant/components/otbr/util.py b/homeassistant/components/otbr/util.py index 4cbf7ce6a08..067282108f1 100644 --- a/homeassistant/components/otbr/util.py +++ b/homeassistant/components/otbr/util.py @@ -14,7 +14,7 @@ from python_otbr_api.tlv_parser import MeshcopTLVType from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( MultiprotocolAddonManager, - get_addon_manager, + get_multiprotocol_addon_manager, is_multiprotocol_url, multi_pan_addon_using_device, ) @@ -146,8 +146,10 @@ async def get_allowed_channel(hass: HomeAssistant, otbr_url: str) -> int | None: # The OTBR is not sharing the radio, no restriction return None - addon_manager: MultiprotocolAddonManager = await get_addon_manager(hass) - return addon_manager.async_get_channel() + multipan_manager: MultiprotocolAddonManager = await get_multiprotocol_addon_manager( + hass + ) + return multipan_manager.async_get_channel() async def _warn_on_channel_collision( diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 8a81648b580..a51d6f387e1 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -166,7 +166,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload ZHA config entry.""" - zha_gateway: ZHAGateway = hass.data[DATA_ZHA].pop(DATA_ZHA_GATEWAY) + try: + zha_gateway: ZHAGateway = hass.data[DATA_ZHA].pop(DATA_ZHA_GATEWAY) + except KeyError: + return False + await zha_gateway.shutdown() GROUP_PROBE.cleanup() diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index ba50839ee44..6ac3a155ed9 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -96,10 +96,12 @@ async def list_serial_ports(hass: HomeAssistant) -> list[ListPortInfo]: yellow_radio.manufacturer = "Nabu Casa" # Present the multi-PAN addon as a setup option, if it's available - addon_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( + hass + ) try: - addon_info = await addon_manager.async_get_addon_info() + addon_info = await multipan_manager.async_get_addon_info() except (AddonError, KeyError): addon_info = None diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 29214083d27..4e70fc2247f 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -9,6 +9,7 @@ import logging import os from typing import Any +from bellows.config import CONF_USE_THREAD import voluptuous as vol from zigpy.application import ControllerApplication import zigpy.backups @@ -47,7 +48,9 @@ RECOMMENDED_RADIOS = ( ) CONNECT_DELAY_S = 1.0 +RETRY_DELAY_S = 1.0 +BACKUP_RETRIES = 5 MIGRATION_RETRIES = 100 HARDWARE_DISCOVERY_SCHEMA = vol.Schema( @@ -134,6 +137,7 @@ class ZhaRadioManager: app_config[CONF_DATABASE] = database_path app_config[CONF_DEVICE] = self.device_settings app_config[CONF_NWK_BACKUP_ENABLED] = False + app_config[CONF_USE_THREAD] = False app_config = self.radio_type.controller.SCHEMA(app_config) app = await self.radio_type.controller.new( @@ -341,7 +345,24 @@ class ZhaMultiPANMigrationHelper: old_radio_mgr.device_path = config_entry_data[CONF_DEVICE][CONF_DEVICE_PATH] old_radio_mgr.device_settings = config_entry_data[CONF_DEVICE] old_radio_mgr.radio_type = RadioType[config_entry_data[CONF_RADIO_TYPE]] - backup = await old_radio_mgr.async_load_network_settings(create_backup=True) + + for retry in range(BACKUP_RETRIES): + try: + backup = await old_radio_mgr.async_load_network_settings( + create_backup=True + ) + break + except OSError as err: + if retry >= BACKUP_RETRIES - 1: + raise + + _LOGGER.debug( + "Failed to create backup %r, retrying in %s seconds", + err, + RETRY_DELAY_S, + ) + + await asyncio.sleep(RETRY_DELAY_S) # Then configure the radio manager for the new radio to use the new settings self._radio_mgr.chosen_backup = backup @@ -381,10 +402,10 @@ class ZhaMultiPANMigrationHelper: _LOGGER.debug( "Failed to restore backup %r, retrying in %s seconds", err, - CONNECT_DELAY_S, + RETRY_DELAY_S, ) - await asyncio.sleep(CONNECT_DELAY_S) + await asyncio.sleep(RETRY_DELAY_S) _LOGGER.debug("Restored backup after %s retries", retry) diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py index 60c766c7204..60083c2de94 100644 --- a/tests/components/homeassistant_hardware/conftest.py +++ b/tests/components/homeassistant_hardware/conftest.py @@ -147,3 +147,21 @@ def start_addon_fixture(): "homeassistant.components.hassio.addon_manager.async_start_addon" ) as start_addon: yield start_addon + + +@pytest.fixture(name="stop_addon") +def stop_addon_fixture(): + """Mock stop add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_stop_addon" + ) as stop_addon: + yield stop_addon + + +@pytest.fixture(name="uninstall_addon") +def uninstall_addon_fixture(): + """Mock uninstall add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_uninstall_addon" + ) as uninstall_addon: + yield uninstall_addon diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index a956214c098..17cd288050c 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -3,10 +3,11 @@ from __future__ import annotations from collections.abc import Generator from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch import pytest +from homeassistant.components.hassio import AddonError, AddonInfo, AddonState, HassIO from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN @@ -14,15 +15,15 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult, FlowResultType +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import ATTR_COMPONENT from tests.common import ( MockConfigEntry, - MockModule, MockPlatform, flush_store, + mock_component, mock_config_flow, - mock_integration, mock_platform, ) @@ -101,6 +102,22 @@ def config_flow_handler( yield +@pytest.fixture +def options_flow_poll_addon_state() -> Generator[None, None, None]: + """Fixture for patching options flow addon state polling.""" + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.WaitingAddonManager.async_wait_until_addon_state" + ): + yield + + +@pytest.fixture(autouse=True) +def hassio_integration(hass: HomeAssistant) -> Generator[None, None, None]: + """Fixture to mock the `hassio` integration.""" + mock_component(hass, "hassio") + hass.data["hassio"] = Mock(spec_set=HassIO) + + class MockMultiprotocolPlatform(MockPlatform): """A mock multiprotocol platform.""" @@ -149,6 +166,48 @@ def get_suggested(schema, key): raise Exception +@patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.ADDON_STATE_POLL_INTERVAL", + 0, +) +async def test_uninstall_addon_waiting( + hass: HomeAssistant, + addon_store_info, + addon_info, + install_addon, + uninstall_addon, +): + """Test the synchronous addon uninstall helper.""" + + multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( + hass + ) + multipan_manager.async_get_addon_info = AsyncMock() + multipan_manager.async_uninstall_addon = AsyncMock( + wraps=multipan_manager.async_uninstall_addon + ) + + # First try uninstalling the addon when it is already uninstalled + multipan_manager.async_get_addon_info.side_effect = [ + Mock(state=AddonState.NOT_INSTALLED) + ] + await multipan_manager.async_uninstall_addon_waiting() + multipan_manager.async_uninstall_addon.assert_not_called() + + # Next, try uninstalling the addon but in a complex case where the API fails first + multipan_manager.async_get_addon_info.side_effect = [ + # First the API fails + AddonError(), + AddonError(), + # Then the addon is still running + Mock(state=AddonState.RUNNING), + # And finally it is uninstalled + Mock(state=AddonState.NOT_INSTALLED), + ] + await multipan_manager.async_uninstall_addon_waiting() + multipan_manager.async_uninstall_addon.assert_called_once() + + async def test_option_flow_install_multi_pan_addon( hass: HomeAssistant, addon_store_info, @@ -156,9 +215,9 @@ async def test_option_flow_install_multi_pan_addon( install_addon, set_addon_options, start_addon, + options_flow_poll_addon_state, ) -> None: """Test installing the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) # Setup the config entry config_entry = MockConfigEntry( @@ -169,13 +228,9 @@ async def test_option_flow_install_multi_pan_addon( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -224,9 +279,9 @@ async def test_option_flow_install_multi_pan_addon_zha( install_addon, set_addon_options, start_addon, + options_flow_poll_addon_state, ) -> None: """Test installing the multi pan addon when a zha config entry exists.""" - mock_integration(hass, MockModule("hassio")) # Setup the config entry config_entry = MockConfigEntry( @@ -245,13 +300,9 @@ async def test_option_flow_install_multi_pan_addon_zha( ) zha_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -268,7 +319,9 @@ async def test_option_flow_install_multi_pan_addon_zha( assert result["step_id"] == "configure_addon" install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") - multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( + hass + ) assert multipan_manager._channel is None with patch( "homeassistant.components.zha.silabs_multiprotocol.async_get_channel", @@ -318,9 +371,9 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( install_addon, set_addon_options, start_addon, + options_flow_poll_addon_state, ) -> None: """Test installing the multi pan addon when a zha config entry exists.""" - mock_integration(hass, MockModule("hassio")) # Setup the config entry config_entry = MockConfigEntry( @@ -346,13 +399,9 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( ) zha_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -408,31 +457,7 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( async def test_option_flow_non_hassio( hass: HomeAssistant, ) -> None: - """Test installing the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) - - # Setup the config entry - config_entry = MockConfigEntry( - data={}, - domain=TEST_DOMAIN, - options={}, - title="Test HW", - ) - config_entry.add_to_hass(hass) - - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "not_hassio" - - -async def test_option_flow_addon_installed_other_device( - hass: HomeAssistant, - addon_store_info, - addon_installed, -) -> None: - """Test installing the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) - + """Test installing the multi pan addon on a Core installation, without hassio.""" # Setup the config entry config_entry = MockConfigEntry( data={}, @@ -444,11 +469,33 @@ async def test_option_flow_addon_installed_other_device( with patch( "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), + return_value=False, ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_installed_other_device" + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_hassio" + + +async def test_option_flow_addon_installed_other_device( + hass: HomeAssistant, + addon_store_info, + addon_installed, +) -> None: + """Test installing the multi pan addon.""" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_installed_other_device" result = await hass.config_entries.options.async_configure(result["flow_id"], {}) assert result["type"] == FlowResultType.CREATE_ENTRY @@ -467,10 +514,12 @@ async def test_option_flow_addon_installed_same_device_reconfigure_unexpected_us suggested_channel: int, ) -> None: """Test reconfiguring the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" - multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( + hass + ) multipan_manager._channel = configured_channel # Setup the config entry @@ -482,13 +531,9 @@ async def test_option_flow_addon_installed_same_device_reconfigure_unexpected_us ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "addon_menu" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -528,10 +573,12 @@ async def test_option_flow_addon_installed_same_device_reconfigure_expected_user suggested_channel: int, ) -> None: """Test reconfiguring the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" - multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( + hass + ) multipan_manager._channel = configured_channel # Setup the config entry @@ -557,13 +604,9 @@ async def test_option_flow_addon_installed_same_device_reconfigure_expected_user hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: domain}) await hass.async_block_till_done() - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "addon_menu" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -593,9 +636,15 @@ async def test_option_flow_addon_installed_same_device_uninstall( addon_info, addon_store_info, addon_installed, + install_addon, + start_addon, + stop_addon, + uninstall_addon, + set_addon_options, + options_flow_poll_addon_state, ) -> None: - """Test installing the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) + """Test uninstalling the multi pan addon.""" + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" # Setup the config entry @@ -607,32 +656,97 @@ async def test_option_flow_addon_installed_same_device_uninstall( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "addon_menu" + zha_config_entry = MockConfigEntry( + data={ + "device": {"path": "socket://core-silabs-multiprotocol:9999"}, + "radio_type": "ezsp", + }, + domain=ZHA_DOMAIN, + options={}, + title="Test Multi-PAN", + ) + zha_config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "uninstall_addon"}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "show_revert_guide" + assert result["step_id"] == "uninstall_addon" - result = await hass.config_entries.options.async_configure(result["flow_id"], {}) + # Make sure the flasher addon is installed + addon_store_info.return_value = { + "installed": None, + "available": True, + "state": "not_installed", + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} + ) + + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_flasher_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "configure_flasher_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "uninstall_multiprotocol_addon" + assert result["progress_action"] == "uninstall_multiprotocol_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "start_flasher_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_flasher_addon" + assert result["progress_action"] == "start_flasher_addon" + assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + install_addon.assert_called_once_with(hass, "core_silabs_flasher") + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "flashing_complete" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.CREATE_ENTRY + # Check the ZHA config entry data is updated + assert zha_config_entry.data == { + "device": { + "path": "/dev/ttyTEST123", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "ezsp", + } + assert zha_config_entry.title == "Test" -async def test_option_flow_do_not_install_multi_pan_addon( + +async def test_option_flow_addon_installed_same_device_do_not_uninstall_multi_pan( hass: HomeAssistant, addon_info, addon_store_info, + addon_installed, + install_addon, + start_addon, + stop_addon, + uninstall_addon, + set_addon_options, ) -> None: - """Test installing the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) + """Test uninstalling the multi pan addon.""" + + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" # Setup the config entry config_entry = MockConfigEntry( @@ -643,13 +757,439 @@ async def test_option_flow_do_not_install_multi_pan_addon( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "uninstall_addon"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "uninstall_addon" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: False} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_option_flow_flasher_already_running_failure( + hass: HomeAssistant, + addon_info, + addon_store_info, + addon_installed, + install_addon, + start_addon, + stop_addon, + uninstall_addon, + set_addon_options, + options_flow_poll_addon_state, +) -> None: + """Test uninstalling the multi pan addon but with the flasher addon running.""" + + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "uninstall_addon"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "uninstall_addon" + + # The flasher addon is already installed and running, this is bad + addon_store_info.return_value["installed"] = True + addon_info.return_value["state"] = "started" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_already_running" + + +async def test_option_flow_addon_installed_same_device_flasher_already_installed( + hass: HomeAssistant, + addon_info, + addon_store_info, + addon_installed, + install_addon, + start_addon, + stop_addon, + uninstall_addon, + set_addon_options, + options_flow_poll_addon_state, +) -> None: + """Test uninstalling the multi pan addon.""" + + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "uninstall_addon"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "uninstall_addon" + + addon_store_info.return_value = { + "installed": True, + "available": True, + "state": "not_running", + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "uninstall_multiprotocol_addon" + assert result["progress_action"] == "uninstall_multiprotocol_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "start_flasher_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_flasher_addon" + assert result["progress_action"] == "start_flasher_addon" + assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} + + addon_store_info.return_value = { + "installed": True, + "available": True, + "state": "not_running", + } + result = await hass.config_entries.options.async_configure(result["flow_id"]) + install_addon.assert_not_called() + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "flashing_complete" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_option_flow_flasher_install_failure( + hass: HomeAssistant, + addon_info, + addon_store_info, + addon_installed, + install_addon, + start_addon, + stop_addon, + uninstall_addon, + set_addon_options, + options_flow_poll_addon_state, +) -> None: + """Test uninstalling the multi pan addon, case where flasher addon fails.""" + + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + zha_config_entry = MockConfigEntry( + data={ + "device": {"path": "socket://core-silabs-multiprotocol:9999"}, + "radio_type": "ezsp", + }, + domain=ZHA_DOMAIN, + options={}, + title="Test Multi-PAN", + ) + zha_config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "uninstall_addon"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "uninstall_addon" + + addon_store_info.return_value = { + "installed": None, + "available": True, + "state": "not_installed", + } + install_addon.side_effect = [AddonError()] + result = await hass.config_entries.options.async_configure( + result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} + ) + + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_flasher_addon" + assert result["progress_action"] == "install_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "install_failed" + install_addon.assert_called_once_with(hass, "core_silabs_flasher") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_install_failed" + + +async def test_option_flow_flasher_addon_flash_failure( + hass: HomeAssistant, + addon_info, + addon_store_info, + addon_installed, + install_addon, + start_addon, + stop_addon, + uninstall_addon, + set_addon_options, + options_flow_poll_addon_state, +) -> None: + """Test where flasher addon fails to flash Zigbee firmware.""" + + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "uninstall_addon"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "uninstall_addon" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} + ) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "uninstall_multiprotocol_addon" + assert result["progress_action"] == "uninstall_multiprotocol_addon" + + start_addon.side_effect = HassioAPIError("Boom") + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "start_flasher_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_flasher_addon" + assert result["progress_action"] == "start_flasher_addon" + assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} + + addon_store_info.return_value["installed"] = True + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "flasher_failed" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_start_failed" + assert result["description_placeholders"]["addon_name"] == "Silicon Labs Flasher" + + +@patch( + "homeassistant.components.zha.radio_manager.ZhaMultiPANMigrationHelper.async_initiate_migration", + side_effect=Exception("Boom!"), +) +async def test_option_flow_uninstall_migration_initiate_failure( + mock_initiate_migration, + hass: HomeAssistant, + addon_info, + addon_store_info, + addon_installed, + install_addon, + start_addon, + stop_addon, + uninstall_addon, + set_addon_options, + options_flow_poll_addon_state, +) -> None: + """Test uninstalling the multi pan addon, case where ZHA migration init fails.""" + + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + zha_config_entry = MockConfigEntry( + data={ + "device": {"path": "socket://core-silabs-multiprotocol:9999"}, + "radio_type": "ezsp", + }, + domain=ZHA_DOMAIN, + options={}, + title="Test Multi-PAN", + ) + zha_config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "uninstall_addon"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "uninstall_addon" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "zha_migration_failed" + mock_initiate_migration.assert_called_once() + + +@patch( + "homeassistant.components.zha.radio_manager.ZhaMultiPANMigrationHelper.async_finish_migration", + side_effect=Exception("Boom!"), +) +async def test_option_flow_uninstall_migration_finish_failure( + mock_finish_migration, + hass: HomeAssistant, + addon_info, + addon_store_info, + addon_installed, + install_addon, + start_addon, + stop_addon, + uninstall_addon, + set_addon_options, + options_flow_poll_addon_state, +) -> None: + """Test uninstalling the multi pan addon, case where ZHA migration init fails.""" + + addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + zha_config_entry = MockConfigEntry( + data={ + "device": {"path": "socket://core-silabs-multiprotocol:9999"}, + "radio_type": "ezsp", + }, + domain=ZHA_DOMAIN, + options={}, + title="Test Multi-PAN", + ) + zha_config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "addon_menu" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "uninstall_addon"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "uninstall_addon" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} + ) + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + uninstall_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "start_flasher_addon" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_flasher_addon" + assert result["progress_action"] == "start_flasher_addon" + assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"} + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE + assert result["step_id"] == "flashing_complete" + + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "zha_migration_failed" + + +async def test_option_flow_do_not_install_multi_pan_addon( + hass: HomeAssistant, + addon_info, + addon_store_info, +) -> None: + """Test installing the multi pan addon.""" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=TEST_DOMAIN, + options={}, + title="Test HW", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -669,7 +1209,7 @@ async def test_option_flow_install_multi_pan_addon_install_fails( start_addon, ) -> None: """Test installing the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) + install_addon.side_effect = HassioAPIError("Boom") # Setup the config entry @@ -681,13 +1221,9 @@ async def test_option_flow_install_multi_pan_addon_install_fails( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -718,7 +1254,7 @@ async def test_option_flow_install_multi_pan_addon_start_fails( start_addon, ) -> None: """Test installing the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) + start_addon.side_effect = HassioAPIError("Boom") # Setup the config entry @@ -730,13 +1266,9 @@ async def test_option_flow_install_multi_pan_addon_start_fails( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -788,7 +1320,7 @@ async def test_option_flow_install_multi_pan_addon_set_options_fails( start_addon, ) -> None: """Test installing the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) + set_addon_options.side_effect = HassioAPIError("Boom") # Setup the config entry @@ -800,13 +1332,9 @@ async def test_option_flow_install_multi_pan_addon_set_options_fails( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -834,7 +1362,7 @@ async def test_option_flow_addon_info_fails( addon_info, ) -> None: """Test installing the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) + addon_store_info.side_effect = HassioAPIError("Boom") # Setup the config entry @@ -846,13 +1374,9 @@ async def test_option_flow_addon_info_fails( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "addon_info_failed" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "addon_info_failed" @patch( @@ -869,7 +1393,6 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_1( start_addon, ) -> None: """Test installing the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) # Setup the config entry config_entry = MockConfigEntry( @@ -888,13 +1411,9 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_1( ) zha_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -929,9 +1448,9 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2( install_addon, set_addon_options, start_addon, + options_flow_poll_addon_state, ) -> None: """Test installing the multi pan addon.""" - mock_integration(hass, MockModule("hassio")) # Setup the config entry config_entry = MockConfigEntry( @@ -950,13 +1469,9 @@ async def test_option_flow_install_multi_pan_addon_zha_migration_fails_step_2( ) zha_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "addon_not_installed" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "addon_not_installed" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -1032,7 +1547,9 @@ async def test_import_channel( new_multipan_channel: int | None, ) -> None: """Test channel is initialized from first platform.""" - multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( + hass + ) multipan_manager._channel = initial_multipan_channel mock_multiprotocol_platform = MockMultiprotocolPlatform() @@ -1066,7 +1583,9 @@ async def test_change_channel( expected_calls: list[int], ) -> None: """Test channel is initialized from first platform.""" - multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( + hass + ) mock_multiprotocol_platform.using_multipan = platform_using_multipan await multipan_manager.async_change_channel(15, 10) @@ -1075,7 +1594,9 @@ async def test_change_channel( async def test_load_preferences(hass: HomeAssistant) -> None: """Make sure that we can load/save data correctly.""" - multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( + hass + ) assert multipan_manager._channel != 11 multipan_manager.async_set_channel(11) @@ -1106,7 +1627,9 @@ async def test_active_plaforms( active_platforms: list[str], ) -> None: """Test async_active_platforms.""" - multipan_manager = await silabs_multiprotocol_addon.get_addon_manager(hass) + multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( + hass + ) for domain, platform_using_multipan in multipan_platforms.items(): mock_multiprotocol_platform = MockMultiprotocolPlatform() @@ -1121,3 +1644,151 @@ async def test_active_plaforms( await hass.async_block_till_done() assert await multipan_manager.async_active_platforms() == active_platforms + + +async def test_check_multi_pan_addon_no_hassio(hass: HomeAssistant) -> None: + """Test `check_multi_pan_addon` without hassio.""" + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + return_value=False, + ), patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager", + autospec=True, + ) as mock_get_addon_manager: + await silabs_multiprotocol_addon.check_multi_pan_addon(hass) + mock_get_addon_manager.assert_not_called() + + +async def test_check_multi_pan_addon_info_error( + hass: HomeAssistant, addon_store_info +) -> None: + """Test `check_multi_pan_addon` where the addon info cannot be read.""" + + addon_store_info.side_effect = HassioAPIError("Boom") + + with pytest.raises(HomeAssistantError): + await silabs_multiprotocol_addon.check_multi_pan_addon(hass) + + +async def test_check_multi_pan_addon_bad_state(hass: HomeAssistant) -> None: + """Test `check_multi_pan_addon` where the addon is in an unexpected state.""" + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager", + return_value=Mock( + spec_set=silabs_multiprotocol_addon.MultiprotocolAddonManager + ), + ) as mock_get_addon_manager: + manager = mock_get_addon_manager.return_value + manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname="core_silabs_multiprotocol", + options={}, + state=AddonState.UPDATING, + update_available=False, + version="1.0.0", + ) + + with pytest.raises(HomeAssistantError): + await silabs_multiprotocol_addon.check_multi_pan_addon(hass) + + manager.async_start_addon.assert_not_called() + + +async def test_check_multi_pan_addon_auto_start( + hass: HomeAssistant, addon_info, addon_store_info, start_addon +) -> None: + """Test `check_multi_pan_addon` auto starting the addon.""" + + addon_info.return_value["state"] = "not_running" + addon_store_info.return_value = { + "installed": True, + "available": True, + "state": "not_running", + } + + # An error is raised even if we auto-start + with pytest.raises(HomeAssistantError): + await silabs_multiprotocol_addon.check_multi_pan_addon(hass) + + start_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") + + +async def test_check_multi_pan_addon( + hass: HomeAssistant, addon_info, addon_store_info, start_addon +) -> None: + """Test `check_multi_pan_addon`.""" + + addon_info.return_value["state"] = "started" + addon_store_info.return_value = { + "installed": True, + "available": True, + "state": "running", + } + + await silabs_multiprotocol_addon.check_multi_pan_addon(hass) + start_addon.assert_not_called() + + +async def test_multi_pan_addon_using_device_no_hassio(hass: HomeAssistant) -> None: + """Test `multi_pan_addon_using_device` without hassio.""" + + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + return_value=False, + ): + assert ( + await silabs_multiprotocol_addon.multi_pan_addon_using_device( + hass, "/dev/ttyAMA1" + ) + is False + ) + + +async def test_multi_pan_addon_using_device_not_running( + hass: HomeAssistant, addon_info, addon_store_info +) -> None: + """Test `multi_pan_addon_using_device` when the addon isn't running.""" + + addon_info.return_value["state"] = "not_running" + addon_store_info.return_value = { + "installed": True, + "available": True, + "state": "not_running", + } + + await silabs_multiprotocol_addon.multi_pan_addon_using_device( + hass, "/dev/ttyAMA1" + ) is False + + +@pytest.mark.parametrize( + ("options_device", "expected_result"), + [("/dev/ttyAMA2", False), ("/dev/ttyAMA1", True)], +) +async def test_multi_pan_addon_using_device( + hass: HomeAssistant, + addon_info, + addon_store_info, + options_device: str, + expected_result: bool, +) -> None: + """Test `multi_pan_addon_using_device` when the addon isn't running.""" + + addon_info.return_value["state"] = "started" + addon_info.return_value["options"] = { + "autoflash_firmware": True, + "device": options_device, + "baudrate": "115200", + "flow_control": True, + } + addon_store_info.return_value = { + "installed": True, + "available": True, + "state": "running", + } + + await silabs_multiprotocol_addon.multi_pan_addon_using_device( + hass, "/dev/ttyAMA1" + ) is expected_result diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index 3677b4ea8f1..85017866db9 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -9,7 +9,7 @@ import pytest def mock_usb_serial_by_id_fixture() -> Generator[MagicMock, None, None]: """Mock usb serial by id.""" with patch( - "homeassistant.components.zwave_js.config_flow.usb.get_serial_by_id" + "homeassistant.components.zha.config_flow.usb.get_serial_by_id" ) as mock_usb_serial_by_id: mock_usb_serial_by_id.side_effect = lambda x: x yield mock_usb_serial_by_id @@ -149,3 +149,21 @@ def start_addon_fixture(): "homeassistant.components.hassio.addon_manager.async_start_addon" ) as start_addon: yield start_addon + + +@pytest.fixture(name="stop_addon") +def stop_addon_fixture(): + """Mock stop add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_stop_addon" + ) as stop_addon: + yield stop_addon + + +@pytest.fixture(name="uninstall_addon") +def uninstall_addon_fixture(): + """Mock uninstall add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_uninstall_addon" + ) as uninstall_addon: + yield uninstall_addon diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index c74adbf32ea..9e1977192e9 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -1,7 +1,10 @@ """Test the Home Assistant SkyConnect config flow.""" +from collections.abc import Generator import copy from unittest.mock import Mock, patch +import pytest + from homeassistant.components import homeassistant_sky_connect, usb from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.components.zha.core.const import ( @@ -25,6 +28,15 @@ USB_DATA = usb.UsbServiceInfo( ) +@pytest.fixture(autouse=True) +def config_flow_handler(hass: HomeAssistant) -> Generator[None, None, None]: + """Fixture for a test config flow.""" + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.WaitingAddonManager.async_wait_until_addon_state" + ): + yield + + async def test_config_flow(hass: HomeAssistant) -> None: """Test the config flow.""" with patch( diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index 746e119082c..cbf1cfa7d36 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -24,6 +24,13 @@ CONFIG_ENTRY_DATA = { } +@pytest.fixture(autouse=True) +def disable_usb_probing() -> Generator[None, None, None]: + """Disallow touching of system USB devices during unit tests.""" + with patch("homeassistant.components.usb.comports", return_value=[]): + yield + + @pytest.fixture def mock_zha_config_flow_setup() -> Generator[None, None, None]: """Mock the radio connection and probing of the ZHA config flow.""" diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 66401bcd7bc..58d47c41987 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Home Assistant Yellow config flow.""" +from collections.abc import Generator from unittest.mock import Mock, patch import pytest @@ -11,6 +12,15 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration +@pytest.fixture(autouse=True) +def config_flow_handler(hass: HomeAssistant) -> Generator[None, None, None]: + """Fixture for a test config flow.""" + with patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.WaitingAddonManager.async_wait_until_addon_state" + ): + yield + + @pytest.fixture(name="get_yellow_settings") def mock_get_yellow_settings(): """Mock getting yellow settings.""" diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index 4d90d83d483..c507db3e6ab 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -31,7 +31,9 @@ def disable_platform_only(): @pytest.fixture(autouse=True) def reduce_reconnect_timeout(): """Reduces reconnect timeout to speed up tests.""" - with patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0.0001): + with patch( + "homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0.0001 + ), patch("homeassistant.components.zha.radio_manager.RETRY_DELAY_S", 0.0001): yield @@ -83,7 +85,7 @@ def com_port(device="/dev/ttyUSB1234"): @pytest.fixture -def mock_connect_zigpy_app() -> Generator[None, None, None]: +def mock_connect_zigpy_app() -> Generator[MagicMock, None, None]: """Mock the radio connection.""" mock_connect_app = MagicMock() @@ -96,7 +98,7 @@ def mock_connect_zigpy_app() -> Generator[None, None, None]: "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", return_value=mock_connect_app, ): - yield + yield mock_connect_app @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) @@ -370,3 +372,52 @@ async def test_migrate_non_matching_port( "radio_type": "ezsp", } assert config_entry.title == "Test" + + +async def test_migrate_initiate_failure( + hass: HomeAssistant, + mock_connect_zigpy_app, +) -> None: + """Test retries with failure.""" + # Set up the config entry + config_entry = MockConfigEntry( + data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, + domain=DOMAIN, + options={}, + title="Test", + ) + config_entry.add_to_hass(hass) + config_entry.state = config_entries.ConfigEntryState.SETUP_IN_PROGRESS + + migration_data = { + "new_discovery_info": { + "name": "Test Updated", + "port": { + "path": "socket://some/virtual_port", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + }, + "old_discovery_info": { + "hw": { + "name": "Test", + "port": { + "path": "/dev/ttyTEST123", + "baudrate": 115200, + "flow_control": "hardware", + }, + "radio_type": "efr32", + } + }, + } + + mock_load_info = AsyncMock(side_effect=OSError()) + mock_connect_zigpy_app.__aenter__.return_value.load_network_info = mock_load_info + + migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) + + with pytest.raises(OSError): + await migration_helper.async_initiate_migration(migration_data) + + assert len(mock_load_info.mock_calls) == radio_manager.BACKUP_RETRIES From 5ec645161de4d68138a236f2c20b259e8628e59b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Aug 2023 23:01:39 -0500 Subject: [PATCH 0983/1151] Bump zeroconf to 0.88.0 (#99248) changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.86.0...0.88.0 --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index e1f1b4ebe69..79b7e514f51 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.86.0"] + "requirements": ["zeroconf==0.88.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e430a25b248..c8c3de858a7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.86.0 +zeroconf==0.88.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 89df11dc43f..ddc80b1ac34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2763,7 +2763,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.86.0 +zeroconf==0.88.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 317254f2142..f6d09d605b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2033,7 +2033,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.86.0 +zeroconf==0.88.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 202b0b5300a9c73d46e092dc4cea2e6810a9a05d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 29 Aug 2023 08:40:35 +0200 Subject: [PATCH 0984/1151] Migrate Venstar to has entity name (#99013) --- homeassistant/components/venstar/__init__.py | 2 ++ .../components/venstar/binary_sensor.py | 2 +- homeassistant/components/venstar/climate.py | 2 +- homeassistant/components/venstar/sensor.py | 18 +++++++++--------- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 3bf74f57413..a92d495f6af 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -128,6 +128,8 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator[None class VenstarEntity(CoordinatorEntity[VenstarDataUpdateCoordinator]): """Representation of a Venstar entity.""" + _attr_has_entity_name = True + def __init__( self, venstar_data_coordinator: VenstarDataUpdateCoordinator, diff --git a/homeassistant/components/venstar/binary_sensor.py b/homeassistant/components/venstar/binary_sensor.py index 6104cd0d4f9..a5e15b04917 100644 --- a/homeassistant/components/venstar/binary_sensor.py +++ b/homeassistant/components/venstar/binary_sensor.py @@ -37,7 +37,7 @@ class VenstarBinarySensor(VenstarEntity, BinarySensorEntity): super().__init__(coordinator, config) self.alert = alert self._attr_unique_id = f"{config.entry_id}_{alert.replace(' ', '_')}" - self._attr_name = f"{self._client.name} {alert}" + self._attr_name = alert @property def is_on(self): diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index b4d3b6c6837..6359cc19e57 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -107,6 +107,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): _attr_fan_modes = [FAN_ON, FAN_AUTO] _attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF, HVACMode.AUTO] _attr_precision = PRECISION_HALVES + _attr_name = None def __init__( self, @@ -121,7 +122,6 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): HVACMode.AUTO: self._client.MODE_AUTO, } self._attr_unique_id = config.entry_id - self._attr_name = self._client.name @property def supported_features(self) -> ClimateEntityFeature: diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py index e20c748f112..2d919bbc1bc 100644 --- a/homeassistant/components/venstar/sensor.py +++ b/homeassistant/components/venstar/sensor.py @@ -70,7 +70,7 @@ class VenstarSensorTypeMixin: """Mixin for sensor required keys.""" value_fn: Callable[[VenstarDataUpdateCoordinator, str], Any] - name_fn: Callable[[VenstarDataUpdateCoordinator, str], str] + name_fn: Callable[[str], str] uom_fn: Callable[[Any], str | None] @@ -156,7 +156,7 @@ class VenstarSensor(VenstarEntity, SensorEntity): @property def name(self): """Return the name of the device.""" - return self.entity_description.name_fn(self.coordinator, self.sensor_name) + return self.entity_description.name_fn(self.sensor_name) @property def native_value(self) -> int: @@ -178,7 +178,7 @@ SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( sensor_name, "hum" ), - name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name} Humidity", + name_fn=lambda sensor_name: f"{sensor_name} Humidity", ), VenstarSensorEntityDescription( key="temp", @@ -188,7 +188,7 @@ SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( value_fn=lambda coordinator, sensor_name: round( float(coordinator.client.get_sensor(sensor_name, "temp")), 1 ), - name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name.replace(' Temp', '')} Temperature", + name_fn=lambda sensor_name: f"{sensor_name.replace(' Temp', '')} Temperature", ), VenstarSensorEntityDescription( key="co2", @@ -198,7 +198,7 @@ SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( sensor_name, "co2" ), - name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name} CO2", + name_fn=lambda sensor_name: f"{sensor_name} CO2", ), VenstarSensorEntityDescription( key="iaq", @@ -208,7 +208,7 @@ SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( sensor_name, "iaq" ), - name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name} IAQ", + name_fn=lambda sensor_name: f"{sensor_name} IAQ", ), VenstarSensorEntityDescription( key="battery", @@ -218,7 +218,7 @@ SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( sensor_name, "battery" ), - name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name} Battery", + name_fn=lambda sensor_name: f"{sensor_name} Battery", ), ) @@ -227,7 +227,7 @@ RUNTIME_ENTITY = VenstarSensorEntityDescription( state_class=SensorStateClass.MEASUREMENT, uom_fn=lambda _: UnitOfTime.MINUTES, value_fn=lambda coordinator, sensor_name: coordinator.runtimes[-1][sensor_name], - name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {RUNTIME_ATTRIBUTES[sensor_name]} Runtime", + name_fn=lambda sensor_name: f"{RUNTIME_ATTRIBUTES[sensor_name]} Runtime", ) INFO_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( @@ -240,6 +240,6 @@ INFO_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( value_fn=lambda coordinator, sensor_name: SCHEDULE_PARTS[ coordinator.client.get_info(sensor_name) ], - name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} Schedule Part", + name_fn=lambda _: "Schedule Part", ), ) From cdf39ec3657843d20f38fef26504be8633308c2c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 29 Aug 2023 08:42:37 +0200 Subject: [PATCH 0985/1151] Migrate Vilfo to has entity name (#99018) --- homeassistant/components/vilfo/sensor.py | 27 ++++++++---------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py index 7bdba371f49..ad2779ed521 100644 --- a/homeassistant/components/vilfo/sensor.py +++ b/homeassistant/components/vilfo/sensor.py @@ -9,6 +9,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 DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -70,18 +71,19 @@ class VilfoRouterSensor(SensorEntity): """Define a Vilfo Router Sensor.""" entity_description: VilfoSensorEntityDescription + _attr_has_entity_name = True def __init__(self, api, description: VilfoSensorEntityDescription) -> None: """Initialize.""" self.entity_description = description self.api = api - self._device_info = { - "identifiers": {(DOMAIN, api.host, api.mac_address)}, - "name": ROUTER_DEFAULT_NAME, - "manufacturer": ROUTER_MANUFACTURER, - "model": ROUTER_DEFAULT_MODEL, - "sw_version": api.firmware_version, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, api.host, api.mac_address)}, # type: ignore[arg-type] + name=ROUTER_DEFAULT_NAME, + manufacturer=ROUTER_MANUFACTURER, + model=ROUTER_DEFAULT_MODEL, + sw_version=api.firmware_version, + ) self._attr_unique_id = f"{api.unique_id}_{description.key}" @property @@ -89,17 +91,6 @@ class VilfoRouterSensor(SensorEntity): """Return whether the sensor is available or not.""" return self.api.available - @property - def device_info(self): - """Return the device info.""" - return self._device_info - - @property - def name(self): - """Return the name of the sensor.""" - parent_device_name = self._device_info["name"] - return f"{parent_device_name} {self.entity_description.name}" - async def async_update(self) -> None: """Update the router data.""" await self.api.async_update() From be126da72d7f088a1bb4cdb3defecfe919e7f2a0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 29 Aug 2023 02:45:43 -0400 Subject: [PATCH 0986/1151] Bump zwave-js-server-python to 0.51.0 (#99250) * Bump zwave-js-server-python to 0.51.0 * Fix how we patch the command --- homeassistant/components/zwave_js/api.py | 4 +-- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 29 +++++++++---------- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 6781ccacdc7..fdf1b83cc6c 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1730,7 +1730,7 @@ async def websocket_subscribe_log_updates( @callback def async_cleanup() -> None: """Remove signal listeners.""" - hass.async_create_task(driver.async_stop_listening_logs()) + hass.async_create_task(client.async_stop_listening_logs()) for unsub in unsubs: unsub() @@ -1771,7 +1771,7 @@ async def websocket_subscribe_log_updates( ] connection.subscriptions[msg["id"]] = async_cleanup - await driver.async_start_listening_logs() + await client.async_start_listening_logs() connection.send_result(msg[ID]) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 43dddd08a1a..7371a7a8896 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.50.1"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.0"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index ddc80b1ac34..d0a18d0d173 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2796,7 +2796,7 @@ zigpy==0.56.4 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.50.1 +zwave-js-server-python==0.51.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6d09d605b0..02194adf253 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2057,7 +2057,7 @@ zigpy-znp==0.11.4 zigpy==0.56.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.50.1 +zwave-js-server-python==0.51.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 5bafe932362..e686def8883 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3298,22 +3298,21 @@ async def test_subscribe_log_updates( } # Test FailedZWaveCommand is caught - with patch( - "zwave_js_server.model.driver.Driver.async_start_listening_logs", - side_effect=FailedZWaveCommand("failed_command", 1, "error message"), - ): - await ws_client.send_json( - { - ID: 2, - TYPE: "zwave_js/subscribe_log_updates", - ENTRY_ID: entry.entry_id, - } - ) - msg = await ws_client.receive_json() + client.async_start_listening_logs.side_effect = FailedZWaveCommand( + "failed_command", 1, "error message" + ) + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/subscribe_log_updates", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() - assert not msg["success"] - assert msg["error"]["code"] == "zwave_error" - assert msg["error"]["message"] == "Z-Wave error 1: error message" + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) From b5ff0b4ec2c304bdcaa13609235f95340c246460 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 29 Aug 2023 08:47:35 +0200 Subject: [PATCH 0987/1151] Add entity translations to Vilfo (#99019) --- homeassistant/components/vilfo/sensor.py | 4 ++-- homeassistant/components/vilfo/strings.json | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py index ad2779ed521..511e25bbfba 100644 --- a/homeassistant/components/vilfo/sensor.py +++ b/homeassistant/components/vilfo/sensor.py @@ -39,14 +39,14 @@ class VilfoSensorEntityDescription(SensorEntityDescription, VilfoRequiredKeysMix SENSOR_TYPES: tuple[VilfoSensorEntityDescription, ...] = ( VilfoSensorEntityDescription( key=ATTR_LOAD, - name="Load", + translation_key=ATTR_LOAD, native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", api_key=ATTR_API_DATA_FIELD_LOAD, ), VilfoSensorEntityDescription( key=ATTR_BOOT_TIME, - name="Boot time", + translation_key=ATTR_BOOT_TIME, icon="mdi:timer-outline", api_key=ATTR_API_DATA_FIELD_BOOT_TIME, device_class=SensorDeviceClass.TIMESTAMP, diff --git a/homeassistant/components/vilfo/strings.json b/homeassistant/components/vilfo/strings.json index 6577c99456c..d559e3a6716 100644 --- a/homeassistant/components/vilfo/strings.json +++ b/homeassistant/components/vilfo/strings.json @@ -16,5 +16,15 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "load": { + "name": "Load" + }, + "boot_time": { + "name": "Boot time" + } + } } } From c81d39f6512206da2a6668e4ed2a96b73e7607d9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 29 Aug 2023 08:58:20 +0200 Subject: [PATCH 0988/1151] Fix Renault AssertionError (#99189) --- .../components/renault/renault_hub.py | 10 +++++++- .../fixtures/vehicle_missing_details.json | 25 +++++++++++++++++++ tests/components/renault/test_init.py | 16 ++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/components/renault/fixtures/vehicle_missing_details.json diff --git a/homeassistant/components/renault/renault_hub.py b/homeassistant/components/renault/renault_hub.py index b7a9b40e2c9..49819dd919f 100644 --- a/homeassistant/components/renault/renault_hub.py +++ b/homeassistant/components/renault/renault_hub.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ATTR_SW_VERSION, ) 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 @@ -57,8 +58,15 @@ class RenaultHub: self._account = await self._client.get_api_account(account_id) vehicles = await self._account.get_vehicles() - device_registry = dr.async_get(self._hass) if vehicles.vehicleLinks: + if any( + vehicle_link.vehicleDetails is None + for vehicle_link in vehicles.vehicleLinks + ): + raise ConfigEntryNotReady( + "Failed to retrieve vehicle details from Renault servers" + ) + device_registry = dr.async_get(self._hass) await asyncio.gather( *( self.async_initialise_vehicle( diff --git a/tests/components/renault/fixtures/vehicle_missing_details.json b/tests/components/renault/fixtures/vehicle_missing_details.json new file mode 100644 index 00000000000..f6467e0c8f8 --- /dev/null +++ b/tests/components/renault/fixtures/vehicle_missing_details.json @@ -0,0 +1,25 @@ +{ + "accountId": "account-id-1", + "country": "FR", + "vehicleLinks": [ + { + "brand": "RENAULT", + "vin": "VF1AAAAA555777999", + "status": "ACTIVE", + "linkType": "OWNER", + "garageBrand": "RENAULT", + "annualMileage": 16000, + "mileage": 26464, + "startDate": "2017-08-07", + "createdDate": "2019-05-23T21:38:16.409008Z", + "lastModifiedDate": "2020-11-17T08:41:40.497400Z", + "ownershipStartDate": "2017-08-01", + "cancellationReason": {}, + "connectedDriver": { + "role": "MAIN_DRIVER", + "createdDate": "2019-06-17T09:49:06.880627Z", + "lastModifiedDate": "2019-06-17T09:49:06.880627Z" + } + } + ] +} diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index e1c782d06d5..0f26bf6fbdb 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -95,3 +95,19 @@ async def test_setup_entry_kamereon_exception( assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) + + +@pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") +@pytest.mark.parametrize("vehicle_type", ["missing_details"], indirect=True) +async def test_setup_entry_missing_vehicle_details( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Test ConfigEntryNotReady when vehicleDetails is missing.""" + # In this case we are testing the condition where renault_hub fails to retrieve + # vehicle details (see #99127). + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) From 63c2a2994f8395c65869fa39005318b525cca554 Mon Sep 17 00:00:00 2001 From: liangjia2019 Date: Tue, 29 Aug 2023 15:17:27 +0800 Subject: [PATCH 0989/1151] Add new zigbee button SONOFF_SNZB_01P to deconz (#99205) add new zigbee button --- homeassistant/components/deconz/device_trigger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index a22c8cb9491..1b257d121b4 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -550,6 +550,7 @@ BUSCH_JAEGER_REMOTE = { SONOFF_SNZB_01_1_MODEL = "WB01" SONOFF_SNZB_01_2_MODEL = "WB-01" +SONOFF_SNZB_01P_MODEL = "SNZB-01P" SONOFF_SNZB_01_SWITCH = { (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002}, (CONF_LONG_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1003}, @@ -639,6 +640,7 @@ REMOTES = { UBISYS_CONTROL_UNIT_C4_MODEL: UBISYS_CONTROL_UNIT_C4, SONOFF_SNZB_01_1_MODEL: SONOFF_SNZB_01_SWITCH, SONOFF_SNZB_01_2_MODEL: SONOFF_SNZB_01_SWITCH, + SONOFF_SNZB_01P_MODEL: SONOFF_SNZB_01_SWITCH, } TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( From ddbf85fc38b40ef2bec2d0310356c9d2a17f1bf0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 29 Aug 2023 09:36:27 +0200 Subject: [PATCH 0990/1151] Abort YouTube configuration if user doesn't have subscriptions (#99140) --- .../components/youtube/config_flow.py | 2 + homeassistant/components/youtube/strings.json | 1 + .../fixtures/get_no_subscriptions.json | 10 +++++ tests/components/youtube/test_config_flow.py | 40 +++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 tests/components/youtube/fixtures/get_no_subscriptions.json diff --git a/homeassistant/components/youtube/config_flow.py b/homeassistant/components/youtube/config_flow.py index 50dee14d61a..cf0d61b5d38 100644 --- a/homeassistant/components/youtube/config_flow.py +++ b/homeassistant/components/youtube/config_flow.py @@ -145,6 +145,8 @@ class OAuth2FlowHandler( ) async for subscription in youtube.get_user_subscriptions() ] + if not selectable_channels: + return self.async_abort(reason="no_subscriptions") return self.async_show_form( step_id="channels", data_schema=vol.Schema( diff --git a/homeassistant/components/youtube/strings.json b/homeassistant/components/youtube/strings.json index ccb7e9c506e..1b9ecbc1cb3 100644 --- a/homeassistant/components/youtube/strings.json +++ b/homeassistant/components/youtube/strings.json @@ -5,6 +5,7 @@ "no_channel": "Please create a YouTube channel to be able to use the integration. Instructions can be found at {support_url}.", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "no_subscriptions": "You need to be subscribed to YouTube channels in order to add them.", "unknown": "[%key:common::config_flow::error::unknown%]" }, "error": { diff --git a/tests/components/youtube/fixtures/get_no_subscriptions.json b/tests/components/youtube/fixtures/get_no_subscriptions.json new file mode 100644 index 00000000000..77a64503fc0 --- /dev/null +++ b/tests/components/youtube/fixtures/get_no_subscriptions.json @@ -0,0 +1,10 @@ +{ + "kind": "youtube#SubscriptionListResponse", + "etag": "6C9iFE7CzKQqPrEoJlE0H2U27xI", + "nextPageToken": "CAEQAA", + "pageInfo": { + "totalResults": 0, + "resultsPerPage": 1 + }, + "items": [] +} diff --git a/tests/components/youtube/test_config_flow.py b/tests/components/youtube/test_config_flow.py index 97875004d11..c4aacc9603d 100644 --- a/tests/components/youtube/test_config_flow.py +++ b/tests/components/youtube/test_config_flow.py @@ -121,6 +121,46 @@ async def test_flow_abort_without_channel( assert result["reason"] == "no_channel" +async def test_flow_abort_without_subscriptions( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + current_request_with_host: None, +) -> None: + """Check abort flow if user has no subscriptions.""" + result = await hass.config_entries.flow.async_init( + "youtube", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope={'+'.join(SCOPES)}" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + service = MockYouTube(subscriptions_fixture="youtube/get_no_subscriptions.json") + with patch( + "homeassistant.components.youtube.async_setup_entry", return_value=True + ), patch( + "homeassistant.components.youtube.config_flow.YouTube", return_value=service + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_subscriptions" + + async def test_flow_http_error( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, From b22b51fe3b0911e16ba1376d9f1e9ab92ac3f549 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Aug 2023 10:28:32 +0200 Subject: [PATCH 0991/1151] Fix stale docstring in trafikverket_camera tests (#99260) --- tests/components/trafikverket_camera/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/trafikverket_camera/conftest.py b/tests/components/trafikverket_camera/conftest.py index 2bbc888b31d..fc6d70ae704 100644 --- a/tests/components/trafikverket_camera/conftest.py +++ b/tests/components/trafikverket_camera/conftest.py @@ -22,7 +22,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def load_integration_from_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, get_camera: CameraInfo ) -> MockConfigEntry: - """Set up the Trafikverket Ferry integration in Home Assistant.""" + """Set up the Trafikverket Camera integration in Home Assistant.""" aioclient_mock.get( "https://www.testurl.com/test_photo.jpg?type=fullsize", content=b"0123456789" ) From 7a690d7359e020400bb6fc3411ea7a26f097f48b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 29 Aug 2023 10:38:59 +0200 Subject: [PATCH 0992/1151] Add deprecation to legacy forecast for Weather (#97294) * Add deprecation to legacy forecast * Mod _reported * issue * remove not need variable * kitchen_sink * 2024.3 * remove demo and mqtt * add checks * fix deprecation * remove variable * fix kitchen_sink * Fix deprecation warning * Expand issue * clean * Fix tests * fix kitchen_sink * not report on core integrations --- homeassistant/components/weather/__init__.py | 59 +++++ homeassistant/components/weather/strings.json | 6 + tests/components/weather/test_init.py | 89 ++++++++ .../custom_components/test/weather.py | 31 +++ .../test_weather/__init__.py | 1 + .../test_weather/manifest.json | 9 + .../custom_components/test_weather/weather.py | 210 ++++++++++++++++++ 7 files changed, 405 insertions(+) create mode 100644 tests/testing_config/custom_components/test_weather/__init__.py create mode 100644 tests/testing_config/custom_components/test_weather/manifest.json create mode 100644 tests/testing_config/custom_components/test_weather/weather.py diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index d73d00ec9df..05a2e725e4a 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import abc +import asyncio from collections.abc import Callable, Iterable from contextlib import suppress from dataclasses import dataclass @@ -47,6 +48,8 @@ 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.entity_platform import EntityPlatform +import homeassistant.helpers.issue_registry as ir from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -318,6 +321,9 @@ class WeatherEntity(Entity, PostInit): Literal["daily", "hourly", "twice_daily"], list[Callable[[list[JsonValueType] | None], None]], ] + __weather_legacy_forecast: bool = False + __weather_legacy_forecast_reported: bool = False + __report_issue: str _weather_option_temperature_unit: str | None = None _weather_option_pressure_unit: str | None = None @@ -381,6 +387,59 @@ class WeatherEntity(Entity, PostInit): cls.__name__, report_issue, ) + if any( + method in cls.__dict__ for method in ("_attr_forecast", "forecast") + ) and not any( + method in cls.__dict__ + for method in ( + "async_forecast_daily", + "async_forecast_hourly", + "async_forecast_twice_daily", + ) + ): + cls.__weather_legacy_forecast = True + + @callback + def add_to_platform_start( + self, + hass: HomeAssistant, + platform: EntityPlatform, + parallel_updates: asyncio.Semaphore | None, + ) -> None: + """Start adding an entity to a platform.""" + super().add_to_platform_start(hass, platform, parallel_updates) + _reported_forecast = False + if self.__weather_legacy_forecast and not _reported_forecast: + module = inspect.getmodule(self) + if module and module.__file__ and "custom_components" in module.__file__: + # Do not report on core integrations as they are already fixed or PR is open. + report_issue = "report it to the custom integration author." + _LOGGER.warning( + ( + "%s::%s is using a forecast attribute on an instance of " + "WeatherEntity, this is deprecated and will be unsupported " + "from Home Assistant 2024.3. Please %s" + ), + self.__module__, + self.entity_id, + report_issue, + ) + ir.async_create_issue( + self.hass, + DOMAIN, + f"deprecated_weather_forecast_{self.platform.platform_name}", + breaks_in_ha_version="2024.3.0", + is_fixable=False, + is_persistent=False, + issue_domain=self.platform.platform_name, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_weather_forecast", + translation_placeholders={ + "platform": self.platform.platform_name, + "report_issue": report_issue, + }, + ) + _reported_forecast = True async def async_internal_added_to_hass(self) -> None: """Call when the weather entity is added to hass.""" diff --git a/homeassistant/components/weather/strings.json b/homeassistant/components/weather/strings.json index 5f08013684c..26388c217eb 100644 --- a/homeassistant/components/weather/strings.json +++ b/homeassistant/components/weather/strings.json @@ -98,5 +98,11 @@ } } } + }, + "issues": { + "deprecated_weather_forecast": { + "title": "The {platform} integration is using deprecated forecast", + "description": "The integration `{platform}` is using the deprecated forecast attribute.\n\nPlease {report_issue}." + } } } diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index d8636330b5e..feef335bec9 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -58,6 +58,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +import homeassistant.helpers.issue_registry as ir from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( @@ -71,6 +72,9 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from . import create_entity from tests.testing_config.custom_components.test import weather as WeatherPlatform +from tests.testing_config.custom_components.test_weather import ( + weather as NewWeatherPlatform, +) from tests.typing import WebSocketGenerator @@ -1225,3 +1229,88 @@ async def test_get_forecast_unsupported( blocking=True, return_response=True, ) + + +async def test_issue_forecast_deprecated( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the issue is raised on deprecated forecast attributes.""" + + kwargs = { + "native_temperature": 38, + "native_temperature_unit": UnitOfTemperature.CELSIUS, + } + platform: WeatherPlatform = getattr(hass.components, "test.weather") + caplog.clear() + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeatherMockLegacyForecastOnly( + name="Testing", + entity_id="weather.testing", + condition=ATTR_CONDITION_SUNNY, + **kwargs, + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test", "name": "testing"}} + ) + await hass.async_block_till_done() + + assert entity0.state == ATTR_CONDITION_SUNNY + + issues = ir.async_get(hass) + issue = issues.async_get_issue("weather", "deprecated_weather_forecast_test") + assert issue + assert issue.issue_domain == "test" + assert issue.issue_id == "deprecated_weather_forecast_test" + assert issue.translation_placeholders == { + "platform": "test", + "report_issue": "report it to the custom integration author.", + } + + assert ( + "custom_components.test.weather::weather.testing is using a forecast attribute on an instance of WeatherEntity" + in caplog.text + ) + + +async def test_issue_forecast_deprecated_no_logging( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the no issue is raised on deprecated forecast attributes if new methods exist.""" + + kwargs = { + "native_temperature": 38, + "native_temperature_unit": UnitOfTemperature.CELSIUS, + } + platform: NewWeatherPlatform = getattr(hass.components, "test_weather.weather") + caplog.clear() + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeatherMockForecast( + name="Test", + entity_id="weather.test", + condition=ATTR_CONDITION_SUNNY, + **kwargs, + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test_weather", "name": "test"}} + ) + await hass.async_block_till_done() + + assert entity0.state == ATTR_CONDITION_SUNNY + + assert "Setting up weather.test_weather" in caplog.text + assert ( + "custom_components.test_weather.weather::weather.test is using a forecast attribute on an instance of WeatherEntity" + not in caplog.text + ) diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index e2d026ec840..405b7b7d822 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -298,6 +298,37 @@ class MockWeatherMockForecast(MockWeather): ] +class MockWeatherMockLegacyForecastOnly(MockWeather): + """Mock weather class with mocked legacy forecast.""" + + def __init__(self, **values: Any) -> None: + """Initialize.""" + super().__init__(**values) + self.forecast_list: list[Forecast] | None = [ + { + ATTR_FORECAST_NATIVE_TEMP: self.native_temperature, + ATTR_FORECAST_NATIVE_APPARENT_TEMP: self.native_apparent_temperature, + ATTR_FORECAST_NATIVE_TEMP_LOW: self.native_temperature, + ATTR_FORECAST_NATIVE_DEW_POINT: self.native_dew_point, + ATTR_FORECAST_CLOUD_COVERAGE: self.cloud_coverage, + ATTR_FORECAST_NATIVE_PRESSURE: self.native_pressure, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: self.native_wind_gust_speed, + ATTR_FORECAST_NATIVE_WIND_SPEED: self.native_wind_speed, + ATTR_FORECAST_WIND_BEARING: self.wind_bearing, + ATTR_FORECAST_UV_INDEX: self.uv_index, + ATTR_FORECAST_NATIVE_PRECIPITATION: self._values.get( + "native_precipitation" + ), + ATTR_FORECAST_HUMIDITY: self.humidity, + } + ] + + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast.""" + return self.forecast_list + + class MockWeatherMockForecastCompat(MockWeatherCompat): """Mock weather class with mocked forecast for compatibility check.""" diff --git a/tests/testing_config/custom_components/test_weather/__init__.py b/tests/testing_config/custom_components/test_weather/__init__.py new file mode 100644 index 00000000000..ddec081ed8b --- /dev/null +++ b/tests/testing_config/custom_components/test_weather/__init__.py @@ -0,0 +1 @@ +"""An integration with Weather platform.""" diff --git a/tests/testing_config/custom_components/test_weather/manifest.json b/tests/testing_config/custom_components/test_weather/manifest.json new file mode 100644 index 00000000000..d1238659b41 --- /dev/null +++ b/tests/testing_config/custom_components/test_weather/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "test_weather", + "name": "Test Weather", + "documentation": "http://example.com", + "requirements": [], + "dependencies": [], + "codeowners": [], + "version": "1.2.3" +} diff --git a/tests/testing_config/custom_components/test_weather/weather.py b/tests/testing_config/custom_components/test_weather/weather.py new file mode 100644 index 00000000000..68d9ccab712 --- /dev/null +++ b/tests/testing_config/custom_components/test_weather/weather.py @@ -0,0 +1,210 @@ +"""Provide a mock weather platform. + +Call init before using it in your tests to ensure clean test data. +""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.weather import ( + ATTR_FORECAST_CLOUD_COVERAGE, + ATTR_FORECAST_HUMIDITY, + ATTR_FORECAST_IS_DAYTIME, + ATTR_FORECAST_NATIVE_APPARENT_TEMP, + ATTR_FORECAST_NATIVE_DEW_POINT, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_UV_INDEX, + ATTR_FORECAST_WIND_BEARING, + Forecast, + WeatherEntity, +) + +from tests.common import MockEntity + +ENTITIES = [] + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + ENTITIES = [] if empty else [MockWeatherMockForecast()] + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES) + + +class MockWeatherMockForecast(MockEntity, WeatherEntity): + """Mock weather class.""" + + def __init__(self, **values: Any) -> None: + """Initialize.""" + super().__init__(**values) + self.forecast_list: list[Forecast] | None = [ + { + ATTR_FORECAST_NATIVE_TEMP: self.native_temperature, + ATTR_FORECAST_NATIVE_APPARENT_TEMP: self.native_apparent_temperature, + ATTR_FORECAST_NATIVE_TEMP_LOW: self.native_temperature, + ATTR_FORECAST_NATIVE_DEW_POINT: self.native_dew_point, + ATTR_FORECAST_CLOUD_COVERAGE: self.cloud_coverage, + ATTR_FORECAST_NATIVE_PRESSURE: self.native_pressure, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: self.native_wind_gust_speed, + ATTR_FORECAST_NATIVE_WIND_SPEED: self.native_wind_speed, + ATTR_FORECAST_WIND_BEARING: self.wind_bearing, + ATTR_FORECAST_UV_INDEX: self.uv_index, + ATTR_FORECAST_NATIVE_PRECIPITATION: self._values.get( + "native_precipitation" + ), + ATTR_FORECAST_HUMIDITY: self.humidity, + } + ] + + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast.""" + return self.forecast_list + + async def async_forecast_daily(self) -> list[Forecast] | None: + """Return the forecast_daily.""" + return self.forecast_list + + async def async_forecast_twice_daily(self) -> list[Forecast] | None: + """Return the forecast_twice_daily.""" + return [ + { + ATTR_FORECAST_NATIVE_TEMP: self.native_temperature, + ATTR_FORECAST_NATIVE_APPARENT_TEMP: self.native_apparent_temperature, + ATTR_FORECAST_NATIVE_TEMP_LOW: self.native_temperature, + ATTR_FORECAST_NATIVE_DEW_POINT: self.native_dew_point, + ATTR_FORECAST_CLOUD_COVERAGE: self.cloud_coverage, + ATTR_FORECAST_NATIVE_PRESSURE: self.native_pressure, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: self.native_wind_gust_speed, + ATTR_FORECAST_NATIVE_WIND_SPEED: self.native_wind_speed, + ATTR_FORECAST_WIND_BEARING: self.wind_bearing, + ATTR_FORECAST_UV_INDEX: self.uv_index, + ATTR_FORECAST_NATIVE_PRECIPITATION: self._values.get( + "native_precipitation" + ), + ATTR_FORECAST_HUMIDITY: self.humidity, + ATTR_FORECAST_IS_DAYTIME: self._values.get("is_daytime"), + } + ] + + async def async_forecast_hourly(self) -> list[Forecast] | None: + """Return the forecast_hourly.""" + return [ + { + ATTR_FORECAST_NATIVE_TEMP: self.native_temperature, + ATTR_FORECAST_NATIVE_APPARENT_TEMP: self.native_apparent_temperature, + ATTR_FORECAST_NATIVE_TEMP_LOW: self.native_temperature, + ATTR_FORECAST_NATIVE_DEW_POINT: self.native_dew_point, + ATTR_FORECAST_CLOUD_COVERAGE: self.cloud_coverage, + ATTR_FORECAST_NATIVE_PRESSURE: self.native_pressure, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: self.native_wind_gust_speed, + ATTR_FORECAST_NATIVE_WIND_SPEED: self.native_wind_speed, + ATTR_FORECAST_WIND_BEARING: self.wind_bearing, + ATTR_FORECAST_UV_INDEX: self.uv_index, + ATTR_FORECAST_NATIVE_PRECIPITATION: self._values.get( + "native_precipitation" + ), + ATTR_FORECAST_HUMIDITY: self.humidity, + } + ] + + @property + def native_temperature(self) -> float | None: + """Return the platform temperature.""" + return self._handle("native_temperature") + + @property + def native_apparent_temperature(self) -> float | None: + """Return the platform apparent temperature.""" + return self._handle("native_apparent_temperature") + + @property + def native_dew_point(self) -> float | None: + """Return the platform dewpoint temperature.""" + return self._handle("native_dew_point") + + @property + def native_temperature_unit(self) -> str | None: + """Return the unit of measurement for temperature.""" + return self._handle("native_temperature_unit") + + @property + def native_pressure(self) -> float | None: + """Return the pressure.""" + return self._handle("native_pressure") + + @property + def native_pressure_unit(self) -> str | None: + """Return the unit of measurement for pressure.""" + return self._handle("native_pressure_unit") + + @property + def humidity(self) -> float | None: + """Return the humidity.""" + return self._handle("humidity") + + @property + def native_wind_gust_speed(self) -> float | None: + """Return the wind speed.""" + return self._handle("native_wind_gust_speed") + + @property + def native_wind_speed(self) -> float | None: + """Return the wind speed.""" + return self._handle("native_wind_speed") + + @property + def native_wind_speed_unit(self) -> str | None: + """Return the unit of measurement for wind speed.""" + return self._handle("native_wind_speed_unit") + + @property + def wind_bearing(self) -> float | str | None: + """Return the wind bearing.""" + return self._handle("wind_bearing") + + @property + def ozone(self) -> float | None: + """Return the ozone level.""" + return self._handle("ozone") + + @property + def cloud_coverage(self) -> float | None: + """Return the cloud coverage in %.""" + return self._handle("cloud_coverage") + + @property + def uv_index(self) -> float | None: + """Return the UV index.""" + return self._handle("uv_index") + + @property + def native_visibility(self) -> float | None: + """Return the visibility.""" + return self._handle("native_visibility") + + @property + def native_visibility_unit(self) -> str | None: + """Return the unit of measurement for visibility.""" + return self._handle("native_visibility_unit") + + @property + def native_precipitation_unit(self) -> str | None: + """Return the native unit of measurement for accumulated precipitation.""" + return self._handle("native_precipitation_unit") + + @property + def condition(self) -> str | None: + """Return the current condition.""" + return self._handle("condition") From 4632a07f3fd97ae04d36ddd85d66cee4a762b063 Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 29 Aug 2023 10:45:37 +0200 Subject: [PATCH 0993/1151] Add possibility to have multiple values for every modbus hvac mode (#98829) Co-authored-by: jan iversen --- homeassistant/components/modbus/__init__.py | 28 +++++++++++++++------ homeassistant/components/modbus/climate.py | 8 +++--- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 920188603fc..cb36661d711 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -240,13 +240,27 @@ CLIMATE_SCHEMA = vol.All( { CONF_ADDRESS: cv.positive_int, CONF_HVAC_MODE_VALUES: { - vol.Optional(CONF_HVAC_MODE_OFF): cv.positive_int, - vol.Optional(CONF_HVAC_MODE_HEAT): cv.positive_int, - vol.Optional(CONF_HVAC_MODE_COOL): cv.positive_int, - vol.Optional(CONF_HVAC_MODE_HEAT_COOL): cv.positive_int, - vol.Optional(CONF_HVAC_MODE_AUTO): cv.positive_int, - vol.Optional(CONF_HVAC_MODE_DRY): cv.positive_int, - vol.Optional(CONF_HVAC_MODE_FAN_ONLY): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_OFF): vol.Any( + cv.positive_int, [cv.positive_int] + ), + vol.Optional(CONF_HVAC_MODE_HEAT): vol.Any( + cv.positive_int, [cv.positive_int] + ), + vol.Optional(CONF_HVAC_MODE_COOL): vol.Any( + cv.positive_int, [cv.positive_int] + ), + vol.Optional(CONF_HVAC_MODE_HEAT_COOL): vol.Any( + cv.positive_int, [cv.positive_int] + ), + vol.Optional(CONF_HVAC_MODE_AUTO): vol.Any( + cv.positive_int, [cv.positive_int] + ), + vol.Optional(CONF_HVAC_MODE_DRY): vol.Any( + cv.positive_int, [cv.positive_int] + ), + vol.Optional(CONF_HVAC_MODE_FAN_ONLY): vol.Any( + cv.positive_int, [cv.positive_int] + ), }, vol.Optional(CONF_WRITE_REGISTERS, default=False): cv.boolean, } diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 7170716d43e..3acf8d7ac29 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -124,9 +124,11 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): (CONF_HVAC_MODE_FAN_ONLY, HVACMode.FAN_ONLY), ): if hvac_mode_kw in mode_value_config: - self._hvac_mode_mapping.append( - (mode_value_config[hvac_mode_kw], hvac_mode) - ) + values = mode_value_config[hvac_mode_kw] + if not isinstance(values, list): + values = [values] + for value in values: + self._hvac_mode_mapping.append((value, hvac_mode)) self._attr_hvac_modes.append(hvac_mode) else: From e6eadc79e9a76bf2edf6797dbde33e0442bb77a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Aug 2023 11:12:34 +0200 Subject: [PATCH 0994/1151] Small typing fix in light group (#99259) --- homeassistant/components/group/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 38da7088c2e..3c1ad7f0d57 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -150,7 +150,7 @@ class LightGroup(GroupEntity, LightEntity): _attr_should_poll = False def __init__( - self, unique_id: str | None, name: str, entity_ids: list[str], mode: str | None + self, unique_id: str | None, name: str, entity_ids: list[str], mode: bool | None ) -> None: """Initialize a light group.""" self._entity_ids = entity_ids From 657ed0bcdbfc6d1e098c1ffac1a84c3c0dc551b4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 29 Aug 2023 11:33:56 +0200 Subject: [PATCH 0995/1151] Clean out compatibility for deprecated methods in Weather (#99263) Clean out compatability in Weather --- homeassistant/components/weather/__init__.py | 186 ------------ tests/components/weather/test_init.py | 281 ------------------ .../custom_components/test/weather.py | 23 -- 3 files changed, 490 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 05a2e725e4a..0d72dbb825e 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -274,35 +274,8 @@ class WeatherEntity(Entity, PostInit): _attr_cloud_coverage: int | None = None _attr_uv_index: float | None = None _attr_precision: float - _attr_pressure: None = ( - None # Provide backwards compatibility. Use _attr_native_pressure - ) - _attr_pressure_unit: None = ( - None # Provide backwards compatibility. Use _attr_native_pressure_unit - ) _attr_state: None = None - _attr_temperature: None = ( - None # Provide backwards compatibility. Use _attr_native_temperature - ) - _attr_temperature_unit: None = ( - None # Provide backwards compatibility. Use _attr_native_temperature_unit - ) - _attr_visibility: None = ( - None # Provide backwards compatibility. Use _attr_native_visibility - ) - _attr_visibility_unit: None = ( - None # Provide backwards compatibility. Use _attr_native_visibility_unit - ) - _attr_precipitation_unit: None = ( - None # Provide backwards compatibility. Use _attr_native_precipitation_unit - ) _attr_wind_bearing: float | str | None = None - _attr_wind_speed: None = ( - None # Provide backwards compatibility. Use _attr_native_wind_speed - ) - _attr_wind_speed_unit: None = ( - None # Provide backwards compatibility. Use _attr_native_wind_speed_unit - ) _attr_native_pressure: float | None = None _attr_native_pressure_unit: str | None = None @@ -322,8 +295,6 @@ class WeatherEntity(Entity, PostInit): list[Callable[[list[JsonValueType] | None], None]], ] __weather_legacy_forecast: bool = False - __weather_legacy_forecast_reported: bool = False - __report_issue: str _weather_option_temperature_unit: str | None = None _weather_option_pressure_unit: str | None = None @@ -338,55 +309,6 @@ class WeatherEntity(Entity, PostInit): def __init_subclass__(cls, **kwargs: Any) -> None: """Post initialisation processing.""" super().__init_subclass__(**kwargs) - - _reported = False - if any( - method in cls.__dict__ - for method in ( - "_attr_temperature", - "temperature", - "_attr_temperature_unit", - "temperature_unit", - "_attr_pressure", - "pressure", - "_attr_pressure_unit", - "pressure_unit", - "_attr_wind_speed", - "wind_speed", - "_attr_wind_speed_unit", - "wind_speed_unit", - "_attr_visibility", - "visibility", - "_attr_visibility_unit", - "visibility_unit", - "_attr_precipitation_unit", - "precipitation_unit", - ) - ): - if _reported is False: - module = inspect.getmodule(cls) - _reported = True - if ( - module - and module.__file__ - and "custom_components" in module.__file__ - ): - report_issue = "report it to the custom integration author." - else: - report_issue = ( - "create a bug report at " - "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" - ) - _LOGGER.warning( - ( - "%s::%s is overriding deprecated methods on an instance of " - "WeatherEntity, this is not valid and will be unsupported " - "from Home Assistant 2023.1. Please %s" - ), - cls.__module__, - cls.__name__, - report_issue, - ) if any( method in cls.__dict__ for method in ("_attr_forecast", "forecast") ) and not any( @@ -453,29 +375,14 @@ class WeatherEntity(Entity, PostInit): """Return the apparent temperature in native units.""" return self._attr_native_temperature - @final - @property - def temperature(self) -> float | None: - """Return the temperature for backward compatibility. - - Should not be set by integrations. - """ - return self._attr_temperature - @property def native_temperature(self) -> float | None: """Return the temperature in native units.""" - if (temperature := self.temperature) is not None: - return temperature - return self._attr_native_temperature @property def native_temperature_unit(self) -> str | None: """Return the native unit of measurement for temperature.""" - if (temperature_unit := self.temperature_unit) is not None: - return temperature_unit - return self._attr_native_temperature_unit @property @@ -483,15 +390,6 @@ class WeatherEntity(Entity, PostInit): """Return the dew point temperature in native units.""" return self._attr_native_dew_point - @final - @property - def temperature_unit(self) -> str | None: - """Return the temperature unit for backward compatibility. - - Should not be set by integrations. - """ - return self._attr_temperature_unit - @final @property def _default_temperature_unit(self) -> str: @@ -515,40 +413,16 @@ class WeatherEntity(Entity, PostInit): return self._default_temperature_unit - @final - @property - def pressure(self) -> float | None: - """Return the pressure for backward compatibility. - - Should not be set by integrations. - """ - return self._attr_pressure - @property def native_pressure(self) -> float | None: """Return the pressure in native units.""" - if (pressure := self.pressure) is not None: - return pressure - return self._attr_native_pressure @property def native_pressure_unit(self) -> str | None: """Return the native unit of measurement for pressure.""" - if (pressure_unit := self.pressure_unit) is not None: - return pressure_unit - return self._attr_native_pressure_unit - @final - @property - def pressure_unit(self) -> str | None: - """Return the pressure unit for backward compatibility. - - Should not be set by integrations. - """ - return self._attr_pressure_unit - @final @property def _default_pressure_unit(self) -> str: @@ -584,40 +458,16 @@ class WeatherEntity(Entity, PostInit): """Return the wind gust speed in native units.""" return self._attr_native_wind_gust_speed - @final - @property - def wind_speed(self) -> float | None: - """Return the wind_speed for backward compatibility. - - Should not be set by integrations. - """ - return self._attr_wind_speed - @property def native_wind_speed(self) -> float | None: """Return the wind speed in native units.""" - if (wind_speed := self.wind_speed) is not None: - return wind_speed - return self._attr_native_wind_speed @property def native_wind_speed_unit(self) -> str | None: """Return the native unit of measurement for wind speed.""" - if (wind_speed_unit := self.wind_speed_unit) is not None: - return wind_speed_unit - return self._attr_native_wind_speed_unit - @final - @property - def wind_speed_unit(self) -> str | None: - """Return the wind_speed unit for backward compatibility. - - Should not be set by integrations. - """ - return self._attr_wind_speed_unit - @final @property def _default_wind_speed_unit(self) -> str: @@ -663,40 +513,16 @@ class WeatherEntity(Entity, PostInit): """Return the UV index.""" return self._attr_uv_index - @final - @property - def visibility(self) -> float | None: - """Return the visibility for backward compatibility. - - Should not be set by integrations. - """ - return self._attr_visibility - @property def native_visibility(self) -> float | None: """Return the visibility in native units.""" - if (visibility := self.visibility) is not None: - return visibility - return self._attr_native_visibility @property def native_visibility_unit(self) -> str | None: """Return the native unit of measurement for visibility.""" - if (visibility_unit := self.visibility_unit) is not None: - return visibility_unit - return self._attr_native_visibility_unit - @final - @property - def visibility_unit(self) -> str | None: - """Return the visibility unit for backward compatibility. - - Should not be set by integrations. - """ - return self._attr_visibility_unit - @final @property def _default_visibility_unit(self) -> str: @@ -743,20 +569,8 @@ class WeatherEntity(Entity, PostInit): @property def native_precipitation_unit(self) -> str | None: """Return the native unit of measurement for accumulated precipitation.""" - if (precipitation_unit := self.precipitation_unit) is not None: - return precipitation_unit - return self._attr_native_precipitation_unit - @final - @property - def precipitation_unit(self) -> str | None: - """Return the precipitation unit for backward compatibility. - - Should not be set by integrations. - """ - return self._attr_precipitation_unit - @final @property def _default_precipitation_unit(self) -> str: diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index feef335bec9..db3a18db914 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -15,7 +15,6 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_UV_INDEX, - ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_GUST_SPEED, ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_APPARENT_TEMPERATURE, @@ -46,7 +45,6 @@ from homeassistant.components.weather.const import ( ATTR_WEATHER_HUMIDITY, ) from homeassistant.const import ( - ATTR_FRIENDLY_NAME, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, @@ -128,31 +126,6 @@ class MockWeatherEntityPrecision(WeatherEntity): self._attr_precision = PRECISION_HALVES -class MockWeatherEntityCompat(WeatherEntity): - """Mock a Weather Entity using old attributes.""" - - def __init__(self) -> None: - """Initiate Entity.""" - super().__init__() - self._attr_condition = ATTR_CONDITION_SUNNY - self._attr_precipitation_unit = UnitOfLength.MILLIMETERS - self._attr_pressure = 10 - self._attr_pressure_unit = UnitOfPressure.HPA - self._attr_temperature = 20 - self._attr_temperature_unit = UnitOfTemperature.CELSIUS - self._attr_visibility = 30 - self._attr_visibility_unit = UnitOfLength.KILOMETERS - self._attr_wind_speed = 3 - self._attr_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND - self._attr_forecast = [ - Forecast( - datetime=datetime(2022, 6, 20, 0, 00, 00, tzinfo=dt_util.UTC), - precipitation=1, - temperature=20, - ) - ] - - @pytest.mark.parametrize( "native_unit", (UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS) ) @@ -775,219 +748,6 @@ async def test_custom_units( ) -async def test_backwards_compatibility( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: - """Test backwards compatibility.""" - wind_speed_value = 5 - wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND - pressure_value = 110000 - pressure_unit = UnitOfPressure.PA - temperature_value = 20 - temperature_unit = UnitOfTemperature.CELSIUS - visibility_value = 11 - visibility_unit = UnitOfLength.KILOMETERS - precipitation_value = 1 - precipitation_unit = UnitOfLength.MILLIMETERS - - hass.config.units = METRIC_SYSTEM - - platform: WeatherPlatform = getattr(hass.components, "test.weather") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockWeatherMockForecastCompat( - name="Test", - condition=ATTR_CONDITION_SUNNY, - temperature=temperature_value, - temperature_unit=temperature_unit, - wind_speed=wind_speed_value, - wind_speed_unit=wind_speed_unit, - pressure=pressure_value, - pressure_unit=pressure_unit, - visibility=visibility_value, - visibility_unit=visibility_unit, - precipitation=precipitation_value, - precipitation_unit=precipitation_unit, - unique_id="very_unique", - ) - ) - platform.ENTITIES.append( - platform.MockWeatherMockForecastCompat( - name="Test2", - condition=ATTR_CONDITION_SUNNY, - temperature=temperature_value, - temperature_unit=temperature_unit, - wind_speed=wind_speed_value, - pressure=pressure_value, - visibility=visibility_value, - precipitation=precipitation_value, - unique_id="very_unique2", - ) - ) - - entity0 = platform.ENTITIES[0] - entity1 = platform.ENTITIES[1] - assert await async_setup_component( - hass, "weather", {"weather": {"platform": "test"}} - ) - assert await async_setup_component( - hass, "weather", {"weather": {"platform": "test2"}} - ) - await hass.async_block_till_done() - - state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] - state1 = hass.states.get(entity1.entity_id) - forecast1 = state1.attributes[ATTR_FORECAST][0] - - assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == pytest.approx( - wind_speed_value * 3.6 - ) - assert ( - state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == pytest.approx( - temperature_value, rel=0.1 - ) - assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == UnitOfTemperature.CELSIUS - assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == pytest.approx( - pressure_value / 100 - ) - assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == UnitOfPressure.HPA - assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == pytest.approx( - visibility_value - ) - assert state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == UnitOfLength.KILOMETERS - assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == pytest.approx( - precipitation_value, rel=1e-2 - ) - assert state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == UnitOfLength.MILLIMETERS - - assert float(state1.attributes[ATTR_WEATHER_WIND_SPEED]) == pytest.approx( - wind_speed_value - ) - assert ( - state1.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] - == UnitOfSpeed.KILOMETERS_PER_HOUR - ) - assert float(state1.attributes[ATTR_WEATHER_TEMPERATURE]) == pytest.approx( - temperature_value, rel=0.1 - ) - assert state1.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == UnitOfTemperature.CELSIUS - assert float(state1.attributes[ATTR_WEATHER_PRESSURE]) == pytest.approx( - pressure_value - ) - assert state1.attributes[ATTR_WEATHER_PRESSURE_UNIT] == UnitOfPressure.HPA - assert float(state1.attributes[ATTR_WEATHER_VISIBILITY]) == pytest.approx( - visibility_value - ) - assert state1.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == UnitOfLength.KILOMETERS - assert float(forecast1[ATTR_FORECAST_PRECIPITATION]) == pytest.approx( - precipitation_value, rel=1e-2 - ) - assert ( - state1.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == UnitOfLength.MILLIMETERS - ) - - -async def test_backwards_compatibility_convert_values( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: - """Test backward compatibility for converting values.""" - wind_speed_value = 5 - wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND - pressure_value = 110000 - pressure_unit = UnitOfPressure.PA - temperature_value = 20 - temperature_unit = UnitOfTemperature.CELSIUS - visibility_value = 11 - visibility_unit = UnitOfLength.KILOMETERS - precipitation_value = 1 - precipitation_unit = UnitOfLength.MILLIMETERS - - hass.config.units = US_CUSTOMARY_SYSTEM - - platform: WeatherPlatform = getattr(hass.components, "test.weather") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockWeatherMockForecastCompat( - name="Test", - condition=ATTR_CONDITION_SUNNY, - temperature=temperature_value, - temperature_unit=temperature_unit, - wind_speed=wind_speed_value, - wind_speed_unit=wind_speed_unit, - pressure=pressure_value, - pressure_unit=pressure_unit, - visibility=visibility_value, - visibility_unit=visibility_unit, - precipitation=precipitation_value, - precipitation_unit=precipitation_unit, - unique_id="very_unique", - ) - ) - - entity0 = platform.ENTITIES[0] - assert await async_setup_component( - hass, "weather", {"weather": {"platform": "test"}} - ) - await hass.async_block_till_done() - - state = hass.states.get(entity0.entity_id) - - expected_wind_speed = round( - SpeedConverter.convert( - wind_speed_value, wind_speed_unit, UnitOfSpeed.MILES_PER_HOUR - ), - ROUNDING_PRECISION, - ) - expected_temperature = TemperatureConverter.convert( - temperature_value, temperature_unit, UnitOfTemperature.FAHRENHEIT - ) - expected_pressure = round( - PressureConverter.convert(pressure_value, pressure_unit, UnitOfPressure.INHG), - ROUNDING_PRECISION, - ) - expected_visibility = round( - DistanceConverter.convert( - visibility_value, visibility_unit, UnitOfLength.MILES - ), - ROUNDING_PRECISION, - ) - expected_precipitation = round( - DistanceConverter.convert( - precipitation_value, precipitation_unit, UnitOfLength.INCHES - ), - ROUNDING_PRECISION, - ) - - assert state.attributes == { - ATTR_FORECAST: [ - { - ATTR_FORECAST_PRECIPITATION: pytest.approx( - expected_precipitation, rel=0.1 - ), - ATTR_FORECAST_PRESSURE: pytest.approx(expected_pressure, rel=0.1), - ATTR_FORECAST_TEMP: pytest.approx(expected_temperature, rel=0.1), - ATTR_FORECAST_TEMP_LOW: pytest.approx(expected_temperature, rel=0.1), - ATTR_FORECAST_WIND_BEARING: None, - ATTR_FORECAST_WIND_SPEED: pytest.approx(expected_wind_speed, rel=0.1), - } - ], - ATTR_FRIENDLY_NAME: "Test", - ATTR_WEATHER_PRECIPITATION_UNIT: UnitOfLength.INCHES, - ATTR_WEATHER_PRESSURE: pytest.approx(expected_pressure, rel=0.1), - ATTR_WEATHER_PRESSURE_UNIT: UnitOfPressure.INHG, - ATTR_WEATHER_TEMPERATURE: pytest.approx(expected_temperature, rel=0.1), - ATTR_WEATHER_TEMPERATURE_UNIT: UnitOfTemperature.FAHRENHEIT, - ATTR_WEATHER_VISIBILITY: pytest.approx(expected_visibility, rel=0.1), - ATTR_WEATHER_VISIBILITY_UNIT: UnitOfLength.MILES, - ATTR_WEATHER_WIND_SPEED: pytest.approx(expected_wind_speed, rel=0.1), - ATTR_WEATHER_WIND_SPEED_UNIT: UnitOfSpeed.MILES_PER_HOUR, - } - - async def test_backwards_compatibility_round_temperature(hass: HomeAssistant) -> None: """Test backward compatibility for rounding temperature.""" @@ -1020,47 +780,6 @@ async def test_attr(hass: HomeAssistant) -> None: assert weather._wind_speed_unit == UnitOfSpeed.KILOMETERS_PER_HOUR -async def test_attr_compatibility(hass: HomeAssistant) -> None: - """Test the _attr attributes in compatibility mode.""" - - weather = MockWeatherEntityCompat() - weather.hass = hass - - assert weather.condition == ATTR_CONDITION_SUNNY - assert weather._precipitation_unit == UnitOfLength.MILLIMETERS - assert weather.pressure == 10 - assert weather._pressure_unit == UnitOfPressure.HPA - assert weather.temperature == 20 - assert weather._temperature_unit == UnitOfTemperature.CELSIUS - assert weather.visibility == 30 - assert weather.visibility_unit == UnitOfLength.KILOMETERS - assert weather.wind_speed == 3 - assert weather._wind_speed_unit == UnitOfSpeed.KILOMETERS_PER_HOUR - - forecast_entry = [ - Forecast( - datetime=datetime(2022, 6, 20, 0, 00, 00, tzinfo=dt_util.UTC), - precipitation=1, - temperature=20, - ) - ] - - assert weather.forecast == forecast_entry - - assert weather.state_attributes == { - ATTR_FORECAST: forecast_entry, - ATTR_WEATHER_PRESSURE: 10.0, - ATTR_WEATHER_PRESSURE_UNIT: UnitOfPressure.HPA, - ATTR_WEATHER_TEMPERATURE: 20.0, - ATTR_WEATHER_TEMPERATURE_UNIT: UnitOfTemperature.CELSIUS, - ATTR_WEATHER_VISIBILITY: 30.0, - ATTR_WEATHER_VISIBILITY_UNIT: UnitOfLength.KILOMETERS, - ATTR_WEATHER_WIND_SPEED: 3.0 * 3.6, - ATTR_WEATHER_WIND_SPEED_UNIT: UnitOfSpeed.KILOMETERS_PER_HOUR, - ATTR_WEATHER_PRECIPITATION_UNIT: UnitOfLength.MILLIMETERS, - } - - async def test_precision_for_temperature(hass: HomeAssistant) -> None: """Test the precision for temperature.""" diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index 405b7b7d822..84864c1dbb2 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -18,13 +18,8 @@ from homeassistant.components.weather import ( ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_NATIVE_WIND_GUST_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRESSURE, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_UV_INDEX, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, Forecast, WeatherEntity, ) @@ -327,21 +322,3 @@ class MockWeatherMockLegacyForecastOnly(MockWeather): def forecast(self) -> list[Forecast] | None: """Return the forecast.""" return self.forecast_list - - -class MockWeatherMockForecastCompat(MockWeatherCompat): - """Mock weather class with mocked forecast for compatibility check.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return [ - { - ATTR_FORECAST_TEMP: self.temperature, - ATTR_FORECAST_TEMP_LOW: self.temperature, - ATTR_FORECAST_PRESSURE: self.pressure, - ATTR_FORECAST_WIND_SPEED: self.wind_speed, - ATTR_FORECAST_WIND_BEARING: self.wind_bearing, - ATTR_FORECAST_PRECIPITATION: self._values.get("precipitation"), - } - ] From f1ec99b9c9f67e87387203e178c945d69322b5c9 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 29 Aug 2023 12:13:01 +0200 Subject: [PATCH 0996/1151] Add Freebox Home battery sensor (#99222) * Add Freebox Home battery sensor * Review * Review 2 * Freebox battery is a SensorEntity, not a FreeboxSensor --- homeassistant/components/freebox/sensor.py | 29 +- tests/components/freebox/const.py | 337 ++++++++++++++++++++- 2 files changed, 348 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 7290ce47c04..d065907a914 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -18,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import DOMAIN +from .home_base import FreeboxHomeEntity from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) @@ -62,7 +63,7 @@ async def async_setup_entry( ) -> None: """Set up the sensors.""" router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] - entities = [] + entities: list[SensorEntity] = [] _LOGGER.debug( "%s - %s - %s temperature sensors", @@ -98,7 +99,17 @@ async def async_setup_entry( for description in DISK_PARTITION_SENSORS ) - async_add_entities(entities, True) + for node in router.home_devices.values(): + for endpoint in node["show_endpoints"]: + if ( + endpoint["name"] == "battery" + and endpoint["ep_type"] == "signal" + and endpoint.get("value") is not None + ): + entities.append(FreeboxBatterySensor(hass, router, node, endpoint)) + + if entities: + async_add_entities(entities, True) class FreeboxSensor(SensorEntity): @@ -125,7 +136,7 @@ class FreeboxSensor(SensorEntity): self._attr_native_value = state @callback - def async_on_demand_update(self): + def async_on_demand_update(self) -> None: """Update state.""" self.async_update_state() self.async_write_ha_state() @@ -213,3 +224,15 @@ class FreeboxDiskSensor(FreeboxSensor): self._partition["free_bytes"] * 100 / self._partition["total_bytes"], 2 ) self._attr_native_value = value + + +class FreeboxBatterySensor(FreeboxHomeEntity, SensorEntity): + """Representation of a Freebox battery sensor.""" + + _attr_device_class = SensorDeviceClass.BATTERY + _attr_native_unit_of_measurement = PERCENTAGE + + @property + def native_value(self) -> int: + """Return the current state of the device.""" + return self.get_value("signal", "battery") diff --git a/tests/components/freebox/const.py b/tests/components/freebox/const.py index 7028366d02b..a6253dbf315 100644 --- a/tests/components/freebox/const.py +++ b/tests/components/freebox/const.py @@ -1995,8 +1995,58 @@ DATA_HOME_GET_NODES = [ "Gateway": 1, "ItemId": "e76c2b75a4a6e2", }, - "show_endpoints": [{...}, {...}, {...}, {...}], - "signal_links": [{...}], + "show_endpoints": [ + { + "category": "", + "ep_type": "slot", + "id": 0, + "label": "Activé", + "name": "enable", + "value": False, + "value_type": "bool", + "visibility": "normal", + }, + { + "category": "", + "ep_type": "signal", + "id": 1, + "label": "Activé", + "name": "enable", + "value": True, + "value_type": "bool", + "visibility": "normal", + }, + { + "category": "", + "ep_type": "signal", + "id": 2, + "label": "Bouton appuyé", + "name": "pushed", + "value": None, + "value_type": "int", + }, + { + "category": "", + "ep_type": "signal", + "id": 3, + "label": "Niveau de Batterie", + "name": "battery", + "refresh": 2000, + "value": 100, + "value_type": "int", + }, + ], + "signal_links": [ + { + "adapter": 5, + "category": "alarm", + "id": 7, + "label": "Système d alarme", + "link_id": 10, + "name": "node_7", + "status": "active", + }, + ], "slot_links": [], "status": "active", "type": { @@ -2081,12 +2131,119 @@ DATA_HOME_GET_NODES = [ "visibility": "normal", }, ], - "signal_links": [{...}], + "signal_links": [ + { + "adapter": 5, + "category": "alarm", + "id": 7, + "label": "Système d alarme", + "link_id": 12, + "name": "node_7", + "status": "active", + } + ], "slot_links": [], "status": "active", "type": { "abstract": False, - "endpoints": [...], + "endpoints": [ + { + "ep_type": "slot", + "id": 0, + "label": "Alarme principale", + "name": "alarm1", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "slot", + "id": 1, + "label": "Alarme secondaire", + "name": "alarm2", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "slot", + "id": 2, + "label": "Zone temporisée", + "name": "timed", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 3, + "label": "Alarme principale", + "name": "alarm1", + "param_type": "void", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 4, + "label": "Alarme secondaire", + "name": "alarm2", + "param_type": "void", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 5, + "label": "Zone temporisée", + "name": "timed", + "param_type": "void", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 6, + "label": "Détection", + "name": "trigger", + "param_type": "void", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 7, + "label": "Couvercle", + "name": "1cover", + "param_type": "void", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 8, + "label": "Niveau de Batterie", + "name": "1battery", + "param_type": "void", + "value_type": "int", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 9, + "label": "Batterie faible", + "name": "battery_warning", + "param_type": "void", + "value_type": "int", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 10, + "label": "Alarme", + "name": "alarm", + "param_type": "void", + "value_type": "void", + "visibility": "internal", + }, + ], "generic": False, "icon": "/resources/images/home/pictos/detecteur_ouverture.png", "inherit": "node::domus", @@ -2112,22 +2269,172 @@ DATA_HOME_GET_NODES = [ "ItemId": "240d000f9fefe576", }, "show_endpoints": [ - {...}, - {...}, - {...}, - {...}, - {...}, - {...}, - {...}, - {...}, - {...}, + { + "category": "", + "ep_type": "slot", + "id": 0, + "label": "Alarme principale", + "name": "alarm1", + "ui": {"access": "w", "display": "toggle"}, + "value": False, + "value_type": "bool", + "visibility": "normal", + }, + { + "category": "", + "ep_type": "slot", + "id": 1, + "label": "Alarme secondaire", + "name": "alarm2", + "ui": {"access": "w", "display": "toggle"}, + "value": False, + "value_type": "bool", + "visibility": "normal", + }, + { + "category": "", + "ep_type": "slot", + "id": 2, + "label": "Zone temporisée", + "name": "timed", + "ui": {"access": "w", "display": "toggle"}, + "value": False, + "value_type": "bool", + "visibility": "normal", + }, + { + "category": "", + "ep_type": "signal", + "id": 8, + "label": "Niveau de Batterie", + "name": "battery", + "refresh": 2000, + "ui": { + "access": "r", + "display": "icon", + "icon_range": [...], + "icon_url": "/resources/images/home/pictos/batt_x.png", + "status_text_range": [...], + "unit": "%", + }, + "value": 100, + "value_type": "int", + "visibility": "normal", + }, + ], + "signal_links": [ + { + "adapter": 5, + "category": "alarm", + "id": 7, + "label": "Système d alarme", + "link_id": 12, + "name": "node_7", + "status": "active", + } ], - "signal_links": [{...}], "slot_links": [], "status": "active", "type": { "abstract": False, - "endpoints": [...], + "endpoints": [ + { + "ep_type": "slot", + "id": 0, + "label": "Alarme principale", + "name": "alarm1", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "slot", + "id": 1, + "label": "Alarme secondaire", + "name": "alarm2", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "slot", + "id": 2, + "label": "Zone temporisée", + "name": "timed", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 3, + "label": "Alarme principale", + "name": "alarm1", + "param_type": "void", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 4, + "label": "Alarme secondaire", + "name": "alarm2", + "param_type": "void", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 5, + "label": "Zone temporisée", + "name": "timed", + "param_type": "void", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 6, + "label": "Détection", + "name": "trigger", + "param_type": "void", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 7, + "label": "Couvercle", + "name": "cover", + "param_type": "void", + "value_type": "bool", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 8, + "label": "Niveau de Batterie", + "name": "battery", + "param_type": "void", + "value_type": "int", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 9, + "label": "Batterie faible", + "name": "battery_warning", + "param_type": "void", + "value_type": "int", + "visibility": "normal", + }, + { + "ep_type": "signal", + "id": 10, + "label": "Alarme", + "name": "alarm", + "param_type": "void", + "value_type": "void", + "visibility": "internal", + }, + ], "generic": False, "icon": "/resources/images/home/pictos/detecteur_xxxx.png", "inherit": "node::domus", From 62b1211dee245be93db84b390d59931e70684e59 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 29 Aug 2023 10:41:19 +0000 Subject: [PATCH 0997/1151] Remove myself from Dune HD codeowners (#99268) --- CODEOWNERS | 2 -- homeassistant/components/dunehd/manifest.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 16fb44fd789..f33d4052304 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -293,8 +293,6 @@ build.json @home-assistant/supervisor /tests/components/dsmr/ @Robbie1221 @frenck /homeassistant/components/dsmr_reader/ @depl0y @glodenox /tests/components/dsmr_reader/ @depl0y @glodenox -/homeassistant/components/dunehd/ @bieniu -/tests/components/dunehd/ @bieniu /homeassistant/components/duotecno/ @cereal2nd /tests/components/duotecno/ @cereal2nd /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo diff --git a/homeassistant/components/dunehd/manifest.json b/homeassistant/components/dunehd/manifest.json index f0d4d71ed0d..b5528e0f565 100644 --- a/homeassistant/components/dunehd/manifest.json +++ b/homeassistant/components/dunehd/manifest.json @@ -1,7 +1,7 @@ { "domain": "dunehd", "name": "Dune HD", - "codeowners": ["@bieniu"], + "codeowners": [], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dunehd", "iot_class": "local_polling", From 750cfeb76a93936b3c77be75ff047748497e0742 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 29 Aug 2023 12:43:22 +0200 Subject: [PATCH 0998/1151] Refactor Freebox Home categories (#99224) --- homeassistant/components/freebox/camera.py | 6 ++-- homeassistant/components/freebox/const.py | 33 ++++++++++++++----- homeassistant/components/freebox/home_base.py | 6 ++-- homeassistant/components/freebox/router.py | 4 +-- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/freebox/camera.py b/homeassistant/components/freebox/camera.py index 9e833aca18b..fd11b949890 100644 --- a/homeassistant/components/freebox/camera.py +++ b/homeassistant/components/freebox/camera.py @@ -12,13 +12,13 @@ from homeassistant.components.ffmpeg.camera import ( FFmpegCamera, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, Platform +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_DETECTION, DOMAIN +from .const import ATTR_DETECTION, DOMAIN, FreeboxHomeCategory from .home_base import FreeboxHomeEntity from .router import FreeboxRouter @@ -50,7 +50,7 @@ def add_entities(hass: HomeAssistant, router, async_add_entities, tracked): new_tracked = [] for nodeid, node in router.home_devices.items(): - if (node["category"] != Platform.CAMERA) or (nodeid in tracked): + if (node["category"] != FreeboxHomeCategory.CAMERA) or (nodeid in tracked): continue new_tracked.append(FreeboxCamera(hass, router, node)) tracked.add(nodeid) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 5a7c7863b4e..59dce75649b 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -1,6 +1,7 @@ """Freebox component constants.""" from __future__ import annotations +import enum import socket from homeassistant.const import Platform @@ -58,16 +59,30 @@ DEVICE_ICONS = { ATTR_DETECTION = "detection" +# Home +class FreeboxHomeCategory(enum.StrEnum): + """Freebox Home categories.""" + + ALARM = "alarm" + CAMERA = "camera" + DWS = "dws" + IOHOME = "iohome" + KFB = "kfb" + OPENER = "opener" + PIR = "pir" + RTS = "rts" + + CATEGORY_TO_MODEL = { - "pir": "F-HAPIR01A", - "camera": "F-HACAM01A", - "dws": "F-HADWS01A", - "kfb": "F-HAKFB01A", - "alarm": "F-MSEC07A", - "rts": "RTS", - "iohome": "IOHome", + FreeboxHomeCategory.PIR: "F-HAPIR01A", + FreeboxHomeCategory.CAMERA: "F-HACAM01A", + FreeboxHomeCategory.DWS: "F-HADWS01A", + FreeboxHomeCategory.KFB: "F-HAKFB01A", + FreeboxHomeCategory.ALARM: "F-MSEC07A", + FreeboxHomeCategory.RTS: "RTS", + FreeboxHomeCategory.IOHOME: "IOHome", } -HOME_COMPATIBLE_PLATFORMS = [ - Platform.CAMERA, +HOME_COMPATIBLE_CATEGORIES = [ + FreeboxHomeCategory.CAMERA, ] diff --git a/homeassistant/components/freebox/home_base.py b/homeassistant/components/freebox/home_base.py index dc887229086..d0bb8b10309 100644 --- a/homeassistant/components/freebox/home_base.py +++ b/homeassistant/components/freebox/home_base.py @@ -9,7 +9,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import CATEGORY_TO_MODEL, DOMAIN +from .const import CATEGORY_TO_MODEL, DOMAIN, FreeboxHomeCategory from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) @@ -48,10 +48,10 @@ class FreeboxHomeEntity(Entity): if self._model is None: if node["type"].get("inherit") == "node::rts": self._manufacturer = "Somfy" - self._model = CATEGORY_TO_MODEL.get("rts") + self._model = CATEGORY_TO_MODEL[FreeboxHomeCategory.RTS] elif node["type"].get("inherit") == "node::ios": self._manufacturer = "Somfy" - self._model = CATEGORY_TO_MODEL.get("iohome") + self._model = CATEGORY_TO_MODEL[FreeboxHomeCategory.IOHOME] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._id)}, diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index f42a386087f..7c83e980540 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -28,7 +28,7 @@ from .const import ( APP_DESC, CONNECTION_SENSORS_KEYS, DOMAIN, - HOME_COMPATIBLE_PLATFORMS, + HOME_COMPATIBLE_CATEGORIES, STORAGE_KEY, STORAGE_VERSION, ) @@ -190,7 +190,7 @@ class FreeboxRouter: new_device = False for home_node in home_nodes: - if home_node["category"] in HOME_COMPATIBLE_PLATFORMS: + if home_node["category"] in HOME_COMPATIBLE_CATEGORIES: if self.home_devices.get(home_node["id"]) is None: new_device = True self.home_devices[home_node["id"]] = home_node From 00ecc108d107012d2884a031f9a09abf01e8a2eb Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 29 Aug 2023 13:08:24 +0200 Subject: [PATCH 0999/1151] Use shorthand attributes for DuneHD (#99237) --- .../components/dunehd/media_player.py | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index c76a4b72e9a..4f6bf6fb677 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -49,10 +49,14 @@ class DuneHDPlayerEntity(MediaPlayerEntity): def __init__(self, player: DuneHDPlayer, name: str, unique_id: str) -> None: """Initialize entity to control Dune HD.""" self._player = player - self._name = name self._media_title: str | None = None self._state: dict[str, Any] = {} - self._unique_id = unique_id + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + name=name, + ) def update(self) -> None: """Update internal status of the entity.""" @@ -78,20 +82,6 @@ class DuneHDPlayerEntity(MediaPlayerEntity): """Return True if entity is available.""" return len(self._state) > 0 - @property - def unique_id(self) -> str: - """Return a unique_id for this entity.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return DeviceInfo( - identifiers={(DOMAIN, self._unique_id)}, - manufacturer=ATTR_MANUFACTURER, - name=self._name, - ) - @property def volume_level(self) -> float: """Return the volume level of the media player (0..1).""" From dac77040a27a8af57ef51c613a39315eaf04068b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 29 Aug 2023 13:41:34 +0200 Subject: [PATCH 1000/1151] Update AEMET-OpenData to v0.4.1 (#99261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index 4d1b25908ef..facd7f58a71 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.4.0"] + "requirements": ["AEMET-OpenData==0.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index d0a18d0d173..95e69088fb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.0 +AEMET-OpenData==0.4.1 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02194adf253..3ed02d7d46b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.0 +AEMET-OpenData==0.4.1 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 From 98cb5b4b5d36e390eb049bcd3294d811f862b523 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 29 Aug 2023 14:46:24 +0200 Subject: [PATCH 1001/1151] Use shorthand attributes for Elkm1 (#99275) --- homeassistant/components/elkm1/climate.py | 53 ++++++----------------- homeassistant/components/elkm1/sensor.py | 53 ++++++----------------- 2 files changed, 27 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index d0094a5b37b..1ece7a7758a 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -5,7 +5,6 @@ from typing import Any from elkm1_lib.const import ThermostatFan, ThermostatMode, ThermostatSetting from elkm1_lib.elements import Element -from elkm1_lib.elk import Elk from elkm1_lib.thermostats import Thermostat from homeassistant.components.climate import ( @@ -80,13 +79,14 @@ class ElkThermostat(ElkEntity, ClimateEntity): | ClimateEntityFeature.AUX_HEAT | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) + _attr_min_temp = 1 + _attr_max_temp = 99 + _attr_hvac_modes = SUPPORT_HVAC + _attr_hvac_mode: HVACMode | None = None + _attr_target_temperature_step = 1 + _attr_fan_modes = [FAN_AUTO, FAN_ON] _element: Thermostat - def __init__(self, element: Element, elk: Elk, elk_data: dict[str, Any]) -> None: - """Initialize climate entity.""" - super().__init__(element, elk, elk_data) - self._state: HVACMode | None = None - @property def temperature_unit(self) -> str: """Return the temperature unit.""" @@ -119,41 +119,16 @@ class ElkThermostat(ElkEntity, ClimateEntity): """Return the low target temperature.""" return self._element.heat_setpoint - @property - def target_temperature_step(self) -> float: - """Return the supported step of target temperature.""" - return 1 - @property def current_humidity(self) -> int | None: """Return the current humidity.""" return self._element.humidity - @property - def hvac_mode(self) -> HVACMode | None: - """Return current operation ie. heat, cool, idle.""" - return self._state - - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available operation modes.""" - return SUPPORT_HVAC - @property def is_aux_heat(self) -> bool: """Return if aux heater is on.""" return self._element.mode == ThermostatMode.EMERGENCY_HEAT - @property - def min_temp(self) -> float: - """Return the minimum temperature supported.""" - return 1 - - @property - def max_temp(self) -> float: - """Return the maximum temperature supported.""" - return 99 - @property def fan_mode(self) -> str | None: """Return the fan setting.""" @@ -180,11 +155,6 @@ class ElkThermostat(ElkEntity, ClimateEntity): """Turn auxiliary heater off.""" self._elk_set(ThermostatMode.HEAT, None) - @property - def fan_modes(self) -> list[str]: - """Return the list of available fan modes.""" - return [FAN_AUTO, FAN_ON] - async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" thermostat_mode, elk_fan_mode = HASS_TO_ELK_FAN_MODES[fan_mode] @@ -201,8 +171,11 @@ class ElkThermostat(ElkEntity, ClimateEntity): def _element_changed(self, element: Element, changeset: Any) -> None: if self._element.mode is None: - self._state = None + self._attr_hvac_mode = None else: - self._state = ELK_TO_HASS_HVAC_MODES[self._element.mode] - if self._state == HVACMode.OFF and self._element.fan == ThermostatFan.ON: - self._state = HVACMode.FAN_ONLY + self._attr_hvac_mode = ELK_TO_HASS_HVAC_MODES[self._element.mode] + if ( + self._attr_hvac_mode == HVACMode.OFF + and self._element.fan == ThermostatFan.ON + ): + self._attr_hvac_mode = HVACMode.FAN_ONLY diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index fb4326e8917..0de97a1710e 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -6,7 +6,6 @@ from typing import Any from elkm1_lib.const import SettingFormat, ZoneType from elkm1_lib.counters import Counter from elkm1_lib.elements import Element -from elkm1_lib.elk import Elk from elkm1_lib.keypads import Keypad from elkm1_lib.panel import Panel from elkm1_lib.settings import Setting @@ -84,15 +83,7 @@ def temperature_to_state(temperature: int, undefined_temperature: int) -> str | class ElkSensor(ElkAttachedEntity, SensorEntity): """Base representation of Elk-M1 sensor.""" - def __init__(self, element: Element, elk: Elk, elk_data: dict[str, Any]) -> None: - """Initialize the base of all Elk sensors.""" - super().__init__(element, elk, elk_data) - self._state: str | None = None - - @property - def native_value(self) -> str | None: - """Return the state of the sensor.""" - return self._state + _attr_native_value: str | None = None async def async_counter_refresh(self) -> None: """Refresh the value of a counter from the panel.""" @@ -124,20 +115,17 @@ class ElkSensor(ElkAttachedEntity, SensorEntity): class ElkCounter(ElkSensor): """Representation of an Elk-M1 Counter.""" + _attr_icon = "mdi:numeric" _element: Counter - @property - def icon(self) -> str: - """Icon to use in the frontend.""" - return "mdi:numeric" - def _element_changed(self, _: Element, changeset: Any) -> None: - self._state = self._element.value + self._attr_native_value = self._element.value class ElkKeypad(ElkSensor): """Representation of an Elk-M1 Keypad.""" + _attr_icon = "mdi:thermometer-lines" _element: Keypad @property @@ -150,17 +138,12 @@ class ElkKeypad(ElkSensor): """Return the unit of measurement.""" return self._temperature_unit - @property - def icon(self) -> str: - """Icon to use in the frontend.""" - return "mdi:thermometer-lines" - @property def extra_state_attributes(self) -> dict[str, Any]: """Attributes of the sensor.""" attrs: dict[str, Any] = self.initial_attrs() attrs["area"] = self._element.area + 1 - attrs["temperature"] = self._state + attrs["temperature"] = self._attr_native_value attrs["last_user_time"] = self._element.last_user_time.isoformat() attrs["last_user"] = self._element.last_user + 1 attrs["code"] = self._element.code @@ -169,7 +152,7 @@ class ElkKeypad(ElkSensor): return attrs def _element_changed(self, _: Element, changeset: Any) -> None: - self._state = temperature_to_state( + self._attr_native_value = temperature_to_state( self._element.temperature, UNDEFINED_TEMPERATURE ) @@ -177,14 +160,10 @@ class ElkKeypad(ElkSensor): class ElkPanel(ElkSensor): """Representation of an Elk-M1 Panel.""" + _attr_icon = "mdi:home" _attr_entity_category = EntityCategory.DIAGNOSTIC _element: Panel - @property - def icon(self) -> str: - """Icon to use in the frontend.""" - return "mdi:home" - @property def extra_state_attributes(self) -> dict[str, Any]: """Attributes of the sensor.""" @@ -194,25 +173,21 @@ class ElkPanel(ElkSensor): def _element_changed(self, _: Element, changeset: Any) -> None: if self._elk.is_connected(): - self._state = ( + self._attr_native_value = ( "Paused" if self._element.remote_programming_status else "Connected" ) else: - self._state = "Disconnected" + self._attr_native_value = "Disconnected" class ElkSetting(ElkSensor): """Representation of an Elk-M1 Setting.""" + _attr_icon = "mdi:numeric" _element: Setting - @property - def icon(self) -> str: - """Icon to use in the frontend.""" - return "mdi:numeric" - def _element_changed(self, _: Element, changeset: Any) -> None: - self._state = self._element.value + self._attr_native_value = self._element.value @property def extra_state_attributes(self) -> dict[str, Any]: @@ -282,10 +257,10 @@ class ElkZone(ElkSensor): def _element_changed(self, _: Element, changeset: Any) -> None: if self._element.definition == ZoneType.TEMPERATURE: - self._state = temperature_to_state( + self._attr_native_value = temperature_to_state( self._element.temperature, UNDEFINED_TEMPERATURE ) elif self._element.definition == ZoneType.ANALOG_ZONE: - self._state = f"{self._element.voltage}" + self._attr_native_value = f"{self._element.voltage}" else: - self._state = pretty_const(self._element.logical_status.name) + self._attr_native_value = pretty_const(self._element.logical_status.name) From fae82731e1aa88b33c6371242482b6b58f9f8b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 29 Aug 2023 15:43:14 +0200 Subject: [PATCH 1002/1151] Simplify and improve AEMET coordinator updates (#99273) --- homeassistant/components/aemet/__init__.py | 11 +- homeassistant/components/aemet/config_flow.py | 2 +- .../aemet/weather_update_coordinator.py | 172 ++++-------------- tests/components/aemet/test_config_flow.py | 2 +- tests/components/aemet/test_coordinator.py | 37 ++++ tests/components/aemet/test_init.py | 28 +++ 6 files changed, 108 insertions(+), 144 deletions(-) create mode 100644 tests/components/aemet/test_coordinator.py diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 772dcd0276b..c8b3f774a97 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,6 +1,8 @@ """The AEMET OpenData component.""" + import logging +from aemet_opendata.exceptions import TownNotFound from aemet_opendata.interface import AEMET, ConnectionOptions from homeassistant.config_entries import ConfigEntry @@ -30,10 +32,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: options = ConnectionOptions(api_key, station_updates) aemet = AEMET(aiohttp_client.async_get_clientsession(hass), options) - weather_coordinator = WeatherUpdateCoordinator( - hass, aemet, latitude, longitude, station_updates - ) + try: + await aemet.select_coordinates(latitude, longitude) + except TownNotFound as err: + _LOGGER.error(err) + return False + weather_coordinator = WeatherUpdateCoordinator(hass, aemet) await weather_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py index 4f3531b19e7..4df25613803 100644 --- a/homeassistant/components/aemet/config_flow.py +++ b/homeassistant/components/aemet/config_flow.py @@ -43,7 +43,7 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options = ConnectionOptions(user_input[CONF_API_KEY], False) aemet = AEMET(aiohttp_client.async_get_clientsession(self.hass), options) try: - await aemet.get_conventional_observation_stations(False) + await aemet.select_coordinates(latitude, longitude) except AuthError: errors["base"] = "invalid_api_key" diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 66a1a2eb891..016dd2ba75c 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -2,9 +2,9 @@ from __future__ import annotations from asyncio import timeout -from dataclasses import dataclass, field from datetime import timedelta import logging +from typing import Any, Final from aemet_opendata.const import ( AEMET_ATTR_DATE, @@ -14,11 +14,8 @@ from aemet_opendata.const import ( AEMET_ATTR_FEEL_TEMPERATURE, AEMET_ATTR_FORECAST, AEMET_ATTR_HUMIDITY, - AEMET_ATTR_ID, - AEMET_ATTR_IDEMA, AEMET_ATTR_MAX, AEMET_ATTR_MIN, - AEMET_ATTR_NAME, AEMET_ATTR_PRECIPITATION, AEMET_ATTR_PRECIPITATION_PROBABILITY, AEMET_ATTR_SKY_STATE, @@ -27,7 +24,6 @@ from aemet_opendata.const import ( AEMET_ATTR_SPEED, AEMET_ATTR_STATION_DATE, AEMET_ATTR_STATION_HUMIDITY, - AEMET_ATTR_STATION_LOCATION, AEMET_ATTR_STATION_PRESSURE, AEMET_ATTR_STATION_PRESSURE_SEA, AEMET_ATTR_STATION_TEMPERATURE, @@ -37,12 +33,15 @@ from aemet_opendata.const import ( AEMET_ATTR_WIND_GUST, ATTR_DATA, ) +from aemet_opendata.exceptions import AemetError from aemet_opendata.helpers import ( get_forecast_day_value, get_forecast_hour_value, get_forecast_interval_value, ) +from aemet_opendata.interface import AEMET +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -83,6 +82,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +API_TIMEOUT: Final[int] = 120 STATION_MAX_DELTA = timedelta(hours=2) WEATHER_UPDATE_INTERVAL = timedelta(minutes=10) @@ -112,130 +112,33 @@ def format_int(value) -> int | None: return None -class TownNotFound(UpdateFailed): - """Raised when town is not found.""" - - class WeatherUpdateCoordinator(DataUpdateCoordinator): """Weather data update coordinator.""" - def __init__(self, hass, aemet, latitude, longitude, station_updates): + def __init__( + self, + hass: HomeAssistant, + aemet: AEMET, + ) -> None: """Initialize coordinator.""" + self.aemet = aemet + super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL + hass, + _LOGGER, + name=DOMAIN, + update_interval=WEATHER_UPDATE_INTERVAL, ) - self._aemet = aemet - self._station = None - self._town = None - self._latitude = latitude - self._longitude = longitude - self._station_updates = station_updates - self._data = { - "daily": None, - "hourly": None, - "station": None, - } - - async def _async_update_data(self): - data = {} - async with timeout(120): - weather_response = await self._get_aemet_weather() - data = self._convert_weather_response(weather_response) - return data - - async def _get_aemet_weather(self): - """Poll weather data from AEMET OpenData.""" - weather = await self._get_weather_and_forecast() - return weather - - async def _get_weather_station(self): - if not self._station: - self._station = ( - await self._aemet.get_conventional_observation_station_by_coordinates( - self._latitude, self._longitude - ) - ) - if self._station: - _LOGGER.debug( - "station found for coordinates [%s, %s]: %s", - self._latitude, - self._longitude, - self._station, - ) - if not self._station: - _LOGGER.debug( - "station not found for coordinates [%s, %s]", - self._latitude, - self._longitude, - ) - return self._station - - async def _get_weather_town(self): - if not self._town: - self._town = await self._aemet.get_town_by_coordinates( - self._latitude, self._longitude - ) - if self._town: - _LOGGER.debug( - "Town found for coordinates [%s, %s]: %s", - self._latitude, - self._longitude, - self._town, - ) - if not self._town: - _LOGGER.error( - "Town not found for coordinates [%s, %s]", - self._latitude, - self._longitude, - ) - raise TownNotFound - return self._town - - async def _get_weather_and_forecast(self): - """Get weather and forecast data from AEMET OpenData.""" - - await self._get_weather_town() - - daily = await self._aemet.get_specific_forecast_town_daily( - self._town[AEMET_ATTR_ID] - ) - if not daily: - _LOGGER.error( - 'Error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID] - ) - - hourly = await self._aemet.get_specific_forecast_town_hourly( - self._town[AEMET_ATTR_ID] - ) - if not hourly: - _LOGGER.error( - 'Error fetching hourly data for town "%s"', self._town[AEMET_ATTR_ID] - ) - - station = None - if self._station_updates and await self._get_weather_station(): - station = await self._aemet.get_conventional_observation_station_data( - self._station[AEMET_ATTR_IDEMA] - ) - if not station: - _LOGGER.error( - 'Error fetching data for station "%s"', - self._station[AEMET_ATTR_IDEMA], - ) - - if daily: - self._data["daily"] = daily - if hourly: - self._data["hourly"] = hourly - if station: - self._data["station"] = station - - return AemetWeather( - self._data["daily"], - self._data["hourly"], - self._data["station"], - ) + async def _async_update_data(self) -> dict[str, Any]: + """Update coordinator data.""" + async with timeout(API_TIMEOUT): + try: + await self.aemet.update() + except AemetError as error: + raise UpdateFailed(error) from error + weather_response = self.aemet.legacy_weather() + return self._convert_weather_response(weather_response) def _convert_weather_response(self, weather_response): """Format the weather response correctly.""" @@ -520,14 +423,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _get_station_id(self): """Get station ID from weather data.""" - if self._station: - return self._station[AEMET_ATTR_IDEMA] + if self.aemet.station: + return self.aemet.station.get_id() return None def _get_station_name(self): """Get station name from weather data.""" - if self._station: - return self._station[AEMET_ATTR_STATION_LOCATION] + if self.aemet.station: + return self.aemet.station.get_name() return None @staticmethod @@ -568,14 +471,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _get_town_id(self): """Get town ID from weather data.""" - if self._town: - return self._town[AEMET_ATTR_ID] + if self.aemet.town: + return self.aemet.town.get_id() return None def _get_town_name(self): """Get town name from weather data.""" - if self._town: - return self._town[AEMET_ATTR_NAME] + if self.aemet.town: + return self.aemet.town.get_name() return None @staticmethod @@ -625,12 +528,3 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): if val: return format_int(val) return None - - -@dataclass -class AemetWeather: - """Class to harmonize weather data model.""" - - daily: dict = field(default_factory=dict) - hourly: dict = field(default_factory=dict) - station: dict = field(default_factory=dict) diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index b311cfd4a54..8c3264d8975 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -142,7 +142,7 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None: async def test_form_auth_error(hass: HomeAssistant) -> None: """Test setting up with api auth error.""" mocked_aemet = MagicMock() - mocked_aemet.get_conventional_observation_stations.side_effect = AuthError + mocked_aemet.select_coordinates.side_effect = AuthError with patch( "homeassistant.components.aemet.config_flow.AEMET", diff --git a/tests/components/aemet/test_coordinator.py b/tests/components/aemet/test_coordinator.py new file mode 100644 index 00000000000..067fc30a2c0 --- /dev/null +++ b/tests/components/aemet/test_coordinator.py @@ -0,0 +1,37 @@ +"""Define tests for the AEMET OpenData coordinator.""" +from unittest.mock import patch + +from aemet_opendata.exceptions import AemetError +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.aemet.weather_update_coordinator import ( + WEATHER_UPDATE_INTERVAL, +) +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant + +from .util import async_init_integration + +from tests.common import async_fire_time_changed + + +async def test_coordinator_error( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test error on coordinator update.""" + + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_init_integration(hass) + + with patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=AemetError, + ): + freezer.tick(WEATHER_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("weather.aemet") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index 24c16ba3ef3..f9eac318c6c 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -1,6 +1,8 @@ """Define tests for the AEMET OpenData init.""" from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory + from homeassistant.components.aemet.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME @@ -41,3 +43,29 @@ async def test_unload_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_init_town_not_found( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test TownNotFound when loading the AEMET integration.""" + + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + with patch( + "homeassistant.components.aemet.AEMET.api_call", + side_effect=mock_api_call, + ): + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "api-key", + CONF_LATITUDE: "0.0", + CONF_LONGITUDE: "0.0", + CONF_NAME: "AEMET", + }, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) is False From 9476ade34a8cf705fbc53b6b283c6c992c7936ee Mon Sep 17 00:00:00 2001 From: David Knowles Date: Tue, 29 Aug 2023 09:43:27 -0400 Subject: [PATCH 1003/1151] Bump pydrawise to 2023.8.0 (#99270) --- homeassistant/components/hydrawise/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index d9e6d809960..f9de9bf30c9 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2023.7.1"] + "requirements": ["pydrawise==2023.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 95e69088fb1..8a277be59d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1650,7 +1650,7 @@ pydiscovergy==2.0.3 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2023.7.1 +pydrawise==2023.8.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 From 691bbedfc871f2010195ba78ede6230233d553f2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Aug 2023 15:54:38 +0200 Subject: [PATCH 1004/1151] Fix typo in TrackTemplateResultInfo (#99276) --- homeassistant/helpers/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 235c1c80534..62a3b91991d 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -964,7 +964,7 @@ class TrackTemplateResultInfo: self._update_time_listeners() _LOGGER.debug( ( - "Template group %s listens for %s, first render blocker by super" + "Template group %s listens for %s, first render blocked by super" " template: %s" ), self._track_templates, From 6223b1f599941b8865000d5c122d9adfd04d2368 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 29 Aug 2023 15:57:54 +0200 Subject: [PATCH 1005/1151] Add ws endpoint "auth/delete_all_refresh_tokens" (#98976) Co-authored-by: Martin Hjelmare --- homeassistant/components/auth/__init__.py | 48 ++++++++++ tests/components/auth/test_init.py | 101 ++++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index deaf3b7892d..78a1383012d 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -124,9 +124,11 @@ as part of a config flow. """ from __future__ import annotations +import asyncio from collections.abc import Callable from datetime import datetime, timedelta from http import HTTPStatus +from logging import getLogger from typing import Any, cast import uuid @@ -138,6 +140,7 @@ from homeassistant.auth import InvalidAuthError from homeassistant.auth.models import ( TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, Credentials, + RefreshToken, User, ) from homeassistant.components import websocket_api @@ -188,6 +191,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_create_long_lived_access_token) websocket_api.async_register_command(hass, websocket_refresh_tokens) websocket_api.async_register_command(hass, websocket_delete_refresh_token) + websocket_api.async_register_command(hass, websocket_delete_all_refresh_tokens) websocket_api.async_register_command(hass, websocket_sign_path) await login_flow.async_setup(hass, store_result) @@ -598,6 +602,50 @@ async def websocket_delete_refresh_token( connection.send_result(msg["id"], {}) +@websocket_api.websocket_command( + { + vol.Required("type"): "auth/delete_all_refresh_tokens", + } +) +@websocket_api.ws_require_user() +@websocket_api.async_response +async def websocket_delete_all_refresh_tokens( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle delete all refresh tokens request.""" + tasks = [] + current_refresh_token: RefreshToken + for token in connection.user.refresh_tokens.values(): + if token.id == connection.refresh_token_id: + # Skip the current refresh token as it has revoke_callback, + # which cancels/closes the connection. + # It will be removed after sending the result. + current_refresh_token = token + continue + tasks.append( + hass.async_create_task(hass.auth.async_remove_refresh_token(token)) + ) + + remove_failed = False + if tasks: + for result in await asyncio.gather(*tasks, return_exceptions=True): + if isinstance(result, Exception): + getLogger(__name__).exception( + "During refresh token removal, the following error occurred: %s", + result, + ) + remove_failed = True + + if remove_failed: + connection.send_error( + msg["id"], "token_removing_error", "During removal, an error was raised." + ) + else: + connection.send_result(msg["id"], {}) + + hass.async_create_task(hass.auth.async_remove_refresh_token(current_refresh_token)) + + @websocket_api.websocket_command( { vol.Required("type"): "auth/sign_path", diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 923a633e76a..a33ca702bcf 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -1,6 +1,7 @@ """Integration tests for the auth component.""" from datetime import timedelta from http import HTTPStatus +import logging from unittest.mock import patch import pytest @@ -519,6 +520,106 @@ async def test_ws_delete_refresh_token( assert refresh_token is None +async def test_ws_delete_all_refresh_tokens_error( + hass: HomeAssistant, + hass_admin_user: MockUser, + hass_admin_credential: Credentials, + hass_ws_client: WebSocketGenerator, + hass_access_token: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test deleting all refresh tokens, where a revoke callback raises an error.""" + assert await async_setup_component(hass, "auth", {"http": {}}) + + # one token already exists + await hass.auth.async_create_refresh_token( + hass_admin_user, CLIENT_ID, credential=hass_admin_credential + ) + token = await hass.auth.async_create_refresh_token( + hass_admin_user, CLIENT_ID + "_1", credential=hass_admin_credential + ) + + def cb(): + raise RuntimeError("I'm bad") + + hass.auth.async_register_revoke_token_callback(token.id, cb) + + ws_client = await hass_ws_client(hass, hass_access_token) + + # get all tokens + await ws_client.send_json({"id": 5, "type": "auth/refresh_tokens"}) + result = await ws_client.receive_json() + assert result["success"], result + + tokens = result["result"] + + await ws_client.send_json( + { + "id": 6, + "type": "auth/delete_all_refresh_tokens", + } + ) + + caplog.clear() + result = await ws_client.receive_json() + assert result, result["success"] is False + assert result["error"] == { + "code": "token_removing_error", + "message": "During removal, an error was raised.", + } + + assert ( + "homeassistant.components.auth", + logging.ERROR, + "During refresh token removal, the following error occurred: I'm bad", + ) in caplog.record_tuples + + for token in tokens: + refresh_token = await hass.auth.async_get_refresh_token(token["id"]) + assert refresh_token is None + + +async def test_ws_delete_all_refresh_tokens( + hass: HomeAssistant, + hass_admin_user: MockUser, + hass_admin_credential: Credentials, + hass_ws_client: WebSocketGenerator, + hass_access_token: str, +) -> None: + """Test deleting all refresh tokens.""" + assert await async_setup_component(hass, "auth", {"http": {}}) + + # one token already exists + await hass.auth.async_create_refresh_token( + hass_admin_user, CLIENT_ID, credential=hass_admin_credential + ) + await hass.auth.async_create_refresh_token( + hass_admin_user, CLIENT_ID + "_1", credential=hass_admin_credential + ) + + ws_client = await hass_ws_client(hass, hass_access_token) + + # get all tokens + await ws_client.send_json({"id": 5, "type": "auth/refresh_tokens"}) + result = await ws_client.receive_json() + assert result["success"], result + + tokens = result["result"] + + await ws_client.send_json( + { + "id": 6, + "type": "auth/delete_all_refresh_tokens", + } + ) + + result = await ws_client.receive_json() + assert result, result["success"] + for token in tokens: + refresh_token = await hass.auth.async_get_refresh_token(token["id"]) + assert refresh_token is None + + async def test_ws_sign_path( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_access_token: str ) -> None: From f28634ea116e82023cc9c7948752362b188978aa Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 29 Aug 2023 16:26:23 +0200 Subject: [PATCH 1006/1151] Migrate PVPC to has entity name (#98894) * Migrate PVPC to has entity name * Set device name * Fix feedback --- .../components/pvpc_hourly_pricing/sensor.py | 14 ++++++-------- .../pvpc_hourly_pricing/test_config_flow.py | 10 +++++----- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 73881d16a4b..3368b24b3ff 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CURRENCY_EURO, UnitOfEnergy +from homeassistant.const import CURRENCY_EURO, UnitOfEnergy from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -31,6 +31,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( icon="mdi:currency-eur", native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", state_class=SensorStateClass.MEASUREMENT, + name="PVPC", ), ) _PRICE_SENSOR_ATTRIBUTES_MAP = { @@ -118,34 +119,31 @@ async def async_setup_entry( ) -> None: """Set up the electricity price sensor from config_entry.""" coordinator: ElecPricesDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - name = entry.data[CONF_NAME] - async_add_entities( - [ElecPriceSensor(coordinator, SENSOR_TYPES[0], entry.unique_id, name)] - ) + async_add_entities([ElecPriceSensor(coordinator, SENSOR_TYPES[0], entry.unique_id)]) class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], SensorEntity): """Class to hold the prices of electricity as a sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: ElecPricesDataUpdateCoordinator, description: SensorEntityDescription, unique_id: str | None, - name: str, ) -> None: """Initialize ESIOS sensor.""" super().__init__(coordinator) self.entity_description = description self._attr_attribution = coordinator.api.attribution self._attr_unique_id = unique_id - self._attr_name = name self._attr_device_info = DeviceInfo( configuration_url="https://api.esios.ree.es", entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.entry_id)}, manufacturer="REE", - name="ESIOS API", + name="ESIOS", ) async def async_added_to_hass(self) -> None: diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index e22ab03eb60..6560c81ebbb 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -57,7 +57,7 @@ async def test_config_flow( assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() - state = hass.states.get("sensor.test") + state = hass.states.get("sensor.esios_pvpc") check_valid_state(state, tariff=TARIFFS[1]) assert pvpc_aioclient_mock.call_count == 1 @@ -74,7 +74,7 @@ async def test_config_flow( # Check removal registry = er.async_get(hass) - registry_entity = registry.async_get("sensor.test") + registry_entity = registry.async_get("sensor.esios_pvpc") assert await hass.config_entries.async_remove(registry_entity.config_entry_id) # and add it again with UI @@ -89,7 +89,7 @@ async def test_config_flow( assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() - state = hass.states.get("sensor.test") + state = hass.states.get("sensor.esios_pvpc") check_valid_state(state, tariff=TARIFFS[1]) assert pvpc_aioclient_mock.call_count == 2 assert state.attributes["period"] == "P3" @@ -110,7 +110,7 @@ async def test_config_flow( user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6}, ) await hass.async_block_till_done() - state = hass.states.get("sensor.test") + state = hass.states.get("sensor.esios_pvpc") check_valid_state(state, tariff=TARIFFS[1]) assert pvpc_aioclient_mock.call_count == 3 assert state.attributes["period"] == "P3" @@ -121,7 +121,7 @@ async def test_config_flow( freezer.tick(timedelta(days=1)) async_fire_time_changed(hass) await hass.async_block_till_done() - state = hass.states.get("sensor.test") + state = hass.states.get("sensor.esios_pvpc") check_valid_state(state, tariff=TARIFFS[0], value="unavailable") assert "period" not in state.attributes assert pvpc_aioclient_mock.call_count == 4 From 5006244f4c009f3bda14d192348eb03487ade83c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 29 Aug 2023 09:31:41 -0500 Subject: [PATCH 1007/1151] Bump aioesphomeapi to 16.0.3 (#99282) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index dca1bce0d24..d0ab27656c2 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async_interrupt==1.1.1", - "aioesphomeapi==16.0.1", + "aioesphomeapi==16.0.3", "bluetooth-data-tools==1.9.1", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 8a277be59d3..639ae033b8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==16.0.1 +aioesphomeapi==16.0.3 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ed02d7d46b..ab5c68bae34 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==16.0.1 +aioesphomeapi==16.0.3 # homeassistant.components.flo aioflo==2021.11.0 From 2e0a22fdafbba4a103b15567d7dfc35a6f3b2802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 29 Aug 2023 16:33:12 +0200 Subject: [PATCH 1008/1151] Use freezegun in AEMET tests (#99253) --- tests/components/aemet/test_config_flow.py | 26 +++++++++------- tests/components/aemet/test_init.py | 15 +++++----- tests/components/aemet/test_sensor.py | 28 +++++++++-------- tests/components/aemet/test_weather.py | 35 +++++++++++----------- 4 files changed, 55 insertions(+), 49 deletions(-) diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index 8c3264d8975..0caacf4e4c0 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from aemet_opendata.exceptions import AuthError +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant import data_entry_flow @@ -9,7 +10,6 @@ from homeassistant.components.aemet.const import CONF_STATION_UPDATES, DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util from .util import mock_api_call @@ -59,13 +59,15 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_options(hass: HomeAssistant) -> None: +async def test_form_options( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: """Test the form options.""" - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ), patch( + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + with patch( "homeassistant.components.aemet.AEMET.api_call", side_effect=mock_api_call, ): @@ -116,13 +118,15 @@ async def test_form_options(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.LOADED -async def test_form_duplicated_id(hass: HomeAssistant) -> None: +async def test_form_duplicated_id( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: """Test setting up duplicated entry.""" - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ), patch( + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + with patch( "homeassistant.components.aemet.AEMET.api_call", side_effect=mock_api_call, ): diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index f9eac318c6c..5055575e3fe 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -7,7 +7,6 @@ from homeassistant.components.aemet.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util from .util import mock_api_call @@ -21,13 +20,15 @@ CONFIG = { } -async def test_unload_entry(hass: HomeAssistant) -> None: - """Test that the options form.""" +async def test_unload_entry( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test (un)loading the AEMET integration.""" - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ), patch( + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + with patch( "homeassistant.components.aemet.AEMET.api_call", side_effect=mock_api_call, ): diff --git a/tests/components/aemet/test_sensor.py b/tests/components/aemet/test_sensor.py index 99bce6b9471..4d61dde34fc 100644 --- a/tests/components/aemet/test_sensor.py +++ b/tests/components/aemet/test_sensor.py @@ -1,5 +1,6 @@ """The sensor tests for the AEMET OpenData platform.""" -from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory from homeassistant.components.weather import ( ATTR_CONDITION_PARTLYCLOUDY, @@ -12,15 +13,15 @@ import homeassistant.util.dt as dt_util from .util import async_init_integration -async def test_aemet_forecast_create_sensors(hass: HomeAssistant) -> None: +async def test_aemet_forecast_create_sensors( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: """Test creation of forecast sensors.""" hass.config.set_time_zone("UTC") - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ): - await async_init_integration(hass) + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_init_integration(hass) state = hass.states.get("sensor.aemet_daily_forecast_condition") assert state.state == ATTR_CONDITION_PARTLYCLOUDY @@ -73,14 +74,15 @@ async def test_aemet_forecast_create_sensors(hass: HomeAssistant) -> None: assert state is None -async def test_aemet_weather_create_sensors(hass: HomeAssistant) -> None: +async def test_aemet_weather_create_sensors( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: """Test creation of weather sensors.""" - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ): - await async_init_integration(hass) + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_init_integration(hass) state = hass.states.get("sensor.aemet_condition") assert state.state == ATTR_CONDITION_SNOWY diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py index 703ef4348f8..ddcc29698fd 100644 --- a/tests/components/aemet/test_weather.py +++ b/tests/components/aemet/test_weather.py @@ -40,15 +40,15 @@ from .util import async_init_integration, mock_api_call from tests.typing import WebSocketGenerator -async def test_aemet_weather(hass: HomeAssistant) -> None: +async def test_aemet_weather( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: """Test states of the weather.""" hass.config.set_time_zone("UTC") - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ): - await async_init_integration(hass) + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_init_integration(hass) state = hass.states.get("weather.aemet") assert state @@ -76,8 +76,11 @@ async def test_aemet_weather(hass: HomeAssistant) -> None: assert state is None -async def test_aemet_weather_legacy(hass: HomeAssistant) -> None: - """Test states of the weather.""" +async def test_aemet_weather_legacy( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test states of legacy weather.""" registry = er.async_get(hass) registry.async_get_or_create( @@ -87,11 +90,8 @@ async def test_aemet_weather_legacy(hass: HomeAssistant) -> None: ) hass.config.set_time_zone("UTC") - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ): - await async_init_integration(hass) + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_init_integration(hass) state = hass.states.get("weather.aemet_daily") assert state @@ -121,15 +121,14 @@ async def test_aemet_weather_legacy(hass: HomeAssistant) -> None: async def test_forecast_service( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, ) -> None: """Test multiple forecast.""" + hass.config.set_time_zone("UTC") - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ): - await async_init_integration(hass) + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_init_integration(hass) response = await hass.services.async_call( WEATHER_DOMAIN, From 10c53dd284de1ba8121fe699ef21aa2f3655b32f Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Tue, 29 Aug 2023 09:38:11 -0500 Subject: [PATCH 1009/1151] Fix Life360 reauthorization config flow (#99227) --- homeassistant/components/life360/config_flow.py | 6 ++++++ tests/components/life360/test_config_flow.py | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index 5153e389d8b..4b59bcadf88 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -137,6 +137,12 @@ class Life360ConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm(self, user_input: dict[str, Any]) -> FlowResult: """Handle reauthorization completion.""" + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema(password_schema(self._password)), + errors={"base": "invalid_auth"}, + ) self._password = user_input[CONF_PASSWORD] return await self._async_verify("reauth_confirm") diff --git a/tests/components/life360/test_config_flow.py b/tests/components/life360/test_config_flow.py index b71b6638eb6..7eec67fc0cc 100644 --- a/tests/components/life360/test_config_flow.py +++ b/tests/components/life360/test_config_flow.py @@ -274,6 +274,15 @@ async def test_reauth_config_flow_login_error( key = list(schema)[0] assert key.default() == TEST_PW + # Simulate hitting RECONFIGURE button. + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] + assert result["errors"]["base"] == "invalid_auth" + # Simulate getting a new, valid password. life360_api.get_authorization.reset_mock(side_effect=True) life360_api.get_authorization.return_value = TEST_AUTHORIZATION_3 From b9fd2ee3b60a2e3b23fbfefcb02a05d62df1d524 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 29 Aug 2023 17:37:26 +0200 Subject: [PATCH 1010/1151] Use functions to get value and unit in Abode (#99084) --- homeassistant/components/abode/sensor.py | 54 +++++++++++++++--------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index bf885485fc3..d964655384b 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -1,6 +1,8 @@ """Support for Abode Security System sensors.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from typing import cast from jaraco.abode.devices.sensor import Sensor as AbodeSense @@ -19,18 +21,38 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AbodeDevice, AbodeSystem from .const import DOMAIN -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( + +@dataclass +class AbodeSensorDescriptionMixin: + """Mixin for Abode sensor.""" + + value_fn: Callable[[AbodeSense], float] + native_unit_of_measurement_fn: Callable[[AbodeSense], str] + + +@dataclass +class AbodeSensorDescription(SensorEntityDescription, AbodeSensorDescriptionMixin): + """Class describing Abode sensor entities.""" + + +SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = ( + AbodeSensorDescription( key=CONST.TEMP_STATUS_KEY, device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement_fn=lambda device: device.temp_unit, + value_fn=lambda device: cast(float, device.temp), ), - SensorEntityDescription( + AbodeSensorDescription( key=CONST.HUMI_STATUS_KEY, device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement_fn=lambda device: device.humidity_unit, + value_fn=lambda device: cast(float, device.humidity), ), - SensorEntityDescription( + AbodeSensorDescription( key=CONST.LUX_STATUS_KEY, device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement_fn=lambda _: LIGHT_LUX, + value_fn=lambda device: cast(float, device.lux), ), ) @@ -52,32 +74,26 @@ async def async_setup_entry( class AbodeSensor(AbodeDevice, SensorEntity): """A sensor implementation for Abode devices.""" + entity_description: AbodeSensorDescription _device: AbodeSense def __init__( self, data: AbodeSystem, device: AbodeSense, - description: SensorEntityDescription, + description: AbodeSensorDescription, ) -> None: """Initialize a sensor for an Abode device.""" super().__init__(data, device) self.entity_description = description self._attr_unique_id = f"{device.uuid}-{description.key}" - if description.key == CONST.TEMP_STATUS_KEY: - self._attr_native_unit_of_measurement = device.temp_unit - elif description.key == CONST.HUMI_STATUS_KEY: - self._attr_native_unit_of_measurement = device.humidity_unit - elif description.key == CONST.LUX_STATUS_KEY: - self._attr_native_unit_of_measurement = LIGHT_LUX @property - def native_value(self) -> float | None: + def native_value(self) -> float: """Return the state of the sensor.""" - if self.entity_description.key == CONST.TEMP_STATUS_KEY: - return cast(float, self._device.temp) - if self.entity_description.key == CONST.HUMI_STATUS_KEY: - return cast(float, self._device.humidity) - if self.entity_description.key == CONST.LUX_STATUS_KEY: - return cast(float, self._device.lux) - return None + return self.entity_description.value_fn(self._device) + + @property + def native_unit_of_measurement(self) -> str: + """Return the native unit of measurement.""" + return self.entity_description.native_unit_of_measurement_fn(self._device) From fe713cec8f563c3171926d30e015bcfb78dd69cf Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 29 Aug 2023 15:52:29 +0000 Subject: [PATCH 1011/1151] Don't assume that the activity/sleep labels are always present in Tractive event (#99197) * Don't assume that the activity_label and sleep_labes are always present in an event * Catch KeyError --- homeassistant/components/tractive/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index c04676768c5..f2853e0032c 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -240,7 +240,9 @@ class TractiveClient: self._config_entry.data[CONF_EMAIL], ) return - + except KeyError as error: + _LOGGER.error("Error while listening for events: %s", error) + continue except aiotractive.exceptions.TractiveError: _LOGGER.debug( ( @@ -283,12 +285,12 @@ class TractiveClient: def _send_wellness_update(self, event: dict[str, Any]) -> None: payload = { - ATTR_ACTIVITY_LABEL: event["wellness"]["activity_label"], + ATTR_ACTIVITY_LABEL: event["wellness"].get("activity_label"), ATTR_CALORIES: event["activity"]["calories"], ATTR_MINUTES_DAY_SLEEP: event["sleep"]["minutes_day_sleep"], ATTR_MINUTES_NIGHT_SLEEP: event["sleep"]["minutes_night_sleep"], ATTR_MINUTES_REST: event["activity"]["minutes_rest"], - ATTR_SLEEP_LABEL: event["wellness"]["sleep_label"], + ATTR_SLEEP_LABEL: event["wellness"].get("sleep_label"), } self._dispatch_tracker_event( TRACKER_WELLNESS_STATUS_UPDATED, event["pet_id"], payload From e2dd7f20697b40e752d3a3277858fcde0c1b854d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 29 Aug 2023 18:15:44 +0200 Subject: [PATCH 1012/1151] Add entity translations to NZBGet (#98805) --- homeassistant/components/nzbget/__init__.py | 20 ++++++---- homeassistant/components/nzbget/sensor.py | 24 +++++------ homeassistant/components/nzbget/strings.json | 42 ++++++++++++++++++++ homeassistant/components/nzbget/switch.py | 4 +- 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index c5512076172..c3b6aab619b 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -5,6 +5,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SCAN_INTERVAL, Platform from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -107,15 +108,20 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non class NZBGetEntity(CoordinatorEntity[NZBGetDataUpdateCoordinator]): """Defines a base NZBGet entity.""" + _attr_has_entity_name = True + def __init__( - self, *, entry_id: str, name: str, coordinator: NZBGetDataUpdateCoordinator + self, + *, + entry_id: str, + entry_name: str, + coordinator: NZBGetDataUpdateCoordinator, ) -> None: """Initialize the NZBGet entity.""" super().__init__(coordinator) - self._name = name self._entry_id = entry_id - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry_id)}, + name=entry_name, + entry_type=DeviceEntryType.SERVICE, + ) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 7f4d31c3adf..d76e004d720 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -25,63 +25,63 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="ArticleCacheMB", - name="Article Cache", + translation_key="article_cache", native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, ), SensorEntityDescription( key="AverageDownloadRate", - name="Average Speed", + translation_key="average_speed", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, ), SensorEntityDescription( key="DownloadPaused", - name="Download Paused", + translation_key="download_paused", ), SensorEntityDescription( key="DownloadRate", - name="Speed", + translation_key="speed", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, ), SensorEntityDescription( key="DownloadedSizeMB", - name="Size", + translation_key="size", native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, ), SensorEntityDescription( key="FreeDiskSpaceMB", - name="Disk Free", + translation_key="disk_free", native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, ), SensorEntityDescription( key="PostJobCount", - name="Post Processing Jobs", + translation_key="post_processing_jobs", native_unit_of_measurement="Jobs", ), SensorEntityDescription( key="PostPaused", - name="Post Processing Paused", + translation_key="post_processing_paused", ), SensorEntityDescription( key="RemainingSizeMB", - name="Queue Size", + translation_key="queue_size", native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, ), SensorEntityDescription( key="UpTimeSec", - name="Uptime", + translation_key="uptime", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key="DownloadLimit", - name="Speed Limit", + translation_key="speed_limit", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, @@ -120,7 +120,7 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): super().__init__( coordinator=coordinator, entry_id=entry_id, - name=f"{entry_name} {description.name}", + entry_name=entry_name, ) self.entity_description = description diff --git a/homeassistant/components/nzbget/strings.json b/homeassistant/components/nzbget/strings.json index 7a3c438d11f..a1faa63bb39 100644 --- a/homeassistant/components/nzbget/strings.json +++ b/homeassistant/components/nzbget/strings.json @@ -32,6 +32,48 @@ } } }, + "entity": { + "sensor": { + "article_cache": { + "name": "Article cache" + }, + "average_speed": { + "name": "Average speed" + }, + "download_paused": { + "name": "Download paused" + }, + "speed": { + "name": "Speed" + }, + "size": { + "name": "Size" + }, + "disk_free": { + "name": "Disk free" + }, + "post_processing_jobs": { + "name": "Post processing jobs" + }, + "post_processing_paused": { + "name": "Post processing paused" + }, + "queue_size": { + "name": "Queue size" + }, + "uptime": { + "name": "Uptime" + }, + "speed_limit": { + "name": "Speed limit" + } + }, + "switch": { + "download": { + "name": "Download" + } + } + }, "services": { "pause": { "name": "[%key:common::action::pause%]", diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py index 74b49b63501..e6a2b213873 100644 --- a/homeassistant/components/nzbget/switch.py +++ b/homeassistant/components/nzbget/switch.py @@ -38,6 +38,8 @@ async def async_setup_entry( class NZBGetDownloadSwitch(NZBGetEntity, SwitchEntity): """Representation of a NZBGet download switch.""" + _attr_translation_key = "download" + def __init__( self, coordinator: NZBGetDataUpdateCoordinator, @@ -50,7 +52,7 @@ class NZBGetDownloadSwitch(NZBGetEntity, SwitchEntity): super().__init__( coordinator=coordinator, entry_id=entry_id, - name=f"{entry_name} Download", + entry_name=entry_name, ) @property From e0eb63c58816818530ccda4e9bf0069bba58ed5c Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 29 Aug 2023 13:57:41 -0400 Subject: [PATCH 1013/1151] Validate slug in addon services (#99232) * Validate slug in addon services * Move validator into hassio component * Fixes from mypy * Fix test for changes * Adjust fixtures to current supervisor * Fix call counts after fixture adjustment * Increase coverage --- homeassistant/components/hassio/__init__.py | 20 +++- homeassistant/components/hassio/handler.py | 6 ++ tests/components/hassio/test_handler.py | 7 ++ tests/components/hassio/test_init.py | 103 ++++++++++++-------- 4 files changed, 95 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 3451195f3cd..0e0d42149fc 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -32,6 +32,7 @@ from homeassistant.core import ( HassJob, HomeAssistant, ServiceCall, + async_get_hass, callback, ) from homeassistant.exceptions import HomeAssistantError @@ -149,9 +150,22 @@ SERVICE_RESTORE_FULL = "restore_full" SERVICE_RESTORE_PARTIAL = "restore_partial" +def valid_addon(value: Any) -> str: + """Validate value is a valid addon slug.""" + value = cv.slug(value) + + hass: HomeAssistant | None = None + with suppress(HomeAssistantError): + hass = async_get_hass() + + if hass and (addons := get_addons_info(hass)) is not None and value not in addons: + raise vol.Invalid("Not a valid add-on slug") + return value + + SCHEMA_NO_DATA = vol.Schema({}) -SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): cv.string}) +SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): valid_addon}) SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend( {vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)} @@ -174,7 +188,7 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( { vol.Optional(ATTR_HOMEASSISTANT): cv.boolean, vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.slug]), } ) @@ -189,7 +203,7 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( { vol.Optional(ATTR_HOMEASSISTANT): cv.boolean, vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.slug]), } ) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index e4a0dd0f77e..020a4365ec6 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -8,6 +8,7 @@ import os from typing import Any import aiohttp +from yarl import URL from homeassistant.components.http import ( CONF_SERVER_HOST, @@ -530,6 +531,11 @@ class HassIO: This method is a coroutine. """ + url = f"http://{self._ip}{command}" + if url != str(URL(url)): + _LOGGER.error("Invalid request %s", command) + raise HassioAPIError() + try: request = await self.websession.request( method, diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index e980bf214a0..5a89ea8335a 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -413,3 +413,10 @@ async def test_api_reboot_host( assert await handler.async_reboot_host(hass) == {} assert aioclient_mock.call_count == 1 + + +async def test_send_command_invalid_command(hass: HomeAssistant, hassio_stubs) -> None: + """Test send command fails when command is invalid.""" + hassio: HassIO = hass.data["hassio"] + with pytest.raises(HassioAPIError): + await hassio.send_command("/test/../bad") diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index b394d439654..4b10c58036e 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -5,6 +5,7 @@ from typing import Any from unittest.mock import patch import pytest +from voluptuous import Invalid from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import frontend @@ -100,29 +101,29 @@ def mock_all(aioclient_mock, request, os_info): "version_latest": "1.0.0", "version": "1.0.0", "auto_update": True, + "addons": [ + { + "name": "test", + "slug": "test", + "state": "stopped", + "update_available": False, + "version": "1.0.0", + "version_latest": "1.0.0", + "repository": "core", + "icon": False, + }, + { + "name": "test2", + "slug": "test2", + "state": "stopped", + "update_available": False, + "version": "1.0.0", + "version_latest": "1.0.0", + "repository": "core", + "icon": False, + }, + ], }, - "addons": [ - { - "name": "test", - "slug": "test", - "installed": True, - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "repository": "core", - "url": "https://github.com/home-assistant/addons/test", - }, - { - "name": "test2", - "slug": "test2", - "installed": True, - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "repository": "core", - "url": "https://github.com", - }, - ], }, ) aioclient_mock.get( @@ -243,7 +244,7 @@ async def test_setup_api_ping( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 18 + assert aioclient_mock.call_count == 22 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -288,7 +289,7 @@ async def test_setup_api_push_api_data( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 18 + assert aioclient_mock.call_count == 22 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -307,7 +308,7 @@ async def test_setup_api_push_api_data_server_host( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 18 + assert aioclient_mock.call_count == 22 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -324,7 +325,7 @@ async def test_setup_api_push_api_data_default( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 18 + assert aioclient_mock.call_count == 22 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -404,7 +405,7 @@ async def test_setup_api_existing_hassio_user( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 18 + assert aioclient_mock.call_count == 22 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -421,7 +422,7 @@ async def test_setup_core_push_timezone( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 18 + assert aioclient_mock.call_count == 22 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -441,7 +442,7 @@ async def test_setup_hassio_no_additional_data( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 18 + assert aioclient_mock.call_count == 22 assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456" @@ -486,13 +487,17 @@ async def test_service_register(hassio_env, hass: HomeAssistant) -> None: @pytest.mark.freeze_time("2021-11-13 11:48:00") async def test_service_calls( - hassio_env, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Call service and check the API calls behind that.""" - assert await async_setup_component(hass, "hassio", {}) + with patch.dict(os.environ, MOCK_ENVIRON), patch( + "homeassistant.components.hassio.HassIO.is_connected", + return_value=None, + ): + assert await async_setup_component(hass, "hassio", {}) + await hass.async_block_till_done() aioclient_mock.post("http://127.0.0.1/addons/test/start", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/addons/test/stop", json={"result": "ok"}) @@ -519,14 +524,14 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 10 + assert aioclient_mock.call_count == 26 assert aioclient_mock.mock_calls[-1][2] == "test" await hass.services.async_call("hassio", "host_shutdown", {}) await hass.services.async_call("hassio", "host_reboot", {}) await hass.async_block_till_done() - assert aioclient_mock.call_count == 12 + assert aioclient_mock.call_count == 28 await hass.services.async_call("hassio", "backup_full", {}) await hass.services.async_call( @@ -541,7 +546,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 14 + assert aioclient_mock.call_count == 30 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 11:48:00", "homeassistant": True, @@ -566,7 +571,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 16 + assert aioclient_mock.call_count == 32 assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], @@ -584,7 +589,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 17 + assert aioclient_mock.call_count == 33 assert aioclient_mock.mock_calls[-1][2] == { "name": "backup_name", "location": "backup_share", @@ -599,13 +604,35 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 18 + assert aioclient_mock.call_count == 34 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 11:48:00", "location": None, } +async def test_invalid_service_calls( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Call service with invalid input and check that it raises.""" + with patch.dict(os.environ, MOCK_ENVIRON), patch( + "homeassistant.components.hassio.HassIO.is_connected", + return_value=None, + ): + assert await async_setup_component(hass, "hassio", {}) + await hass.async_block_till_done() + + with pytest.raises(Invalid): + await hass.services.async_call( + "hassio", "addon_start", {"addon": "does_not_exist"} + ) + with pytest.raises(Invalid): + await hass.services.async_call( + "hassio", "addon_stdin", {"addon": "does_not_exist", "input": "test"} + ) + + async def test_service_calls_core( hassio_env, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: @@ -889,7 +916,7 @@ async def test_setup_hardware_integration( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count == 18 + assert aioclient_mock.call_count == 22 assert len(mock_setup_entry.mock_calls) == 1 From c3ef518551e798be22d13c61abac9fb77412a307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 29 Aug 2023 20:06:46 +0200 Subject: [PATCH 1014/1151] Update AEMET-OpenData to v0.4.2 (#99286) --- homeassistant/components/aemet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index facd7f58a71..6e1989d4f20 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.4.1"] + "requirements": ["AEMET-OpenData==0.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 639ae033b8e..e337f04f532 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.1 +AEMET-OpenData==0.4.2 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab5c68bae34..b94da831294 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.1 +AEMET-OpenData==0.4.2 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 From 50150f557782463a79286c05dd50530638184d7a Mon Sep 17 00:00:00 2001 From: kel30a <1787606+kel30a@users.noreply.github.com> Date: Wed, 30 Aug 2023 04:08:39 +1000 Subject: [PATCH 1015/1151] Bump pydaikin version to 2.11.1 (#99288) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index c6334dfaeca..7c7f5ce7f2a 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "loggers": ["pydaikin"], "quality_scale": "platinum", - "requirements": ["pydaikin==2.10.5"], + "requirements": ["pydaikin==2.11.1"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index e337f04f532..f1bcd71b1ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1629,7 +1629,7 @@ pycsspeechtts==1.0.8 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.10.5 +pydaikin==2.11.1 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b94da831294..121f2b069da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1211,7 +1211,7 @@ pycoolmasternet-async==0.1.5 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.10.5 +pydaikin==2.11.1 # homeassistant.components.deconz pydeconz==113 From 0366e14630676b2d96f529c9113f5fceb86c705b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 29 Aug 2023 20:14:33 +0200 Subject: [PATCH 1016/1151] Allows defining list of attributes excluded from history in manifest.json (#99283) * Move list of attributes excluded from history to manifest.json * Address comments --- .../components/automation/__init__.py | 7 --- .../components/automation/manifest.json | 9 +++- .../components/automation/recorder.py | 12 ----- homeassistant/components/recorder/__init__.py | 3 +- homeassistant/generated/recorder.py | 14 ++++++ script/hassfest/__main__.py | 2 + script/hassfest/manifest.py | 1 + script/hassfest/recorder.py | 45 +++++++++++++++++++ 8 files changed, 72 insertions(+), 21 deletions(-) delete mode 100644 homeassistant/components/automation/recorder.py create mode 100644 homeassistant/generated/recorder.py create mode 100644 script/hassfest/recorder.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f4db7831235..885427a9f80 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -57,9 +57,6 @@ from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.integration_platform import ( - async_process_integration_platform_for_component, -) from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import ( @@ -249,10 +246,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: LOGGER, DOMAIN, hass ) - # Process integration platforms right away since - # we will create entities before firing EVENT_COMPONENT_LOADED - await async_process_integration_platform_for_component(hass, DOMAIN) - # Register automation as valid domain for Blueprint async_get_blueprints(hass) diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index a22abbee3b2..de72d45d756 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -6,5 +6,12 @@ "dependencies": ["blueprint", "trace"], "documentation": "https://www.home-assistant.io/integrations/automation", "integration_type": "system", - "quality_scale": "internal" + "quality_scale": "internal", + "recorder_excluded_attributes": [ + "current", + "id", + "last_triggered", + "max", + "mode" + ] } diff --git a/homeassistant/components/automation/recorder.py b/homeassistant/components/automation/recorder.py deleted file mode 100644 index 3083d271d1f..00000000000 --- a/homeassistant/components/automation/recorder.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Integration platform for recorder.""" -from __future__ import annotations - -from homeassistant.core import HomeAssistant, callback - -from . import ATTR_CUR, ATTR_LAST_TRIGGERED, ATTR_MAX, ATTR_MODE, CONF_ID - - -@callback -def exclude_attributes(hass: HomeAssistant) -> set[str]: - """Exclude extra attributes from being recorded in the database.""" - return {ATTR_LAST_TRIGGERED, ATTR_MODE, ATTR_CUR, ATTR_MAX, CONF_ID} diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 72d825d9e78..746b845c420 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.const import CONF_EXCLUDE, EVENT_STATE_CHANGED from homeassistant.core import HomeAssistant +from homeassistant.generated.recorder import EXCLUDED_ATTRIBUTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -132,7 +133,7 @@ def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" - exclude_attributes_by_domain: dict[str, set[str]] = {} + exclude_attributes_by_domain: dict[str, set[str]] = dict(EXCLUDED_ATTRIBUTES) hass.data[EXCLUDE_ATTRIBUTES] = exclude_attributes_by_domain conf = config[DOMAIN] entity_filter = convert_include_exclude_filter(conf).get_filter() diff --git a/homeassistant/generated/recorder.py b/homeassistant/generated/recorder.py new file mode 100644 index 00000000000..d9213c60125 --- /dev/null +++ b/homeassistant/generated/recorder.py @@ -0,0 +1,14 @@ +"""Automatically generated file. + +To update, run python3 -m script.hassfest +""" + +EXCLUDED_ATTRIBUTES = { + "automation": { + "current", + "id", + "last_triggered", + "max", + "mode", + }, +} diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 1c626ac3c5b..f263c594bc5 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -20,6 +20,7 @@ from . import ( metadata, mqtt, mypy_config, + recorder, requirements, services, ssdp, @@ -39,6 +40,7 @@ INTEGRATION_PLUGINS = [ json, manifest, mqtt, + recorder, requirements, services, ssdp, diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 65e37aa515d..5dbb7896dee 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -264,6 +264,7 @@ INTEGRATION_MANIFEST_SCHEMA = vol.Schema( vol.Optional("loggers"): [str], vol.Optional("disabled"): str, vol.Optional("iot_class"): vol.In(SUPPORTED_IOT_CLASSES), + vol.Optional("recorder_excluded_attributes"): [str], } ) diff --git a/script/hassfest/recorder.py b/script/hassfest/recorder.py new file mode 100644 index 00000000000..752f9523d3a --- /dev/null +++ b/script/hassfest/recorder.py @@ -0,0 +1,45 @@ +"""Generate recorder file.""" +from __future__ import annotations + +from .model import Config, Integration +from .serializer import format_python_namespace + + +def generate_and_validate(integrations: dict[str, Integration]) -> str: + """Validate and generate recorder data.""" + + data: dict[str, set[str]] = {} + + for domain in sorted(integrations): + exclude_list = integrations[domain].manifest.get("recorder_excluded_attributes") + + if not exclude_list: + continue + + data[domain] = set(exclude_list) + + return format_python_namespace({"EXCLUDED_ATTRIBUTES": data}) + + +def validate(integrations: dict[str, Integration], config: Config) -> None: + """Validate recorder file.""" + recorder_path = config.root / "homeassistant/generated/recorder.py" + config.cache["recorder"] = content = generate_and_validate(integrations) + + if config.specific_integrations: + return + + with open(str(recorder_path)) as fp: + if fp.read() != content: + config.add_error( + "recorder", + "File recorder.py is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + + +def generate(integrations: dict[str, Integration], config: Config) -> None: + """Generate recorder file.""" + recorder_path = config.root / "homeassistant/generated/recorder.py" + with open(str(recorder_path), "w") as fp: + fp.write(f"{config.cache['recorder']}") From 53c5b187c09378ac6cb4fcc97db58bc540148d4e Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 29 Aug 2023 20:43:32 +0200 Subject: [PATCH 1017/1151] Update Home Assistant base image to 2023.08.0 (#99281) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 882fa31f121..cc13a4e595f 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.07.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.07.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.07.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.07.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.07.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.08.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.08.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.08.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.08.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.08.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 4508e341c9ec35f14dab387f6856220f50ca29ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 29 Aug 2023 21:14:37 +0200 Subject: [PATCH 1018/1151] Add wind gust to AEMET hourly forecasts (#99289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/const.py | 1 + homeassistant/components/aemet/weather.py | 3 + .../aemet/weather_update_coordinator.py | 2 + .../aemet/fixtures/station-3195-data.json | 115 ++++++++++++---- .../aemet/fixtures/station-list-data.json | 15 +- .../aemet/snapshots/test_weather.ambr | 129 ++++++++++++++++++ 6 files changed, 239 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index bd10d09bea0..c6c4a9c1628 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -33,6 +33,7 @@ ATTR_API_FORECAST_TEMP = "temperature" ATTR_API_FORECAST_TEMP_LOW = "templow" ATTR_API_FORECAST_TIME = "datetime" ATTR_API_FORECAST_WIND_BEARING = "wind_bearing" +ATTR_API_FORECAST_WIND_MAX_SPEED = "wind_max_speed" ATTR_API_FORECAST_WIND_SPEED = "wind_speed" ATTR_API_HUMIDITY = "humidity" ATTR_API_PRESSURE = "pressure" diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index 60289f4723a..e3a1922c2f1 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -6,6 +6,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, @@ -35,6 +36,7 @@ from .const import ( ATTR_API_FORECAST_TEMP_LOW, ATTR_API_FORECAST_TIME, ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_MAX_SPEED, ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, @@ -69,6 +71,7 @@ FORECAST_MAP = { ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_MAX_SPEED: ATTR_FORECAST_NATIVE_WIND_GUST_SPEED, ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, }, } diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 016dd2ba75c..c6e27374f8f 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -56,6 +56,7 @@ from .const import ( ATTR_API_FORECAST_TEMP_LOW, ATTR_API_FORECAST_TIME, ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_MAX_SPEED, ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, @@ -333,6 +334,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ), ATTR_API_FORECAST_TEMP: self._get_temperature(day, hour), ATTR_API_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), + ATTR_API_FORECAST_WIND_MAX_SPEED: self._get_wind_max_speed(day, hour), ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } diff --git a/tests/components/aemet/fixtures/station-3195-data.json b/tests/components/aemet/fixtures/station-3195-data.json index b050ee16d67..bbde98b1cb2 100644 --- a/tests/components/aemet/fixtures/station-3195-data.json +++ b/tests/components/aemet/fixtures/station-3195-data.json @@ -1,5 +1,6 @@ [ { + "dv": 100.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-08T14:00:00", @@ -14,9 +15,12 @@ "ta": 0.1, "tamax": 0.2, "tpr": -0.3, - "rviento": 132.0 + "rviento": 132.0, + "vv": 1.0, + "vmax": 10.0 }, { + "dv": 101.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-08T15:00:00", @@ -31,9 +35,12 @@ "ta": 0.2, "tamax": 0.3, "tpr": 0.0, - "rviento": 154.0 + "rviento": 154.0, + "vv": 1.1, + "vmax": 10.1 }, { + "dv": 102.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-08T16:00:00", @@ -48,9 +55,12 @@ "ta": 0.3, "tamax": 0.3, "tpr": 0.0, - "rviento": 177.0 + "rviento": 177.0, + "vv": 1.2, + "vmax": 10.2 }, { + "dv": 103.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-08T17:00:00", @@ -65,9 +75,12 @@ "ta": 0.1, "tamax": 0.3, "tpr": 0.0, - "rviento": 174.0 + "rviento": 174.0, + "vv": 1.3, + "vmax": 10.3 }, { + "dv": 104.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-08T18:00:00", @@ -82,9 +95,12 @@ "ta": -0.1, "tamax": 0.1, "tpr": -0.3, - "rviento": 163.0 + "rviento": 163.0, + "vv": 1.4, + "vmax": 10.4 }, { + "dv": 105.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-08T19:00:00", @@ -99,9 +115,12 @@ "ta": -0.3, "tamax": 0.0, "tpr": -0.5, - "rviento": 79.0 + "rviento": 79.0, + "vv": 1.5, + "vmax": 10.5 }, { + "dv": 106.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-08T20:00:00", @@ -116,9 +135,12 @@ "ta": -0.6, "tamax": -0.3, "tpr": -0.7, - "rviento": 0.0 + "rviento": 0.0, + "vv": 1.6, + "vmax": 10.6 }, { + "dv": 107.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-08T21:00:00", @@ -133,9 +155,12 @@ "ta": -0.7, "tamax": -0.5, "tpr": -0.7, - "rviento": 0.0 + "rviento": 0.0, + "vv": 1.7, + "vmax": 10.7 }, { + "dv": 108.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-08T22:00:00", @@ -150,9 +175,12 @@ "ta": -0.8, "tamax": -0.7, "tpr": -1.0, - "rviento": 0.0 + "rviento": 0.0, + "vv": 1.8, + "vmax": 10.8 }, { + "dv": 109.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-08T23:00:00", @@ -167,9 +195,12 @@ "ta": -0.9, "tamax": -0.7, "tpr": -1.0, - "rviento": 0.0 + "rviento": 0.0, + "vv": 1.9, + "vmax": 10.9 }, { + "dv": 110.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T00:00:00", @@ -184,9 +215,12 @@ "ta": -1.0, "tamax": -0.8, "tpr": -1.2, - "rviento": 0.0 + "rviento": 0.0, + "vv": 2.0, + "vmax": 11.0 }, { + "dv": 111.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T01:00:00", @@ -201,9 +235,12 @@ "ta": -1.3, "tamax": -1.0, "tpr": -1.4, - "rviento": 0.0 + "rviento": 0.0, + "vv": 2.1, + "vmax": 11.1 }, { + "dv": 112.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T02:00:00", @@ -218,9 +255,12 @@ "ta": -1.4, "tamax": -1.3, "tpr": -1.4, - "rviento": 0.0 + "rviento": 0.0, + "vv": 2.2, + "vmax": 11.2 }, { + "dv": 113.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T03:00:00", @@ -235,9 +275,12 @@ "ta": -1.4, "tamax": -1.4, "tpr": -1.4, - "rviento": 0.0 + "rviento": 0.0, + "vv": 2.3, + "vmax": 11.3 }, { + "dv": 114.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T04:00:00", @@ -252,9 +295,12 @@ "ta": -1.5, "tamax": -1.4, "tpr": -1.7, - "rviento": 0.0 + "rviento": 0.0, + "vv": 2.4, + "vmax": 11.4 }, { + "dv": 115.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T05:00:00", @@ -269,9 +315,12 @@ "ta": -1.5, "tamax": -1.4, "tpr": -1.7, - "rviento": 0.0 + "rviento": 0.0, + "vv": 2.5, + "vmax": 11.5 }, { + "dv": 116.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T06:00:00", @@ -286,9 +335,12 @@ "ta": -1.6, "tamax": -1.5, "tpr": -1.7, - "rviento": 0.0 + "rviento": 0.0, + "vv": 2.6, + "vmax": 11.6 }, { + "dv": 117.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T07:00:00", @@ -303,9 +355,12 @@ "ta": -1.6, "tamax": -1.6, "tpr": -1.7, - "rviento": 0.0 + "rviento": 0.0, + "vv": 2.7, + "vmax": 11.7 }, { + "dv": 118.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T08:00:00", @@ -320,9 +375,12 @@ "ta": -1.6, "tamax": -1.5, "tpr": -1.7, - "rviento": 0.0 + "rviento": 0.0, + "vv": 2.8, + "vmax": 11.8 }, { + "dv": 119.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T09:00:00", @@ -337,9 +395,12 @@ "ta": -1.3, "tamax": -1.3, "tpr": -1.4, - "rviento": 0.0 + "rviento": 0.0, + "vv": 2.9, + "vmax": 11.9 }, { + "dv": 120.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T10:00:00", @@ -354,9 +415,12 @@ "ta": -1.2, "tamax": -1.1, "tpr": -1.4, - "rviento": 0.0 + "rviento": 0.0, + "vv": 3.0, + "vmax": 12.0 }, { + "dv": 121.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T11:00:00", @@ -371,9 +435,12 @@ "ta": -1.0, "tamax": -1.0, "tpr": -1.2, - "rviento": 0.0 + "rviento": 0.0, + "vv": 3.1, + "vmax": 12.1 }, { + "dv": 122.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-09T12:00:00", @@ -388,6 +455,8 @@ "ta": -0.7, "tamax": -0.6, "tpr": -0.7, - "rviento": 0.0 + "rviento": 0.0, + "vv": 3.2, + "vmax": 12.2 } ] diff --git a/tests/components/aemet/fixtures/station-list-data.json b/tests/components/aemet/fixtures/station-list-data.json index 2507cca7328..d540b0fad1c 100644 --- a/tests/components/aemet/fixtures/station-list-data.json +++ b/tests/components/aemet/fixtures/station-list-data.json @@ -1,5 +1,6 @@ [ { + "dv": 90.0, "idema": "3194U", "lon": -3.724167, "fint": "2021-01-08T14:00:00", @@ -11,9 +12,12 @@ "tamin": 0.6, "ta": 0.9, "tamax": 1.0, - "tpr": 0.6 + "tpr": 0.6, + "vv": 2.0, + "vmax": 2.5 }, { + "dv": 120.0, "idema": "3194Y", "lon": -3.813369, "fint": "2021-01-08T14:00:00", @@ -24,9 +28,12 @@ "hr": 93.0, "tamin": 0.5, "ta": 0.6, - "tamax": 0.6 + "tamax": 0.6, + "vv": 3.0, + "vmax": 3.5 }, { + "dv": 100.0, "idema": "3195", "lon": -3.678095, "fint": "2021-01-08T14:00:00", @@ -41,6 +48,8 @@ "ta": 0.1, "tamax": 0.2, "tpr": -0.3, - "rviento": 132.0 + "rviento": 132.0, + "vv": 1.0, + "vmax": 10.0 } ] diff --git a/tests/components/aemet/snapshots/test_weather.ambr b/tests/components/aemet/snapshots/test_weather.ambr index e9c922f041e..3078cab4480 100644 --- a/tests/components/aemet/snapshots/test_weather.ambr +++ b/tests/components/aemet/snapshots/test_weather.ambr @@ -65,6 +65,7 @@ 'precipitation_probability': 100, 'temperature': 0.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 24.0, 'wind_speed': 15.0, }), dict({ @@ -74,6 +75,7 @@ 'precipitation_probability': 100, 'temperature': 0.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 22.0, 'wind_speed': 15.0, }), dict({ @@ -83,6 +85,7 @@ 'precipitation_probability': 100, 'temperature': 0.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 24.0, 'wind_speed': 14.0, }), dict({ @@ -92,6 +95,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 20.0, 'wind_speed': 10.0, }), dict({ @@ -101,6 +105,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 14.0, 'wind_speed': 8.0, }), dict({ @@ -110,6 +115,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 13.0, 'wind_speed': 9.0, }), dict({ @@ -119,6 +125,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 13.0, 'wind_speed': 7.0, }), dict({ @@ -128,6 +135,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 12.0, 'wind_speed': 8.0, }), dict({ @@ -137,6 +145,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 12.0, 'wind_speed': 6.0, }), dict({ @@ -145,6 +154,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 8.0, 'wind_speed': 6.0, }), dict({ @@ -153,6 +163,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 9.0, 'wind_speed': 6.0, }), dict({ @@ -161,6 +172,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 11.0, 'wind_speed': 8.0, }), dict({ @@ -169,6 +181,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 12.0, 'wind_speed': 6.0, }), dict({ @@ -177,6 +190,7 @@ 'precipitation_probability': 10, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 10.0, 'wind_speed': 5.0, }), dict({ @@ -185,6 +199,7 @@ 'precipitation_probability': 10, 'temperature': 0.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 11.0, 'wind_speed': 6.0, }), dict({ @@ -193,6 +208,7 @@ 'precipitation_probability': 10, 'temperature': 0.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 9.0, 'wind_speed': 6.0, }), dict({ @@ -201,6 +217,7 @@ 'precipitation_probability': 10, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 12.0, 'wind_speed': 8.0, }), dict({ @@ -209,6 +226,7 @@ 'precipitation_probability': 10, 'temperature': -1.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 11.0, 'wind_speed': 5.0, }), dict({ @@ -217,6 +235,7 @@ 'precipitation_probability': 10, 'temperature': -1.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 13.0, 'wind_speed': 9.0, }), dict({ @@ -225,6 +244,7 @@ 'precipitation_probability': 15, 'temperature': -2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 18.0, 'wind_speed': 13.0, }), dict({ @@ -233,6 +253,7 @@ 'precipitation_probability': 15, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 17.0, }), dict({ @@ -241,6 +262,7 @@ 'precipitation_probability': 15, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 31.0, 'wind_speed': 21.0, }), dict({ @@ -249,6 +271,7 @@ 'precipitation_probability': 15, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 32.0, 'wind_speed': 21.0, }), dict({ @@ -257,6 +280,7 @@ 'precipitation_probability': 15, 'temperature': 2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 21.0, }), dict({ @@ -265,6 +289,7 @@ 'precipitation_probability': 15, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 32.0, 'wind_speed': 22.0, }), dict({ @@ -273,6 +298,7 @@ 'precipitation_probability': 5, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 32.0, 'wind_speed': 20.0, }), dict({ @@ -281,6 +307,7 @@ 'precipitation_probability': 5, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 19.0, }), dict({ @@ -289,6 +316,7 @@ 'precipitation_probability': 5, 'temperature': 4.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 28.0, 'wind_speed': 17.0, }), dict({ @@ -297,6 +325,7 @@ 'precipitation_probability': 5, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 16.0, }), dict({ @@ -305,6 +334,7 @@ 'precipitation_probability': 5, 'temperature': 2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 24.0, 'wind_speed': 16.0, }), dict({ @@ -313,6 +343,7 @@ 'precipitation_probability': 5, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 24.0, 'wind_speed': 17.0, }), dict({ @@ -321,6 +352,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 17.0, }), dict({ @@ -329,6 +361,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 16.0, }), dict({ @@ -337,6 +370,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 24.0, 'wind_speed': 17.0, }), dict({ @@ -345,6 +379,7 @@ 'precipitation_probability': None, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 27.0, 'wind_speed': 19.0, }), dict({ @@ -353,6 +388,7 @@ 'precipitation_probability': None, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 21.0, }), dict({ @@ -361,6 +397,7 @@ 'precipitation_probability': None, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 19.0, }), dict({ @@ -369,6 +406,7 @@ 'precipitation_probability': None, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 27.0, 'wind_speed': 16.0, }), dict({ @@ -377,6 +415,7 @@ 'precipitation_probability': None, 'temperature': -2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 22.0, 'wind_speed': 12.0, }), dict({ @@ -385,6 +424,7 @@ 'precipitation_probability': None, 'temperature': -2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 17.0, 'wind_speed': 10.0, }), dict({ @@ -393,6 +433,7 @@ 'precipitation_probability': None, 'temperature': -3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 15.0, 'wind_speed': 11.0, }), dict({ @@ -401,6 +442,7 @@ 'precipitation_probability': None, 'temperature': -4.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 15.0, 'wind_speed': 10.0, }), dict({ @@ -409,6 +451,7 @@ 'precipitation_probability': None, 'temperature': -4.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 15.0, 'wind_speed': 10.0, }), ]), @@ -531,6 +574,7 @@ 'precipitation_probability': 100, 'temperature': 0.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 24.0, 'wind_speed': 15.0, }), dict({ @@ -540,6 +584,7 @@ 'precipitation_probability': 100, 'temperature': 0.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 22.0, 'wind_speed': 15.0, }), dict({ @@ -549,6 +594,7 @@ 'precipitation_probability': 100, 'temperature': 0.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 24.0, 'wind_speed': 14.0, }), dict({ @@ -558,6 +604,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 20.0, 'wind_speed': 10.0, }), dict({ @@ -567,6 +614,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 14.0, 'wind_speed': 8.0, }), dict({ @@ -576,6 +624,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 13.0, 'wind_speed': 9.0, }), dict({ @@ -585,6 +634,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 13.0, 'wind_speed': 7.0, }), dict({ @@ -594,6 +644,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 12.0, 'wind_speed': 8.0, }), dict({ @@ -603,6 +654,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 12.0, 'wind_speed': 6.0, }), dict({ @@ -611,6 +663,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 8.0, 'wind_speed': 6.0, }), dict({ @@ -619,6 +672,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 9.0, 'wind_speed': 6.0, }), dict({ @@ -627,6 +681,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 11.0, 'wind_speed': 8.0, }), dict({ @@ -635,6 +690,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 12.0, 'wind_speed': 6.0, }), dict({ @@ -643,6 +699,7 @@ 'precipitation_probability': 10, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 10.0, 'wind_speed': 5.0, }), dict({ @@ -651,6 +708,7 @@ 'precipitation_probability': 10, 'temperature': 0.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 11.0, 'wind_speed': 6.0, }), dict({ @@ -659,6 +717,7 @@ 'precipitation_probability': 10, 'temperature': 0.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 9.0, 'wind_speed': 6.0, }), dict({ @@ -667,6 +726,7 @@ 'precipitation_probability': 10, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 12.0, 'wind_speed': 8.0, }), dict({ @@ -675,6 +735,7 @@ 'precipitation_probability': 10, 'temperature': -1.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 11.0, 'wind_speed': 5.0, }), dict({ @@ -683,6 +744,7 @@ 'precipitation_probability': 10, 'temperature': -1.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 13.0, 'wind_speed': 9.0, }), dict({ @@ -691,6 +753,7 @@ 'precipitation_probability': 15, 'temperature': -2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 18.0, 'wind_speed': 13.0, }), dict({ @@ -699,6 +762,7 @@ 'precipitation_probability': 15, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 17.0, }), dict({ @@ -707,6 +771,7 @@ 'precipitation_probability': 15, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 31.0, 'wind_speed': 21.0, }), dict({ @@ -715,6 +780,7 @@ 'precipitation_probability': 15, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 32.0, 'wind_speed': 21.0, }), dict({ @@ -723,6 +789,7 @@ 'precipitation_probability': 15, 'temperature': 2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 21.0, }), dict({ @@ -731,6 +798,7 @@ 'precipitation_probability': 15, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 32.0, 'wind_speed': 22.0, }), dict({ @@ -739,6 +807,7 @@ 'precipitation_probability': 5, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 32.0, 'wind_speed': 20.0, }), dict({ @@ -747,6 +816,7 @@ 'precipitation_probability': 5, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 19.0, }), dict({ @@ -755,6 +825,7 @@ 'precipitation_probability': 5, 'temperature': 4.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 28.0, 'wind_speed': 17.0, }), dict({ @@ -763,6 +834,7 @@ 'precipitation_probability': 5, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 16.0, }), dict({ @@ -771,6 +843,7 @@ 'precipitation_probability': 5, 'temperature': 2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 24.0, 'wind_speed': 16.0, }), dict({ @@ -779,6 +852,7 @@ 'precipitation_probability': 5, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 24.0, 'wind_speed': 17.0, }), dict({ @@ -787,6 +861,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 17.0, }), dict({ @@ -795,6 +870,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 16.0, }), dict({ @@ -803,6 +879,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 24.0, 'wind_speed': 17.0, }), dict({ @@ -811,6 +888,7 @@ 'precipitation_probability': None, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 27.0, 'wind_speed': 19.0, }), dict({ @@ -819,6 +897,7 @@ 'precipitation_probability': None, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 21.0, }), dict({ @@ -827,6 +906,7 @@ 'precipitation_probability': None, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 19.0, }), dict({ @@ -835,6 +915,7 @@ 'precipitation_probability': None, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 27.0, 'wind_speed': 16.0, }), dict({ @@ -843,6 +924,7 @@ 'precipitation_probability': None, 'temperature': -2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 22.0, 'wind_speed': 12.0, }), dict({ @@ -851,6 +933,7 @@ 'precipitation_probability': None, 'temperature': -2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 17.0, 'wind_speed': 10.0, }), dict({ @@ -859,6 +942,7 @@ 'precipitation_probability': None, 'temperature': -3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 15.0, 'wind_speed': 11.0, }), dict({ @@ -867,6 +951,7 @@ 'precipitation_probability': None, 'temperature': -4.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 15.0, 'wind_speed': 10.0, }), dict({ @@ -875,6 +960,7 @@ 'precipitation_probability': None, 'temperature': -4.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 15.0, 'wind_speed': 10.0, }), ]) @@ -888,6 +974,7 @@ 'precipitation_probability': 100, 'temperature': 0.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 24.0, 'wind_speed': 15.0, }), dict({ @@ -897,6 +984,7 @@ 'precipitation_probability': 100, 'temperature': 0.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 22.0, 'wind_speed': 15.0, }), dict({ @@ -906,6 +994,7 @@ 'precipitation_probability': 100, 'temperature': 0.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 24.0, 'wind_speed': 14.0, }), dict({ @@ -915,6 +1004,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 20.0, 'wind_speed': 10.0, }), dict({ @@ -924,6 +1014,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 14.0, 'wind_speed': 8.0, }), dict({ @@ -933,6 +1024,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 13.0, 'wind_speed': 9.0, }), dict({ @@ -942,6 +1034,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 13.0, 'wind_speed': 7.0, }), dict({ @@ -951,6 +1044,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 12.0, 'wind_speed': 8.0, }), dict({ @@ -960,6 +1054,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 135.0, + 'wind_gust_speed': 12.0, 'wind_speed': 6.0, }), dict({ @@ -968,6 +1063,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 8.0, 'wind_speed': 6.0, }), dict({ @@ -976,6 +1072,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 9.0, 'wind_speed': 6.0, }), dict({ @@ -984,6 +1081,7 @@ 'precipitation_probability': 100, 'temperature': 1.0, 'wind_bearing': 90.0, + 'wind_gust_speed': 11.0, 'wind_speed': 8.0, }), dict({ @@ -992,6 +1090,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 12.0, 'wind_speed': 6.0, }), dict({ @@ -1000,6 +1099,7 @@ 'precipitation_probability': 10, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 10.0, 'wind_speed': 5.0, }), dict({ @@ -1008,6 +1108,7 @@ 'precipitation_probability': 10, 'temperature': 0.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 11.0, 'wind_speed': 6.0, }), dict({ @@ -1016,6 +1117,7 @@ 'precipitation_probability': 10, 'temperature': 0.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 9.0, 'wind_speed': 6.0, }), dict({ @@ -1024,6 +1126,7 @@ 'precipitation_probability': 10, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 12.0, 'wind_speed': 8.0, }), dict({ @@ -1032,6 +1135,7 @@ 'precipitation_probability': 10, 'temperature': -1.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 11.0, 'wind_speed': 5.0, }), dict({ @@ -1040,6 +1144,7 @@ 'precipitation_probability': 10, 'temperature': -1.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 13.0, 'wind_speed': 9.0, }), dict({ @@ -1048,6 +1153,7 @@ 'precipitation_probability': 15, 'temperature': -2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 18.0, 'wind_speed': 13.0, }), dict({ @@ -1056,6 +1162,7 @@ 'precipitation_probability': 15, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 17.0, }), dict({ @@ -1064,6 +1171,7 @@ 'precipitation_probability': 15, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 31.0, 'wind_speed': 21.0, }), dict({ @@ -1072,6 +1180,7 @@ 'precipitation_probability': 15, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 32.0, 'wind_speed': 21.0, }), dict({ @@ -1080,6 +1189,7 @@ 'precipitation_probability': 15, 'temperature': 2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 21.0, }), dict({ @@ -1088,6 +1198,7 @@ 'precipitation_probability': 15, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 32.0, 'wind_speed': 22.0, }), dict({ @@ -1096,6 +1207,7 @@ 'precipitation_probability': 5, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 32.0, 'wind_speed': 20.0, }), dict({ @@ -1104,6 +1216,7 @@ 'precipitation_probability': 5, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 19.0, }), dict({ @@ -1112,6 +1225,7 @@ 'precipitation_probability': 5, 'temperature': 4.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 28.0, 'wind_speed': 17.0, }), dict({ @@ -1120,6 +1234,7 @@ 'precipitation_probability': 5, 'temperature': 3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 16.0, }), dict({ @@ -1128,6 +1243,7 @@ 'precipitation_probability': 5, 'temperature': 2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 24.0, 'wind_speed': 16.0, }), dict({ @@ -1136,6 +1252,7 @@ 'precipitation_probability': 5, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 24.0, 'wind_speed': 17.0, }), dict({ @@ -1144,6 +1261,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 17.0, }), dict({ @@ -1152,6 +1270,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 25.0, 'wind_speed': 16.0, }), dict({ @@ -1160,6 +1279,7 @@ 'precipitation_probability': None, 'temperature': 1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 24.0, 'wind_speed': 17.0, }), dict({ @@ -1168,6 +1288,7 @@ 'precipitation_probability': None, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 27.0, 'wind_speed': 19.0, }), dict({ @@ -1176,6 +1297,7 @@ 'precipitation_probability': None, 'temperature': 0.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 21.0, }), dict({ @@ -1184,6 +1306,7 @@ 'precipitation_probability': None, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 30.0, 'wind_speed': 19.0, }), dict({ @@ -1192,6 +1315,7 @@ 'precipitation_probability': None, 'temperature': -1.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 27.0, 'wind_speed': 16.0, }), dict({ @@ -1200,6 +1324,7 @@ 'precipitation_probability': None, 'temperature': -2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 22.0, 'wind_speed': 12.0, }), dict({ @@ -1208,6 +1333,7 @@ 'precipitation_probability': None, 'temperature': -2.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 17.0, 'wind_speed': 10.0, }), dict({ @@ -1216,6 +1342,7 @@ 'precipitation_probability': None, 'temperature': -3.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 15.0, 'wind_speed': 11.0, }), dict({ @@ -1224,6 +1351,7 @@ 'precipitation_probability': None, 'temperature': -4.0, 'wind_bearing': 45.0, + 'wind_gust_speed': 15.0, 'wind_speed': 10.0, }), dict({ @@ -1232,6 +1360,7 @@ 'precipitation_probability': None, 'temperature': -4.0, 'wind_bearing': 0.0, + 'wind_gust_speed': 15.0, 'wind_speed': 10.0, }), ]) From b403cb41c0f86803886c5b1e012822b1a35aff1e Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Tue, 29 Aug 2023 21:41:50 +0200 Subject: [PATCH 1019/1151] Allow one retry before raising ConfigEntryAuthFailed for bmw_connected_drive (#99168) * Allow one retry before raising ConfigEntryAuthFailed * Move time with freezer * Update homeassistant/components/bmw_connected_drive/coordinator.py --------- Co-authored-by: rikroe Co-authored-by: G Johansson --- .../bmw_connected_drive/coordinator.py | 5 +- .../bmw_connected_drive/test_coordinator.py | 92 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 tests/components/bmw_connected_drive/test_coordinator.py diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index 4a586aab373..2634c6069c9 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -58,7 +58,10 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]): try: await self.account.get_vehicles() except MyBMWAuthError as err: - # Clear refresh token and trigger reauth + # Allow one retry interval before raising AuthFailed to avoid flaky API issues + if self.last_update_success: + raise UpdateFailed(err) from err + # Clear refresh token and trigger reauth if previous update failed as well self._update_config_entry_refresh_token(None) raise ConfigEntryAuthFailed(err) from err except (MyBMWAPIError, RequestError) as err: diff --git a/tests/components/bmw_connected_drive/test_coordinator.py b/tests/components/bmw_connected_drive/test_coordinator.py new file mode 100644 index 00000000000..ab2d08376dd --- /dev/null +++ b/tests/components/bmw_connected_drive/test_coordinator.py @@ -0,0 +1,92 @@ +"""Test BMW coordinator.""" +from datetime import timedelta +from unittest.mock import patch + +from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError +from freezegun.api import FrozenDateTimeFactory +import respx + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import UpdateFailed + +from . import FIXTURE_CONFIG_ENTRY + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_update_success(hass: HomeAssistant, bmw_fixture: respx.Router) -> None: + """Test the reauth form.""" + config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + hass.data[config_entry.domain][config_entry.entry_id].last_update_success + is True + ) + + +async def test_update_failed( + hass: HomeAssistant, bmw_fixture: respx.Router, freezer: FrozenDateTimeFactory +) -> None: + """Test the reauth form.""" + config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + coordinator = hass.data[config_entry.domain][config_entry.entry_id] + + assert coordinator.last_update_success is True + + freezer.tick(timedelta(minutes=5, seconds=1)) + async_fire_time_changed(hass) + with patch( + "bimmer_connected.account.MyBMWAccount.get_vehicles", + side_effect=MyBMWAPIError("Test error"), + ): + await hass.async_block_till_done() + + assert coordinator.last_update_success is False + assert isinstance(coordinator.last_exception, UpdateFailed) is True + + +async def test_update_reauth( + hass: HomeAssistant, bmw_fixture: respx.Router, freezer: FrozenDateTimeFactory +) -> None: + """Test the reauth form.""" + config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + coordinator = hass.data[config_entry.domain][config_entry.entry_id] + + assert coordinator.last_update_success is True + + freezer.tick(timedelta(minutes=5, seconds=1)) + async_fire_time_changed(hass) + with patch( + "bimmer_connected.account.MyBMWAccount.get_vehicles", + side_effect=MyBMWAuthError("Test error"), + ): + await hass.async_block_till_done() + + assert coordinator.last_update_success is False + assert isinstance(coordinator.last_exception, UpdateFailed) is True + + freezer.tick(timedelta(minutes=5, seconds=1)) + async_fire_time_changed(hass) + with patch( + "bimmer_connected.account.MyBMWAccount.get_vehicles", + side_effect=MyBMWAuthError("Test error"), + ): + await hass.async_block_till_done() + + assert coordinator.last_update_success is False + assert isinstance(coordinator.last_exception, ConfigEntryAuthFailed) is True From de30712d76c3fa1f7d5a7f9a1f99edf7d0a4741d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Tue, 29 Aug 2023 22:02:09 +0200 Subject: [PATCH 1020/1151] Verisure: propagate lock code digits configuration immediately (#99241) --- homeassistant/components/verisure/__init__.py | 10 ++++++++++ homeassistant/components/verisure/lock.py | 10 +++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 62f41913862..dfd9d9cdc04 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -48,9 +48,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Set up all platforms for this device/entry. await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # Update options + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True +async def update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + # Propagate configuration change. + coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator.async_update_listeners() + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Verisure config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index ad9590d2524..1a81b437116 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -70,9 +70,6 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt self.serial_number = serial_number self._state: str | None = None - self._digits = coordinator.entry.options.get( - CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS - ) @property def device_info(self) -> DeviceInfo: @@ -111,8 +108,11 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt @property def code_format(self) -> str: - """Return the required six digit code.""" - return "^\\d{%s}$" % self._digits + """Return the configured code format.""" + digits = self.coordinator.entry.options.get( + CONF_LOCK_CODE_DIGITS, DEFAULT_LOCK_CODE_DIGITS + ) + return "^\\d{%s}$" % digits @property def is_locked(self) -> bool: From 054a63c3a288bdc1a915897732579c63d1b40d93 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 29 Aug 2023 22:07:27 -0500 Subject: [PATCH 1021/1151] Add option to save Assist pipeline audio (#98928) * Add pipeline option to save wake/stt audio to media * Add debug_recording_dir to assist_pipeline YAML config * Clean up and additional tests * Remove I/O in event loop * Organize saved audio by pipeline name and device id * Record wake/stt debug audio in separate thread * Fix after rebase * Use timestamp instead of pipeline id for directory name * Add WAV write error test * Join thread in executor --- .../components/assist_pipeline/__init__.py | 11 +- .../components/assist_pipeline/const.py | 2 + .../components/assist_pipeline/pipeline.py | 199 ++++++++++++++---- tests/components/assist_pipeline/conftest.py | 3 +- .../snapshots/test_websocket.ambr | 27 +++ tests/components/assist_pipeline/test_init.py | 186 +++++++++++++++- .../assist_pipeline/test_websocket.py | 38 +++- 7 files changed, 417 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/assist_pipeline/__init__.py b/homeassistant/components/assist_pipeline/__init__.py index 4c2fe01036f..ad1fd194271 100644 --- a/homeassistant/components/assist_pipeline/__init__.py +++ b/homeassistant/components/assist_pipeline/__init__.py @@ -3,12 +3,13 @@ from __future__ import annotations from collections.abc import AsyncIterable +import voluptuous as vol + from homeassistant.components import stt from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DATA_CONFIG, DOMAIN from .error import PipelineNotFound from .pipeline import ( Pipeline, @@ -39,11 +40,15 @@ __all__ = ( "WakeWordSettings", ) -CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): {vol.Optional("debug_recording_dir"): str}} +) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Assist pipeline integration.""" + hass.data[DATA_CONFIG] = config.get(DOMAIN, {}) + await async_setup_pipeline_store(hass) async_register_websocket_api(hass) diff --git a/homeassistant/components/assist_pipeline/const.py b/homeassistant/components/assist_pipeline/const.py index 5cbdd5d6350..e21d9003a69 100644 --- a/homeassistant/components/assist_pipeline/const.py +++ b/homeassistant/components/assist_pipeline/const.py @@ -1,2 +1,4 @@ """Constants for the Assist pipeline integration.""" DOMAIN = "assist_pipeline" + +DATA_CONFIG = f"{DOMAIN}.config" diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 3759fc12c75..520daa9f5c2 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -6,7 +6,12 @@ from collections.abc import AsyncGenerator, AsyncIterable, Callable, Iterable from dataclasses import asdict, dataclass, field from enum import StrEnum import logging +from pathlib import Path +from queue import Queue +from threading import Thread +import time from typing import Any, cast +import wave import voluptuous as vol @@ -39,7 +44,7 @@ from homeassistant.util import ( ) from homeassistant.util.limited_size_dict import LimitedSizeDict -from .const import DOMAIN +from .const import DATA_CONFIG, DOMAIN from .error import ( IntentRecognitionError, PipelineError, @@ -378,6 +383,12 @@ class PipelineRun: wake_word_engine: str = field(init=False) wake_word_provider: wake_word.WakeWordDetectionEntity = field(init=False) + debug_recording_thread: Thread | None = None + """Thread that records audio to debug_recording_dir""" + + debug_recording_queue: Queue[str | bytes | None] | None = None + """Queue to communicate with debug recording thread""" + def __post_init__(self) -> None: """Set language for pipeline.""" self.language = self.pipeline.language or self.hass.config.language @@ -405,8 +416,10 @@ class PipelineRun: return pipeline_data.pipeline_runs[self.pipeline.id][self.id].events.append(event) - def start(self) -> None: + def start(self, device_id: str | None) -> None: """Emit run start event.""" + self._start_debug_recording_thread(device_id) + data = { "pipeline": self.pipeline.id, "language": self.language, @@ -416,8 +429,12 @@ class PipelineRun: self.process_event(PipelineEvent(PipelineEventType.RUN_START, data)) - def end(self) -> None: + async def end(self) -> None: """Emit run end event.""" + # Stop the recording thread before emitting run-end. + # This ensures that files are properly closed if the event handler reads them. + await self._stop_debug_recording_thread() + self.process_event( PipelineEvent( PipelineEventType.RUN_END, @@ -475,6 +492,9 @@ class PipelineRun: ) ) + if self.debug_recording_queue is not None: + self.debug_recording_queue.put_nowait(f"00_wake-{self.wake_word_engine}") + wake_word_settings = self.wake_word_settings or WakeWordSettings() wake_word_vad: VoiceActivityTimeout | None = None @@ -496,7 +516,7 @@ class PipelineRun: try: # Detect wake word(s) result = await self.wake_word_provider.async_process_audio_stream( - _wake_word_audio_stream( + self._wake_word_audio_stream( audio_stream=stream, stt_audio_buffer=stt_audio_buffer, wake_word_vad=wake_word_vad, @@ -546,6 +566,39 @@ class PipelineRun: return result + async def _wake_word_audio_stream( + self, + audio_stream: AsyncIterable[bytes], + stt_audio_buffer: RingBuffer | None, + wake_word_vad: VoiceActivityTimeout | None, + sample_rate: int = 16000, + sample_width: int = 2, + ) -> AsyncIterable[tuple[bytes, int]]: + """Yield audio chunks with timestamps (milliseconds since start of stream). + + Adds audio to a ring buffer that will be forwarded to speech-to-text after + detection. Times out if VAD detects enough silence. + """ + ms_per_sample = sample_rate // 1000 + timestamp_ms = 0 + async for chunk in audio_stream: + if self.debug_recording_queue is not None: + self.debug_recording_queue.put_nowait(chunk) + + yield chunk, timestamp_ms + timestamp_ms += (len(chunk) // sample_width) // ms_per_sample + + # Wake-word-detection occurs *after* the wake word was actually + # spoken. Keeping audio right before detection allows the voice + # command to be spoken immediately after the wake word. + if stt_audio_buffer is not None: + stt_audio_buffer.put(chunk) + + 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" + ) + 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 @@ -595,6 +648,10 @@ class PipelineRun: ) ) + if self.debug_recording_queue is not None: + # New recording + self.debug_recording_queue.put_nowait(f"01_stt-{engine}") + try: # Transcribe audio stream result = await self.stt_provider.async_process_audio_stream( @@ -648,6 +705,9 @@ class PipelineRun: sent_vad_start = False timestamp_ms = 0 async for chunk in audio_stream: + if self.debug_recording_queue is not None: + self.debug_recording_queue.put_nowait(chunk) + if stt_vad is not None: if not stt_vad.process(chunk): # Silence detected at the end of voice command @@ -829,6 +889,96 @@ class PipelineRun: return tts_media.url + def _start_debug_recording_thread(self, device_id: str | None) -> None: + """Start thread to record wake/stt audio if debug_recording_dir is set.""" + if self.debug_recording_thread is not None: + # Already started + return + + # Directory to save audio for each pipeline run. + # Configured in YAML for assist_pipeline. + if debug_recording_dir := self.hass.data[DATA_CONFIG].get( + "debug_recording_dir" + ): + if device_id is None: + # // + run_recording_dir = ( + Path(debug_recording_dir) + / self.pipeline.name + / str(time.monotonic_ns()) + ) + else: + # /// + run_recording_dir = ( + Path(debug_recording_dir) + / device_id + / self.pipeline.name + / str(time.monotonic_ns()) + ) + + self.debug_recording_queue = Queue() + self.debug_recording_thread = Thread( + target=_pipeline_debug_recording_thread_proc, + args=(run_recording_dir, self.debug_recording_queue), + daemon=True, + ) + self.debug_recording_thread.start() + + async def _stop_debug_recording_thread(self) -> None: + """Stop recording thread.""" + if (self.debug_recording_thread is None) or ( + self.debug_recording_queue is None + ): + # Not running + return + + # Signal thread to stop gracefully + self.debug_recording_queue.put(None) + + # Wait until the thread has finished to ensure that files are fully written + await self.hass.async_add_executor_job(self.debug_recording_thread.join) + + self.debug_recording_queue = None + self.debug_recording_thread = None + + +def _pipeline_debug_recording_thread_proc( + run_recording_dir: Path, + queue: Queue[str | bytes | None], + message_timeout: float = 5, +) -> None: + wav_writer: wave.Wave_write | None = None + + try: + _LOGGER.debug("Saving wake/stt audio to %s", run_recording_dir) + run_recording_dir.mkdir(parents=True, exist_ok=True) + + while True: + message = queue.get(timeout=message_timeout) + if message is None: + # Stop signal + break + + if isinstance(message, str): + # New WAV file name + if wav_writer is not None: + wav_writer.close() + + wav_path = run_recording_dir / f"{message}.wav" + wav_writer = wave.open(str(wav_path), "wb") + wav_writer.setframerate(16000) + wav_writer.setsampwidth(2) + wav_writer.setnchannels(1) + elif isinstance(message, bytes): + # Chunk of 16-bit mono audio at 16Khz + if wav_writer is not None: + wav_writer.writeframes(message) + except Exception: # pylint: disable=broad-exception-caught + _LOGGER.exception("Unexpected error in debug recording thread") + finally: + if wav_writer is not None: + wav_writer.close() + @dataclass class PipelineInput: @@ -854,7 +1004,7 @@ class PipelineInput: async def execute(self) -> None: """Run pipeline.""" - self.run.start() + self.run.start(device_id=self.device_id) current_stage: PipelineStage | None = self.run.start_stage stt_audio_buffer: list[bytes] = [] @@ -867,7 +1017,7 @@ class PipelineInput: ) if detect_result is None: # No wake word. Abort the rest of the pipeline. - self.run.end() + await self.run.end() return current_stage = PipelineStage.STT @@ -927,9 +1077,10 @@ class PipelineInput: {"code": err.code, "message": err.message}, ) ) - return - - self.run.end() + finally: + # Always end the run since it needs to shut down the debug recording + # thread, etc. + await self.run.end() async def validate(self) -> None: """Validate pipeline input against start stage.""" @@ -1000,36 +1151,6 @@ class PipelineInput: await asyncio.gather(*prepare_tasks) -async def _wake_word_audio_stream( - audio_stream: AsyncIterable[bytes], - stt_audio_buffer: RingBuffer | None, - wake_word_vad: VoiceActivityTimeout | None, - sample_rate: int = 16000, - sample_width: int = 2, -) -> AsyncIterable[tuple[bytes, int]]: - """Yield audio chunks with timestamps (milliseconds since start of stream). - - Adds audio to a ring buffer that will be forwarded to speech-to-text after - detection. Times out if VAD detects enough silence. - """ - ms_per_sample = sample_rate // 1000 - timestamp_ms = 0 - async for chunk in audio_stream: - yield chunk, timestamp_ms - timestamp_ms += (len(chunk) // sample_width) // ms_per_sample - - # Wake-word-detection occurs *after* the wake word was actually - # spoken. Keeping audio right before detection allows the voice - # command to be spoken immediately after the wake word. - if stt_audio_buffer is not None: - stt_audio_buffer.put(chunk) - - 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" - ) - - class PipelinePreferred(CollectionError): """Raised when attempting to delete the preferred pipelen.""" diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index 0cc18d73e6f..d2ec3553cf0 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -191,7 +191,7 @@ class MockWakeWordEntity(wake_word.WakeWordDetectionEntity): ) -> 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": + if chunk.startswith(b"wake word"): return wake_word.DetectionResult( ww_id=self.supported_wake_words[0].ww_id, timestamp=timestamp, @@ -301,7 +301,6 @@ async def init_supporting_components( 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_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index ea642546e6d..57fbe5f4908 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -77,6 +77,9 @@ }), }) # --- +# name: test_audio_pipeline.7 + None +# --- # name: test_audio_pipeline_debug dict({ 'language': 'en', @@ -155,6 +158,9 @@ }), }) # --- +# name: test_audio_pipeline_debug.7 + None +# --- # name: test_audio_pipeline_no_wake_word_engine dict({ 'code': 'wake-engine-missing', @@ -364,6 +370,9 @@ }), }) # --- +# name: test_audio_pipeline_with_wake_word_no_timeout.9 + None +# --- # name: test_audio_pipeline_with_wake_word_timeout dict({ 'language': 'en', @@ -392,6 +401,9 @@ 'message': 'Wake word was not detected', }) # --- +# name: test_audio_pipeline_with_wake_word_timeout.3 + None +# --- # name: test_intent_failed dict({ 'language': 'en', @@ -411,6 +423,9 @@ 'language': 'en', }) # --- +# name: test_intent_failed.2 + None +# --- # name: test_intent_timeout dict({ 'language': 'en', @@ -431,6 +446,9 @@ }) # --- # name: test_intent_timeout.2 + None +# --- +# name: test_intent_timeout.3 dict({ 'code': 'timeout', 'message': 'Timeout running pipeline', @@ -482,6 +500,9 @@ }), }) # --- +# name: test_stt_stream_failed.2 + None +# --- # name: test_text_only_pipeline dict({ 'language': 'en', @@ -523,6 +544,9 @@ }), }) # --- +# name: test_text_only_pipeline.3 + None +# --- # name: test_text_pipeline_timeout dict({ 'code': 'timeout', @@ -547,3 +571,6 @@ 'voice': 'james_earl_jones', }) # --- +# name: test_tts_failed.2 + None +# --- diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index aba9862614b..8687e2ad40c 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -1,13 +1,17 @@ """Test Voice Assistant init.""" from dataclasses import asdict import itertools as it +from pathlib import Path +import tempfile from unittest.mock import ANY, patch +import wave import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components import assist_pipeline, stt from homeassistant.core import Context, HomeAssistant +from homeassistant.setup import async_setup_component from .conftest import MockSttProvider, MockSttProviderEntity, MockWakeWordEntity @@ -305,7 +309,7 @@ async def test_pipeline_from_audio_stream_wake_word( async def audio_data(): yield wake_chunk_1 # 1 second yield wake_chunk_2 # 1 second - yield b"wake word" + yield b"wake word!" yield b"part1" yield b"part2" yield b"end" @@ -353,3 +357,183 @@ async def test_pipeline_from_audio_stream_wake_word( 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"] + + +async def test_pipeline_save_audio( + hass: HomeAssistant, + mock_stt_provider: MockSttProvider, + mock_wake_word_provider_entity: MockWakeWordEntity, + init_supporting_components, + snapshot: SnapshotAssertion, +) -> None: + """Test saving audio during a pipeline run.""" + with tempfile.TemporaryDirectory() as temp_dir_str: + # Enable audio recording to temporary directory + temp_dir = Path(temp_dir_str) + assert await async_setup_component( + hass, + "assist_pipeline", + {"assist_pipeline": {"debug_recording_dir": temp_dir_str}}, + ) + + pipeline = assist_pipeline.async_get_pipeline(hass) + events: list[assist_pipeline.PipelineEvent] = [] + + # Pad out to an even number of bytes since these "samples" will be saved + # as 16-bit values. + async def audio_data(): + yield b"wake word_" + # queued audio + yield b"part1_" + yield b"part2_" + yield b"" + + await assist_pipeline.async_pipeline_from_audio_stream( + hass, + context=Context(), + event_callback=events.append, + stt_metadata=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, + ), + stt_stream=audio_data(), + pipeline_id=pipeline.id, + start_stage=assist_pipeline.PipelineStage.WAKE_WORD, + end_stage=assist_pipeline.PipelineStage.STT, + ) + + pipeline_dirs = list(temp_dir.iterdir()) + + # Only one pipeline run + # // + assert len(pipeline_dirs) == 1 + assert pipeline_dirs[0].is_dir() + assert pipeline_dirs[0].name == pipeline.name + + # Wake and stt files + run_dirs = list(pipeline_dirs[0].iterdir()) + assert run_dirs[0].is_dir() + run_files = list(run_dirs[0].iterdir()) + + assert len(run_files) == 2 + wake_file = run_files[0] if "wake" in run_files[0].name else run_files[1] + stt_file = run_files[0] if "stt" in run_files[0].name else run_files[1] + assert wake_file != stt_file + + # Verify wake file + with wave.open(str(wake_file), "rb") as wake_wav: + wake_data = wake_wav.readframes(wake_wav.getnframes()) + assert wake_data == b"wake word_" + + # Verify stt file + with wave.open(str(stt_file), "rb") as stt_wav: + stt_data = stt_wav.readframes(stt_wav.getnframes()) + assert stt_data == b"queued audiopart1_part2_" + + +async def test_pipeline_saved_audio_with_device_id( + hass: HomeAssistant, + mock_stt_provider: MockSttProvider, + mock_wake_word_provider_entity: MockWakeWordEntity, + init_supporting_components, + snapshot: SnapshotAssertion, +) -> None: + """Test that saved audio directory uses device id.""" + device_id = "test-device-id" + + with tempfile.TemporaryDirectory() as temp_dir_str: + # Enable audio recording to temporary directory + temp_dir = Path(temp_dir_str) + assert await async_setup_component( + hass, + "assist_pipeline", + {"assist_pipeline": {"debug_recording_dir": temp_dir_str}}, + ) + + def event_callback(event: assist_pipeline.PipelineEvent): + if event.type == "run-end": + # Verify that saved audio directory is named after device id + device_dirs = list(temp_dir.iterdir()) + assert device_dirs[0].name == device_id + + async def audio_data(): + yield b"not used" + + # Force a timeout during wake word detection + with patch.object( + mock_wake_word_provider_entity, + "async_process_audio_stream", + side_effect=assist_pipeline.error.WakeWordTimeoutError( + code="timeout", message="timeout" + ), + ): + await assist_pipeline.async_pipeline_from_audio_stream( + hass, + context=Context(), + event_callback=event_callback, + stt_metadata=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, + ), + stt_stream=audio_data(), + start_stage=assist_pipeline.PipelineStage.WAKE_WORD, + end_stage=assist_pipeline.PipelineStage.STT, + device_id=device_id, + ) + + +async def test_pipeline_saved_audio_write_error( + hass: HomeAssistant, + mock_stt_provider: MockSttProvider, + mock_wake_word_provider_entity: MockWakeWordEntity, + init_supporting_components, + snapshot: SnapshotAssertion, +) -> None: + """Test that saved audio thread closes WAV file even if there's a write error.""" + with tempfile.TemporaryDirectory() as temp_dir_str: + # Enable audio recording to temporary directory + temp_dir = Path(temp_dir_str) + assert await async_setup_component( + hass, + "assist_pipeline", + {"assist_pipeline": {"debug_recording_dir": temp_dir_str}}, + ) + + def event_callback(event: assist_pipeline.PipelineEvent): + if event.type == "run-end": + # Verify WAV file exists, but contains no data + pipeline_dirs = list(temp_dir.iterdir()) + run_dirs = list(pipeline_dirs[0].iterdir()) + wav_path = next(run_dirs[0].iterdir()) + with wave.open(str(wav_path), "rb") as wav_file: + assert wav_file.getnframes() == 0 + + async def audio_data(): + yield b"not used" + + # Force a timeout during wake word detection + with patch("wave.Wave_write.writeframes", raises=RuntimeError()): + await assist_pipeline.async_pipeline_from_audio_stream( + hass, + context=Context(), + event_callback=event_callback, + stt_metadata=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, + ), + stt_stream=audio_data(), + start_stage=assist_pipeline.PipelineStage.WAKE_WORD, + end_stage=assist_pipeline.PipelineStage.STT, + ) diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 1f2b657dcfa..ca631be4549 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -58,7 +58,7 @@ async def test_text_only_pipeline( # run end msg = await client.receive_json() assert msg["event"]["type"] == "run-end" - assert msg["event"]["data"] is None + assert msg["event"]["data"] == snapshot events.append(msg["event"]) pipeline_data: PipelineData = hass.data[DOMAIN] @@ -148,7 +148,7 @@ async def test_audio_pipeline( # run end msg = await client.receive_json() assert msg["event"]["type"] == "run-end" - assert msg["event"]["data"] is None + assert msg["event"]["data"] == snapshot events.append(msg["event"]) pipeline_data: PipelineData = hass.data[DOMAIN] @@ -215,6 +215,12 @@ async def test_audio_pipeline_with_wake_word_timeout( 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"] == snapshot + events.append(msg["event"]) + async def test_audio_pipeline_with_wake_word_no_timeout( hass: HomeAssistant, @@ -302,7 +308,7 @@ async def test_audio_pipeline_with_wake_word_no_timeout( # run end msg = await client.receive_json() assert msg["event"]["type"] == "run-end" - assert msg["event"]["data"] is None + assert msg["event"]["data"] == snapshot events.append(msg["event"]) pipeline_data: PipelineData = hass.data[DOMAIN] @@ -429,6 +435,12 @@ async def test_intent_timeout( 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"] == snapshot + events.append(msg["event"]) + # timeout error msg = await client.receive_json() assert msg["event"]["type"] == "error" @@ -550,6 +562,12 @@ async def test_intent_failed( assert msg["event"]["data"]["code"] == "intent-failed" events.append(msg["event"]) + # run end + msg = await client.receive_json() + assert msg["event"]["type"] == "run-end" + assert msg["event"]["data"] == snapshot + 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] @@ -730,6 +748,12 @@ async def test_stt_stream_failed( assert msg["event"]["data"]["code"] == "stt-stream-failed" events.append(msg["event"]) + # run end + msg = await client.receive_json() + assert msg["event"]["type"] == "run-end" + assert msg["event"]["data"] == snapshot + 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] @@ -792,6 +816,12 @@ async def test_tts_failed( assert msg["event"]["data"]["code"] == "tts-failed" events.append(msg["event"]) + # run end + msg = await client.receive_json() + assert msg["event"]["type"] == "run-end" + assert msg["event"]["data"] == snapshot + 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] @@ -1460,7 +1490,7 @@ async def test_audio_pipeline_debug( # run end msg = await client.receive_json() assert msg["event"]["type"] == "run-end" - assert msg["event"]["data"] is None + assert msg["event"]["data"] == snapshot events.append(msg["event"]) # Get the id of the pipeline From 1e37e1e355250b14c78c0f96c994a9f064eb6502 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Wed, 30 Aug 2023 05:23:20 +0200 Subject: [PATCH 1022/1151] Bump python-bsblan to 0.5.16 (#99238) --- homeassistant/components/bsblan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../bsblan/snapshots/test_diagnostics.ambr | 18 +++++++++--------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index 5abb888513d..59d52c3ae00 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["bsblan"], - "requirements": ["python-bsblan==0.5.11"] + "requirements": ["python-bsblan==0.5.16"] } diff --git a/requirements_all.txt b/requirements_all.txt index f1bcd71b1ba..6224caab074 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2074,7 +2074,7 @@ python-awair==0.2.4 python-blockchain-api==0.0.2 # homeassistant.components.bsblan -python-bsblan==0.5.11 +python-bsblan==0.5.16 # homeassistant.components.clementine python-clementine-remote==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 121f2b069da..ce11b3fe61e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1542,7 +1542,7 @@ pytautulli==23.1.1 python-awair==0.2.4 # homeassistant.components.bsblan -python-bsblan==0.5.11 +python-bsblan==0.5.16 # homeassistant.components.ecobee python-ecobee-api==0.2.14 diff --git a/tests/components/bsblan/snapshots/test_diagnostics.ambr b/tests/components/bsblan/snapshots/test_diagnostics.ambr index 2fff33de046..b172d26c249 100644 --- a/tests/components/bsblan/snapshots/test_diagnostics.ambr +++ b/tests/components/bsblan/snapshots/test_diagnostics.ambr @@ -9,21 +9,21 @@ }), 'info': dict({ 'controller_family': dict({ - 'dataType': 0, + 'data_type': 0, 'desc': '', 'name': 'Device family', 'unit': '', 'value': '211', }), 'controller_variant': dict({ - 'dataType': 0, + 'data_type': 0, 'desc': '', 'name': 'Device variant', 'unit': '', 'value': '127', }), 'device_identification': dict({ - 'dataType': 7, + 'data_type': 7, 'desc': '', 'name': 'Gerte-Identifikation', 'unit': '', @@ -32,42 +32,42 @@ }), 'state': dict({ 'current_temperature': dict({ - 'dataType': 0, + 'data_type': 0, 'desc': '', 'name': 'Room temp 1 actual value', 'unit': '°C', 'value': '18.6', }), 'hvac_action': dict({ - 'dataType': 1, + 'data_type': 1, 'desc': 'Raumtemp’begrenzung', 'name': 'Status heating circuit 1', 'unit': '', 'value': '122', }), 'hvac_mode': dict({ - 'dataType': 1, + 'data_type': 1, 'desc': 'Komfort', 'name': 'Operating mode', 'unit': '', 'value': 'heat', }), 'hvac_mode2': dict({ - 'dataType': 1, + 'data_type': 1, 'desc': 'Reduziert', 'name': 'Operating mode', 'unit': '', 'value': '2', }), 'room1_thermostat_mode': dict({ - 'dataType': 1, + 'data_type': 1, 'desc': 'Kein Bedarf', 'name': 'Raumthermostat 1', 'unit': '', 'value': '0', }), 'target_temperature': dict({ - 'dataType': 0, + 'data_type': 0, 'desc': '', 'name': 'Room temperature Comfort setpoint', 'unit': '°C', From cc8f5ca827027781545d85192deaa06afc678b44 Mon Sep 17 00:00:00 2001 From: rct Date: Tue, 29 Aug 2023 23:35:19 -0400 Subject: [PATCH 1023/1151] Opower add new virtual integrations ConEd and ORU (#99230) --- homeassistant/components/coned/__init__.py | 1 + homeassistant/components/coned/manifest.json | 6 ++++++ homeassistant/components/oru_opower/__init__.py | 1 + homeassistant/components/oru_opower/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 10 ++++++++++ 5 files changed, 24 insertions(+) create mode 100644 homeassistant/components/coned/__init__.py create mode 100644 homeassistant/components/coned/manifest.json create mode 100644 homeassistant/components/oru_opower/__init__.py create mode 100644 homeassistant/components/oru_opower/manifest.json diff --git a/homeassistant/components/coned/__init__.py b/homeassistant/components/coned/__init__.py new file mode 100644 index 00000000000..d5130f53d05 --- /dev/null +++ b/homeassistant/components/coned/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Consolidated Edison (ConEd).""" diff --git a/homeassistant/components/coned/manifest.json b/homeassistant/components/coned/manifest.json new file mode 100644 index 00000000000..9e1f0ef6a4f --- /dev/null +++ b/homeassistant/components/coned/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "coned", + "name": "Consolidated Edison (ConEd)", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/components/oru_opower/__init__.py b/homeassistant/components/oru_opower/__init__.py new file mode 100644 index 00000000000..c213a0f8883 --- /dev/null +++ b/homeassistant/components/oru_opower/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Orange and Rockland Utilities (ORU) Opower.""" diff --git a/homeassistant/components/oru_opower/manifest.json b/homeassistant/components/oru_opower/manifest.json new file mode 100644 index 00000000000..bed159912be --- /dev/null +++ b/homeassistant/components/oru_opower/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "oru_opower", + "name": "Orange and Rockland Utilities (ORU) Opower", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 81afb1cecd8..325b9333c26 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -913,6 +913,11 @@ "config_flow": false, "iot_class": "local_polling" }, + "coned": { + "name": "Consolidated Edison (ConEd)", + "integration_type": "virtual", + "supported_by": "opower" + }, "control4": { "name": "Control4", "integration_type": "hub", @@ -4075,6 +4080,11 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "oru_opower": { + "name": "Orange and Rockland Utilities (ORU) Opower", + "integration_type": "virtual", + "supported_by": "opower" + }, "orvibo": { "name": "Orvibo", "integration_type": "hub", From 6e8b3837b0e92269ec004a774706971b05602bcd Mon Sep 17 00:00:00 2001 From: Sebastian Mayr Date: Tue, 29 Aug 2023 23:37:37 -0400 Subject: [PATCH 1024/1151] Add support for MFA auth in opower (#97878) * Add support for MFA auth in opower * Make MFA an extra step --------- Co-authored-by: Paulus Schoutsen --- .../components/opower/config_flow.py | 77 +++++++-- homeassistant/components/opower/const.py | 1 + .../components/opower/coordinator.py | 3 +- homeassistant/components/opower/strings.json | 6 +- tests/components/opower/test_config_flow.py | 155 ++++++++++++++++++ 5 files changed, 226 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py index fdf007c3b68..9f2ec56423d 100644 --- a/homeassistant/components/opower/config_flow.py +++ b/homeassistant/components/opower/config_flow.py @@ -10,17 +10,19 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -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_create_clientsession -from .const import CONF_UTILITY, DOMAIN +from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names()), + vol.Required(CONF_UTILITY): vol.In( + get_supported_utility_names(supports_mfa=True) + ), vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } @@ -36,6 +38,7 @@ async def _validate_login( login_data[CONF_UTILITY], login_data[CONF_USERNAME], login_data[CONF_PASSWORD], + login_data.get(CONF_TOTP_SECRET, None), ) errors: dict[str, str] = {} try: @@ -47,6 +50,12 @@ async def _validate_login( return errors +@callback +def _supports_mfa(utility: str) -> bool: + """Return whether the utility supports MFA.""" + return utility not in get_supported_utility_names(supports_mfa=False) + + class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Opower.""" @@ -55,6 +64,7 @@ class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize a new OpowerConfigFlow.""" self.reauth_entry: config_entries.ConfigEntry | None = None + self.utility_info: dict[str, Any] | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -68,16 +78,56 @@ class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_USERNAME: user_input[CONF_USERNAME], } ) + if _supports_mfa(user_input[CONF_UTILITY]): + self.utility_info = user_input + return await self.async_step_mfa() + errors = await _validate_login(self.hass, user_input) if not errors: - return self.async_create_entry( - title=f"{user_input[CONF_UTILITY]} ({user_input[CONF_USERNAME]})", - data=user_input, - ) + return self._async_create_opower_entry(user_input) + return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) + async def async_step_mfa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle MFA step.""" + assert self.utility_info is not None + errors: dict[str, str] = {} + if user_input is not None: + data = {**self.utility_info, **user_input} + errors = await _validate_login(self.hass, data) + if not errors: + return self._async_create_opower_entry(data) + + if errors: + schema = { + vol.Required( + CONF_USERNAME, default=self.utility_info[CONF_USERNAME] + ): str, + vol.Required(CONF_PASSWORD): str, + } + else: + schema = {} + + schema[vol.Required(CONF_TOTP_SECRET)] = str + + return self.async_show_form( + step_id="mfa", + data_schema=vol.Schema(schema), + errors=errors, + ) + + @callback + def _async_create_opower_entry(self, data: dict[str, Any]) -> FlowResult: + """Create the config entry.""" + return self.async_create_entry( + title=f"{data[CONF_UTILITY]} ({data[CONF_USERNAME]})", + data=data, + ) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self.reauth_entry = self.hass.config_entries.async_get_entry( @@ -100,13 +150,14 @@ class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") + schema = { + vol.Required(CONF_USERNAME): self.reauth_entry.data[CONF_USERNAME], + vol.Required(CONF_PASSWORD): str, + } + if _supports_mfa(self.reauth_entry.data[CONF_UTILITY]): + schema[vol.Optional(CONF_TOTP_SECRET)] = str return self.async_show_form( step_id="reauth_confirm", - data_schema=vol.Schema( - { - vol.Required(CONF_USERNAME): self.reauth_entry.data[CONF_USERNAME], - vol.Required(CONF_PASSWORD): str, - } - ), + data_schema=vol.Schema(schema), errors=errors, ) diff --git a/homeassistant/components/opower/const.py b/homeassistant/components/opower/const.py index b996a214a05..c07d41bbdcf 100644 --- a/homeassistant/components/opower/const.py +++ b/homeassistant/components/opower/const.py @@ -3,3 +3,4 @@ DOMAIN = "opower" CONF_UTILITY = "utility" +CONF_TOTP_SECRET = "totp_secret" diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index 4e2b68df579..1410b62b7b6 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -28,7 +28,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_UTILITY, DOMAIN +from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -55,6 +55,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): entry_data[CONF_UTILITY], entry_data[CONF_USERNAME], entry_data[CONF_PASSWORD], + entry_data.get(CONF_TOTP_SECRET, None), ) async def _async_update_data( diff --git a/homeassistant/components/opower/strings.json b/homeassistant/components/opower/strings.json index 037983eb6ff..ac931bf9308 100644 --- a/homeassistant/components/opower/strings.json +++ b/homeassistant/components/opower/strings.json @@ -5,14 +5,16 @@ "data": { "utility": "Utility name", "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "totp_secret": "TOTP Secret (only for some utilities, see documentation)" } }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "data": { "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "totp_secret": "TOTP Secret (only for some utilities, see documentation)" } } }, diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index 6a45a0dcc56..0391e42ca16 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -68,6 +68,110 @@ async def test_form( assert mock_login.call_count == 1 +async def test_form_with_mfa( + recorder_mock: Recorder, hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "utility": "Consolidated Edison (ConEd)", + "username": "test-username", + "password": "test-password", + }, + ) + assert result2["type"] == FlowResultType.FORM + assert not result2["errors"] + + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + ) as mock_login: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "totp_secret": "test-totp", + }, + ) + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Consolidated Edison (ConEd) (test-username)" + assert result3["data"] == { + "utility": "Consolidated Edison (ConEd)", + "username": "test-username", + "password": "test-password", + "totp_secret": "test-totp", + } + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_login.call_count == 1 + + +async def test_form_with_mfa_bad_secret( + recorder_mock: Recorder, hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test MFA asks for password again when validation fails.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "utility": "Consolidated Edison (ConEd)", + "username": "test-username", + "password": "test-password", + }, + ) + assert result2["type"] == FlowResultType.FORM + assert not result2["errors"] + + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + side_effect=InvalidAuth, + ) as mock_login: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "totp_secret": "test-totp", + }, + ) + + assert result3["type"] == FlowResultType.FORM + assert result3["errors"] == { + "base": "invalid_auth", + } + + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + ) as mock_login: + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "updated-password", + "totp_secret": "updated-totp", + }, + ) + + assert result4["type"] == FlowResultType.CREATE_ENTRY + assert result4["title"] == "Consolidated Edison (ConEd) (test-username)" + assert result4["data"] == { + "utility": "Consolidated Edison (ConEd)", + "username": "test-username", + "password": "updated-password", + "totp_secret": "updated-totp", + } + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_login.call_count == 1 + + @pytest.mark.parametrize( ("api_exception", "expected_error"), [ @@ -204,3 +308,54 @@ async def test_form_valid_reauth( assert len(mock_unload_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 assert mock_login.call_count == 1 + + +async def test_form_valid_reauth_with_mfa( + recorder_mock: Recorder, + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_unload_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that we can handle a valid reauth.""" + hass.config_entries.async_update_entry( + mock_config_entry, + data={ + **mock_config_entry.data, + # Requires MFA + "utility": "Consolidated Edison (ConEd)", + }, + ) + mock_config_entry.state = ConfigEntryState.LOADED + mock_config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + + with patch( + "homeassistant.components.opower.config_flow.Opower.async_login", + ) as mock_login: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password2", + "totp_secret": "test-totp", + }, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + await hass.async_block_till_done() + assert hass.config_entries.async_entries(DOMAIN)[0].data == { + "utility": "Consolidated Edison (ConEd)", + "username": "test-username", + "password": "test-password2", + "totp_secret": "test-totp", + } + assert len(mock_unload_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_login.call_count == 1 From d7d989b9fbc5ad01882b1505b6493168f5bd6052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20J=C3=A1l?= Date: Wed, 30 Aug 2023 06:03:08 +0200 Subject: [PATCH 1025/1151] Switchbot nightlatch feature (#98620) Co-authored-by: J. Nick Koston --- homeassistant/components/switchbot/lock.py | 12 +++++++++++- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/lock.py b/homeassistant/components/switchbot/lock.py index 7710cde12a9..60f4fe66c26 100644 --- a/homeassistant/components/switchbot/lock.py +++ b/homeassistant/components/switchbot/lock.py @@ -4,7 +4,7 @@ from typing import Any import switchbot from switchbot.const import LockStatus -from homeassistant.components.lock import LockEntity +from homeassistant.components.lock import LockEntity, LockEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -34,6 +34,8 @@ class SwitchBotLock(SwitchbotEntity, LockEntity): """Initialize the entity.""" super().__init__(coordinator) self._async_update_attrs() + if self._device.is_night_latch_enabled(): + self._attr_supported_features = LockEntityFeature.OPEN def _async_update_attrs(self) -> None: """Update the entity attributes.""" @@ -53,5 +55,13 @@ class SwitchBotLock(SwitchbotEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" + if self._device.is_night_latch_enabled(): + self._last_run_success = await self._device.unlock_without_unlatch() + else: + self._last_run_success = await self._device.unlock() + self.async_write_ha_state() + + async def async_open(self, **kwargs: Any) -> None: + """Open the lock.""" self._last_run_success = await self._device.unlock() self.async_write_ha_state() diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 237ba98c19d..2259a450559 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.38.0"] + "requirements": ["PySwitchbot==0.39.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6224caab074..14ecb74f0aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -96,7 +96,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.38.0 +PySwitchbot==0.39.1 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce11b3fe61e..c377616a49e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -86,7 +86,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.38.0 +PySwitchbot==0.39.1 # homeassistant.components.syncthru PySyncThru==0.7.10 From 9e4bcd298e15188194aa07e5b712d85abaeafe79 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Aug 2023 00:03:37 -0400 Subject: [PATCH 1026/1151] Move more Oral-B entities to be diagnostic (#99297) --- homeassistant/components/oralb/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/oralb/sensor.py b/homeassistant/components/oralb/sensor.py index 16118361ab8..c7cdaddf382 100644 --- a/homeassistant/components/oralb/sensor.py +++ b/homeassistant/components/oralb/sensor.py @@ -38,12 +38,15 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { ), OralBSensor.SECTOR: SensorEntityDescription( key=OralBSensor.SECTOR, + entity_category=EntityCategory.DIAGNOSTIC, ), OralBSensor.NUMBER_OF_SECTORS: SensorEntityDescription( key=OralBSensor.NUMBER_OF_SECTORS, + entity_category=EntityCategory.DIAGNOSTIC, ), OralBSensor.SECTOR_TIMER: SensorEntityDescription( key=OralBSensor.SECTOR_TIMER, + entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), OralBSensor.TOOTHBRUSH_STATE: SensorEntityDescription( @@ -52,6 +55,7 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { OralBSensor.PRESSURE: SensorEntityDescription(key=OralBSensor.PRESSURE), OralBSensor.MODE: SensorEntityDescription( key=OralBSensor.MODE, + entity_category=EntityCategory.DIAGNOSTIC, ), OralBSensor.SIGNAL_STRENGTH: SensorEntityDescription( key=OralBSensor.SIGNAL_STRENGTH, @@ -66,6 +70,7 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), } From fb4e93071ec891b04d01ea25cda08ee730be6327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 30 Aug 2023 06:03:56 +0200 Subject: [PATCH 1027/1151] Update Mill lib, improve error handling (#99296) --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 66a358648fd..b2dbf993dae 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.11.0", "mill-local==0.2.0"] + "requirements": ["millheater==0.11.1", "mill-local==0.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 14ecb74f0aa..9705e49d6c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1213,7 +1213,7 @@ micloud==0.5 mill-local==0.2.0 # homeassistant.components.mill -millheater==0.11.0 +millheater==0.11.1 # homeassistant.components.minio minio==7.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c377616a49e..a9bcaf2761a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -927,7 +927,7 @@ micloud==0.5 mill-local==0.2.0 # homeassistant.components.mill -millheater==0.11.0 +millheater==0.11.1 # homeassistant.components.minio minio==7.1.12 From 7e7cb15d5b7b63786d73a42bbfb8dc4b513296be Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Aug 2023 08:26:26 +0200 Subject: [PATCH 1028/1151] Revert "Allows defining list of attributes excluded from history in manifest.json" (#99300) Revert "Allows defining list of attributes excluded from history in manifest.json (#99283)" This reverts commit 0366e14630676b2d96f529c9113f5fceb86c705b. --- .../components/automation/__init__.py | 7 +++ .../components/automation/manifest.json | 9 +--- .../components/automation/recorder.py | 12 +++++ homeassistant/components/recorder/__init__.py | 3 +- homeassistant/generated/recorder.py | 14 ------ script/hassfest/__main__.py | 2 - script/hassfest/manifest.py | 1 - script/hassfest/recorder.py | 45 ------------------- 8 files changed, 21 insertions(+), 72 deletions(-) create mode 100644 homeassistant/components/automation/recorder.py delete mode 100644 homeassistant/generated/recorder.py delete mode 100644 script/hassfest/recorder.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 885427a9f80..f4db7831235 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -57,6 +57,9 @@ from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.integration_platform import ( + async_process_integration_platform_for_component, +) from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import ( @@ -246,6 +249,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: LOGGER, DOMAIN, hass ) + # Process integration platforms right away since + # we will create entities before firing EVENT_COMPONENT_LOADED + await async_process_integration_platform_for_component(hass, DOMAIN) + # Register automation as valid domain for Blueprint async_get_blueprints(hass) diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index de72d45d756..a22abbee3b2 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -6,12 +6,5 @@ "dependencies": ["blueprint", "trace"], "documentation": "https://www.home-assistant.io/integrations/automation", "integration_type": "system", - "quality_scale": "internal", - "recorder_excluded_attributes": [ - "current", - "id", - "last_triggered", - "max", - "mode" - ] + "quality_scale": "internal" } diff --git a/homeassistant/components/automation/recorder.py b/homeassistant/components/automation/recorder.py new file mode 100644 index 00000000000..3083d271d1f --- /dev/null +++ b/homeassistant/components/automation/recorder.py @@ -0,0 +1,12 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_CUR, ATTR_LAST_TRIGGERED, ATTR_MAX, ATTR_MODE, CONF_ID + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude extra attributes from being recorded in the database.""" + return {ATTR_LAST_TRIGGERED, ATTR_MODE, ATTR_CUR, ATTR_MAX, CONF_ID} diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 746b845c420..72d825d9e78 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -8,7 +8,6 @@ import voluptuous as vol from homeassistant.const import CONF_EXCLUDE, EVENT_STATE_CHANGED from homeassistant.core import HomeAssistant -from homeassistant.generated.recorder import EXCLUDED_ATTRIBUTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -133,7 +132,7 @@ def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" - exclude_attributes_by_domain: dict[str, set[str]] = dict(EXCLUDED_ATTRIBUTES) + exclude_attributes_by_domain: dict[str, set[str]] = {} hass.data[EXCLUDE_ATTRIBUTES] = exclude_attributes_by_domain conf = config[DOMAIN] entity_filter = convert_include_exclude_filter(conf).get_filter() diff --git a/homeassistant/generated/recorder.py b/homeassistant/generated/recorder.py deleted file mode 100644 index d9213c60125..00000000000 --- a/homeassistant/generated/recorder.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Automatically generated file. - -To update, run python3 -m script.hassfest -""" - -EXCLUDED_ATTRIBUTES = { - "automation": { - "current", - "id", - "last_triggered", - "max", - "mode", - }, -} diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index f263c594bc5..1c626ac3c5b 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -20,7 +20,6 @@ from . import ( metadata, mqtt, mypy_config, - recorder, requirements, services, ssdp, @@ -40,7 +39,6 @@ INTEGRATION_PLUGINS = [ json, manifest, mqtt, - recorder, requirements, services, ssdp, diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 5dbb7896dee..65e37aa515d 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -264,7 +264,6 @@ INTEGRATION_MANIFEST_SCHEMA = vol.Schema( vol.Optional("loggers"): [str], vol.Optional("disabled"): str, vol.Optional("iot_class"): vol.In(SUPPORTED_IOT_CLASSES), - vol.Optional("recorder_excluded_attributes"): [str], } ) diff --git a/script/hassfest/recorder.py b/script/hassfest/recorder.py deleted file mode 100644 index 752f9523d3a..00000000000 --- a/script/hassfest/recorder.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Generate recorder file.""" -from __future__ import annotations - -from .model import Config, Integration -from .serializer import format_python_namespace - - -def generate_and_validate(integrations: dict[str, Integration]) -> str: - """Validate and generate recorder data.""" - - data: dict[str, set[str]] = {} - - for domain in sorted(integrations): - exclude_list = integrations[domain].manifest.get("recorder_excluded_attributes") - - if not exclude_list: - continue - - data[domain] = set(exclude_list) - - return format_python_namespace({"EXCLUDED_ATTRIBUTES": data}) - - -def validate(integrations: dict[str, Integration], config: Config) -> None: - """Validate recorder file.""" - recorder_path = config.root / "homeassistant/generated/recorder.py" - config.cache["recorder"] = content = generate_and_validate(integrations) - - if config.specific_integrations: - return - - with open(str(recorder_path)) as fp: - if fp.read() != content: - config.add_error( - "recorder", - "File recorder.py is not up to date. Run python3 -m script.hassfest", - fixable=True, - ) - - -def generate(integrations: dict[str, Integration], config: Config) -> None: - """Generate recorder file.""" - recorder_path = config.root / "homeassistant/generated/recorder.py" - with open(str(recorder_path), "w") as fp: - fp.write(f"{config.cache['recorder']}") From 56e5c34283c4e2e7519090929b58b91698151975 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 09:46:28 +0200 Subject: [PATCH 1029/1151] Add entity translations to Garages Amsterdam (#98584) --- .../garages_amsterdam/binary_sensor.py | 9 +-------- .../components/garages_amsterdam/entity.py | 1 + .../components/garages_amsterdam/sensor.py | 2 +- .../components/garages_amsterdam/strings.json | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/binary_sensor.py b/homeassistant/components/garages_amsterdam/binary_sensor.py index 41237fc7423..ad0630249aa 100644 --- a/homeassistant/components/garages_amsterdam/binary_sensor.py +++ b/homeassistant/components/garages_amsterdam/binary_sensor.py @@ -8,7 +8,6 @@ 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 DataUpdateCoordinator from . import get_coordinator from .entity import GaragesAmsterdamEntity @@ -38,13 +37,7 @@ class GaragesAmsterdamBinarySensor(GaragesAmsterdamEntity, BinarySensorEntity): """Binary Sensor representing garages amsterdam data.""" _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, garage_name, info_type) - self._attr_name = garage_name + _attr_name = None @property def is_on(self) -> bool: diff --git a/homeassistant/components/garages_amsterdam/entity.py b/homeassistant/components/garages_amsterdam/entity.py index df06f47dff5..45c85a101a9 100644 --- a/homeassistant/components/garages_amsterdam/entity.py +++ b/homeassistant/components/garages_amsterdam/entity.py @@ -14,6 +14,7 @@ class GaragesAmsterdamEntity(CoordinatorEntity): """Base Entity for garages amsterdam data.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, coordinator: DataUpdateCoordinator, garage_name: str, info_type: str diff --git a/homeassistant/components/garages_amsterdam/sensor.py b/homeassistant/components/garages_amsterdam/sensor.py index b4acb36691e..a79ddc27379 100644 --- a/homeassistant/components/garages_amsterdam/sensor.py +++ b/homeassistant/components/garages_amsterdam/sensor.py @@ -49,7 +49,7 @@ class GaragesAmsterdamSensor(GaragesAmsterdamEntity, SensorEntity): ) -> None: """Initialize garages amsterdam sensor.""" super().__init__(coordinator, garage_name, info_type) - self._attr_name = f"{garage_name} - {info_type}".replace("_", " ") + self._attr_translation_key = info_type self._attr_icon = SENSORS[info_type] @property diff --git a/homeassistant/components/garages_amsterdam/strings.json b/homeassistant/components/garages_amsterdam/strings.json index c8c3968aa59..89a85f97448 100644 --- a/homeassistant/components/garages_amsterdam/strings.json +++ b/homeassistant/components/garages_amsterdam/strings.json @@ -12,5 +12,21 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "entity": { + "sensor": { + "free_space_short": { + "name": "Short parking free space" + }, + "free_space_long": { + "name": "Long parking free space" + }, + "short_capacity": { + "name": "Short parking capacity" + }, + "long_capacity": { + "name": "Long parking capacity" + } + } } } From e911b73b61db8834e7063c687301cb2b45cc9e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Wed, 30 Aug 2023 10:20:45 +0200 Subject: [PATCH 1030/1151] Add extra sensors to Blebox (#90516) --- homeassistant/components/blebox/sensor.py | 27 +++++++++++++++++++++++ tests/components/blebox/conftest.py | 8 +++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 82c9bb876d7..dbdf034faee 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -7,10 +7,14 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + PERCENTAGE, + UnitOfEnergy, + UnitOfSpeed, UnitOfTemperature, ) from homeassistant.core import HomeAssistant @@ -40,6 +44,22 @@ SENSOR_TYPES = ( device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), + SensorEntityDescription( + key="powerMeasurement", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key="wind_speed", + device_class=SensorDeviceClass.WIND_SPEED, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, + ), ) @@ -75,3 +95,10 @@ class BleBoxSensorEntity(BleBoxEntity[blebox_uniapi.sensor.BaseSensor], SensorEn def native_value(self): """Return the state.""" return self._feature.native_value + + @property + def last_reset(self): + """Return the time when the sensor was last reset, if implemented.""" + native_implementation = getattr(self._feature, "last_reset", None) + + return native_implementation or super().last_reset diff --git a/tests/components/blebox/conftest.py b/tests/components/blebox/conftest.py index 4bda47bb414..82698633c30 100644 --- a/tests/components/blebox/conftest.py +++ b/tests/components/blebox/conftest.py @@ -37,14 +37,14 @@ def setup_product_mock(category, feature_mocks, path=None): return product_mock -def mock_only_feature(spec, **kwargs): +def mock_only_feature(spec, set_spec: bool = True, **kwargs): """Mock just the feature, without the product setup.""" - return mock.create_autospec(spec, True, True, **kwargs) + return mock.create_autospec(spec, set_spec, True, **kwargs) -def mock_feature(category, spec, **kwargs): +def mock_feature(category, spec, set_spec: bool = True, **kwargs): """Mock a feature along with whole product setup.""" - feature_mock = mock_only_feature(spec, **kwargs) + feature_mock = mock_only_feature(spec, set_spec, **kwargs) feature_mock.async_update = AsyncMock() product = setup_product_mock(category, [feature_mock]) From bd04cafb9149127138f0aa4ca6cf7d0366c82c58 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 10:25:53 +0200 Subject: [PATCH 1031/1151] Use shorthand attributes for Daikin (#99225) Co-authored-by: Robert Resch --- homeassistant/components/daikin/climate.py | 27 ++++++------------ homeassistant/components/daikin/sensor.py | 13 ++------- homeassistant/components/daikin/switch.py | 33 ++++++---------------- 3 files changed, 18 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 5ede11c60b6..c848e0b703e 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -124,14 +124,16 @@ class DaikinClimate(ClimateEntity): _attr_name = None _attr_has_entity_name = True _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_hvac_modes = list(HA_STATE_TO_DAIKIN) + _attr_target_temperature_step = 1 def __init__(self, api: DaikinApi) -> None: """Initialize the climate device.""" self._api = api - self._attr_hvac_modes = list(HA_STATE_TO_DAIKIN) - self._attr_fan_modes = self._api.device.fan_rate - self._attr_swing_modes = self._api.device.swing_modes + self._attr_fan_modes = api.device.fan_rate + self._attr_swing_modes = api.device.swing_modes + self._attr_device_info = api.device_info self._list = { ATTR_HVAC_MODE: self._attr_hvac_modes, ATTR_FAN_MODE: self._attr_fan_modes, @@ -140,16 +142,13 @@ class DaikinClimate(ClimateEntity): self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - if ( - self._api.device.support_away_mode - or self._api.device.support_advanced_modes - ): + if api.device.support_away_mode or api.device.support_advanced_modes: self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE - if self._api.device.support_fan_rate: + if api.device.support_fan_rate: self._attr_supported_features |= ClimateEntityFeature.FAN_MODE - if self._api.device.support_swing_mode: + if api.device.support_swing_mode: self._attr_supported_features |= ClimateEntityFeature.SWING_MODE async def _set(self, settings): @@ -195,11 +194,6 @@ class DaikinClimate(ClimateEntity): """Return the temperature we try to reach.""" return self._api.device.target_temperature - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return 1 - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self._set(kwargs) @@ -310,8 +304,3 @@ class DaikinClimate(ClimateEntity): await self._api.device.set( {HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVACMode.OFF]} ) - - @property - def device_info(self): - """Return a device description for device registry.""" - return self._api.device_info diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index ef231c45862..1646e292ee9 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -21,7 +21,6 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -192,13 +191,10 @@ class DaikinSensor(SensorEntity): ) -> None: """Initialize the sensor.""" self.entity_description = description + self._attr_device_info = api.device_info + self._attr_unique_id = f"{api.device.mac}-{description.key}" self._api = api - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self._api.device.mac}-{self.entity_description.key}" - @property def native_value(self) -> float | None: """Return the state of the sensor.""" @@ -207,8 +203,3 @@ class DaikinSensor(SensorEntity): async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() - - @property - def device_info(self) -> DeviceInfo: - """Return a device description for device registry.""" - return self._api.device_info diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 4438b83132c..8dd75916685 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -6,7 +6,6 @@ from typing import Any from homeassistant.components.switch import SwitchEntity 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 homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -59,15 +58,12 @@ class DaikinZoneSwitch(SwitchEntity): _attr_icon = ZONE_ICON _attr_has_entity_name = True - def __init__(self, daikin_api: DaikinApi, zone_id) -> None: + def __init__(self, api: DaikinApi, zone_id) -> None: """Initialize the zone.""" - self._api = daikin_api + self._api = api self._zone_id = zone_id - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self._api.device.mac}-zone{self._zone_id}" + self._attr_device_info = api.device_info + self._attr_unique_id = f"{api.device.mac}-zone{zone_id}" @property def name(self) -> str: @@ -79,11 +75,6 @@ class DaikinZoneSwitch(SwitchEntity): """Return the state of the sensor.""" return self._api.device.zones[self._zone_id][1] == "1" - @property - def device_info(self) -> DeviceInfo: - """Return a device description for device registry.""" - return self._api.device_info - async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() @@ -104,14 +95,11 @@ class DaikinStreamerSwitch(SwitchEntity): _attr_name = "Streamer" _attr_has_entity_name = True - def __init__(self, daikin_api: DaikinApi) -> None: + def __init__(self, api: DaikinApi) -> None: """Initialize streamer switch.""" - self._api = daikin_api - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return f"{self._api.device.mac}-streamer" + self._api = api + self._attr_device_info = api.device_info + self._attr_unique_id = f"{api.device.mac}-streamer" @property def is_on(self) -> bool: @@ -120,11 +108,6 @@ class DaikinStreamerSwitch(SwitchEntity): DAIKIN_ATTR_STREAMER in self._api.device.represent(DAIKIN_ATTR_ADVANCED)[1] ) - @property - def device_info(self) -> DeviceInfo: - """Return a device description for device registry.""" - return self._api.device_info - async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() From e7462e916ade937d26362551331bac145226cff0 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 30 Aug 2023 10:29:35 +0200 Subject: [PATCH 1032/1151] Conditional category for temperature sensor entities in AVM Fritz!Smarthome (#98981) --- homeassistant/components/fritzbox/sensor.py | 18 ++++++- tests/components/fritzbox/test_sensor.py | 58 ++++++++++++++------- tests/components/fritzbox/test_switch.py | 12 +++++ 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 46fa1a26561..013c1dfc7b5 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -48,6 +48,8 @@ class FritzSensorEntityDescription( ): """Description for Fritz!Smarthome sensor entities.""" + entity_category_fn: Callable[[FritzhomeDevice], EntityCategory | None] | None = None + def suitable_eco_temperature(device: FritzhomeDevice) -> bool: """Check suitablity for eco temperature sensor.""" @@ -74,6 +76,13 @@ def suitable_temperature(device: FritzhomeDevice) -> bool: return device.has_temperature_sensor and not device.has_thermostat +def entity_category_temperature(device: FritzhomeDevice) -> EntityCategory | None: + """Determine proper entity category for temperature sensor.""" + if device.has_switch or device.has_lightbulb: + return EntityCategory.DIAGNOSTIC + return None + + def value_nextchange_preset(device: FritzhomeDevice) -> str: """Return native value for next scheduled preset sensor.""" if device.nextchange_temperature == device.eco_temperature: @@ -94,7 +103,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, + entity_category_fn=entity_category_temperature, suitable=suitable_temperature, native_value=lambda device: device.temperature, ), @@ -224,3 +233,10 @@ class FritzBoxSensor(FritzBoxDeviceEntity, SensorEntity): def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.entity_description.native_value(self.data) + + @property + def entity_category(self) -> EntityCategory | None: + """Return the category of the entity, if any.""" + if self.entity_description.entity_category_fn is not None: + return self.entity_description.entity_category_fn(self.data) + return super().entity_category diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 8a01665134b..b4c0209e9af 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -11,9 +11,11 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, PERCENTAGE, + EntityCategory, UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from . import FritzDeviceSensorMock, setup_config_entry @@ -32,26 +34,44 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: ) await hass.async_block_till_done() - state = hass.states.get(f"{ENTITY_ID}_temperature") - assert state - assert state.state == "1.23" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Temperature" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS - assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + sensors = ( + [ + f"{ENTITY_ID}_temperature", + "1.23", + f"{CONF_FAKE_NAME} Temperature", + UnitOfTemperature.CELSIUS, + SensorStateClass.MEASUREMENT, + None, + ], + [ + f"{ENTITY_ID}_humidity", + "42", + f"{CONF_FAKE_NAME} Humidity", + PERCENTAGE, + SensorStateClass.MEASUREMENT, + None, + ], + [ + f"{ENTITY_ID}_battery", + "23", + f"{CONF_FAKE_NAME} Battery", + PERCENTAGE, + None, + EntityCategory.DIAGNOSTIC, + ], + ) - state = hass.states.get(f"{ENTITY_ID}_humidity") - assert state - assert state.state == "42" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Humidity" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT - - state = hass.states.get(f"{ENTITY_ID}_battery") - assert state - assert state.state == "23" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Battery" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - assert ATTR_STATE_CLASS not in state.attributes + entity_registry = er.async_get(hass) + for sensor in sensors: + state = hass.states.get(sensor[0]) + assert state + assert state.state == sensor[1] + assert state.attributes[ATTR_FRIENDLY_NAME] == sensor[2] + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == sensor[3] + assert state.attributes.get(ATTR_STATE_CLASS) == sensor[4] + entry = entity_registry.async_get(sensor[0]) + assert entry + assert entry.entity_category is sensor[5] async def test_update(hass: HomeAssistant, fritz: Mock) -> None: diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 4ac36a13284..53cdf5147fc 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -20,6 +20,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, STATE_UNAVAILABLE, + EntityCategory, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, @@ -27,6 +28,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from . import FritzDeviceSwitchMock, setup_config_entry @@ -60,6 +62,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: f"{CONF_FAKE_NAME} Temperature", UnitOfTemperature.CELSIUS, SensorStateClass.MEASUREMENT, + EntityCategory.DIAGNOSTIC, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_power", @@ -67,6 +70,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: f"{CONF_FAKE_NAME} Power", UnitOfPower.WATT, SensorStateClass.MEASUREMENT, + None, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_energy", @@ -74,6 +78,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: f"{CONF_FAKE_NAME} Energy", UnitOfEnergy.KILO_WATT_HOUR, SensorStateClass.TOTAL_INCREASING, + None, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_voltage", @@ -81,6 +86,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: f"{CONF_FAKE_NAME} Voltage", UnitOfElectricPotential.VOLT, SensorStateClass.MEASUREMENT, + None, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_current", @@ -88,9 +94,11 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: f"{CONF_FAKE_NAME} Current", UnitOfElectricCurrent.AMPERE, SensorStateClass.MEASUREMENT, + None, ], ) + entity_registry = er.async_get(hass) for sensor in sensors: state = hass.states.get(sensor[0]) assert state @@ -98,6 +106,10 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: assert state.attributes[ATTR_FRIENDLY_NAME] == sensor[2] assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == sensor[3] assert state.attributes[ATTR_STATE_CLASS] == sensor[4] + assert state.attributes[ATTR_STATE_CLASS] == sensor[4] + entry = entity_registry.async_get(sensor[0]) + assert entry + assert entry.entity_category is sensor[5] async def test_turn_on(hass: HomeAssistant, fritz: Mock) -> None: From a89a5f486d840b71b2430c13830f6f1513cae821 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 30 Aug 2023 10:53:52 +0200 Subject: [PATCH 1033/1151] Migrate Melcloud to has entity name (#99025) --- homeassistant/components/melcloud/__init__.py | 12 +++++ homeassistant/components/melcloud/climate.py | 11 ++--- homeassistant/components/melcloud/sensor.py | 46 +++++++++---------- .../components/melcloud/strings.json | 22 +++++++++ .../components/melcloud/water_heater.py | 24 +++------- 5 files changed, 67 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 68b40d8567f..5f007f3a8e5 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -8,6 +8,7 @@ from typing import Any from aiohttp import ClientConnectionError from pymelcloud import Device, get_devices +from pymelcloud.atw_device import Zone import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -139,6 +140,17 @@ class MelCloudDevice: name=self.name, ) + def zone_device_info(self, zone: Zone) -> DeviceInfo: + """Return a zone device description for device registry.""" + dev = self.device + return DeviceInfo( + identifiers={(DOMAIN, f"{dev.mac}-{dev.serial}-{zone.zone_index}")}, + manufacturer="Mitsubishi Electric", + model="ATW zone device", + name=f"{self.name} {zone.name}", + via_device=(DOMAIN, f"{dev.mac}-{dev.serial}"), + ) + @property def daily_energy_consumed(self) -> float | None: """Return energy consumed during the current day in kWh.""" diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 2cac1abcf88..589223dc0f3 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -99,6 +99,8 @@ class MelCloudClimate(ClimateEntity): """Base climate device.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_has_entity_name = True + _attr_name = None def __init__(self, device: MelCloudDevice) -> None: """Initialize the climate.""" @@ -109,11 +111,6 @@ class MelCloudClimate(ClimateEntity): """Update state from MELCloud.""" await self.api.async_update() - @property - def device_info(self): - """Return a device description for device registry.""" - return self.api.device_info - @property def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" @@ -134,8 +131,8 @@ class AtaDeviceClimate(MelCloudClimate): super().__init__(device) self._device = ata_device - self._attr_name = device.name self._attr_unique_id = f"{self.api.device.serial}-{self.api.device.mac}" + self._attr_device_info = self.api.device_info @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -310,8 +307,8 @@ class AtwDeviceZoneClimate(MelCloudClimate): self._device = atw_device self._zone = atw_zone - self._attr_name = f"{device.name} {self._zone.name}" self._attr_unique_id = f"{self.api.device.serial}-{atw_zone.zone_index}" + self._attr_device_info = self.api.zone_device_info(atw_zone) @property def extra_state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 74187948d7d..ca02d15db01 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -41,28 +41,30 @@ class MelcloudSensorEntityDescription( ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( key="room_temperature", - name="Room Temperature", + translation_key="room_temperature", icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.device.room_temperature, enabled=lambda x: True, ), MelcloudSensorEntityDescription( key="energy", - name="Energy", icon="mdi:factory", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda x: x.device.total_energy_consumed, enabled=lambda x: x.device.has_energy_consumed_meter, ), MelcloudSensorEntityDescription( key="daily_energy", - name="Daily Energy Consumed", + translation_key="daily_energy", icon="mdi:factory", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda x: x.device.daily_energy_consumed, enabled=lambda x: True, ), @@ -70,28 +72,31 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( key="outside_temperature", - name="Outside Temperature", + translation_key="outside_temperature", icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.device.outside_temperature, enabled=lambda x: True, ), MelcloudSensorEntityDescription( key="tank_temperature", - name="Tank Temperature", + translation_key="tank_temperature", icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, value_fn=lambda x: x.device.tank_temperature, enabled=lambda x: True, ), MelcloudSensorEntityDescription( key="daily_energy", - name="Daily Energy Consumed", + translation_key="daily_energy", icon="mdi:factory", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda x: x.device.daily_energy_consumed, enabled=lambda x: True, ), @@ -99,28 +104,31 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( key="room_temperature", - name="Room Temperature", + translation_key="room_temperature", icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, value_fn=lambda zone: zone.room_temperature, enabled=lambda x: True, ), MelcloudSensorEntityDescription( key="flow_temperature", - name="Flow Temperature", + translation_key="flow_temperature", icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, value_fn=lambda zone: zone.flow_temperature, enabled=lambda x: True, ), MelcloudSensorEntityDescription( key="return_temperature", - name="Flow Return Temperature", + translation_key="return_temperature", icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, value_fn=lambda zone: zone.return_temperature, enabled=lambda x: True, ), @@ -160,6 +168,7 @@ class MelDeviceSensor(SensorEntity): """Representation of a Sensor.""" entity_description: MelcloudSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -170,16 +179,11 @@ class MelDeviceSensor(SensorEntity): self._api = api self.entity_description = description - self._attr_name = f"{api.name} {description.name}" self._attr_unique_id = f"{api.device.serial}-{api.device.mac}-{description.key}" - - if description.device_class == SensorDeviceClass.ENERGY: - self._attr_state_class = SensorStateClass.TOTAL_INCREASING - else: - self._attr_state_class = SensorStateClass.MEASUREMENT + self._attr_device_info = api.device_info @property - def native_value(self): + def native_value(self) -> float | None: """Return the state of the sensor.""" return self.entity_description.value_fn(self._api) @@ -187,11 +191,6 @@ class MelDeviceSensor(SensorEntity): """Retrieve latest state.""" await self._api.async_update() - @property - def device_info(self): - """Return a device description for device registry.""" - return self._api.device_info - class AtwZoneSensor(MelDeviceSensor): """Air-to-Air device sensor.""" @@ -206,10 +205,11 @@ class AtwZoneSensor(MelDeviceSensor): if zone.zone_index != 1: description.key = f"{description.key}-zone-{zone.zone_index}" super().__init__(api, description) + + self._attr_device_info = api.zone_device_info(zone) self._zone = zone - self._attr_name = f"{api.name} {zone.name} {description.name}" @property - def native_value(self): + def native_value(self) -> float | None: """Return zone based state.""" return self.entity_description.value_fn(self._zone) diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json index 23c1c63d328..eefd5a07a8d 100644 --- a/homeassistant/components/melcloud/strings.json +++ b/homeassistant/components/melcloud/strings.json @@ -50,5 +50,27 @@ "title": "The MELCloud YAML configuration import failed", "description": "Configuring MELCloud using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to MELCloud works and restart Home Assistant to try again or remove the MELCloud YAML configuration from your configuration.yaml file and continue to [set up the integration](/config/integrations/dashboard/add?domain=melcoud) manually." } + }, + "entity": { + "sensor": { + "room_temperature": { + "name": "Room temperature" + }, + "daily_energy": { + "name": "Daily energy consumed" + }, + "outside_temperature": { + "name": "Outside temperature" + }, + "tank_temperature": { + "name": "Tank temperature" + }, + "flow_temperature": { + "name": "Flow temperature" + }, + "return_temperature": { + "name": "Flow return temperature" + } + } } } diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index cf4b788480f..210b8bd51e2 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -47,32 +47,20 @@ class AtwWaterHeater(WaterHeaterEntity): | WaterHeaterEntityFeature.ON_OFF | WaterHeaterEntityFeature.OPERATION_MODE ) + _attr_has_entity_name = True + _attr_name = None def __init__(self, api: MelCloudDevice, device: AtwDevice) -> None: """Initialize water heater device.""" self._api = api self._device = device - self._name = device.name + self._attr_unique_id = api.device.serial + self._attr_device_info = api.device_info async def async_update(self) -> None: """Update state from MELCloud.""" await self._api.async_update() - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return f"{self._api.device.serial}" - - @property - def name(self): - """Return the display name of this entity.""" - return self._name - - @property - def device_info(self): - """Return a device description for device registry.""" - return self._api.device_info - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" await self._device.set({PROPERTY_POWER: True}) @@ -82,7 +70,7 @@ class AtwWaterHeater(WaterHeaterEntity): await self._device.set({PROPERTY_POWER: False}) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the optional state attributes with device specific additions.""" data = {ATTR_STATUS: self._device.status} return data @@ -108,7 +96,7 @@ class AtwWaterHeater(WaterHeaterEntity): return self._device.tank_temperature @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._device.target_tank_temperature From 56b99d2bc63755ddbe2273c4768c7345c2ee8abc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 11:20:15 +0200 Subject: [PATCH 1034/1151] Add entity translations to QNAP QSW (#98915) --- .../components/qnap_qsw/binary_sensor.py | 8 +++-- homeassistant/components/qnap_qsw/button.py | 4 +-- homeassistant/components/qnap_qsw/entity.py | 2 ++ homeassistant/components/qnap_qsw/sensor.py | 25 ++++++++------- .../components/qnap_qsw/strings.json | 31 +++++++++++++++++++ homeassistant/components/qnap_qsw/update.py | 6 ---- .../components/qnap_qsw/test_binary_sensor.py | 2 +- tests/components/qnap_qsw/test_button.py | 4 +-- tests/components/qnap_qsw/test_coordinator.py | 2 +- tests/components/qnap_qsw/test_update.py | 6 ++-- 10 files changed, 61 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/qnap_qsw/binary_sensor.py b/homeassistant/components/qnap_qsw/binary_sensor.py index 27387447b51..5c3fbe13aff 100644 --- a/homeassistant/components/qnap_qsw/binary_sensor.py +++ b/homeassistant/components/qnap_qsw/binary_sensor.py @@ -23,6 +23,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import UNDEFINED from .const import ATTR_MESSAGE, DOMAIN, QSW_COORD_DATA from .coordinator import QswDataCoordinator @@ -48,7 +49,6 @@ BINARY_SENSOR_TYPES: Final[tuple[QswBinarySensorEntityDescription, ...]] = ( device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, key=QSD_FIRMWARE_CONDITION, - name="Anomaly", subkey=QSD_ANOMALY, ), ) @@ -140,8 +140,10 @@ class QswBinarySensor(QswSensorEntity, BinarySensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator, entry, type_id) - - self._attr_name = f"{self.product} {description.name}" + if description.name == UNDEFINED: + self._attr_has_entity_name = True + else: + self._attr_name = f"{self.product} {description.name}" self._attr_unique_id = ( f"{entry.unique_id}_{description.key}" f"{description.sep_key}{description.subkey}" diff --git a/homeassistant/components/qnap_qsw/button.py b/homeassistant/components/qnap_qsw/button.py index 1c89504e810..acd8d3bd1ef 100644 --- a/homeassistant/components/qnap_qsw/button.py +++ b/homeassistant/components/qnap_qsw/button.py @@ -39,7 +39,6 @@ BUTTON_TYPES: Final[tuple[QswButtonDescription, ...]] = ( device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, key=QSW_REBOOT, - name="Reboot", press_action=lambda qsw: qsw.reboot(), ), ) @@ -58,6 +57,8 @@ async def async_setup_entry( class QswButton(QswDataEntity, ButtonEntity): """Define a QNAP QSW button.""" + _attr_has_entity_name = True + entity_description: QswButtonDescription def __init__( @@ -68,7 +69,6 @@ class QswButton(QswDataEntity, ButtonEntity): ) -> None: """Initialize.""" super().__init__(coordinator, entry) - self._attr_name = f"{self.product} {description.name}" self._attr_unique_id = f"{entry.unique_id}_{description.key}" self.entity_description = description diff --git a/homeassistant/components/qnap_qsw/entity.py b/homeassistant/components/qnap_qsw/entity.py index c1af235bfc3..4bbfba423e9 100644 --- a/homeassistant/components/qnap_qsw/entity.py +++ b/homeassistant/components/qnap_qsw/entity.py @@ -120,6 +120,8 @@ class QswSensorEntity(QswDataEntity): class QswFirmwareEntity(CoordinatorEntity[QswFirmwareCoordinator]): """Define a QNAP QSW firmware entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: QswFirmwareCoordinator, diff --git a/homeassistant/components/qnap_qsw/sensor.py b/homeassistant/components/qnap_qsw/sensor.py index 676fe586c37..0c287c66073 100644 --- a/homeassistant/components/qnap_qsw/sensor.py +++ b/homeassistant/components/qnap_qsw/sensor.py @@ -43,6 +43,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import UNDEFINED from .const import ATTR_MAX, DOMAIN, QSW_COORD_DATA, RPM from .coordinator import QswDataCoordinator @@ -60,57 +61,57 @@ class QswSensorEntityDescription(SensorEntityDescription, QswEntityDescription): SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( QswSensorEntityDescription( + translation_key="fan_1_speed", icon="mdi:fan-speed-1", key=QSD_SYSTEM_SENSOR, - name="Fan 1 Speed", native_unit_of_measurement=RPM, state_class=SensorStateClass.MEASUREMENT, subkey=QSD_FAN1_SPEED, ), QswSensorEntityDescription( + translation_key="fan_2_speed", icon="mdi:fan-speed-2", key=QSD_SYSTEM_SENSOR, - name="Fan 2 Speed", native_unit_of_measurement=RPM, state_class=SensorStateClass.MEASUREMENT, subkey=QSD_FAN2_SPEED, ), QswSensorEntityDescription( + translation_key="ports", attributes={ ATTR_MAX: [QSD_SYSTEM_BOARD, QSD_PORT_NUM], }, entity_registry_enabled_default=False, icon="mdi:ethernet", key=QSD_PORTS_STATUS, - name="Ports", state_class=SensorStateClass.MEASUREMENT, subkey=QSD_LINK, ), QswSensorEntityDescription( entity_registry_enabled_default=False, + translation_key="rx", device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:download-network", key=QSD_PORTS_STATISTICS, - name="RX", native_unit_of_measurement=UnitOfInformation.BYTES, state_class=SensorStateClass.TOTAL_INCREASING, subkey=QSD_RX_OCTETS, ), QswSensorEntityDescription( entity_registry_enabled_default=False, + translation_key="rx_errors", icon="mdi:close-network", key=QSD_PORTS_STATISTICS, entity_category=EntityCategory.DIAGNOSTIC, - name="RX Errors", state_class=SensorStateClass.TOTAL_INCREASING, subkey=QSD_RX_ERRORS, ), QswSensorEntityDescription( entity_registry_enabled_default=False, + translation_key="rx_speed", device_class=SensorDeviceClass.DATA_RATE, icon="mdi:download-network", key=QSD_PORTS_STATISTICS, - name="RX Speed", native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, subkey=QSD_RX_SPEED, @@ -121,36 +122,35 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( }, device_class=SensorDeviceClass.TEMPERATURE, key=QSD_SYSTEM_SENSOR, - name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, subkey=QSD_TEMP, ), QswSensorEntityDescription( entity_registry_enabled_default=False, + translation_key="tx", device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:upload-network", key=QSD_PORTS_STATISTICS, - name="TX", native_unit_of_measurement=UnitOfInformation.BYTES, state_class=SensorStateClass.TOTAL_INCREASING, subkey=QSD_TX_OCTETS, ), QswSensorEntityDescription( entity_registry_enabled_default=False, + translation_key="tx_speed", device_class=SensorDeviceClass.DATA_RATE, icon="mdi:upload-network", key=QSD_PORTS_STATISTICS, - name="TX Speed", native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, subkey=QSD_TX_SPEED, ), QswSensorEntityDescription( + translation_key="uptime", icon="mdi:timer-outline", key=QSD_SYSTEM_TIME, entity_category=EntityCategory.DIAGNOSTIC, - name="Uptime", native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.TOTAL_INCREASING, subkey=QSD_UPTIME, @@ -363,7 +363,10 @@ class QswSensor(QswSensorEntity, SensorEntity): """Initialize.""" super().__init__(coordinator, entry, type_id) - self._attr_name = f"{self.product} {description.name}" + if description.name == UNDEFINED: + self._attr_has_entity_name = True + else: + self._attr_name = f"{self.product} {description.name}" self._attr_unique_id = ( f"{entry.unique_id}_{description.key}" f"{description.sep_key}{description.subkey}" diff --git a/homeassistant/components/qnap_qsw/strings.json b/homeassistant/components/qnap_qsw/strings.json index ba0cb28ba77..c8cd5ffb861 100644 --- a/homeassistant/components/qnap_qsw/strings.json +++ b/homeassistant/components/qnap_qsw/strings.json @@ -23,5 +23,36 @@ } } } + }, + "entity": { + "sensor": { + "fan_1_speed": { + "name": "Fan 1 speed" + }, + "fan_2_speed": { + "name": "Fan 2 speed" + }, + "ports": { + "name": "Ports" + }, + "rx": { + "name": "RX" + }, + "rx_errors": { + "name": "RX errors" + }, + "rx_speed": { + "name": "RX speed" + }, + "tx": { + "name": "TX" + }, + "tx_speed": { + "name": "TX speed" + }, + "uptime": { + "name": "Uptime" + } + } } } diff --git a/homeassistant/components/qnap_qsw/update.py b/homeassistant/components/qnap_qsw/update.py index 38a963818d4..5ea6e80f4bb 100644 --- a/homeassistant/components/qnap_qsw/update.py +++ b/homeassistant/components/qnap_qsw/update.py @@ -7,8 +7,6 @@ from aioqsw.const import ( QSD_DESCRIPTION, QSD_FIRMWARE_CHECK, QSD_FIRMWARE_INFO, - QSD_PRODUCT, - QSD_SYSTEM_BOARD, QSD_VERSION, ) @@ -32,7 +30,6 @@ UPDATE_TYPES: Final[tuple[UpdateEntityDescription, ...]] = ( device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.CONFIG, key=QSW_UPDATE, - name="Firmware Update", ), ) @@ -63,9 +60,6 @@ class QswUpdate(QswFirmwareEntity, UpdateEntity): ) -> None: """Initialize.""" super().__init__(coordinator, entry) - self._attr_name = ( - f"{self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT)} {description.name}" - ) self._attr_unique_id = f"{entry.unique_id}_{description.key}" self.entity_description = description diff --git a/tests/components/qnap_qsw/test_binary_sensor.py b/tests/components/qnap_qsw/test_binary_sensor.py index 47eb6a00ba7..3540eb6ba4a 100644 --- a/tests/components/qnap_qsw/test_binary_sensor.py +++ b/tests/components/qnap_qsw/test_binary_sensor.py @@ -17,7 +17,7 @@ async def test_qnap_qsw_create_binary_sensors( await async_init_integration(hass) - state = hass.states.get("binary_sensor.qsw_m408_4c_anomaly") + state = hass.states.get("binary_sensor.qsw_m408_4c_problem") assert state.state == STATE_OFF assert state.attributes.get(ATTR_MESSAGE) is None diff --git a/tests/components/qnap_qsw/test_button.py b/tests/components/qnap_qsw/test_button.py index 43e0ee4ba38..27b5fcb075d 100644 --- a/tests/components/qnap_qsw/test_button.py +++ b/tests/components/qnap_qsw/test_button.py @@ -14,7 +14,7 @@ async def test_qnap_buttons(hass: HomeAssistant) -> None: await async_init_integration(hass) - state = hass.states.get("button.qsw_m408_4c_reboot") + state = hass.states.get("button.qsw_m408_4c_restart") assert state assert state.state == STATE_UNKNOWN @@ -28,7 +28,7 @@ async def test_qnap_buttons(hass: HomeAssistant) -> None: await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.qsw_m408_4c_reboot"}, + {ATTR_ENTITY_ID: "button.qsw_m408_4c_restart"}, blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/qnap_qsw/test_coordinator.py b/tests/components/qnap_qsw/test_coordinator.py index b0163f7b7ec..8a5f07e8173 100644 --- a/tests/components/qnap_qsw/test_coordinator.py +++ b/tests/components/qnap_qsw/test_coordinator.py @@ -127,5 +127,5 @@ async def test_coordinator_client_connector_error( mock_firmware_update_check.assert_called_once() - update = hass.states.get("update.qsw_m408_4c_firmware_update") + update = hass.states.get("update.qsw_m408_4c_firmware") assert update.state == STATE_UNAVAILABLE diff --git a/tests/components/qnap_qsw/test_update.py b/tests/components/qnap_qsw/test_update.py index 26b7157f64d..f6eb9705912 100644 --- a/tests/components/qnap_qsw/test_update.py +++ b/tests/components/qnap_qsw/test_update.py @@ -34,7 +34,7 @@ async def test_qnap_qsw_update(hass: HomeAssistant) -> None: await async_init_integration(hass) - update = hass.states.get("update.qsw_m408_4c_firmware_update") + update = hass.states.get("update.qsw_m408_4c_firmware") assert update is not None assert update.state == STATE_ON assert ( @@ -62,7 +62,7 @@ async def test_qnap_qsw_update(hass: HomeAssistant) -> None: SERVICE_INSTALL, { ATTR_BACKUP: False, - ATTR_ENTITY_ID: "update.qsw_m408_4c_firmware_update", + ATTR_ENTITY_ID: "update.qsw_m408_4c_firmware", }, blocking=True, ) @@ -71,7 +71,7 @@ async def test_qnap_qsw_update(hass: HomeAssistant) -> None: mock_firmware_update_live.assert_called_once() mock_users_verification.assert_called() - update = hass.states.get("update.qsw_m408_4c_firmware_update") + update = hass.states.get("update.qsw_m408_4c_firmware") assert update is not None assert update.state == STATE_OFF assert ( From 40748a6c341f69c900fd43960748bcef5848883b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 30 Aug 2023 05:32:41 -0400 Subject: [PATCH 1035/1151] Add zwave_js controller identify event (#99254) --- homeassistant/components/zwave_js/__init__.py | 41 +++++++++++++++ tests/components/zwave_js/test_init.py | 51 ++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 2e6ff4f0b34..c86c4ae5688 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -21,6 +21,7 @@ from zwave_js_server.model.notification import ( from zwave_js_server.model.value import Value, ValueNotification from homeassistant.components.hassio import AddonError, AddonManager, AddonState +from homeassistant.components.persistent_notification import async_create from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, @@ -290,6 +291,11 @@ class DriverEvents: controller.on("node removed", self.controller_events.async_on_node_removed) ) + # listen for identify events for the controller + self.config_entry.async_on_unload( + controller.on("identify", self.controller_events.async_on_identify) + ) + async def async_setup_platform(self, platform: Platform) -> None: """Set up platform if needed.""" if platform not in self.platform_setup_tasks: @@ -417,6 +423,41 @@ class ControllerEvents: else: self.remove_device(device) + @callback + def async_on_identify(self, event: dict) -> None: + """Handle identify event.""" + # Get node device + node: ZwaveNode = event["node"] + dev_id = get_device_id(self.driver_events.driver, node) + device = self.dev_reg.async_get_device(identifiers={dev_id}) + assert device + device_name = device.name_by_user or device.name + home_id = self.driver_events.driver.controller.home_id + # We do this because we know at this point the controller has its home ID as + # as it is part of the device ID + assert home_id + # In case the user has multiple networks, we should give them more information + # about the network for the controller being identified. + identifier = "" + if len(self.hass.config_entries.async_entries(DOMAIN)) > 1: + if str(home_id) != self.config_entry.title: + identifier = ( + f"`{self.config_entry.title}`, with the home ID `{home_id}`, " + ) + else: + identifier = f"with the home ID `{home_id}` " + async_create( + self.hass, + ( + f"`{device_name}` has just requested the controller for your Z-Wave " + f"network {identifier}to identify itself. No action is needed from " + "you other than to note the source of the request, and you can safely " + "dismiss this notification when ready." + ), + "New Z-Wave Identify Controller Request", + f"{DOMAIN}.identify_controller.{dev_id[1]}", + ) + @callback def register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntry: """Register node in dev reg.""" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index c421e043413..212ac9d751e 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -11,6 +11,7 @@ from zwave_js_server.model.node import Node from zwave_js_server.model.version import VersionInfo from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.components.persistent_notification import async_dismiss from homeassistant.components.zwave_js import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState @@ -25,7 +26,7 @@ from homeassistant.helpers import ( from .common import AIR_TEMPERATURE_SENSOR, EATON_RF9640_ENTITY -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_get_persistent_notifications @pytest.fixture(name="connect_timeout") @@ -1501,3 +1502,51 @@ async def test_disabled_entity_on_value_removed( } == new_unavailable_entities ) + + +async def test_identify_event( + hass: HomeAssistant, client, multisensor_6, integration +) -> None: + """Test controller identify event.""" + # One config entry scenario + event = Event( + type="identify", + data={ + "source": "controller", + "event": "identify", + "nodeId": multisensor_6.node_id, + }, + ) + dev_id = get_device_id(client.driver, multisensor_6) + msg_id = f"{DOMAIN}.identify_controller.{dev_id[1]}" + + client.driver.controller.receive_event(event) + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 1 + assert list(notifications)[0] == msg_id + assert notifications[msg_id]["message"].startswith("`Multisensor 6`") + assert "with the home ID" not in notifications[msg_id]["message"] + async_dismiss(hass, msg_id) + + # Add mock config entry to simulate having multiple entries + new_entry = MockConfigEntry(domain=DOMAIN) + new_entry.add_to_hass(hass) + + # Test case where config entry title and home ID don't match + client.driver.controller.receive_event(event) + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 1 + assert list(notifications)[0] == msg_id + assert ( + "network `Mock Title`, with the home ID `3245146787`" + in notifications[msg_id]["message"] + ) + async_dismiss(hass, msg_id) + + # Test case where config entry title and home ID do match + hass.config_entries.async_update_entry(integration, title="3245146787") + client.driver.controller.receive_event(event) + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 1 + assert list(notifications)[0] == msg_id + assert "network with the home ID `3245146787`" in notifications[msg_id]["message"] From 9e178ae2ce18e14cb510f7731a8ca6fdacbdb991 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Aug 2023 11:33:41 +0200 Subject: [PATCH 1036/1151] Fix assist_pipeline schema (#99318) --- homeassistant/components/assist_pipeline/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/assist_pipeline/__init__.py b/homeassistant/components/assist_pipeline/__init__.py index ad1fd194271..7f87bd254d0 100644 --- a/homeassistant/components/assist_pipeline/__init__.py +++ b/homeassistant/components/assist_pipeline/__init__.py @@ -41,7 +41,12 @@ __all__ = ( ) CONFIG_SCHEMA = vol.Schema( - {vol.Optional(DOMAIN): {vol.Optional("debug_recording_dir"): str}} + { + DOMAIN: vol.Schema( + {vol.Optional("debug_recording_dir"): str}, + ) + }, + extra=vol.ALLOW_EXTRA, ) From 9ef3ec3dd3014994f35e2c4489d65beb0f5a4701 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 30 Aug 2023 11:34:51 +0200 Subject: [PATCH 1037/1151] Add modbus test for configuration errors (#98697) --- tests/components/modbus/test_init.py | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index e305a0294c8..6f88a4b7399 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -49,6 +49,7 @@ from homeassistant.components.modbus.const import ( CONF_SWAP, CONF_SWAP_BYTE, CONF_SWAP_WORD, + CONF_SWAP_WORD_BYTE, DEFAULT_SCAN_INTERVAL, MODBUS_DOMAIN as DOMAIN, RTUOVERTCP, @@ -182,6 +183,30 @@ async def test_nan_validator() -> None: CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: ">i", }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_SLAVE: 5, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWAP: CONF_SWAP_BYTE, + }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_SLAVE: 5, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWAP: CONF_SWAP_WORD, + }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_SLAVE: 5, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWAP: CONF_SWAP_WORD_BYTE, + }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_SLAVE: 5, + CONF_DATA_TYPE: DataType.INT16, + CONF_SWAP: CONF_SWAP_BYTE, + }, ], ) async def test_ok_struct_validator(do_config) -> None: @@ -254,6 +279,16 @@ async def test_ok_struct_validator(do_config) -> None: CONF_SLAVE_COUNT: 2, CONF_DATA_TYPE: DataType.INT32, }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_DATA_TYPE: DataType.INT16, + CONF_SWAP: CONF_SWAP_WORD, + }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_DATA_TYPE: DataType.INT16, + CONF_SWAP: CONF_SWAP_WORD_BYTE, + }, ], ) async def test_exception_struct_validator(do_config) -> None: From 021b14fc17379c7dccf865fb7242600665a62ad8 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Wed, 30 Aug 2023 11:45:09 +0200 Subject: [PATCH 1038/1151] Refactor & enhance BMW tests (#97895) Co-authored-by: rikroe --- .../bmw_connected_drive/__init__.py | 149 +- .../bmw_connected_drive/conftest.py | 47 +- .../remote_service/eventposition.json | 12 - ...x-crccs_v2_vehicles_WBA00000000DEMO02.json | 80 - .../G26/bmw-eadrax-vcs_v4_vehicles.json | 50 - ...s_v4_vehicles_state_WBA00000000DEMO02.json | 317 -- ...x-crccs_v2_vehicles_WBY00000000REXI01.json | 60 - .../I01_REX/bmw-eadrax-vcs_v4_vehicles.json | 47 - ...s_v4_vehicles_state_WBY00000000REXI01.json | 206 - .../snapshots/test_button.ambr | 120 + .../snapshots/test_diagnostics.ambr | 4147 ++++++++++++++++- .../snapshots/test_number.ambr | 17 + .../snapshots/test_select.ambr | 44 + .../snapshots/test_switch.ambr | 34 +- .../bmw_connected_drive/test_button.py | 144 +- .../bmw_connected_drive/test_number.py | 45 +- .../bmw_connected_drive/test_select.py | 58 +- .../bmw_connected_drive/test_sensor.py | 4 +- .../bmw_connected_drive/test_switch.py | 60 +- 19 files changed, 4557 insertions(+), 1084 deletions(-) delete mode 100644 tests/components/bmw_connected_drive/fixtures/remote_service/eventposition.json delete mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-crccs_v2_vehicles_WBA00000000DEMO02.json delete mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-vcs_v4_vehicles.json delete mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-vcs_v4_vehicles_state_WBA00000000DEMO02.json delete mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-crccs_v2_vehicles_WBY00000000REXI01.json delete mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-vcs_v4_vehicles.json delete mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-vcs_v4_vehicles_state_WBY00000000REXI01.json diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index 89a34682c1b..020e4c978ed 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -1,16 +1,7 @@ """Tests for the for the BMW Connected Drive integration.""" -from pathlib import Path -from urllib.parse import urlparse -from bimmer_connected.api.authentication import MyBMWAuthentication -from bimmer_connected.const import ( - REMOTE_SERVICE_POSITION_URL, - VEHICLE_CHARGING_DETAILS_URL, - VEHICLE_STATE_URL, - VEHICLES_URL, -) -import httpx +from bimmer_connected.const import REMOTE_SERVICE_BASE_URL, VEHICLE_CHARGING_BASE_URL import respx from homeassistant import config_entries @@ -23,12 +14,7 @@ from homeassistant.components.bmw_connected_drive.const import ( from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant -from tests.common import ( - MockConfigEntry, - get_fixture_path, - load_json_array_fixture, - load_json_object_fixture, -) +from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { CONF_USERNAME: "user@domain.com", @@ -54,88 +40,6 @@ FIXTURE_CONFIG_ENTRY = { "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } -FIXTURE_PATH = Path(get_fixture_path("", integration=BMW_DOMAIN)) -FIXTURE_FILES = { - "vehicles": sorted(FIXTURE_PATH.rglob("*-eadrax-vcs_v4_vehicles.json")), - "states": { - p.stem.split("_")[-1]: p - for p in FIXTURE_PATH.rglob("*-eadrax-vcs_v4_vehicles_state_*.json") - }, - "charging": { - p.stem.split("_")[-1]: p - for p in FIXTURE_PATH.rglob("*-eadrax-crccs_v2_vehicles_*.json") - }, -} - - -def vehicles_sideeffect(request: httpx.Request) -> httpx.Response: - """Return /vehicles response based on x-user-agent.""" - x_user_agent = request.headers.get("x-user-agent", "").split(";") - brand = x_user_agent[1] - vehicles = [] - for vehicle_file in FIXTURE_FILES["vehicles"]: - if vehicle_file.name.startswith(brand): - vehicles.extend( - load_json_array_fixture(vehicle_file, integration=BMW_DOMAIN) - ) - return httpx.Response(200, json=vehicles) - - -def vehicle_state_sideeffect(request: httpx.Request) -> httpx.Response: - """Return /vehicles/state response.""" - try: - state_file = FIXTURE_FILES["states"][request.headers["bmw-vin"]] - return httpx.Response( - 200, json=load_json_object_fixture(state_file, integration=BMW_DOMAIN) - ) - except KeyError: - return httpx.Response(404) - - -def vehicle_charging_sideeffect(request: httpx.Request) -> httpx.Response: - """Return /vehicles/state response.""" - try: - charging_file = FIXTURE_FILES["charging"][request.headers["bmw-vin"]] - return httpx.Response( - 200, json=load_json_object_fixture(charging_file, integration=BMW_DOMAIN) - ) - except KeyError: - return httpx.Response(404) - - -def mock_vehicles() -> respx.Router: - """Return mocked adapter for vehicles.""" - router = respx.mock(assert_all_called=False) - - # Get vehicle list - router.get(VEHICLES_URL).mock(side_effect=vehicles_sideeffect) - - # Get vehicle state - router.get(VEHICLE_STATE_URL).mock(side_effect=vehicle_state_sideeffect) - - # Get vehicle charging details - router.get(VEHICLE_CHARGING_DETAILS_URL).mock( - side_effect=vehicle_charging_sideeffect - ) - - # Get vehicle position after remote service - router.post(urlparse(REMOTE_SERVICE_POSITION_URL).netloc).mock( - httpx.Response( - 200, - json=load_json_object_fixture( - FIXTURE_PATH / "remote_service" / "eventposition.json", - integration=BMW_DOMAIN, - ), - ) - ) - - return router - - -async def mock_login(auth: MyBMWAuthentication) -> None: - """Mock a successful login.""" - auth.access_token = "SOME_ACCESS_TOKEN" - async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: """Mock a fully setup config entry and all components based on fixtures.""" @@ -147,3 +51,52 @@ async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: await hass.async_block_till_done() return mock_config_entry + + +def check_remote_service_call( + router: respx.MockRouter, + remote_service: str = None, + remote_service_params: dict = None, + remote_service_payload: dict = None, +): + """Check if the last call was a successful remote service call.""" + + # Check if remote service call was made correctly + if remote_service: + # Get remote service call + first_remote_service_call: respx.models.Call = next( + c + for c in router.calls + if c.request.url.path.startswith(REMOTE_SERVICE_BASE_URL) + or c.request.url.path.startswith( + VEHICLE_CHARGING_BASE_URL.replace("/{vin}", "") + ) + ) + assert ( + first_remote_service_call.request.url.path.endswith(remote_service) is True + ) + assert first_remote_service_call.has_response is True + assert first_remote_service_call.response.is_success is True + + # test params. + # we don't test payload as this creates a lot of noise in the tests + # and is end-to-end tested with the HA states + if remote_service_params: + assert ( + dict(first_remote_service_call.request.url.params.items()) + == remote_service_params + ) + + # Now check final result + last_event_status_call = next( + c for c in reversed(router.calls) if c.request.url.path.endswith("eventStatus") + ) + + assert last_event_status_call is not None + assert ( + last_event_status_call.request.url.path + == "/eadrax-vrccs/v3/presentation/remote-commands/eventStatus" + ) + assert last_event_status_call.has_response is True + assert last_event_status_call.response.is_success is True + assert last_event_status_call.response.json() == {"eventStatus": "EXECUTED"} diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py index b65adb5b2c0..4191c7a4dd2 100644 --- a/tests/components/bmw_connected_drive/conftest.py +++ b/tests/components/bmw_connected_drive/conftest.py @@ -1,34 +1,39 @@ """Fixtures for BMW tests.""" -from unittest.mock import AsyncMock, Mock -from bimmer_connected.api.authentication import MyBMWAuthentication -from bimmer_connected.vehicle.remote_services import RemoteServices, RemoteServiceStatus +from collections.abc import Generator + +from bimmer_connected.tests import ALL_CHARGING_SETTINGS, ALL_STATES +from bimmer_connected.tests.common import MyBMWMockRouter +from bimmer_connected.vehicle import remote_services import pytest - -from homeassistant.components.bmw_connected_drive.coordinator import ( - BMWDataUpdateCoordinator, -) - -from . import mock_login, mock_vehicles +import respx @pytest.fixture -async def bmw_fixture(monkeypatch): - """Patch the MyBMW Login and mock HTTP calls.""" - monkeypatch.setattr(MyBMWAuthentication, "login", mock_login) +def bmw_fixture( + request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch +) -> Generator[respx.MockRouter, None, None]: + """Patch MyBMW login API calls.""" - monkeypatch.setattr( - RemoteServices, - "trigger_remote_service", - AsyncMock(return_value=RemoteServiceStatus({"eventStatus": "EXECUTED"})), + # we use the library's mock router to mock the API calls, but only with a subset of vehicles + router = MyBMWMockRouter( + vehicles_to_load=[ + "WBA00000000DEMO01", + "WBA00000000DEMO02", + "WBA00000000DEMO03", + "WBY00000000REXI01", + ], + states=ALL_STATES, + charging_settings=ALL_CHARGING_SETTINGS, ) + # we don't want to wait when triggering a remote service monkeypatch.setattr( - BMWDataUpdateCoordinator, - "async_update_listeners", - Mock(), + remote_services, + "_POLLING_CYCLE", + 0, ) - with mock_vehicles(): - yield mock_vehicles + with router: + yield router diff --git a/tests/components/bmw_connected_drive/fixtures/remote_service/eventposition.json b/tests/components/bmw_connected_drive/fixtures/remote_service/eventposition.json deleted file mode 100644 index 92d1a6a9db0..00000000000 --- a/tests/components/bmw_connected_drive/fixtures/remote_service/eventposition.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "positionData": { - "status": "OK", - "position": { - "latitude": 123.456, - "longitude": 34.5678, - "formattedAddress": "some_formatted_address", - "heading": 121 - } - }, - "errorDetails": null -} diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-crccs_v2_vehicles_WBA00000000DEMO02.json b/tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-crccs_v2_vehicles_WBA00000000DEMO02.json deleted file mode 100644 index af850f1ff2c..00000000000 --- a/tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-crccs_v2_vehicles_WBA00000000DEMO02.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "chargeAndClimateSettings": { - "chargeAndClimateTimer": { - "chargingMode": "Sofort laden", - "chargingModeSemantics": "Sofort laden", - "departureTimer": ["Aus"], - "departureTimerSemantics": "Aus", - "preconditionForDeparture": "Aus", - "showDepartureTimers": false - }, - "chargingFlap": { - "permanentlyUnlockLabel": "Aus" - }, - "chargingSettings": { - "acCurrentLimitLabel": "16A", - "acCurrentLimitLabelSemantics": "16 Ampere", - "chargingTargetLabel": "80%", - "dcLoudnessLabel": "Nicht begrenzt", - "unlockCableAutomaticallyLabel": "Aus" - } - }, - "chargeAndClimateTimerDetail": { - "chargingMode": { - "chargingPreference": "NO_PRESELECTION", - "endTimeSlot": "0001-01-01T00:00:00", - "startTimeSlot": "0001-01-01T00:00:00", - "type": "CHARGING_IMMEDIATELY" - }, - "departureTimer": { - "type": "WEEKLY_DEPARTURE_TIMER", - "weeklyTimers": [ - { - "daysOfTheWeek": [], - "id": 1, - "time": "0001-01-01T00:00:00", - "timerAction": "DEACTIVATE" - }, - { - "daysOfTheWeek": [], - "id": 2, - "time": "0001-01-01T00:00:00", - "timerAction": "DEACTIVATE" - }, - { - "daysOfTheWeek": [], - "id": 3, - "time": "0001-01-01T00:00:00", - "timerAction": "DEACTIVATE" - }, - { - "daysOfTheWeek": [], - "id": 4, - "time": "0001-01-01T00:00:00", - "timerAction": "DEACTIVATE" - } - ] - }, - "isPreconditionForDepartureActive": false - }, - "chargingFlapDetail": { - "isPermanentlyUnlock": false - }, - "chargingSettingsDetail": { - "acLimit": { - "current": { - "unit": "A", - "value": 16 - }, - "isUnlimited": false, - "max": 32, - "min": 6, - "values": [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 32] - }, - "chargingTarget": 80, - "dcLoudness": "UNLIMITED_LOUD", - "isUnlockCableActive": false, - "minChargingTargetToWarning": 0 - }, - "servicePack": "WAVE_01" -} diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-vcs_v4_vehicles.json b/tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-vcs_v4_vehicles.json deleted file mode 100644 index f954fb103ae..00000000000 --- a/tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-vcs_v4_vehicles.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "appVehicleType": "DEMO", - "attributes": { - "a4aType": "NOT_SUPPORTED", - "bodyType": "G26", - "brand": "BMW", - "color": 4284245350, - "countryOfOrigin": "DE", - "driveTrain": "ELECTRIC", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" - }, - "headUnitRaw": "HU_MGU", - "headUnitType": "MGU", - "hmiVersion": "ID8", - "lastFetched": "2023-01-04T14:57:06.019Z", - "model": "i4 eDrive40", - "softwareVersionCurrent": { - "iStep": 470, - "puStep": { - "month": 11, - "year": 21 - }, - "seriesCluster": "G026" - }, - "softwareVersionExFactory": { - "iStep": 470, - "puStep": { - "month": 11, - "year": 21 - }, - "seriesCluster": "G026" - }, - "telematicsUnit": "WAVE01", - "year": 2021 - }, - "mappingInfo": { - "isAssociated": false, - "isLmmEnabled": false, - "isPrimaryUser": true, - "lmmStatusReasons": [], - "mappingStatus": "CONFIRMED" - }, - "vin": "WBA00000000DEMO02" - } -] diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-vcs_v4_vehicles_state_WBA00000000DEMO02.json b/tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-vcs_v4_vehicles_state_WBA00000000DEMO02.json deleted file mode 100644 index a0974854295..00000000000 --- a/tests/components/bmw_connected_drive/fixtures/vehicles/G26/bmw-eadrax-vcs_v4_vehicles_state_WBA00000000DEMO02.json +++ /dev/null @@ -1,317 +0,0 @@ -{ - "capabilities": { - "a4aType": "NOT_SUPPORTED", - "checkSustainabilityDPP": false, - "climateFunction": "AIR_CONDITIONING", - "climateNow": true, - "digitalKey": { - "bookedServicePackage": "SMACC_1_5", - "readerGraphics": "readerGraphics", - "state": "ACTIVATED" - }, - "horn": true, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": true, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": true, - "isChargingLoudnessEnabled": true, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnabled": true, - "isChargingSettingsEnabled": true, - "isChargingTargetSocEnabled": true, - "isClimateTimerWeeklyActive": false, - "isCustomerEsimSupported": true, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeEnabled": true, - "isEvGoChargingSupported": false, - "isMiniChargingSupported": false, - "isNonLscFeatureEnabled": false, - "isPersonalPictureUploadSupported": false, - "isRemoteEngineStartSupported": false, - "isRemoteHistoryDeletionSupported": false, - "isRemoteHistorySupported": true, - "isRemoteParkingSupported": false, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": true, - "isSustainabilityAccumulatedViewEnabled": false, - "isSustainabilitySupported": false, - "isWifiHotspotServiceSupported": false, - "lastStateCallState": "ACTIVATED", - "lights": true, - "lock": true, - "remote360": true, - "remoteChargingCommands": { - "chargingControl": ["START", "STOP"], - "flapControl": ["NOT_SUPPORTED"], - "plugControl": ["NOT_SUPPORTED"] - }, - "remoteSoftwareUpgrade": true, - "sendPoi": true, - "specialThemeSupport": [], - "speechThirdPartyAlexa": false, - "speechThirdPartyAlexaSDK": false, - "unlock": true, - "vehicleFinder": true, - "vehicleStateSource": "LAST_STATE_CALL" - }, - "state": { - "chargingProfile": { - "chargingControlType": "WEEKLY_PLANNER", - "chargingMode": "IMMEDIATE_CHARGING", - "chargingPreference": "NO_PRESELECTION", - "chargingSettings": { - "acCurrentLimit": 16, - "hospitality": "NO_ACTION", - "idcc": "UNLIMITED_LOUD", - "targetSoc": 80 - }, - "departureTimes": [ - { - "action": "DEACTIVATE", - "id": 1, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "DEACTIVATE", - "id": 2, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "DEACTIVATE", - "id": 3, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "DEACTIVATE", - "id": 4, - "timeStamp": { - "hour": 0, - "minute": 0 - }, - "timerWeekDays": [] - } - ] - }, - "checkControlMessages": [ - { - "severity": "LOW", - "type": "TIRE_PRESSURE" - } - ], - "climateControlState": { - "activity": "STANDBY" - }, - "climateTimers": [ - { - "departureTime": { - "hour": 0, - "minute": 0 - }, - "isWeeklyTimer": false, - "timerAction": "DEACTIVATE", - "timerWeekDays": [] - }, - { - "departureTime": { - "hour": 0, - "minute": 0 - }, - "isWeeklyTimer": true, - "timerAction": "DEACTIVATE", - "timerWeekDays": [] - }, - { - "departureTime": { - "hour": 0, - "minute": 0 - }, - "isWeeklyTimer": true, - "timerAction": "DEACTIVATE", - "timerWeekDays": [] - } - ], - "combustionFuelLevel": {}, - "currentMileage": 1121, - "doorsState": { - "combinedSecurityState": "LOCKED", - "combinedState": "CLOSED", - "hood": "CLOSED", - "leftFront": "CLOSED", - "leftRear": "CLOSED", - "rightFront": "CLOSED", - "rightRear": "CLOSED", - "trunk": "CLOSED" - }, - "driverPreferences": { - "lscPrivacyMode": "OFF" - }, - "electricChargingState": { - "chargingConnectionType": "UNKNOWN", - "chargingLevelPercent": 80, - "chargingStatus": "CHARGING", - "chargingTarget": 80, - "isChargerConnected": true, - "range": 472, - "remainingChargingMinutes": 10 - }, - "isLeftSteering": true, - "isLscSupported": true, - "lastFetched": "2023-01-04T14:57:06.386Z", - "lastUpdatedAt": "2023-01-04T14:57:06.407Z", - "location": { - "address": { - "formatted": "Am Olympiapark 1, 80809 München" - }, - "coordinates": { - "latitude": 48.177334, - "longitude": 11.556274 - }, - "heading": 180 - }, - "range": 472, - "requiredServices": [ - { - "dateTime": "2024-12-01T00:00:00.000Z", - "description": "", - "mileage": 50000, - "status": "OK", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2024-12-01T00:00:00.000Z", - "description": "", - "mileage": 50000, - "status": "OK", - "type": "VEHICLE_TUV" - }, - { - "dateTime": "2024-12-01T00:00:00.000Z", - "description": "", - "mileage": 50000, - "status": "OK", - "type": "VEHICLE_CHECK" - }, - { - "status": "OK", - "type": "TIRE_WEAR_REAR" - }, - { - "status": "OK", - "type": "TIRE_WEAR_FRONT" - } - ], - "tireState": { - "frontLeft": { - "details": { - "dimension": "225/35 R20 90Y XL", - "isOptimizedForOemBmw": true, - "manufacturer": "Pirelli", - "manufacturingWeek": 4021, - "mountingDate": "2022-03-07T00:00:00.000Z", - "partNumber": "2461756", - "season": 2, - "speedClassification": { - "atLeast": false, - "speedRating": 300 - }, - "treadDesign": "P-ZERO" - }, - "status": { - "currentPressure": 241, - "pressureStatus": 0, - "targetPressure": 269, - "wearStatus": 0 - } - }, - "frontRight": { - "details": { - "dimension": "225/35 R20 90Y XL", - "isOptimizedForOemBmw": true, - "manufacturer": "Pirelli", - "manufacturingWeek": 2419, - "mountingDate": "2022-03-07T00:00:00.000Z", - "partNumber": "2461756", - "season": 2, - "speedClassification": { - "atLeast": false, - "speedRating": 300 - }, - "treadDesign": "P-ZERO" - }, - "status": { - "currentPressure": 255, - "pressureStatus": 0, - "targetPressure": 269, - "wearStatus": 0 - } - }, - "rearLeft": { - "details": { - "dimension": "255/30 R20 92Y XL", - "isOptimizedForOemBmw": true, - "manufacturer": "Pirelli", - "manufacturingWeek": 1219, - "mountingDate": "2022-03-07T00:00:00.000Z", - "partNumber": "2461757", - "season": 2, - "speedClassification": { - "atLeast": false, - "speedRating": 300 - }, - "treadDesign": "P-ZERO" - }, - "status": { - "currentPressure": 324, - "pressureStatus": 0, - "targetPressure": 303, - "wearStatus": 0 - } - }, - "rearRight": { - "details": { - "dimension": "255/30 R20 92Y XL", - "isOptimizedForOemBmw": true, - "manufacturer": "Pirelli", - "manufacturingWeek": 1219, - "mountingDate": "2022-03-07T00:00:00.000Z", - "partNumber": "2461757", - "season": 2, - "speedClassification": { - "atLeast": false, - "speedRating": 300 - }, - "treadDesign": "P-ZERO" - }, - "status": { - "currentPressure": 331, - "pressureStatus": 0, - "targetPressure": 303, - "wearStatus": 0 - } - } - }, - "windowsState": { - "combinedState": "CLOSED", - "leftFront": "CLOSED", - "leftRear": "CLOSED", - "rear": "CLOSED", - "rightFront": "CLOSED", - "rightRear": "CLOSED" - } - } -} diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-crccs_v2_vehicles_WBY00000000REXI01.json b/tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-crccs_v2_vehicles_WBY00000000REXI01.json deleted file mode 100644 index 03bfc1cae04..00000000000 --- a/tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-crccs_v2_vehicles_WBY00000000REXI01.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "chargeAndClimateSettings": { - "chargeAndClimateTimer": { - "showDepartureTimers": false - } - }, - "chargeAndClimateTimerDetail": { - "chargingMode": { - "chargingPreference": "CHARGING_WINDOW", - "endTimeSlot": "0001-01-01T01:30:00", - "startTimeSlot": "0001-01-01T18:01:00", - "type": "TIME_SLOT" - }, - "departureTimer": { - "type": "WEEKLY_DEPARTURE_TIMER", - "weeklyTimers": [ - { - "daysOfTheWeek": [ - "MONDAY", - "TUESDAY", - "WEDNESDAY", - "THURSDAY", - "FRIDAY" - ], - "id": 1, - "time": "0001-01-01T07:35:00", - "timerAction": "DEACTIVATE" - }, - { - "daysOfTheWeek": [ - "MONDAY", - "TUESDAY", - "WEDNESDAY", - "THURSDAY", - "FRIDAY", - "SATURDAY", - "SUNDAY" - ], - "id": 2, - "time": "0001-01-01T18:00:00", - "timerAction": "DEACTIVATE" - }, - { - "daysOfTheWeek": [], - "id": 3, - "time": "0001-01-01T07:00:00", - "timerAction": "DEACTIVATE" - }, - { - "daysOfTheWeek": [], - "id": 4, - "time": "0001-01-01T00:00:00", - "timerAction": "DEACTIVATE" - } - ] - }, - "isPreconditionForDepartureActive": false - }, - "servicePack": "TCB1" -} diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-vcs_v4_vehicles.json b/tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-vcs_v4_vehicles.json deleted file mode 100644 index 145bc13378e..00000000000 --- a/tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-vcs_v4_vehicles.json +++ /dev/null @@ -1,47 +0,0 @@ -[ - { - "appVehicleType": "CONNECTED", - "attributes": { - "a4aType": "USB_ONLY", - "bodyType": "I01", - "brand": "BMW_I", - "color": 4284110934, - "countryOfOrigin": "CZ", - "driveTrain": "ELECTRIC_WITH_RANGE_EXTENDER", - "driverGuideInfo": { - "androidAppScheme": "com.bmwgroup.driversguide.row", - "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", - "iosAppScheme": "bmwdriversguide:///open", - "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" - }, - "headUnitType": "NBT", - "hmiVersion": "ID4", - "lastFetched": "2022-07-10T09:25:53.104Z", - "model": "i3 (+ REX)", - "softwareVersionCurrent": { - "iStep": 510, - "puStep": { - "month": 11, - "year": 21 - }, - "seriesCluster": "I001" - }, - "softwareVersionExFactory": { - "iStep": 502, - "puStep": { - "month": 3, - "year": 15 - }, - "seriesCluster": "I001" - }, - "year": 2015 - }, - "mappingInfo": { - "isAssociated": false, - "isLmmEnabled": false, - "isPrimaryUser": true, - "mappingStatus": "CONFIRMED" - }, - "vin": "WBY00000000REXI01" - } -] diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-vcs_v4_vehicles_state_WBY00000000REXI01.json b/tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-vcs_v4_vehicles_state_WBY00000000REXI01.json deleted file mode 100644 index adc2bde3650..00000000000 --- a/tests/components/bmw_connected_drive/fixtures/vehicles/I01_REX/bmw-eadrax-vcs_v4_vehicles_state_WBY00000000REXI01.json +++ /dev/null @@ -1,206 +0,0 @@ -{ - "capabilities": { - "climateFunction": "AIR_CONDITIONING", - "climateNow": true, - "climateTimerTrigger": "DEPARTURE_TIMER", - "horn": true, - "isBmwChargingSupported": true, - "isCarSharingSupported": false, - "isChargeNowForBusinessSupported": false, - "isChargingHistorySupported": true, - "isChargingHospitalityEnabled": false, - "isChargingLoudnessEnabled": false, - "isChargingPlanSupported": true, - "isChargingPowerLimitEnabled": false, - "isChargingSettingsEnabled": false, - "isChargingTargetSocEnabled": false, - "isClimateTimerSupported": true, - "isCustomerEsimSupported": false, - "isDCSContractManagementSupported": true, - "isDataPrivacyEnabled": false, - "isEasyChargeEnabled": false, - "isEvGoChargingSupported": false, - "isMiniChargingSupported": false, - "isNonLscFeatureEnabled": false, - "isRemoteEngineStartSupported": false, - "isRemoteHistoryDeletionSupported": false, - "isRemoteHistorySupported": true, - "isRemoteParkingSupported": false, - "isRemoteServicesActivationRequired": false, - "isRemoteServicesBookingRequired": false, - "isScanAndChargeSupported": false, - "isSustainabilitySupported": false, - "isWifiHotspotServiceSupported": false, - "lastStateCallState": "ACTIVATED", - "lights": true, - "lock": true, - "remoteChargingCommands": {}, - "sendPoi": true, - "specialThemeSupport": [], - "unlock": true, - "vehicleFinder": false, - "vehicleStateSource": "LAST_STATE_CALL" - }, - "state": { - "chargingProfile": { - "chargingControlType": "WEEKLY_PLANNER", - "chargingMode": "DELAYED_CHARGING", - "chargingPreference": "CHARGING_WINDOW", - "chargingSettings": { - "hospitality": "NO_ACTION", - "idcc": "NO_ACTION", - "targetSoc": 100 - }, - "climatisationOn": false, - "departureTimes": [ - { - "action": "DEACTIVATE", - "id": 1, - "timeStamp": { - "hour": 7, - "minute": 35 - }, - "timerWeekDays": [ - "MONDAY", - "TUESDAY", - "WEDNESDAY", - "THURSDAY", - "FRIDAY" - ] - }, - { - "action": "DEACTIVATE", - "id": 2, - "timeStamp": { - "hour": 18, - "minute": 0 - }, - "timerWeekDays": [ - "MONDAY", - "TUESDAY", - "WEDNESDAY", - "THURSDAY", - "FRIDAY", - "SATURDAY", - "SUNDAY" - ] - }, - { - "action": "DEACTIVATE", - "id": 3, - "timeStamp": { - "hour": 7, - "minute": 0 - }, - "timerWeekDays": [] - }, - { - "action": "DEACTIVATE", - "id": 4, - "timerWeekDays": [] - } - ], - "reductionOfChargeCurrent": { - "end": { - "hour": 1, - "minute": 30 - }, - "start": { - "hour": 18, - "minute": 1 - } - } - }, - "checkControlMessages": [], - "climateTimers": [ - { - "departureTime": { - "hour": 6, - "minute": 40 - }, - "isWeeklyTimer": true, - "timerAction": "ACTIVATE", - "timerWeekDays": ["THURSDAY", "SUNDAY"] - }, - { - "departureTime": { - "hour": 12, - "minute": 50 - }, - "isWeeklyTimer": false, - "timerAction": "ACTIVATE", - "timerWeekDays": ["MONDAY"] - }, - { - "departureTime": { - "hour": 18, - "minute": 59 - }, - "isWeeklyTimer": true, - "timerAction": "DEACTIVATE", - "timerWeekDays": ["WEDNESDAY"] - } - ], - "combustionFuelLevel": { - "range": 105, - "remainingFuelLiters": 6, - "remainingFuelPercent": 65 - }, - "currentMileage": 137009, - "doorsState": { - "combinedSecurityState": "UNLOCKED", - "combinedState": "CLOSED", - "hood": "CLOSED", - "leftFront": "CLOSED", - "leftRear": "CLOSED", - "rightFront": "CLOSED", - "rightRear": "CLOSED", - "trunk": "CLOSED" - }, - "driverPreferences": { - "lscPrivacyMode": "OFF" - }, - "electricChargingState": { - "chargingConnectionType": "CONDUCTIVE", - "chargingLevelPercent": 82, - "chargingStatus": "WAITING_FOR_CHARGING", - "chargingTarget": 100, - "isChargerConnected": true, - "range": 174 - }, - "isLeftSteering": true, - "isLscSupported": true, - "lastFetched": "2022-06-22T14:24:23.982Z", - "lastUpdatedAt": "2022-06-22T13:58:52Z", - "range": 174, - "requiredServices": [ - { - "dateTime": "2022-10-01T00:00:00.000Z", - "description": "Next service due by the specified date.", - "status": "OK", - "type": "BRAKE_FLUID" - }, - { - "dateTime": "2023-05-01T00:00:00.000Z", - "description": "Next vehicle check due after the specified distance or date.", - "status": "OK", - "type": "VEHICLE_CHECK" - }, - { - "dateTime": "2023-05-01T00:00:00.000Z", - "description": "Next state inspection due by the specified date.", - "status": "OK", - "type": "VEHICLE_TUV" - } - ], - "roofState": { - "roofState": "CLOSED", - "roofStateType": "SUN_ROOF" - }, - "windowsState": { - "combinedState": "CLOSED", - "leftFront": "CLOSED", - "rightFront": "CLOSED" - } - } -} diff --git a/tests/components/bmw_connected_drive/snapshots/test_button.ambr b/tests/components/bmw_connected_drive/snapshots/test_button.ambr index af43f118a77..51e15e5ff43 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_button.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_button.ambr @@ -1,6 +1,66 @@ # serializer version: 1 # name: test_entity_state_attrs list([ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Flash lights', + 'icon': 'mdi:car-light-alert', + }), + 'context': , + 'entity_id': 'button.ix_xdrive50_flash_lights', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Sound horn', + 'icon': 'mdi:bullhorn', + }), + 'context': , + 'entity_id': 'button.ix_xdrive50_sound_horn', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Activate air conditioning', + 'icon': 'mdi:hvac', + }), + 'context': , + 'entity_id': 'button.ix_xdrive50_activate_air_conditioning', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Deactivate air conditioning', + 'icon': 'mdi:hvac-off', + }), + 'context': , + 'entity_id': 'button.ix_xdrive50_deactivate_air_conditioning', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Find vehicle', + 'icon': 'mdi:crosshairs-question', + }), + 'context': , + 'entity_id': 'button.ix_xdrive50_find_vehicle', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -61,6 +121,66 @@ 'last_updated': , 'state': 'unknown', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Flash lights', + 'icon': 'mdi:car-light-alert', + }), + 'context': , + 'entity_id': 'button.m340i_xdrive_flash_lights', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Sound horn', + 'icon': 'mdi:bullhorn', + }), + 'context': , + 'entity_id': 'button.m340i_xdrive_sound_horn', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Activate air conditioning', + 'icon': 'mdi:hvac', + }), + 'context': , + 'entity_id': 'button.m340i_xdrive_activate_air_conditioning', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Deactivate air conditioning', + 'icon': 'mdi:hvac-off', + }), + 'context': , + 'entity_id': 'button.m340i_xdrive_deactivate_air_conditioning', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'M340i xDrive Find vehicle', + 'icon': 'mdi:crosshairs-question', + }), + 'context': , + 'entity_id': 'button.m340i_xdrive_find_vehicle', + 'last_changed': , + 'last_updated': , + 'state': 'unknown', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', diff --git a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr index 5befe3f0dcf..70224b41ff5 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr @@ -150,7 +150,7 @@ 'UTC', ]), }), - 'activity': 'STANDBY', + 'activity': 'INACTIVE', 'activity_end_time': None, 'activity_end_time_no_tz': None, 'is_climate_on': False, @@ -205,6 +205,888 @@ }), ]), }), + 'data': dict({ + 'appVehicleType': 'DEMO', + 'attributes': dict({ + 'a4aType': 'BLUETOOTH', + 'bodyType': 'I20', + 'brand': 'BMW_I', + 'color': 4285537312, + 'countryOfOrigin': 'DE', + 'driveTrain': 'ELECTRIC', + 'driverGuideInfo': dict({ + 'androidAppScheme': 'com.bmwgroup.driversguide.row', + 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', + 'iosAppScheme': 'bmwdriversguide:///open', + 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', + }), + 'headUnitRaw': 'HU_MGU', + 'headUnitType': 'MGU', + 'hmiVersion': 'ID8', + 'lastFetched': '2023-01-04T14:57:06.019Z', + 'model': 'iX xDrive50', + 'softwareVersionCurrent': dict({ + 'iStep': 300, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S21A', + }), + 'softwareVersionExFactory': dict({ + 'iStep': 300, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S21A', + }), + 'telematicsUnit': 'WAVE01', + 'year': 2021, + }), + 'capabilities': dict({ + 'a4aType': 'BLUETOOTH', + 'checkSustainabilityDPP': False, + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'digitalKey': dict({ + 'bookedServicePackage': 'SMACC_2_UWB', + 'readerGraphics': 'readerGraphics', + 'state': 'ACTIVATED', + }), + 'horn': True, + 'inCarCamera': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': True, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': True, + 'isChargingLoudnessEnabled': True, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': True, + 'isChargingSettingsEnabled': True, + 'isChargingTargetSocEnabled': True, + 'isClimateTimerWeeklyActive': False, + 'isCustomerEsimSupported': True, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': True, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isPersonalPictureUploadSupported': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': True, + 'isSustainabilityAccumulatedViewEnabled': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remote360': True, + 'remoteChargingCommands': dict({ + 'chargingControl': list([ + 'START', + 'STOP', + ]), + 'flapControl': list([ + 'NOT_SUPPORTED', + ]), + 'plugControl': list([ + 'NOT_SUPPORTED', + ]), + }), + 'remoteSoftwareUpgrade': True, + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': True, + 'unlock': True, + 'vehicleFinder': True, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'charging_settings': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'chargingMode': 'Sofort laden', + 'chargingModeSemantics': 'Sofort laden', + 'departureTimer': list([ + 'Aus', + ]), + 'departureTimerSemantics': 'Aus', + 'preconditionForDeparture': 'Aus', + 'showDepartureTimers': False, + }), + 'chargingFlap': dict({ + 'permanentlyUnlockLabel': 'Aus', + }), + 'chargingSettings': dict({ + 'acCurrentLimitLabel': '16A', + 'acCurrentLimitLabelSemantics': '16 Ampere', + 'chargingTargetLabel': '80%', + 'dcLoudnessLabel': 'Nicht begrenzt', + 'unlockCableAutomaticallyLabel': 'Aus', + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'NO_PRESELECTION', + 'endTimeSlot': '0001-01-01T00:00:00', + 'startTimeSlot': '0001-01-01T00:00:00', + 'type': 'CHARGING_IMMEDIATELY', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 1, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 2, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'chargingFlapDetail': dict({ + 'isPermanentlyUnlock': False, + }), + 'chargingSettingsDetail': dict({ + 'acLimit': dict({ + 'current': dict({ + 'unit': 'A', + 'value': 16, + }), + 'isUnlimited': False, + 'max': 32, + 'min': 6, + 'values': list([ + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 32, + ]), + }), + 'chargingTarget': 80, + 'dcLoudness': 'UNLIMITED_LOUD', + 'isUnlockCableActive': False, + 'minChargingTargetToWarning': 70, + }), + 'servicePack': 'WAVE_01', + }), + 'fetched_at': '2022-07-10T11:00:00+00:00', + 'is_metric': True, + 'mappingInfo': dict({ + 'isAssociated': False, + 'isLmmEnabled': False, + 'isPrimaryUser': True, + 'lmmStatusReasons': list([ + ]), + 'mappingStatus': 'CONFIRMED', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'IMMEDIATE_CHARGING', + 'chargingPreference': 'NO_PRESELECTION', + 'chargingSettings': dict({ + 'acCurrentLimit': 16, + 'hospitality': 'NO_ACTION', + 'idcc': 'UNLIMITED_LOUD', + 'targetSoc': 80, + }), + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + ]), + }), + 'checkControlMessages': list([ + dict({ + 'severity': 'LOW', + 'type': 'TIRE_PRESSURE', + }), + ]), + 'climateControlState': dict({ + 'activity': 'INACTIVE', + }), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': False, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'remainingFuelPercent': 10, + }), + 'currentMileage': 1121, + 'doorsState': dict({ + 'combinedSecurityState': 'LOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'UNKNOWN', + 'chargingLevelPercent': 70, + 'chargingStatus': 'CHARGING', + 'chargingTarget': 80, + 'isChargerConnected': True, + 'range': 340, + 'remainingChargingMinutes': 10, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2023-01-04T14:57:06.371Z', + 'lastUpdatedAt': '2023-01-04T14:57:06.383Z', + 'location': dict({ + 'address': dict({ + 'formatted': '**REDACTED**', + }), + 'coordinates': dict({ + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'heading': '**REDACTED**', + }), + 'range': 340, + 'requiredServices': list([ + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_REAR', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_FRONT', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'tireState': dict({ + 'frontLeft': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'targetPressure': 241, + 'wearStatus': 0, + }), + }), + 'frontRight': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'targetPressure': 241, + 'wearStatus': 0, + }), + }), + 'rearLeft': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 261, + 'pressureStatus': 0, + 'targetPressure': 269, + 'wearStatus': 0, + }), + }), + 'rearRight': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 269, + 'pressureStatus': 0, + 'targetPressure': 269, + 'wearStatus': 0, + }), + }), + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + }), + }), + 'vin': '**REDACTED**', + }), + 'doors_and_windows': dict({ + 'all_lids_closed': True, + 'all_windows_closed': True, + 'door_lock_state': 'LOCKED', + 'lids': list([ + dict({ + 'is_closed': True, + 'name': 'hood', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'leftFront', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'leftRear', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'rightFront', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'rightRear', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'trunk', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'sunRoof', + 'state': 'CLOSED', + }), + ]), + 'open_lids': list([ + ]), + 'open_windows': list([ + ]), + 'windows': list([ + dict({ + 'is_closed': True, + 'name': 'leftFront', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'leftRear', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'rear', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'rightFront', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'rightRear', + 'state': 'CLOSED', + }), + ]), + }), + 'drive_train': 'ELECTRIC', + 'drive_train_attributes': list([ + 'remaining_range_total', + 'mileage', + 'charging_time_remaining', + 'charging_start_time', + 'charging_end_time', + 'charging_time_label', + 'charging_status', + 'connection_status', + 'remaining_battery_percent', + 'remaining_range_electric', + 'last_charging_end_result', + 'ac_current_limit', + 'charging_target', + 'charging_mode', + 'charging_preferences', + 'is_pre_entry_climatization_enabled', + ]), + 'fuel_and_battery': dict({ + 'account_timezone': dict({ + '_dst_offset': '0:00:00', + '_dst_saved': '0:00:00', + '_hasdst': False, + '_std_offset': '0:00:00', + '_tznames': list([ + 'UTC', + 'UTC', + ]), + }), + 'charging_end_time': '2022-07-10T11:10:00+00:00', + 'charging_start_time': None, + 'charging_start_time_no_tz': None, + 'charging_status': 'CHARGING', + 'charging_target': 80, + 'is_charger_connected': True, + 'remaining_battery_percent': 70, + 'remaining_fuel': list([ + None, + None, + ]), + 'remaining_fuel_percent': None, + 'remaining_range_electric': list([ + 340, + 'km', + ]), + 'remaining_range_fuel': list([ + None, + None, + ]), + 'remaining_range_total': list([ + 340, + 'km', + ]), + }), + 'has_combustion_drivetrain': False, + 'has_electric_drivetrain': True, + 'is_charging_plan_supported': True, + 'is_lsc_enabled': True, + 'is_remote_charge_start_enabled': True, + 'is_remote_charge_stop_enabled': True, + 'is_remote_climate_start_enabled': True, + 'is_remote_climate_stop_enabled': True, + 'is_remote_horn_enabled': True, + 'is_remote_lights_enabled': True, + 'is_remote_lock_enabled': True, + 'is_remote_sendpoi_enabled': True, + 'is_remote_set_ac_limit_enabled': True, + 'is_remote_set_target_soc_enabled': True, + 'is_remote_unlock_enabled': True, + 'is_vehicle_active': False, + 'is_vehicle_tracking_enabled': True, + 'lsc_type': 'ACTIVATED', + 'mileage': list([ + 1121, + 'km', + ]), + 'name': 'iX xDrive50', + 'timestamp': '2023-01-04T14:57:06+00:00', + 'tires': dict({ + 'front_left': dict({ + 'current_pressure': 241, + 'manufacturing_week': '2021-10-04T00:00:00', + 'season': 2, + 'target_pressure': 241, + }), + 'front_right': dict({ + 'current_pressure': 241, + 'manufacturing_week': '2021-10-04T00:00:00', + 'season': 2, + 'target_pressure': 241, + }), + 'rear_left': dict({ + 'current_pressure': 261, + 'manufacturing_week': '2021-10-04T00:00:00', + 'season': 2, + 'target_pressure': 269, + }), + 'rear_right': dict({ + 'current_pressure': 269, + 'manufacturing_week': '2021-10-04T00:00:00', + 'season': 2, + 'target_pressure': 269, + }), + }), + 'vehicle_location': dict({ + 'account_region': 'row', + 'heading': '**REDACTED**', + 'location': dict({ + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'remote_service_position': None, + 'vehicle_update_timestamp': '2023-01-04T14:57:06+00:00', + }), + 'vin': '**REDACTED**', + }), + dict({ + 'available_attributes': list([ + 'gps_position', + 'vin', + 'remaining_range_total', + 'mileage', + 'charging_time_remaining', + 'charging_start_time', + 'charging_end_time', + 'charging_time_label', + 'charging_status', + 'connection_status', + 'remaining_battery_percent', + 'remaining_range_electric', + 'last_charging_end_result', + 'ac_current_limit', + 'charging_target', + 'charging_mode', + 'charging_preferences', + 'is_pre_entry_climatization_enabled', + 'condition_based_services', + 'check_control_messages', + 'door_lock_state', + 'timestamp', + 'lids', + 'windows', + ]), + 'brand': 'bmw', + 'charging_profile': dict({ + 'ac_available_limits': list([ + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 32, + ]), + 'ac_current_limit': 16, + 'charging_mode': 'IMMEDIATE_CHARGING', + 'charging_preferences': 'NO_PRESELECTION', + 'charging_preferences_service_pack': 'WAVE_01', + 'departure_times': list([ + dict({ + '_timer_dict': dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + 'action': 'DEACTIVATE', + 'start_time': '00:00:00', + 'timer_id': 1, + 'weekdays': list([ + ]), + }), + dict({ + '_timer_dict': dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + 'action': 'DEACTIVATE', + 'start_time': '00:00:00', + 'timer_id': 2, + 'weekdays': list([ + ]), + }), + dict({ + '_timer_dict': dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + 'action': 'DEACTIVATE', + 'start_time': '00:00:00', + 'timer_id': 3, + 'weekdays': list([ + ]), + }), + dict({ + '_timer_dict': dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + 'action': 'DEACTIVATE', + 'start_time': '00:00:00', + 'timer_id': 4, + 'weekdays': list([ + ]), + }), + ]), + 'is_pre_entry_climatization_enabled': False, + 'preferred_charging_window': dict({ + '_window_dict': dict({ + }), + 'end_time': '00:00:00', + 'start_time': '00:00:00', + }), + 'timer_type': 'WEEKLY_PLANNER', + }), + 'check_control_messages': dict({ + 'has_check_control_messages': False, + 'messages': list([ + dict({ + 'description_long': None, + 'description_short': 'TIRE_PRESSURE', + 'state': 'LOW', + }), + ]), + }), + 'climate': dict({ + 'account_timezone': dict({ + '_dst_offset': '0:00:00', + '_dst_saved': '0:00:00', + '_hasdst': False, + '_std_offset': '0:00:00', + '_tznames': list([ + 'UTC', + 'UTC', + ]), + }), + 'activity': 'HEATING', + 'activity_end_time': '2022-07-10T11:29:50+00:00', + 'activity_end_time_no_tz': '2022-07-10T11:29:50', + 'is_climate_on': True, + }), + 'condition_based_services': dict({ + 'is_service_required': False, + 'messages': list([ + dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), + dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'VEHICLE_TUV', + 'state': 'OK', + }), + dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'VEHICLE_CHECK', + 'state': 'OK', + }), + dict({ + 'due_date': None, + 'due_distance': list([ + None, + None, + ]), + 'service_type': 'TIRE_WEAR_REAR', + 'state': 'OK', + }), + dict({ + 'due_date': None, + 'due_distance': list([ + None, + None, + ]), + 'service_type': 'TIRE_WEAR_FRONT', + 'state': 'OK', + }), + ]), + }), 'data': dict({ 'appVehicleType': 'DEMO', 'attributes': dict({ @@ -289,16 +1171,6 @@ 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ - 'chargingControl': list([ - 'START', - 'STOP', - ]), - 'flapControl': list([ - 'NOT_SUPPORTED', - ]), - 'plugControl': list([ - 'NOT_SUPPORTED', - ]), }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, @@ -481,7 +1353,8 @@ }), ]), 'climateControlState': dict({ - 'activity': 'STANDBY', + 'activity': 'HEATING', + 'remainingSeconds': 1790.846, }), 'climateTimers': list([ dict({ @@ -534,9 +1407,9 @@ 'electricChargingState': dict({ 'chargingConnectionType': 'UNKNOWN', 'chargingLevelPercent': 80, - 'chargingStatus': 'CHARGING', + 'chargingStatus': 'INVALID', 'chargingTarget': 80, - 'isChargerConnected': True, + 'isChargerConnected': False, 'range': 472, 'remainingChargingMinutes': 10, }), @@ -788,9 +1661,9 @@ 'charging_end_time': '2022-07-10T11:10:00+00:00', 'charging_start_time': None, 'charging_start_time_no_tz': None, - 'charging_status': 'CHARGING', + 'charging_status': 'NOT_CHARGING', 'charging_target': 80, - 'is_charger_connected': True, + 'is_charger_connected': False, 'remaining_battery_percent': 80, 'remaining_fuel': list([ None, @@ -814,8 +1687,8 @@ 'has_electric_drivetrain': True, 'is_charging_plan_supported': True, 'is_lsc_enabled': True, - 'is_remote_charge_start_enabled': True, - 'is_remote_charge_stop_enabled': True, + 'is_remote_charge_start_enabled': False, + 'is_remote_charge_stop_enabled': False, 'is_remote_climate_start_enabled': True, 'is_remote_climate_stop_enabled': True, 'is_remote_horn_enabled': True, @@ -872,6 +1745,639 @@ }), 'vin': '**REDACTED**', }), + dict({ + 'available_attributes': list([ + 'gps_position', + 'vin', + 'remaining_range_total', + 'mileage', + 'remaining_fuel', + 'remaining_range_fuel', + 'remaining_fuel_percent', + 'condition_based_services', + 'check_control_messages', + 'door_lock_state', + 'timestamp', + 'lids', + 'windows', + ]), + 'brand': 'bmw', + 'charging_profile': dict({ + 'ac_available_limits': None, + 'ac_current_limit': None, + 'charging_mode': 'IMMEDIATE_CHARGING', + 'charging_preferences': 'NO_PRESELECTION', + 'charging_preferences_service_pack': None, + 'departure_times': list([ + ]), + 'is_pre_entry_climatization_enabled': False, + 'preferred_charging_window': dict({ + '_window_dict': dict({ + }), + 'end_time': '00:00:00', + 'start_time': '00:00:00', + }), + 'timer_type': 'UNKNOWN', + }), + 'check_control_messages': dict({ + 'has_check_control_messages': False, + 'messages': list([ + dict({ + 'description_long': None, + 'description_short': 'TIRE_PRESSURE', + 'state': 'LOW', + }), + dict({ + 'description_long': None, + 'description_short': 'ENGINE_OIL', + 'state': 'LOW', + }), + ]), + }), + 'climate': dict({ + 'account_timezone': dict({ + '_dst_offset': '0:00:00', + '_dst_saved': '0:00:00', + '_hasdst': False, + '_std_offset': '0:00:00', + '_tznames': list([ + 'UTC', + 'UTC', + ]), + }), + 'activity': 'INACTIVE', + 'activity_end_time': None, + 'activity_end_time_no_tz': None, + 'is_climate_on': False, + }), + 'condition_based_services': dict({ + 'is_service_required': False, + 'messages': list([ + dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'OIL', + 'state': 'OK', + }), + dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'BRAKE_FLUID', + 'state': 'OK', + }), + dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'VEHICLE_TUV', + 'state': 'OK', + }), + dict({ + 'due_date': '2024-12-01T00:00:00+00:00', + 'due_distance': list([ + 50000, + 'km', + ]), + 'service_type': 'VEHICLE_CHECK', + 'state': 'OK', + }), + dict({ + 'due_date': None, + 'due_distance': list([ + None, + None, + ]), + 'service_type': 'TIRE_WEAR_REAR', + 'state': 'OK', + }), + dict({ + 'due_date': None, + 'due_distance': list([ + None, + None, + ]), + 'service_type': 'TIRE_WEAR_FRONT', + 'state': 'OK', + }), + ]), + }), + 'data': dict({ + 'appVehicleType': 'DEMO', + 'attributes': dict({ + 'a4aType': 'NOT_SUPPORTED', + 'bodyType': 'G20', + 'brand': 'BMW', + 'color': 4280233344, + 'countryOfOrigin': 'PT', + 'driveTrain': 'COMBUSTION', + 'driverGuideInfo': dict({ + 'androidAppScheme': 'com.bmwgroup.driversguide.row', + 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', + 'iosAppScheme': 'bmwdriversguide:///open', + 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', + }), + 'headUnitRaw': 'HU_MGU', + 'headUnitType': 'MGU', + 'hmiVersion': 'ID7', + 'lastFetched': '2023-01-04T14:57:06.019Z', + 'model': 'M340i xDrive', + 'softwareVersionCurrent': dict({ + 'iStep': 470, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S18A', + }), + 'softwareVersionExFactory': dict({ + 'iStep': 420, + 'puStep': dict({ + 'month': 7, + 'year': 20, + }), + 'seriesCluster': 'S18A', + }), + 'telematicsUnit': 'ATM2', + 'year': 2022, + }), + 'capabilities': dict({ + 'a4aType': 'NOT_SUPPORTED', + 'checkSustainabilityDPP': False, + 'climateFunction': 'VENTILATION', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'digitalKey': dict({ + 'bookedServicePackage': 'SMACC_1_5', + 'readerGraphics': 'readerGraphics', + 'state': 'ACTIVATED', + }), + 'horn': True, + 'isBmwChargingSupported': False, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': False, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': False, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isClimateTimerWeeklyActive': False, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': False, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isPersonalPictureUploadSupported': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilityAccumulatedViewEnabled': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': True, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remote360': True, + 'remoteChargingCommands': dict({ + }), + 'remoteSoftwareUpgrade': True, + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexaSDK': False, + 'unlock': True, + 'vehicleFinder': True, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'charging_settings': dict({ + }), + 'fetched_at': '2022-07-10T11:00:00+00:00', + 'is_metric': True, + 'mappingInfo': dict({ + 'isAssociated': False, + 'isLmmEnabled': False, + 'isPrimaryUser': True, + 'lmmStatusReasons': list([ + ]), + 'mappingStatus': 'CONFIRMED', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingMode': 'IMMEDIATE_CHARGING', + 'chargingPreference': 'NO_PRESELECTION', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 0, + }), + 'departureTimes': list([ + ]), + }), + 'checkControlMessages': list([ + dict({ + 'severity': 'LOW', + 'type': 'TIRE_PRESSURE', + }), + dict({ + 'severity': 'LOW', + 'type': 'ENGINE_OIL', + }), + ]), + 'climateControlState': dict({ + 'activity': 'INACTIVE', + }), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': False, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 629, + 'remainingFuelLiters': 40, + 'remainingFuelPercent': 80, + }), + 'currentMileage': 1121, + 'doorsState': dict({ + 'combinedSecurityState': 'LOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2023-01-04T14:57:06.336Z', + 'lastUpdatedAt': '2023-01-04T14:57:06.348Z', + 'location': dict({ + 'address': dict({ + 'formatted': '**REDACTED**', + }), + 'coordinates': dict({ + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'heading': '**REDACTED**', + }), + 'range': 629, + 'requiredServices': list([ + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'OIL', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_REAR', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_FRONT', + }), + ]), + 'tireState': dict({ + 'frontLeft': dict({ + 'details': dict({ + 'dimension': '225/35 R20 90Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461756', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'frontRight': dict({ + 'details': dict({ + 'dimension': '225/35 R20 90Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 2419, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461756', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 255, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'rearLeft': dict({ + 'details': dict({ + 'dimension': '255/30 R20 92Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 1219, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461757', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 324, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'rearRight': dict({ + 'details': dict({ + 'dimension': '255/30 R20 92Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 1219, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461757', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 331, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + }), + }), + 'vin': '**REDACTED**', + }), + 'doors_and_windows': dict({ + 'all_lids_closed': True, + 'all_windows_closed': True, + 'door_lock_state': 'LOCKED', + 'lids': list([ + dict({ + 'is_closed': True, + 'name': 'hood', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'leftFront', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'leftRear', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'rightFront', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'rightRear', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'trunk', + 'state': 'CLOSED', + }), + ]), + 'open_lids': list([ + ]), + 'open_windows': list([ + ]), + 'windows': list([ + dict({ + 'is_closed': True, + 'name': 'leftFront', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'leftRear', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'rear', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'rightFront', + 'state': 'CLOSED', + }), + dict({ + 'is_closed': True, + 'name': 'rightRear', + 'state': 'CLOSED', + }), + ]), + }), + 'drive_train': 'COMBUSTION', + 'drive_train_attributes': list([ + 'remaining_range_total', + 'mileage', + 'remaining_fuel', + 'remaining_range_fuel', + 'remaining_fuel_percent', + ]), + 'fuel_and_battery': dict({ + 'account_timezone': dict({ + '_dst_offset': '0:00:00', + '_dst_saved': '0:00:00', + '_hasdst': False, + '_std_offset': '0:00:00', + '_tznames': list([ + 'UTC', + 'UTC', + ]), + }), + 'charging_end_time': None, + 'charging_start_time': None, + 'charging_start_time_no_tz': None, + 'charging_status': None, + 'charging_target': None, + 'is_charger_connected': False, + 'remaining_battery_percent': None, + 'remaining_fuel': list([ + 40, + 'L', + ]), + 'remaining_fuel_percent': 80, + 'remaining_range_electric': list([ + None, + None, + ]), + 'remaining_range_fuel': list([ + 629, + 'km', + ]), + 'remaining_range_total': list([ + 629, + 'km', + ]), + }), + 'has_combustion_drivetrain': True, + 'has_electric_drivetrain': False, + 'is_charging_plan_supported': False, + 'is_lsc_enabled': True, + 'is_remote_charge_start_enabled': False, + 'is_remote_charge_stop_enabled': False, + 'is_remote_climate_start_enabled': True, + 'is_remote_climate_stop_enabled': True, + 'is_remote_horn_enabled': True, + 'is_remote_lights_enabled': True, + 'is_remote_lock_enabled': True, + 'is_remote_sendpoi_enabled': True, + 'is_remote_set_ac_limit_enabled': False, + 'is_remote_set_target_soc_enabled': False, + 'is_remote_unlock_enabled': True, + 'is_vehicle_active': False, + 'is_vehicle_tracking_enabled': True, + 'lsc_type': 'ACTIVATED', + 'mileage': list([ + 1121, + 'km', + ]), + 'name': 'M340i xDrive', + 'timestamp': '2023-01-04T14:57:06+00:00', + 'tires': dict({ + 'front_left': dict({ + 'current_pressure': 241, + 'manufacturing_week': '2021-10-04T00:00:00', + 'season': 2, + 'target_pressure': None, + }), + 'front_right': dict({ + 'current_pressure': 255, + 'manufacturing_week': '2019-06-10T00:00:00', + 'season': 2, + 'target_pressure': None, + }), + 'rear_left': dict({ + 'current_pressure': 324, + 'manufacturing_week': '2019-03-18T00:00:00', + 'season': 2, + 'target_pressure': None, + }), + 'rear_right': dict({ + 'current_pressure': 331, + 'manufacturing_week': '2019-03-18T00:00:00', + 'season': 2, + 'target_pressure': None, + }), + }), + 'vehicle_location': dict({ + 'account_region': 'row', + 'heading': '**REDACTED**', + 'location': dict({ + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'remote_service_position': None, + 'vehicle_update_timestamp': '2023-01-04T14:57:06+00:00', + }), + 'vin': '**REDACTED**', + }), dict({ 'available_attributes': list([ 'gps_position', @@ -1086,7 +2592,7 @@ }), 'headUnitType': 'NBT', 'hmiVersion': 'ID4', - 'lastFetched': '2022-07-10T09:25:53.104Z', + 'lastFetched': '2022-06-01T19:48:46.540Z', 'model': 'i3 (+ REX)', 'softwareVersionCurrent': dict({ 'iStep': 510, @@ -1215,8 +2721,6 @@ 'fetched_at': '2022-07-10T11:00:00+00:00', 'is_metric': True, 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, 'isPrimaryUser': True, 'mappingStatus': 'CONFIRMED', }), @@ -1333,7 +2837,6 @@ 'combustionFuelLevel': dict({ 'range': 105, 'remainingFuelLiters': 6, - 'remainingFuelPercent': 65, }), 'currentMileage': 137009, 'doorsState': dict({ @@ -1496,7 +2999,7 @@ 6, 'L', ]), - 'remaining_fuel_percent': 65, + 'remaining_fuel_percent': None, 'remaining_range_electric': list([ 174, 'km', @@ -1533,14 +3036,14 @@ 'km', ]), 'name': 'i3 (+ REX)', - 'timestamp': '2022-07-10T09:25:53+00:00', + 'timestamp': '2022-06-22T14:24:23+00:00', 'tires': None, 'vehicle_location': dict({ 'account_region': 'row', 'heading': None, 'location': None, 'remote_service_position': None, - 'vehicle_update_timestamp': '2022-07-10T09:25:53+00:00', + 'vehicle_update_timestamp': '2022-06-22T14:24:23+00:00', }), 'vin': '**REDACTED**', }), @@ -1548,6 +3051,55 @@ 'fingerprint': list([ dict({ 'content': list([ + dict({ + 'appVehicleType': 'DEMO', + 'attributes': dict({ + 'a4aType': 'BLUETOOTH', + 'bodyType': 'I20', + 'brand': 'BMW_I', + 'color': 4285537312, + 'countryOfOrigin': 'DE', + 'driveTrain': 'ELECTRIC', + 'driverGuideInfo': dict({ + 'androidAppScheme': 'com.bmwgroup.driversguide.row', + 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', + 'iosAppScheme': 'bmwdriversguide:///open', + 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', + }), + 'headUnitRaw': 'HU_MGU', + 'headUnitType': 'MGU', + 'hmiVersion': 'ID8', + 'lastFetched': '2023-01-04T14:57:06.019Z', + 'model': 'iX xDrive50', + 'softwareVersionCurrent': dict({ + 'iStep': 300, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S21A', + }), + 'softwareVersionExFactory': dict({ + 'iStep': 300, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S21A', + }), + 'telematicsUnit': 'WAVE01', + 'year': 2021, + }), + 'mappingInfo': dict({ + 'isAssociated': False, + 'isLmmEnabled': False, + 'isPrimaryUser': True, + 'lmmStatusReasons': list([ + ]), + 'mappingStatus': 'CONFIRMED', + }), + 'vin': '**REDACTED**', + }), dict({ 'appVehicleType': 'DEMO', 'attributes': dict({ @@ -1597,6 +3149,55 @@ }), 'vin': '**REDACTED**', }), + dict({ + 'appVehicleType': 'DEMO', + 'attributes': dict({ + 'a4aType': 'NOT_SUPPORTED', + 'bodyType': 'G20', + 'brand': 'BMW', + 'color': 4280233344, + 'countryOfOrigin': 'PT', + 'driveTrain': 'COMBUSTION', + 'driverGuideInfo': dict({ + 'androidAppScheme': 'com.bmwgroup.driversguide.row', + 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', + 'iosAppScheme': 'bmwdriversguide:///open', + 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', + }), + 'headUnitRaw': 'HU_MGU', + 'headUnitType': 'MGU', + 'hmiVersion': 'ID7', + 'lastFetched': '2023-01-04T14:57:06.019Z', + 'model': 'M340i xDrive', + 'softwareVersionCurrent': dict({ + 'iStep': 470, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S18A', + }), + 'softwareVersionExFactory': dict({ + 'iStep': 420, + 'puStep': dict({ + 'month': 7, + 'year': 20, + }), + 'seriesCluster': 'S18A', + }), + 'telematicsUnit': 'ATM2', + 'year': 2022, + }), + 'mappingInfo': dict({ + 'isAssociated': False, + 'isLmmEnabled': False, + 'isPrimaryUser': True, + 'lmmStatusReasons': list([ + ]), + 'mappingStatus': 'CONFIRMED', + }), + 'vin': '**REDACTED**', + }), dict({ 'appVehicleType': 'CONNECTED', 'attributes': dict({ @@ -1614,7 +3215,7 @@ }), 'headUnitType': 'NBT', 'hmiVersion': 'ID4', - 'lastFetched': '2022-07-10T09:25:53.104Z', + 'lastFetched': '2022-06-01T19:48:46.540Z', 'model': 'i3 (+ REX)', 'softwareVersionCurrent': dict({ 'iStep': 510, @@ -1635,8 +3236,6 @@ 'year': 2015, }), 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, 'isPrimaryUser': True, 'mappingStatus': 'CONFIRMED', }), @@ -1650,6 +3249,452 @@ ]), 'filename': 'mini-eadrax-vcs_v4_vehicles.json', }), + dict({ + 'content': dict({ + 'capabilities': dict({ + 'a4aType': 'BLUETOOTH', + 'checkSustainabilityDPP': False, + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'digitalKey': dict({ + 'bookedServicePackage': 'SMACC_2_UWB', + 'readerGraphics': 'readerGraphics', + 'state': 'ACTIVATED', + }), + 'horn': True, + 'inCarCamera': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': True, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': True, + 'isChargingLoudnessEnabled': True, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': True, + 'isChargingSettingsEnabled': True, + 'isChargingTargetSocEnabled': True, + 'isClimateTimerWeeklyActive': False, + 'isCustomerEsimSupported': True, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': True, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isPersonalPictureUploadSupported': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': True, + 'isSustainabilityAccumulatedViewEnabled': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remote360': True, + 'remoteChargingCommands': dict({ + 'chargingControl': list([ + 'START', + 'STOP', + ]), + 'flapControl': list([ + 'NOT_SUPPORTED', + ]), + 'plugControl': list([ + 'NOT_SUPPORTED', + ]), + }), + 'remoteSoftwareUpgrade': True, + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': True, + 'unlock': True, + 'vehicleFinder': True, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'IMMEDIATE_CHARGING', + 'chargingPreference': 'NO_PRESELECTION', + 'chargingSettings': dict({ + 'acCurrentLimit': 16, + 'hospitality': 'NO_ACTION', + 'idcc': 'UNLIMITED_LOUD', + 'targetSoc': 80, + }), + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + ]), + }), + 'checkControlMessages': list([ + dict({ + 'severity': 'LOW', + 'type': 'TIRE_PRESSURE', + }), + ]), + 'climateControlState': dict({ + 'activity': 'INACTIVE', + }), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': False, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'remainingFuelPercent': 10, + }), + 'currentMileage': 1121, + 'doorsState': dict({ + 'combinedSecurityState': 'LOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'UNKNOWN', + 'chargingLevelPercent': 70, + 'chargingStatus': 'CHARGING', + 'chargingTarget': 80, + 'isChargerConnected': True, + 'range': 340, + 'remainingChargingMinutes': 10, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2023-01-04T14:57:06.371Z', + 'lastUpdatedAt': '2023-01-04T14:57:06.383Z', + 'location': dict({ + 'address': dict({ + 'formatted': '**REDACTED**', + }), + 'coordinates': dict({ + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'heading': '**REDACTED**', + }), + 'range': 340, + 'requiredServices': list([ + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_REAR', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_FRONT', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'tireState': dict({ + 'frontLeft': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'targetPressure': 241, + 'wearStatus': 0, + }), + }), + 'frontRight': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'targetPressure': 241, + 'wearStatus': 0, + }), + }), + 'rearLeft': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 261, + 'pressureStatus': 0, + 'targetPressure': 269, + 'wearStatus': 0, + }), + }), + 'rearRight': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 269, + 'pressureStatus': 0, + 'targetPressure': 269, + 'wearStatus': 0, + }), + }), + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + }), + }), + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT01.json', + }), + dict({ + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'chargingMode': 'Sofort laden', + 'chargingModeSemantics': 'Sofort laden', + 'departureTimer': list([ + 'Aus', + ]), + 'departureTimerSemantics': 'Aus', + 'preconditionForDeparture': 'Aus', + 'showDepartureTimers': False, + }), + 'chargingFlap': dict({ + 'permanentlyUnlockLabel': 'Aus', + }), + 'chargingSettings': dict({ + 'acCurrentLimitLabel': '16A', + 'acCurrentLimitLabelSemantics': '16 Ampere', + 'chargingTargetLabel': '80%', + 'dcLoudnessLabel': 'Nicht begrenzt', + 'unlockCableAutomaticallyLabel': 'Aus', + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'NO_PRESELECTION', + 'endTimeSlot': '0001-01-01T00:00:00', + 'startTimeSlot': '0001-01-01T00:00:00', + 'type': 'CHARGING_IMMEDIATELY', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 1, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 2, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'chargingFlapDetail': dict({ + 'isPermanentlyUnlock': False, + }), + 'chargingSettingsDetail': dict({ + 'acLimit': dict({ + 'current': dict({ + 'unit': 'A', + 'value': 16, + }), + 'isUnlimited': False, + 'max': 32, + 'min': 6, + 'values': list([ + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 32, + ]), + }), + 'chargingTarget': 80, + 'dcLoudness': 'UNLIMITED_LOUD', + 'isUnlockCableActive': False, + 'minChargingTargetToWarning': 70, + }), + 'servicePack': 'WAVE_01', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBA0FINGERPRINT01.json', + }), dict({ 'content': dict({ 'capabilities': dict({ @@ -1697,16 +3742,6 @@ 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ - 'chargingControl': list([ - 'START', - 'STOP', - ]), - 'flapControl': list([ - 'NOT_SUPPORTED', - ]), - 'plugControl': list([ - 'NOT_SUPPORTED', - ]), }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, @@ -1779,7 +3814,8 @@ }), ]), 'climateControlState': dict({ - 'activity': 'STANDBY', + 'activity': 'HEATING', + 'remainingSeconds': 1790.846, }), 'climateTimers': list([ dict({ @@ -1832,9 +3868,9 @@ 'electricChargingState': dict({ 'chargingConnectionType': 'UNKNOWN', 'chargingLevelPercent': 80, - 'chargingStatus': 'CHARGING', + 'chargingStatus': 'INVALID', 'chargingTarget': 80, - 'isChargerConnected': True, + 'isChargerConnected': False, 'range': 472, 'remainingChargingMinutes': 10, }), @@ -1984,7 +4020,7 @@ }), }), }), - 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT01.json', + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT02.json', }), dict({ 'content': dict({ @@ -2087,7 +4123,294 @@ }), 'servicePack': 'WAVE_01', }), - 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBA0FINGERPRINT01.json', + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBA0FINGERPRINT02.json', + }), + dict({ + 'content': dict({ + 'capabilities': dict({ + 'a4aType': 'NOT_SUPPORTED', + 'checkSustainabilityDPP': False, + 'climateFunction': 'VENTILATION', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'digitalKey': dict({ + 'bookedServicePackage': 'SMACC_1_5', + 'readerGraphics': 'readerGraphics', + 'state': 'ACTIVATED', + }), + 'horn': True, + 'isBmwChargingSupported': False, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': False, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': False, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isClimateTimerWeeklyActive': False, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': False, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isPersonalPictureUploadSupported': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilityAccumulatedViewEnabled': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': True, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remote360': True, + 'remoteChargingCommands': dict({ + }), + 'remoteSoftwareUpgrade': True, + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexaSDK': False, + 'unlock': True, + 'vehicleFinder': True, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingMode': 'IMMEDIATE_CHARGING', + 'chargingPreference': 'NO_PRESELECTION', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 0, + }), + 'departureTimes': list([ + ]), + }), + 'checkControlMessages': list([ + dict({ + 'severity': 'LOW', + 'type': 'TIRE_PRESSURE', + }), + dict({ + 'severity': 'LOW', + 'type': 'ENGINE_OIL', + }), + ]), + 'climateControlState': dict({ + 'activity': 'INACTIVE', + }), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': False, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 629, + 'remainingFuelLiters': 40, + 'remainingFuelPercent': 80, + }), + 'currentMileage': 1121, + 'doorsState': dict({ + 'combinedSecurityState': 'LOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2023-01-04T14:57:06.336Z', + 'lastUpdatedAt': '2023-01-04T14:57:06.348Z', + 'location': dict({ + 'address': dict({ + 'formatted': '**REDACTED**', + }), + 'coordinates': dict({ + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'heading': '**REDACTED**', + }), + 'range': 629, + 'requiredServices': list([ + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'OIL', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_REAR', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_FRONT', + }), + ]), + 'tireState': dict({ + 'frontLeft': dict({ + 'details': dict({ + 'dimension': '225/35 R20 90Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461756', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'frontRight': dict({ + 'details': dict({ + 'dimension': '225/35 R20 90Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 2419, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461756', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 255, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'rearLeft': dict({ + 'details': dict({ + 'dimension': '255/30 R20 92Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 1219, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461757', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 324, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'rearRight': dict({ + 'details': dict({ + 'dimension': '255/30 R20 92Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 1219, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461757', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 331, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + }), + }), + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT03.json', }), dict({ 'content': dict({ @@ -2248,7 +4571,6 @@ 'combustionFuelLevel': dict({ 'range': 105, 'remainingFuelLiters': 6, - 'remainingFuelPercent': 65, }), 'currentMileage': 137009, 'doorsState': dict({ @@ -2308,7 +4630,7 @@ }), }), }), - 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT02.json', + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ 'content': dict({ @@ -2373,7 +4695,7 @@ }), 'servicePack': 'TCB1', }), - 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT02.json', + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', }), ]), 'info': dict({ @@ -2601,7 +4923,7 @@ }), 'headUnitType': 'NBT', 'hmiVersion': 'ID4', - 'lastFetched': '2022-07-10T09:25:53.104Z', + 'lastFetched': '2022-06-01T19:48:46.540Z', 'model': 'i3 (+ REX)', 'softwareVersionCurrent': dict({ 'iStep': 510, @@ -2730,8 +5052,6 @@ 'fetched_at': '2022-07-10T11:00:00+00:00', 'is_metric': True, 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, 'isPrimaryUser': True, 'mappingStatus': 'CONFIRMED', }), @@ -2848,7 +5168,6 @@ 'combustionFuelLevel': dict({ 'range': 105, 'remainingFuelLiters': 6, - 'remainingFuelPercent': 65, }), 'currentMileage': 137009, 'doorsState': dict({ @@ -3011,7 +5330,7 @@ 6, 'L', ]), - 'remaining_fuel_percent': 65, + 'remaining_fuel_percent': None, 'remaining_range_electric': list([ 174, 'km', @@ -3048,20 +5367,69 @@ 'km', ]), 'name': 'i3 (+ REX)', - 'timestamp': '2022-07-10T09:25:53+00:00', + 'timestamp': '2022-06-22T14:24:23+00:00', 'tires': None, 'vehicle_location': dict({ 'account_region': 'row', 'heading': None, 'location': None, 'remote_service_position': None, - 'vehicle_update_timestamp': '2022-07-10T09:25:53+00:00', + 'vehicle_update_timestamp': '2022-06-22T14:24:23+00:00', }), 'vin': '**REDACTED**', }), 'fingerprint': list([ dict({ 'content': list([ + dict({ + 'appVehicleType': 'DEMO', + 'attributes': dict({ + 'a4aType': 'BLUETOOTH', + 'bodyType': 'I20', + 'brand': 'BMW_I', + 'color': 4285537312, + 'countryOfOrigin': 'DE', + 'driveTrain': 'ELECTRIC', + 'driverGuideInfo': dict({ + 'androidAppScheme': 'com.bmwgroup.driversguide.row', + 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', + 'iosAppScheme': 'bmwdriversguide:///open', + 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', + }), + 'headUnitRaw': 'HU_MGU', + 'headUnitType': 'MGU', + 'hmiVersion': 'ID8', + 'lastFetched': '2023-01-04T14:57:06.019Z', + 'model': 'iX xDrive50', + 'softwareVersionCurrent': dict({ + 'iStep': 300, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S21A', + }), + 'softwareVersionExFactory': dict({ + 'iStep': 300, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S21A', + }), + 'telematicsUnit': 'WAVE01', + 'year': 2021, + }), + 'mappingInfo': dict({ + 'isAssociated': False, + 'isLmmEnabled': False, + 'isPrimaryUser': True, + 'lmmStatusReasons': list([ + ]), + 'mappingStatus': 'CONFIRMED', + }), + 'vin': '**REDACTED**', + }), dict({ 'appVehicleType': 'DEMO', 'attributes': dict({ @@ -3111,6 +5479,55 @@ }), 'vin': '**REDACTED**', }), + dict({ + 'appVehicleType': 'DEMO', + 'attributes': dict({ + 'a4aType': 'NOT_SUPPORTED', + 'bodyType': 'G20', + 'brand': 'BMW', + 'color': 4280233344, + 'countryOfOrigin': 'PT', + 'driveTrain': 'COMBUSTION', + 'driverGuideInfo': dict({ + 'androidAppScheme': 'com.bmwgroup.driversguide.row', + 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', + 'iosAppScheme': 'bmwdriversguide:///open', + 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', + }), + 'headUnitRaw': 'HU_MGU', + 'headUnitType': 'MGU', + 'hmiVersion': 'ID7', + 'lastFetched': '2023-01-04T14:57:06.019Z', + 'model': 'M340i xDrive', + 'softwareVersionCurrent': dict({ + 'iStep': 470, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S18A', + }), + 'softwareVersionExFactory': dict({ + 'iStep': 420, + 'puStep': dict({ + 'month': 7, + 'year': 20, + }), + 'seriesCluster': 'S18A', + }), + 'telematicsUnit': 'ATM2', + 'year': 2022, + }), + 'mappingInfo': dict({ + 'isAssociated': False, + 'isLmmEnabled': False, + 'isPrimaryUser': True, + 'lmmStatusReasons': list([ + ]), + 'mappingStatus': 'CONFIRMED', + }), + 'vin': '**REDACTED**', + }), dict({ 'appVehicleType': 'CONNECTED', 'attributes': dict({ @@ -3128,7 +5545,7 @@ }), 'headUnitType': 'NBT', 'hmiVersion': 'ID4', - 'lastFetched': '2022-07-10T09:25:53.104Z', + 'lastFetched': '2022-06-01T19:48:46.540Z', 'model': 'i3 (+ REX)', 'softwareVersionCurrent': dict({ 'iStep': 510, @@ -3149,8 +5566,6 @@ 'year': 2015, }), 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, 'isPrimaryUser': True, 'mappingStatus': 'CONFIRMED', }), @@ -3164,6 +5579,452 @@ ]), 'filename': 'mini-eadrax-vcs_v4_vehicles.json', }), + dict({ + 'content': dict({ + 'capabilities': dict({ + 'a4aType': 'BLUETOOTH', + 'checkSustainabilityDPP': False, + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'digitalKey': dict({ + 'bookedServicePackage': 'SMACC_2_UWB', + 'readerGraphics': 'readerGraphics', + 'state': 'ACTIVATED', + }), + 'horn': True, + 'inCarCamera': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': True, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': True, + 'isChargingLoudnessEnabled': True, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': True, + 'isChargingSettingsEnabled': True, + 'isChargingTargetSocEnabled': True, + 'isClimateTimerWeeklyActive': False, + 'isCustomerEsimSupported': True, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': True, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isPersonalPictureUploadSupported': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': True, + 'isSustainabilityAccumulatedViewEnabled': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remote360': True, + 'remoteChargingCommands': dict({ + 'chargingControl': list([ + 'START', + 'STOP', + ]), + 'flapControl': list([ + 'NOT_SUPPORTED', + ]), + 'plugControl': list([ + 'NOT_SUPPORTED', + ]), + }), + 'remoteSoftwareUpgrade': True, + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': True, + 'unlock': True, + 'vehicleFinder': True, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'IMMEDIATE_CHARGING', + 'chargingPreference': 'NO_PRESELECTION', + 'chargingSettings': dict({ + 'acCurrentLimit': 16, + 'hospitality': 'NO_ACTION', + 'idcc': 'UNLIMITED_LOUD', + 'targetSoc': 80, + }), + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + ]), + }), + 'checkControlMessages': list([ + dict({ + 'severity': 'LOW', + 'type': 'TIRE_PRESSURE', + }), + ]), + 'climateControlState': dict({ + 'activity': 'INACTIVE', + }), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': False, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'remainingFuelPercent': 10, + }), + 'currentMileage': 1121, + 'doorsState': dict({ + 'combinedSecurityState': 'LOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'UNKNOWN', + 'chargingLevelPercent': 70, + 'chargingStatus': 'CHARGING', + 'chargingTarget': 80, + 'isChargerConnected': True, + 'range': 340, + 'remainingChargingMinutes': 10, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2023-01-04T14:57:06.371Z', + 'lastUpdatedAt': '2023-01-04T14:57:06.383Z', + 'location': dict({ + 'address': dict({ + 'formatted': '**REDACTED**', + }), + 'coordinates': dict({ + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'heading': '**REDACTED**', + }), + 'range': 340, + 'requiredServices': list([ + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_REAR', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_FRONT', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'tireState': dict({ + 'frontLeft': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'targetPressure': 241, + 'wearStatus': 0, + }), + }), + 'frontRight': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'targetPressure': 241, + 'wearStatus': 0, + }), + }), + 'rearLeft': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 261, + 'pressureStatus': 0, + 'targetPressure': 269, + 'wearStatus': 0, + }), + }), + 'rearRight': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 269, + 'pressureStatus': 0, + 'targetPressure': 269, + 'wearStatus': 0, + }), + }), + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + }), + }), + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT01.json', + }), + dict({ + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'chargingMode': 'Sofort laden', + 'chargingModeSemantics': 'Sofort laden', + 'departureTimer': list([ + 'Aus', + ]), + 'departureTimerSemantics': 'Aus', + 'preconditionForDeparture': 'Aus', + 'showDepartureTimers': False, + }), + 'chargingFlap': dict({ + 'permanentlyUnlockLabel': 'Aus', + }), + 'chargingSettings': dict({ + 'acCurrentLimitLabel': '16A', + 'acCurrentLimitLabelSemantics': '16 Ampere', + 'chargingTargetLabel': '80%', + 'dcLoudnessLabel': 'Nicht begrenzt', + 'unlockCableAutomaticallyLabel': 'Aus', + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'NO_PRESELECTION', + 'endTimeSlot': '0001-01-01T00:00:00', + 'startTimeSlot': '0001-01-01T00:00:00', + 'type': 'CHARGING_IMMEDIATELY', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 1, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 2, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'chargingFlapDetail': dict({ + 'isPermanentlyUnlock': False, + }), + 'chargingSettingsDetail': dict({ + 'acLimit': dict({ + 'current': dict({ + 'unit': 'A', + 'value': 16, + }), + 'isUnlimited': False, + 'max': 32, + 'min': 6, + 'values': list([ + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 32, + ]), + }), + 'chargingTarget': 80, + 'dcLoudness': 'UNLIMITED_LOUD', + 'isUnlockCableActive': False, + 'minChargingTargetToWarning': 70, + }), + 'servicePack': 'WAVE_01', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBA0FINGERPRINT01.json', + }), dict({ 'content': dict({ 'capabilities': dict({ @@ -3211,16 +6072,6 @@ 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ - 'chargingControl': list([ - 'START', - 'STOP', - ]), - 'flapControl': list([ - 'NOT_SUPPORTED', - ]), - 'plugControl': list([ - 'NOT_SUPPORTED', - ]), }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, @@ -3293,7 +6144,8 @@ }), ]), 'climateControlState': dict({ - 'activity': 'STANDBY', + 'activity': 'HEATING', + 'remainingSeconds': 1790.846, }), 'climateTimers': list([ dict({ @@ -3346,9 +6198,9 @@ 'electricChargingState': dict({ 'chargingConnectionType': 'UNKNOWN', 'chargingLevelPercent': 80, - 'chargingStatus': 'CHARGING', + 'chargingStatus': 'INVALID', 'chargingTarget': 80, - 'isChargerConnected': True, + 'isChargerConnected': False, 'range': 472, 'remainingChargingMinutes': 10, }), @@ -3498,7 +6350,7 @@ }), }), }), - 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT01.json', + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT02.json', }), dict({ 'content': dict({ @@ -3601,7 +6453,294 @@ }), 'servicePack': 'WAVE_01', }), - 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBA0FINGERPRINT01.json', + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBA0FINGERPRINT02.json', + }), + dict({ + 'content': dict({ + 'capabilities': dict({ + 'a4aType': 'NOT_SUPPORTED', + 'checkSustainabilityDPP': False, + 'climateFunction': 'VENTILATION', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'digitalKey': dict({ + 'bookedServicePackage': 'SMACC_1_5', + 'readerGraphics': 'readerGraphics', + 'state': 'ACTIVATED', + }), + 'horn': True, + 'isBmwChargingSupported': False, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': False, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': False, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isClimateTimerWeeklyActive': False, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': False, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isPersonalPictureUploadSupported': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilityAccumulatedViewEnabled': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': True, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remote360': True, + 'remoteChargingCommands': dict({ + }), + 'remoteSoftwareUpgrade': True, + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexaSDK': False, + 'unlock': True, + 'vehicleFinder': True, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingMode': 'IMMEDIATE_CHARGING', + 'chargingPreference': 'NO_PRESELECTION', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 0, + }), + 'departureTimes': list([ + ]), + }), + 'checkControlMessages': list([ + dict({ + 'severity': 'LOW', + 'type': 'TIRE_PRESSURE', + }), + dict({ + 'severity': 'LOW', + 'type': 'ENGINE_OIL', + }), + ]), + 'climateControlState': dict({ + 'activity': 'INACTIVE', + }), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': False, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 629, + 'remainingFuelLiters': 40, + 'remainingFuelPercent': 80, + }), + 'currentMileage': 1121, + 'doorsState': dict({ + 'combinedSecurityState': 'LOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2023-01-04T14:57:06.336Z', + 'lastUpdatedAt': '2023-01-04T14:57:06.348Z', + 'location': dict({ + 'address': dict({ + 'formatted': '**REDACTED**', + }), + 'coordinates': dict({ + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'heading': '**REDACTED**', + }), + 'range': 629, + 'requiredServices': list([ + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'OIL', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_REAR', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_FRONT', + }), + ]), + 'tireState': dict({ + 'frontLeft': dict({ + 'details': dict({ + 'dimension': '225/35 R20 90Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461756', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'frontRight': dict({ + 'details': dict({ + 'dimension': '225/35 R20 90Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 2419, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461756', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 255, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'rearLeft': dict({ + 'details': dict({ + 'dimension': '255/30 R20 92Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 1219, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461757', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 324, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'rearRight': dict({ + 'details': dict({ + 'dimension': '255/30 R20 92Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 1219, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461757', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 331, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + }), + }), + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT03.json', }), dict({ 'content': dict({ @@ -3762,7 +6901,6 @@ 'combustionFuelLevel': dict({ 'range': 105, 'remainingFuelLiters': 6, - 'remainingFuelPercent': 65, }), 'currentMileage': 137009, 'doorsState': dict({ @@ -3822,7 +6960,7 @@ }), }), }), - 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT02.json', + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ 'content': dict({ @@ -3887,7 +7025,7 @@ }), 'servicePack': 'TCB1', }), - 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT02.json', + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', }), ]), 'info': dict({ @@ -3905,6 +7043,55 @@ 'fingerprint': list([ dict({ 'content': list([ + dict({ + 'appVehicleType': 'DEMO', + 'attributes': dict({ + 'a4aType': 'BLUETOOTH', + 'bodyType': 'I20', + 'brand': 'BMW_I', + 'color': 4285537312, + 'countryOfOrigin': 'DE', + 'driveTrain': 'ELECTRIC', + 'driverGuideInfo': dict({ + 'androidAppScheme': 'com.bmwgroup.driversguide.row', + 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', + 'iosAppScheme': 'bmwdriversguide:///open', + 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', + }), + 'headUnitRaw': 'HU_MGU', + 'headUnitType': 'MGU', + 'hmiVersion': 'ID8', + 'lastFetched': '2023-01-04T14:57:06.019Z', + 'model': 'iX xDrive50', + 'softwareVersionCurrent': dict({ + 'iStep': 300, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S21A', + }), + 'softwareVersionExFactory': dict({ + 'iStep': 300, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S21A', + }), + 'telematicsUnit': 'WAVE01', + 'year': 2021, + }), + 'mappingInfo': dict({ + 'isAssociated': False, + 'isLmmEnabled': False, + 'isPrimaryUser': True, + 'lmmStatusReasons': list([ + ]), + 'mappingStatus': 'CONFIRMED', + }), + 'vin': '**REDACTED**', + }), dict({ 'appVehicleType': 'DEMO', 'attributes': dict({ @@ -3954,6 +7141,55 @@ }), 'vin': '**REDACTED**', }), + dict({ + 'appVehicleType': 'DEMO', + 'attributes': dict({ + 'a4aType': 'NOT_SUPPORTED', + 'bodyType': 'G20', + 'brand': 'BMW', + 'color': 4280233344, + 'countryOfOrigin': 'PT', + 'driveTrain': 'COMBUSTION', + 'driverGuideInfo': dict({ + 'androidAppScheme': 'com.bmwgroup.driversguide.row', + 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', + 'iosAppScheme': 'bmwdriversguide:///open', + 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', + }), + 'headUnitRaw': 'HU_MGU', + 'headUnitType': 'MGU', + 'hmiVersion': 'ID7', + 'lastFetched': '2023-01-04T14:57:06.019Z', + 'model': 'M340i xDrive', + 'softwareVersionCurrent': dict({ + 'iStep': 470, + 'puStep': dict({ + 'month': 7, + 'year': 21, + }), + 'seriesCluster': 'S18A', + }), + 'softwareVersionExFactory': dict({ + 'iStep': 420, + 'puStep': dict({ + 'month': 7, + 'year': 20, + }), + 'seriesCluster': 'S18A', + }), + 'telematicsUnit': 'ATM2', + 'year': 2022, + }), + 'mappingInfo': dict({ + 'isAssociated': False, + 'isLmmEnabled': False, + 'isPrimaryUser': True, + 'lmmStatusReasons': list([ + ]), + 'mappingStatus': 'CONFIRMED', + }), + 'vin': '**REDACTED**', + }), dict({ 'appVehicleType': 'CONNECTED', 'attributes': dict({ @@ -3971,7 +7207,7 @@ }), 'headUnitType': 'NBT', 'hmiVersion': 'ID4', - 'lastFetched': '2022-07-10T09:25:53.104Z', + 'lastFetched': '2022-06-01T19:48:46.540Z', 'model': 'i3 (+ REX)', 'softwareVersionCurrent': dict({ 'iStep': 510, @@ -3992,8 +7228,6 @@ 'year': 2015, }), 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, 'isPrimaryUser': True, 'mappingStatus': 'CONFIRMED', }), @@ -4007,6 +7241,452 @@ ]), 'filename': 'mini-eadrax-vcs_v4_vehicles.json', }), + dict({ + 'content': dict({ + 'capabilities': dict({ + 'a4aType': 'BLUETOOTH', + 'checkSustainabilityDPP': False, + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'digitalKey': dict({ + 'bookedServicePackage': 'SMACC_2_UWB', + 'readerGraphics': 'readerGraphics', + 'state': 'ACTIVATED', + }), + 'horn': True, + 'inCarCamera': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': True, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': True, + 'isChargingLoudnessEnabled': True, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': True, + 'isChargingSettingsEnabled': True, + 'isChargingTargetSocEnabled': True, + 'isClimateTimerWeeklyActive': False, + 'isCustomerEsimSupported': True, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': True, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isPersonalPictureUploadSupported': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': True, + 'isSustainabilityAccumulatedViewEnabled': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remote360': True, + 'remoteChargingCommands': dict({ + 'chargingControl': list([ + 'START', + 'STOP', + ]), + 'flapControl': list([ + 'NOT_SUPPORTED', + ]), + 'plugControl': list([ + 'NOT_SUPPORTED', + ]), + }), + 'remoteSoftwareUpgrade': True, + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexaSDK': False, + 'surroundViewRecorder': True, + 'unlock': True, + 'vehicleFinder': True, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'IMMEDIATE_CHARGING', + 'chargingPreference': 'NO_PRESELECTION', + 'chargingSettings': dict({ + 'acCurrentLimit': 16, + 'hospitality': 'NO_ACTION', + 'idcc': 'UNLIMITED_LOUD', + 'targetSoc': 80, + }), + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timeStamp': dict({ + 'hour': 0, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + ]), + }), + 'checkControlMessages': list([ + dict({ + 'severity': 'LOW', + 'type': 'TIRE_PRESSURE', + }), + ]), + 'climateControlState': dict({ + 'activity': 'INACTIVE', + }), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': False, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'remainingFuelPercent': 10, + }), + 'currentMileage': 1121, + 'doorsState': dict({ + 'combinedSecurityState': 'LOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'UNKNOWN', + 'chargingLevelPercent': 70, + 'chargingStatus': 'CHARGING', + 'chargingTarget': 80, + 'isChargerConnected': True, + 'range': 340, + 'remainingChargingMinutes': 10, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2023-01-04T14:57:06.371Z', + 'lastUpdatedAt': '2023-01-04T14:57:06.383Z', + 'location': dict({ + 'address': dict({ + 'formatted': '**REDACTED**', + }), + 'coordinates': dict({ + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'heading': '**REDACTED**', + }), + 'range': 340, + 'requiredServices': list([ + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_REAR', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_FRONT', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'tireState': dict({ + 'frontLeft': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'targetPressure': 241, + 'wearStatus': 0, + }), + }), + 'frontRight': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'targetPressure': 241, + 'wearStatus': 0, + }), + }), + 'rearLeft': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 261, + 'pressureStatus': 0, + 'targetPressure': 269, + 'wearStatus': 0, + }), + }), + 'rearRight': dict({ + 'details': dict({ + 'dimension': '275/40 R22 107Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-04-20T00:00:00.000Z', + 'partNumber': '5A401A1', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 269, + 'pressureStatus': 0, + 'targetPressure': 269, + 'wearStatus': 0, + }), + }), + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + }), + }), + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT01.json', + }), + dict({ + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'chargingMode': 'Sofort laden', + 'chargingModeSemantics': 'Sofort laden', + 'departureTimer': list([ + 'Aus', + ]), + 'departureTimerSemantics': 'Aus', + 'preconditionForDeparture': 'Aus', + 'showDepartureTimers': False, + }), + 'chargingFlap': dict({ + 'permanentlyUnlockLabel': 'Aus', + }), + 'chargingSettings': dict({ + 'acCurrentLimitLabel': '16A', + 'acCurrentLimitLabelSemantics': '16 Ampere', + 'chargingTargetLabel': '80%', + 'dcLoudnessLabel': 'Nicht begrenzt', + 'unlockCableAutomaticallyLabel': 'Aus', + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'NO_PRESELECTION', + 'endTimeSlot': '0001-01-01T00:00:00', + 'startTimeSlot': '0001-01-01T00:00:00', + 'type': 'CHARGING_IMMEDIATELY', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 1, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 2, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'chargingFlapDetail': dict({ + 'isPermanentlyUnlock': False, + }), + 'chargingSettingsDetail': dict({ + 'acLimit': dict({ + 'current': dict({ + 'unit': 'A', + 'value': 16, + }), + 'isUnlimited': False, + 'max': 32, + 'min': 6, + 'values': list([ + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 20, + 32, + ]), + }), + 'chargingTarget': 80, + 'dcLoudness': 'UNLIMITED_LOUD', + 'isUnlockCableActive': False, + 'minChargingTargetToWarning': 70, + }), + 'servicePack': 'WAVE_01', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBA0FINGERPRINT01.json', + }), dict({ 'content': dict({ 'capabilities': dict({ @@ -4054,16 +7734,6 @@ 'lock': True, 'remote360': True, 'remoteChargingCommands': dict({ - 'chargingControl': list([ - 'START', - 'STOP', - ]), - 'flapControl': list([ - 'NOT_SUPPORTED', - ]), - 'plugControl': list([ - 'NOT_SUPPORTED', - ]), }), 'remoteSoftwareUpgrade': True, 'sendPoi': True, @@ -4136,7 +7806,8 @@ }), ]), 'climateControlState': dict({ - 'activity': 'STANDBY', + 'activity': 'HEATING', + 'remainingSeconds': 1790.846, }), 'climateTimers': list([ dict({ @@ -4189,9 +7860,9 @@ 'electricChargingState': dict({ 'chargingConnectionType': 'UNKNOWN', 'chargingLevelPercent': 80, - 'chargingStatus': 'CHARGING', + 'chargingStatus': 'INVALID', 'chargingTarget': 80, - 'isChargerConnected': True, + 'isChargerConnected': False, 'range': 472, 'remainingChargingMinutes': 10, }), @@ -4341,7 +8012,7 @@ }), }), }), - 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT01.json', + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT02.json', }), dict({ 'content': dict({ @@ -4444,7 +8115,294 @@ }), 'servicePack': 'WAVE_01', }), - 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBA0FINGERPRINT01.json', + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBA0FINGERPRINT02.json', + }), + dict({ + 'content': dict({ + 'capabilities': dict({ + 'a4aType': 'NOT_SUPPORTED', + 'checkSustainabilityDPP': False, + 'climateFunction': 'VENTILATION', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'digitalKey': dict({ + 'bookedServicePackage': 'SMACC_1_5', + 'readerGraphics': 'readerGraphics', + 'state': 'ACTIVATED', + }), + 'horn': True, + 'isBmwChargingSupported': False, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': False, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': False, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isClimateTimerWeeklyActive': False, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': False, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isPersonalPictureUploadSupported': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilityAccumulatedViewEnabled': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': True, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remote360': True, + 'remoteChargingCommands': dict({ + }), + 'remoteSoftwareUpgrade': True, + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'speechThirdPartyAlexa': False, + 'speechThirdPartyAlexaSDK': False, + 'unlock': True, + 'vehicleFinder': True, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingMode': 'IMMEDIATE_CHARGING', + 'chargingPreference': 'NO_PRESELECTION', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 0, + }), + 'departureTimes': list([ + ]), + }), + 'checkControlMessages': list([ + dict({ + 'severity': 'LOW', + 'type': 'TIRE_PRESSURE', + }), + dict({ + 'severity': 'LOW', + 'type': 'ENGINE_OIL', + }), + ]), + 'climateControlState': dict({ + 'activity': 'INACTIVE', + }), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': False, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 0, + 'minute': 0, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 629, + 'remainingFuelLiters': 40, + 'remainingFuelPercent': 80, + }), + 'currentMileage': 1121, + 'doorsState': dict({ + 'combinedSecurityState': 'LOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2023-01-04T14:57:06.336Z', + 'lastUpdatedAt': '2023-01-04T14:57:06.348Z', + 'location': dict({ + 'address': dict({ + 'formatted': '**REDACTED**', + }), + 'coordinates': dict({ + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), + 'heading': '**REDACTED**', + }), + 'range': 629, + 'requiredServices': list([ + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'OIL', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + dict({ + 'dateTime': '2024-12-01T00:00:00.000Z', + 'description': '', + 'mileage': 50000, + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_REAR', + }), + dict({ + 'status': 'OK', + 'type': 'TIRE_WEAR_FRONT', + }), + ]), + 'tireState': dict({ + 'frontLeft': dict({ + 'details': dict({ + 'dimension': '225/35 R20 90Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 4021, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461756', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 241, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'frontRight': dict({ + 'details': dict({ + 'dimension': '225/35 R20 90Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 2419, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461756', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 255, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'rearLeft': dict({ + 'details': dict({ + 'dimension': '255/30 R20 92Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 1219, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461757', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 324, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + 'rearRight': dict({ + 'details': dict({ + 'dimension': '255/30 R20 92Y XL', + 'isOptimizedForOemBmw': True, + 'manufacturer': 'Pirelli', + 'manufacturingWeek': 1219, + 'mountingDate': '2022-03-07T00:00:00.000Z', + 'partNumber': '2461757', + 'season': 2, + 'speedClassification': dict({ + 'atLeast': False, + 'speedRating': 300, + }), + 'treadDesign': 'P-ZERO', + }), + 'status': dict({ + 'currentPressure': 331, + 'pressureStatus': 0, + 'wearStatus': 0, + }), + }), + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + }), + }), + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBA0FINGERPRINT03.json', }), dict({ 'content': dict({ @@ -4605,7 +8563,6 @@ 'combustionFuelLevel': dict({ 'range': 105, 'remainingFuelLiters': 6, - 'remainingFuelPercent': 65, }), 'currentMileage': 137009, 'doorsState': dict({ @@ -4665,7 +8622,7 @@ }), }), }), - 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT02.json', + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ 'content': dict({ @@ -4730,7 +8687,7 @@ }), 'servicePack': 'TCB1', }), - 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT02.json', + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', }), ]), 'info': dict({ diff --git a/tests/components/bmw_connected_drive/snapshots/test_number.ambr b/tests/components/bmw_connected_drive/snapshots/test_number.ambr index a99d8bb3e0f..ab3668664f4 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_number.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_number.ambr @@ -1,6 +1,23 @@ # serializer version: 1 # name: test_entity_state_attrs list([ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'device_class': 'battery', + 'friendly_name': 'iX xDrive50 Target SoC', + 'icon': 'mdi:battery-charging-medium', + 'max': 100.0, + 'min': 20.0, + 'mode': , + 'step': 5.0, + }), + 'context': , + 'entity_id': 'number.ix_xdrive50_target_soc', + 'last_changed': , + 'last_updated': , + 'state': '80', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', diff --git a/tests/components/bmw_connected_drive/snapshots/test_select.ambr b/tests/components/bmw_connected_drive/snapshots/test_select.ambr index 522e74c61e2..cac71c3049d 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_select.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_select.ambr @@ -1,6 +1,50 @@ # serializer version: 1 # name: test_entity_state_attrs list([ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 AC Charging Limit', + 'icon': 'mdi:current-ac', + 'options': list([ + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '20', + '32', + ]), + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'select.ix_xdrive50_ac_charging_limit', + 'last_changed': , + 'last_updated': , + 'state': '16', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Charging Mode', + 'icon': 'mdi:vector-point-select', + 'options': list([ + 'IMMEDIATE_CHARGING', + 'DELAYED_CHARGING', + ]), + }), + 'context': , + 'entity_id': 'select.ix_xdrive50_charging_mode', + 'last_changed': , + 'last_updated': , + 'state': 'IMMEDIATE_CHARGING', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', diff --git a/tests/components/bmw_connected_drive/snapshots/test_switch.ambr b/tests/components/bmw_connected_drive/snapshots/test_switch.ambr index de5a44637c3..974f3d785ff 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_switch.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_switch.ambr @@ -1,6 +1,30 @@ # serializer version: 1 # name: test_entity_state_attrs list([ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Climate', + 'icon': 'mdi:fan', + }), + 'context': , + 'entity_id': 'switch.ix_xdrive50_climate', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Charging', + 'icon': 'mdi:ev-station', + }), + 'context': , + 'entity_id': 'switch.ix_xdrive50_charging', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -11,19 +35,19 @@ 'entity_id': 'switch.i4_edrive40_climate', 'last_changed': , 'last_updated': , - 'state': 'off', + 'state': 'on', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Charging', - 'icon': 'mdi:ev-station', + 'friendly_name': 'M340i xDrive Climate', + 'icon': 'mdi:fan', }), 'context': , - 'entity_id': 'switch.i4_edrive40_charging', + 'entity_id': 'switch.m340i_xdrive_climate', 'last_changed': , 'last_updated': , - 'state': 'on', + 'state': 'off', }), ]) # --- diff --git a/tests/components/bmw_connected_drive/test_button.py b/tests/components/bmw_connected_drive/test_button.py index 16803756702..9cea5f2fd91 100644 --- a/tests/components/bmw_connected_drive/test_button.py +++ b/tests/components/bmw_connected_drive/test_button.py @@ -7,13 +7,10 @@ import pytest import respx from syrupy.assertion import SnapshotAssertion -from homeassistant.components.bmw_connected_drive.coordinator import ( - BMWDataUpdateCoordinator, -) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from . import setup_mocked_integration +from . import check_remote_service_call, setup_mocked_integration async def test_entity_state_attrs( @@ -31,25 +28,22 @@ async def test_entity_state_attrs( @pytest.mark.parametrize( - ("entity_id"), + ("entity_id", "remote_service"), [ - ("button.i4_edrive40_flash_lights"), - ("button.i4_edrive40_sound_horn"), - ("button.i4_edrive40_activate_air_conditioning"), - ("button.i4_edrive40_deactivate_air_conditioning"), - ("button.i4_edrive40_find_vehicle"), + ("button.i4_edrive40_flash_lights", "light-flash"), + ("button.i4_edrive40_sound_horn", "horn-blow"), ], ) -async def test_update_triggers_success( +async def test_service_call_success( hass: HomeAssistant, entity_id: str, + remote_service: str, bmw_fixture: respx.Router, ) -> None: - """Test button press.""" + """Test successful button press.""" # Setup component assert await setup_mocked_integration(hass) - BMWDataUpdateCoordinator.async_update_listeners.reset_mock() # Test await hass.services.async_call( @@ -58,20 +52,20 @@ async def test_update_triggers_success( blocking=True, target={"entity_id": entity_id}, ) - assert RemoteServices.trigger_remote_service.call_count == 1 - assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 1 + check_remote_service_call(bmw_fixture, remote_service) -async def test_update_failed( +async def test_service_call_fail( hass: HomeAssistant, bmw_fixture: respx.Router, monkeypatch: pytest.MonkeyPatch, ) -> None: - """Test button press.""" + """Test failed button press.""" # Setup component assert await setup_mocked_integration(hass) - BMWDataUpdateCoordinator.async_update_listeners.reset_mock() + entity_id = "switch.i4_edrive40_climate" + old_value = hass.states.get(entity_id).state # Setup exception monkeypatch.setattr( @@ -86,7 +80,115 @@ async def test_update_failed( "button", "press", blocking=True, - target={"entity_id": "button.i4_edrive40_flash_lights"}, + target={"entity_id": "button.i4_edrive40_activate_air_conditioning"}, ) - assert RemoteServices.trigger_remote_service.call_count == 1 - assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 0 + assert hass.states.get(entity_id).state == old_value + + +@pytest.mark.parametrize( + ( + "entity_id", + "state_entity_id", + "new_value", + "old_value", + "remote_service", + "remote_service_params", + ), + [ + ( + "button.i4_edrive40_activate_air_conditioning", + "switch.i4_edrive40_climate", + "on", + "off", + "climate-now", + {"action": "START"}, + ), + ( + "button.i4_edrive40_deactivate_air_conditioning", + "switch.i4_edrive40_climate", + "off", + "on", + "climate-now", + {"action": "STOP"}, + ), + ( + "button.i4_edrive40_find_vehicle", + "device_tracker.i4_edrive40", + "not_home", + "home", + "vehicle-finder", + {}, + ), + ], +) +async def test_service_call_success_state_change( + hass: HomeAssistant, + entity_id: str, + state_entity_id: str, + new_value: str, + old_value: str, + remote_service: str, + remote_service_params: dict, + bmw_fixture: respx.Router, +) -> None: + """Test successful button press with state change.""" + + # Setup component + assert await setup_mocked_integration(hass) + hass.states.async_set(state_entity_id, old_value) + assert hass.states.get(state_entity_id).state == old_value + + # Test + await hass.services.async_call( + "button", + "press", + blocking=True, + target={"entity_id": entity_id}, + ) + check_remote_service_call(bmw_fixture, remote_service, remote_service_params) + assert hass.states.get(state_entity_id).state == new_value + + +@pytest.mark.parametrize( + ("entity_id", "state_entity_id", "new_attrs", "old_attrs"), + [ + ( + "button.i4_edrive40_find_vehicle", + "device_tracker.i4_edrive40", + {"latitude": 123.456, "longitude": 34.5678, "direction": 121}, + {"latitude": 48.177334, "longitude": 11.556274, "direction": 180}, + ), + ], +) +async def test_service_call_success_attr_change( + hass: HomeAssistant, + entity_id: str, + state_entity_id: str, + new_attrs: dict, + old_attrs: dict, + bmw_fixture: respx.Router, +) -> None: + """Test successful button press with attribute change.""" + + # Setup component + assert await setup_mocked_integration(hass) + + assert { + k: v + for k, v in hass.states.get(state_entity_id).attributes.items() + if k in old_attrs + } == old_attrs + + # Test + await hass.services.async_call( + "button", + "press", + blocking=True, + target={"entity_id": entity_id}, + ) + check_remote_service_call(bmw_fixture) + assert { + k: v + for k, v in hass.states.get(state_entity_id).attributes.items() + if k in new_attrs + } == new_attrs diff --git a/tests/components/bmw_connected_drive/test_number.py b/tests/components/bmw_connected_drive/test_number.py index d8cd5d47867..bcd880fa0a6 100644 --- a/tests/components/bmw_connected_drive/test_number.py +++ b/tests/components/bmw_connected_drive/test_number.py @@ -7,13 +7,10 @@ import pytest import respx from syrupy.assertion import SnapshotAssertion -from homeassistant.components.bmw_connected_drive.coordinator import ( - BMWDataUpdateCoordinator, -) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from . import setup_mocked_integration +from . import check_remote_service_call, setup_mocked_integration async def test_entity_state_attrs( @@ -31,33 +28,36 @@ async def test_entity_state_attrs( @pytest.mark.parametrize( - ("entity_id", "value"), + ("entity_id", "new_value", "old_value", "remote_service"), [ - ("number.i4_edrive40_target_soc", "80"), + ("number.i4_edrive40_target_soc", "80", "100", "charging-settings"), ], ) -async def test_update_triggers_success( +async def test_service_call_success( hass: HomeAssistant, entity_id: str, - value: str, + new_value: str, + old_value: str, + remote_service: str, bmw_fixture: respx.Router, ) -> None: - """Test allowed values for number inputs.""" + """Test successful number change.""" # Setup component assert await setup_mocked_integration(hass) - BMWDataUpdateCoordinator.async_update_listeners.reset_mock() + hass.states.async_set(entity_id, old_value) + assert hass.states.get(entity_id).state == old_value # Test await hass.services.async_call( "number", "set_value", - service_data={"value": value}, + service_data={"value": new_value}, blocking=True, target={"entity_id": entity_id}, ) - assert RemoteServices.trigger_remote_service.call_count == 1 - assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 1 + check_remote_service_call(bmw_fixture, remote_service) + assert hass.states.get(entity_id).state == new_value @pytest.mark.parametrize( @@ -66,7 +66,7 @@ async def test_update_triggers_success( ("number.i4_edrive40_target_soc", "81"), ], ) -async def test_update_triggers_fail( +async def test_service_call_invalid_input( hass: HomeAssistant, entity_id: str, value: str, @@ -76,7 +76,7 @@ async def test_update_triggers_fail( # Setup component assert await setup_mocked_integration(hass) - BMWDataUpdateCoordinator.async_update_listeners.reset_mock() + old_value = hass.states.get(entity_id).state # Test with pytest.raises(ValueError): @@ -87,8 +87,7 @@ async def test_update_triggers_fail( blocking=True, target={"entity_id": entity_id}, ) - assert RemoteServices.trigger_remote_service.call_count == 0 - assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 0 + assert hass.states.get(entity_id).state == old_value @pytest.mark.parametrize( @@ -99,18 +98,19 @@ async def test_update_triggers_fail( (ValueError, ValueError), ], ) -async def test_update_triggers_exceptions( +async def test_service_call_fail( hass: HomeAssistant, raised: Exception, expected: Exception, bmw_fixture: respx.Router, monkeypatch: pytest.MonkeyPatch, ) -> None: - """Test not allowed values for number inputs.""" + """Test exception handling.""" # Setup component assert await setup_mocked_integration(hass) - BMWDataUpdateCoordinator.async_update_listeners.reset_mock() + entity_id = "number.i4_edrive40_target_soc" + old_value = hass.states.get(entity_id).state # Setup exception monkeypatch.setattr( @@ -126,7 +126,6 @@ async def test_update_triggers_exceptions( "set_value", service_data={"value": "80"}, blocking=True, - target={"entity_id": "number.i4_edrive40_target_soc"}, + target={"entity_id": entity_id}, ) - assert RemoteServices.trigger_remote_service.call_count == 1 - assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 0 + assert hass.states.get(entity_id).state == old_value diff --git a/tests/components/bmw_connected_drive/test_select.py b/tests/components/bmw_connected_drive/test_select.py index 97da6f81d6e..2dbe66139b2 100644 --- a/tests/components/bmw_connected_drive/test_select.py +++ b/tests/components/bmw_connected_drive/test_select.py @@ -7,13 +7,10 @@ import pytest import respx from syrupy.assertion import SnapshotAssertion -from homeassistant.components.bmw_connected_drive.coordinator import ( - BMWDataUpdateCoordinator, -) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from . import setup_mocked_integration +from . import check_remote_service_call, setup_mocked_integration async def test_entity_state_attrs( @@ -31,44 +28,58 @@ async def test_entity_state_attrs( @pytest.mark.parametrize( - ("entity_id", "value"), + ("entity_id", "new_value", "old_value", "remote_service"), [ - ("select.i3_rex_charging_mode", "IMMEDIATE_CHARGING"), - ("select.i4_edrive40_ac_charging_limit", "16"), - ("select.i4_edrive40_charging_mode", "DELAYED_CHARGING"), + ( + "select.i3_rex_charging_mode", + "IMMEDIATE_CHARGING", + "DELAYED_CHARGING", + "charging-profile", + ), + ("select.i4_edrive40_ac_charging_limit", "12", "16", "charging-settings"), + ( + "select.i4_edrive40_charging_mode", + "DELAYED_CHARGING", + "IMMEDIATE_CHARGING", + "charging-profile", + ), ], ) -async def test_update_triggers_success( +async def test_service_call_success( hass: HomeAssistant, entity_id: str, - value: str, + new_value: str, + old_value: str, + remote_service: str, bmw_fixture: respx.Router, ) -> None: - """Test allowed values for select inputs.""" + """Test successful input change.""" # Setup component assert await setup_mocked_integration(hass) - BMWDataUpdateCoordinator.async_update_listeners.reset_mock() + hass.states.async_set(entity_id, old_value) + assert hass.states.get(entity_id).state == old_value # Test await hass.services.async_call( "select", "select_option", - service_data={"option": value}, + service_data={"option": new_value}, blocking=True, target={"entity_id": entity_id}, ) - assert RemoteServices.trigger_remote_service.call_count == 1 - assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 1 + check_remote_service_call(bmw_fixture, remote_service) + assert hass.states.get(entity_id).state == new_value @pytest.mark.parametrize( ("entity_id", "value"), [ ("select.i4_edrive40_ac_charging_limit", "17"), + ("select.i4_edrive40_charging_mode", "BONKERS_MODE"), ], ) -async def test_update_triggers_fail( +async def test_service_call_invalid_input( hass: HomeAssistant, entity_id: str, value: str, @@ -78,7 +89,7 @@ async def test_update_triggers_fail( # Setup component assert await setup_mocked_integration(hass) - BMWDataUpdateCoordinator.async_update_listeners.reset_mock() + old_value = hass.states.get(entity_id).state # Test with pytest.raises(ValueError): @@ -89,8 +100,7 @@ async def test_update_triggers_fail( blocking=True, target={"entity_id": entity_id}, ) - assert RemoteServices.trigger_remote_service.call_count == 0 - assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 0 + assert hass.states.get(entity_id).state == old_value @pytest.mark.parametrize( @@ -101,17 +111,19 @@ async def test_update_triggers_fail( (ValueError, ValueError), ], ) -async def test_remote_service_exceptions( +async def test_service_call_fail( hass: HomeAssistant, raised: Exception, expected: Exception, bmw_fixture: respx.Router, monkeypatch: pytest.MonkeyPatch, ) -> None: - """Test exception handling for remote services.""" + """Test exception handling.""" # Setup component assert await setup_mocked_integration(hass) + entity_id = "select.i4_edrive40_ac_charging_limit" + old_value = hass.states.get(entity_id).state # Setup exception monkeypatch.setattr( @@ -127,6 +139,6 @@ async def test_remote_service_exceptions( "select_option", service_data={"option": "16"}, blocking=True, - target={"entity_id": "select.i4_edrive40_ac_charging_limit"}, + target={"entity_id": entity_id}, ) - assert RemoteServices.trigger_remote_service.call_count == 1 + assert hass.states.get(entity_id).state == old_value diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py index 03f836529be..95b1145d9d6 100644 --- a/tests/components/bmw_connected_drive/test_sensor.py +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -26,8 +26,8 @@ from . import setup_mocked_integration ("sensor.i3_rex_remaining_fuel", IMPERIAL, "1.59", "gal"), ("sensor.i3_rex_remaining_range_fuel", METRIC, "105", "km"), ("sensor.i3_rex_remaining_range_fuel", IMPERIAL, "65.24", "mi"), - ("sensor.i3_rex_remaining_fuel_percent", METRIC, "65", "%"), - ("sensor.i3_rex_remaining_fuel_percent", IMPERIAL, "65", "%"), + ("sensor.m340i_xdrive_remaining_fuel_percent", METRIC, "80", "%"), + ("sensor.m340i_xdrive_remaining_fuel_percent", IMPERIAL, "80", "%"), ], ) async def test_unit_conversion( diff --git a/tests/components/bmw_connected_drive/test_switch.py b/tests/components/bmw_connected_drive/test_switch.py index 26de4d3b6e8..c050f4b6cc2 100644 --- a/tests/components/bmw_connected_drive/test_switch.py +++ b/tests/components/bmw_connected_drive/test_switch.py @@ -7,13 +7,10 @@ import pytest import respx from syrupy.assertion import SnapshotAssertion -from homeassistant.components.bmw_connected_drive.coordinator import ( - BMWDataUpdateCoordinator, -) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from . import setup_mocked_integration +from . import check_remote_service_call, setup_mocked_integration async def test_entity_state_attrs( @@ -25,42 +22,45 @@ async def test_entity_state_attrs( # Setup component assert await setup_mocked_integration(hass) - BMWDataUpdateCoordinator.async_update_listeners.reset_mock() # Get all switch entities assert hass.states.async_all("switch") == snapshot @pytest.mark.parametrize( - ("entity_id", "value"), + ("entity_id", "new_value", "old_value", "remote_service", "remote_service_params"), [ - ("switch.i4_edrive40_climate", "ON"), - ("switch.i4_edrive40_climate", "OFF"), - ("switch.i4_edrive40_charging", "ON"), - ("switch.i4_edrive40_charging", "OFF"), + ("switch.i4_edrive40_climate", "on", "off", "climate-now", {"action": "START"}), + ("switch.i4_edrive40_climate", "off", "on", "climate-now", {"action": "STOP"}), + ("switch.iX_xdrive50_charging", "on", "off", "start-charging", {}), + ("switch.iX_xdrive50_charging", "off", "on", "stop-charging", {}), ], ) -async def test_update_triggers_success( +async def test_service_call_success( hass: HomeAssistant, entity_id: str, - value: str, + new_value: str, + old_value: str, + remote_service: str, + remote_service_params: dict, bmw_fixture: respx.Router, ) -> None: - """Test allowed values for switch inputs.""" + """Test successful switch change.""" # Setup component assert await setup_mocked_integration(hass) - BMWDataUpdateCoordinator.async_update_listeners.reset_mock() + hass.states.async_set(entity_id, old_value) + assert hass.states.get(entity_id).state == old_value # Test await hass.services.async_call( "switch", - f"turn_{value.lower()}", + f"turn_{new_value}", blocking=True, target={"entity_id": entity_id}, ) - assert RemoteServices.trigger_remote_service.call_count == 1 - assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 1 + check_remote_service_call(bmw_fixture, remote_service, remote_service_params) + assert hass.states.get(entity_id).state == new_value @pytest.mark.parametrize( @@ -71,18 +71,18 @@ async def test_update_triggers_success( (ValueError, ValueError), ], ) -async def test_update_triggers_exceptions( +async def test_service_call_fail( hass: HomeAssistant, raised: Exception, expected: Exception, bmw_fixture: respx.Router, monkeypatch: pytest.MonkeyPatch, ) -> None: - """Test not allowed values for switch inputs.""" + """Test exception handling.""" # Setup component assert await setup_mocked_integration(hass) - BMWDataUpdateCoordinator.async_update_listeners.reset_mock() + entity_id = "switch.i4_edrive40_climate" # Setup exception monkeypatch.setattr( @@ -91,20 +91,32 @@ async def test_update_triggers_exceptions( AsyncMock(side_effect=raised), ) + # Turning switch to ON + old_value = "off" + hass.states.async_set(entity_id, old_value) + assert hass.states.get(entity_id).state == old_value + # Test with pytest.raises(expected): await hass.services.async_call( "switch", "turn_on", blocking=True, - target={"entity_id": "switch.i4_edrive40_climate"}, + target={"entity_id": entity_id}, ) + assert hass.states.get(entity_id).state == old_value + + # Turning switch to OFF + old_value = "on" + hass.states.async_set(entity_id, old_value) + assert hass.states.get(entity_id).state == old_value + + # Test with pytest.raises(expected): await hass.services.async_call( "switch", "turn_off", blocking=True, - target={"entity_id": "switch.i4_edrive40_climate"}, + target={"entity_id": entity_id}, ) - assert RemoteServices.trigger_remote_service.call_count == 2 - assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 0 + assert hass.states.get(entity_id).state == old_value From 2ce5b08fc36e77a2594a39040e5440d2ca01dff8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 30 Aug 2023 11:48:13 +0200 Subject: [PATCH 1039/1151] Deprecate timer start optional duration parameter (#93471) * Deprecate timer start option duration parameter * Add test * Fix strings * breaks_in_ha_version * strings * Mod string --- homeassistant/components/timer/__init__.py | 13 +++++++++++++ homeassistant/components/timer/strings.json | 13 +++++++++++++ tests/components/timer/test_init.py | 16 ++++++++++++++-- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 228e2071b4a..1bc8eb8fd5e 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -22,6 +22,7 @@ from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store @@ -303,6 +304,18 @@ class Timer(collection.CollectionEntity, RestoreEntity): @callback def async_start(self, duration: timedelta | None = None): """Start a timer.""" + if duration: + async_create_issue( + self.hass, + DOMAIN, + "deprecated_duration_in_start", + breaks_in_ha_version="2024.3.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_duration_in_start", + ) + if self._listener: self._listener() self._listener = None diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json index 56cb46d26b4..c85a9f4c55e 100644 --- a/homeassistant/components/timer/strings.json +++ b/homeassistant/components/timer/strings.json @@ -63,5 +63,18 @@ } } } + }, + "issues": { + "deprecated_duration_in_start": { + "title": "The timer start service duration parameter is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::timer::issues::deprecated_duration_in_start::title%]", + "description": "The timer service `timer.start` optional duration parameter is being removed and use of it has been detected. To change the duration please create a new timer.\n\nPlease remove the use of the `duration` parameter in the `timer.start` service in your automations and scripts and select **submit** to close this issue." + } + } + } + } } } diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index eabc5e04e0b..7bc2df87f35 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -46,7 +46,11 @@ from homeassistant.const import ( ) from homeassistant.core import Context, CoreState, HomeAssistant, State from homeassistant.exceptions import HomeAssistantError, Unauthorized -from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers import ( + config_validation as cv, + entity_registry as er, + issue_registry as ir, +) from homeassistant.helpers.restore_state import StoredState, async_get from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -266,7 +270,9 @@ async def test_methods_and_events(hass: HomeAssistant) -> None: @pytest.mark.freeze_time("2023-06-05 17:47:50") -async def test_start_service(hass: HomeAssistant) -> None: +async def test_start_service( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: """Test the start/stop service.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}}) @@ -311,6 +317,12 @@ async def test_start_service(hass: HomeAssistant) -> None: blocking=True, ) await hass.async_block_till_done() + + # Ensure an issue is raised for the use of this deprecated service + assert issue_registry.async_get_issue( + domain=DOMAIN, issue_id="deprecated_duration_in_start" + ) + state = hass.states.get("timer.test1") assert state assert state.state == STATUS_ACTIVE From fae50169d9ecc1969ed07fc2f5749ac4cfe5acf3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 11:50:47 +0200 Subject: [PATCH 1040/1151] Add typing to Blink config flow (#98873) --- homeassistant/components/blink/config_flow.py | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index 3fd1b7d91e5..445a84f838c 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -9,7 +9,7 @@ from blinkpy.auth import Auth, LoginError, TokenRefreshFailed from blinkpy.blinkpy import Blink, BlinkSetupError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import ( CONF_PASSWORD, CONF_PIN, @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, @@ -48,7 +49,7 @@ OPTIONS_FLOW = { } -def validate_input(hass: core.HomeAssistant, auth): +def validate_input(auth: Auth) -> None: """Validate the user input allows us to connect.""" try: auth.startup() @@ -58,7 +59,7 @@ def validate_input(hass: core.HomeAssistant, auth): raise Require2FA -def _send_blink_2fa_pin(auth, pin): +def _send_blink_2fa_pin(auth: Auth, pin: str) -> bool: """Send 2FA pin to blink servers.""" blink = Blink() blink.auth = auth @@ -67,38 +68,34 @@ def _send_blink_2fa_pin(auth, pin): return auth.send_auth_key(blink, pin) -class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class BlinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a Blink config flow.""" VERSION = 3 def __init__(self) -> None: """Initialize the blink flow.""" - self.auth = None + self.auth: Auth | None = None @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> SchemaOptionsFlowHandler: """Get options flow for this handler.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} - data = {CONF_USERNAME: "", CONF_PASSWORD: "", "device_id": DEVICE_ID} if user_input is not None: - data[CONF_USERNAME] = user_input["username"] - data[CONF_PASSWORD] = user_input["password"] - - self.auth = Auth(data, no_prompt=True) - await self.async_set_unique_id(data[CONF_USERNAME]) + self.auth = Auth({**user_input, "device_id": DEVICE_ID}, no_prompt=True) + await self.async_set_unique_id(user_input[CONF_USERNAME]) try: - await self.hass.async_add_executor_job( - validate_input, self.hass, self.auth - ) + await self.hass.async_add_executor_job(validate_input, self.auth) return self._async_finish_flow() except Require2FA: return await self.async_step_2fa() @@ -108,18 +105,20 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" - data_schema = { - vol.Required("username"): str, - vol.Required("password"): str, - } - return self.async_show_form( step_id="user", - data_schema=vol.Schema(data_schema), + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), errors=errors, ) - async def async_step_2fa(self, user_input=None): + async def async_step_2fa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle 2FA step.""" errors = {} if user_input is not None: @@ -142,7 +141,7 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="2fa", data_schema=vol.Schema( - {vol.Optional("pin"): vol.All(str, vol.Length(min=1))} + {vol.Optional(CONF_PIN): vol.All(str, vol.Length(min=1))} ), errors=errors, ) @@ -152,14 +151,15 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user(dict(entry_data)) @callback - def _async_finish_flow(self): + def _async_finish_flow(self) -> FlowResult: """Finish with setup.""" + assert self.auth return self.async_create_entry(title=DOMAIN, data=self.auth.login_attributes) -class Require2FA(exceptions.HomeAssistantError): +class Require2FA(HomeAssistantError): """Error to indicate we require 2FA.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" From 38267699e513c89d963aef7a46368ce1f21c9767 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 11:52:06 +0200 Subject: [PATCH 1041/1151] Use device info object in ezviz (#99280) --- .../components/ezviz/alarm_control_panel.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ezviz/alarm_control_panel.py b/homeassistant/components/ezviz/alarm_control_panel.py index 3ce33028629..4dd16b23480 100644 --- a/homeassistant/components/ezviz/alarm_control_panel.py +++ b/homeassistant/components/ezviz/alarm_control_panel.py @@ -66,12 +66,12 @@ async def async_setup_entry( DATA_COORDINATOR ] - device_info: DeviceInfo = { - "identifiers": {(DOMAIN, entry.unique_id)}, # type: ignore[arg-type] - "name": "EZVIZ Alarm", - "model": "EZVIZ Alarm", - "manufacturer": MANUFACTURER, - } + device_info = DeviceInfo( + identifiers={(DOMAIN, entry.unique_id)}, # type: ignore[arg-type] + name="EZVIZ Alarm", + model="EZVIZ Alarm", + manufacturer=MANUFACTURER, + ) async_add_entities( [EzvizAlarm(coordinator, entry.entry_id, device_info, ALARM_TYPE)] From 7d70b42e4a5e257d23a81d903a356198a484ab93 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 11:57:56 +0200 Subject: [PATCH 1042/1151] Use shorthand attributes for EnOcean (#99278) --- .../components/enocean/binary_sensor.py | 15 ++----- homeassistant/components/enocean/device.py | 3 +- homeassistant/components/enocean/light.py | 40 +++++-------------- homeassistant/components/enocean/sensor.py | 2 +- homeassistant/components/enocean/switch.py | 25 ++++-------- 5 files changed, 24 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/enocean/binary_sensor.py b/homeassistant/components/enocean/binary_sensor.py index e7f94647941..25fc8c4f50a 100644 --- a/homeassistant/components/enocean/binary_sensor.py +++ b/homeassistant/components/enocean/binary_sensor.py @@ -60,21 +60,12 @@ class EnOceanBinarySensor(EnOceanEntity, BinarySensorEntity): device_class: BinarySensorDeviceClass | None, ) -> None: """Initialize the EnOcean binary sensor.""" - super().__init__(dev_id, dev_name) - self._device_class = device_class + super().__init__(dev_id) + self._attr_device_class = device_class self.which = -1 self.onoff = -1 self._attr_unique_id = f"{combine_hex(dev_id)}-{device_class}" - - @property - def name(self): - """Return the default name for the binary sensor.""" - return self.dev_name - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class + self._attr_name = dev_name def value_changed(self, packet): """Fire an event with the data that have changed. diff --git a/homeassistant/components/enocean/device.py b/homeassistant/components/enocean/device.py index 1c98b4dd234..220f940f37f 100644 --- a/homeassistant/components/enocean/device.py +++ b/homeassistant/components/enocean/device.py @@ -11,10 +11,9 @@ from .const import SIGNAL_RECEIVE_MESSAGE, SIGNAL_SEND_MESSAGE class EnOceanEntity(Entity): """Parent class for all entities associated with the EnOcean component.""" - def __init__(self, dev_id: list[int], dev_name: str) -> None: + def __init__(self, dev_id: list[int]) -> None: """Initialize the device.""" self.dev_id = dev_id - self.dev_name = dev_name async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index e2a194af8ba..2500ad7ce94 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -53,47 +53,29 @@ class EnOceanLight(EnOceanEntity, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + _attr_brightness = 50 + _attr_is_on = False def __init__(self, sender_id: list[int], dev_id: list[int], dev_name: str) -> None: """Initialize the EnOcean light source.""" - super().__init__(dev_id, dev_name) - self._on_state = False - self._brightness = 50 + super().__init__(dev_id) self._sender_id = sender_id - self._attr_unique_id = f"{combine_hex(dev_id)}" - - @property - def name(self): - """Return the name of the device if any.""" - return self.dev_name - - @property - def brightness(self): - """Brightness of the light. - - This method is optional. Removing it indicates to Home Assistant - that brightness is not supported for this light. - """ - return self._brightness - - @property - def is_on(self): - """If light is on.""" - return self._on_state + self._attr_unique_id = str(combine_hex(dev_id)) + self._attr_name = dev_name def turn_on(self, **kwargs: Any) -> None: """Turn the light source on or sets a specific dimmer value.""" if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: - self._brightness = brightness + self._attr_brightness = brightness - bval = math.floor(self._brightness / 256.0 * 100.0) + bval = math.floor(self._attr_brightness / 256.0 * 100.0) if bval == 0: bval = 1 command = [0xA5, 0x02, bval, 0x01, 0x09] command.extend(self._sender_id) command.extend([0x00]) self.send_command(command, [], 0x01) - self._on_state = True + self._attr_is_on = True def turn_off(self, **kwargs: Any) -> None: """Turn the light source off.""" @@ -101,7 +83,7 @@ class EnOceanLight(EnOceanEntity, LightEntity): command.extend(self._sender_id) command.extend([0x00]) self.send_command(command, [], 0x01) - self._on_state = False + self._attr_is_on = False def value_changed(self, packet): """Update the internal state of this device. @@ -111,6 +93,6 @@ class EnOceanLight(EnOceanEntity, LightEntity): """ if packet.data[0] == 0xA5 and packet.data[1] == 0x02: val = packet.data[2] - self._brightness = math.floor(val / 100.0 * 256.0) - self._on_state = bool(val != 0) + self._attr_brightness = math.floor(val / 100.0 * 256.0) + self._attr_is_on = bool(val != 0) self.schedule_update_ha_state() diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index db386a2d9fc..f63fd7239d0 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -160,7 +160,7 @@ class EnOceanSensor(EnOceanEntity, RestoreSensor): description: EnOceanSensorEntityDescription, ) -> None: """Initialize the EnOcean sensor device.""" - super().__init__(dev_id, dev_name) + super().__init__(dev_id) self.entity_description = description self._attr_name = f"{description.name} {dev_name}" self._attr_unique_id = description.unique_id(dev_id) diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index c69821c8372..13920f08e85 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -76,24 +76,15 @@ async def async_setup_platform( class EnOceanSwitch(EnOceanEntity, SwitchEntity): """Representation of an EnOcean switch device.""" + _attr_is_on = False + def __init__(self, dev_id: list[int], dev_name: str, channel: int) -> None: """Initialize the EnOcean switch device.""" - super().__init__(dev_id, dev_name) + super().__init__(dev_id) self._light = None - self._on_state = False - self._on_state2 = False self.channel = channel self._attr_unique_id = generate_unique_id(dev_id, channel) - - @property - def is_on(self): - """Return whether the switch is on or off.""" - return self._on_state - - @property - def name(self): - """Return the device name.""" - return self.dev_name + self._attr_name = dev_name def turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" @@ -105,7 +96,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): optional=optional, packet_type=0x01, ) - self._on_state = True + self._attr_is_on = True def turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" @@ -117,7 +108,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): optional=optional, packet_type=0x01, ) - self._on_state = False + self._attr_is_on = False def value_changed(self, packet): """Update the internal state of the switch.""" @@ -129,7 +120,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): divisor = packet.parsed["DIV"]["raw_value"] watts = raw_val / (10**divisor) if watts > 1: - self._on_state = True + self._attr_is_on = True self.schedule_update_ha_state() elif packet.data[0] == 0xD2: # actuator status telegram @@ -138,5 +129,5 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): channel = packet.parsed["IO"]["raw_value"] output = packet.parsed["OV"]["raw_value"] if channel == self.channel: - self._on_state = output > 0 + self._attr_is_on = output > 0 self.schedule_update_ha_state() From a4818c5f54537f8495a23471e2f87e4745e2da66 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 12:07:55 +0200 Subject: [PATCH 1043/1151] Use shorthand attributes for Elmax (#99277) --- .../components/elmax/binary_sensor.py | 7 ++-- homeassistant/components/elmax/common.py | 35 +++++-------------- homeassistant/components/elmax/switch.py | 2 +- 3 files changed, 11 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/elmax/binary_sensor.py b/homeassistant/components/elmax/binary_sensor.py index 6eb4cd654c5..0defbe464f9 100644 --- a/homeassistant/components/elmax/binary_sensor.py +++ b/homeassistant/components/elmax/binary_sensor.py @@ -60,12 +60,9 @@ async def async_setup_entry( class ElmaxSensor(ElmaxEntity, BinarySensorEntity): """Elmax Sensor entity implementation.""" + _attr_device_class = BinarySensorDeviceClass.DOOR + @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" return self.coordinator.get_zone_state(self._device.endpoint_id).opened - - @property - def device_class(self) -> BinarySensorDeviceClass: - """Return the class of this device, from component DEVICE_CLASSES.""" - return BinarySensorDeviceClass.DOOR diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index b593ae399f4..440344fb839 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -157,35 +157,16 @@ class ElmaxEntity(CoordinatorEntity[ElmaxCoordinator]): super().__init__(coordinator=coordinator) self._panel = panel self._device = elmax_device - self._panel_version = panel_version - self._client = coordinator.http_client - - @property - def panel_id(self) -> str: - """Retrieve the panel id.""" - return self._panel.hash - - @property - def unique_id(self) -> str | None: - """Provide a unique id for this entity.""" - return self._device.endpoint_id - - @property - def name(self) -> str | None: - """Return the entity name.""" - return self._device.name - - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes.""" - return DeviceInfo( - identifiers={(DOMAIN, self._panel.hash)}, - name=self._panel.get_name_by_user( - self.coordinator.http_client.get_authenticated_username() + self._attr_unique_id = elmax_device.endpoint_id + self._attr_name = elmax_device.name + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, panel.hash)}, + name=panel.get_name_by_user( + coordinator.http_client.get_authenticated_username() ), manufacturer="Elmax", - model=self._panel_version, - sw_version=self._panel_version, + model=panel_version, + sw_version=panel_version, ) @property diff --git a/homeassistant/components/elmax/switch.py b/homeassistant/components/elmax/switch.py index 431e75a0883..877330892e5 100644 --- a/homeassistant/components/elmax/switch.py +++ b/homeassistant/components/elmax/switch.py @@ -68,7 +68,7 @@ class ElmaxSwitch(ElmaxEntity, SwitchEntity): return self.coordinator.get_actuator_state(self._device.endpoint_id).opened async def _wait_for_state_change(self) -> bool: - """Refresh data and wait until the state state changes.""" + """Refresh data and wait until the state changes.""" old_state = self.coordinator.get_actuator_state(self._device.endpoint_id).opened # Wait a bit at first to let Elmax cloud assimilate the new state. From 775f815afca19415699927148fa730d211c7b98f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 12:11:13 +0200 Subject: [PATCH 1044/1151] Use shorthand attributes for Ecobee (#99239) * Use shorthand attributes for Ecobee * Use shorthand attributes for Ecobee --- .../components/ecobee/binary_sensor.py | 8 +---- homeassistant/components/ecobee/climate.py | 36 +++++-------------- homeassistant/components/ecobee/humidifier.py | 24 +++---------- 3 files changed, 14 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index f194884f377..4ad0190e01a 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -43,7 +43,6 @@ class EcobeeBinarySensor(BinarySensorEntity): self.data = data self.sensor_name = sensor_name.rstrip() self.index = sensor_index - self._state = None @property def unique_id(self): @@ -93,11 +92,6 @@ class EcobeeBinarySensor(BinarySensorEntity): thermostat = self.data.ecobee.get_thermostat(self.index) return thermostat["runtime"]["connected"] - @property - def is_on(self): - """Return the status of the sensor.""" - return self._state == "true" - async def async_update(self) -> None: """Get the latest state of the sensor.""" await self.data.update() @@ -107,5 +101,5 @@ class EcobeeBinarySensor(BinarySensorEntity): for item in sensor["capability"]: if item["type"] != "occupancy": continue - self._state = item["value"] + self._attr_is_on = item["value"] == "true" break diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 5e1cff625a4..b18f646add7 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -310,6 +310,9 @@ class Thermostat(ClimateEntity): _attr_precision = PRECISION_TENTHS _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT + _attr_min_humidity = DEFAULT_MIN_HUMIDITY + _attr_max_humidity = DEFAULT_MAX_HUMIDITY + _attr_fan_modes = [FAN_AUTO, FAN_ON] _attr_name = None _attr_has_entity_name = True @@ -324,20 +327,19 @@ class Thermostat(ClimateEntity): self.vacation = None self._last_active_hvac_mode = HVACMode.HEAT_COOL - self._operation_list = [] + self._attr_hvac_modes = [] if self.settings["heatStages"] or self.settings["hasHeatPump"]: - self._operation_list.append(HVACMode.HEAT) + self._attr_hvac_modes.append(HVACMode.HEAT) if self.settings["coolStages"]: - self._operation_list.append(HVACMode.COOL) - if len(self._operation_list) == 2: - self._operation_list.insert(0, HVACMode.HEAT_COOL) - self._operation_list.append(HVACMode.OFF) + self._attr_hvac_modes.append(HVACMode.COOL) + if len(self._attr_hvac_modes) == 2: + self._attr_hvac_modes.insert(0, HVACMode.HEAT_COOL) + self._attr_hvac_modes.append(HVACMode.OFF) self._preset_modes = { comfort["climateRef"]: comfort["name"] for comfort in self.thermostat["program"]["climates"] } - self._fan_modes = [FAN_AUTO, FAN_ON] self.update_without_throttle = False async def async_update(self) -> None: @@ -432,16 +434,6 @@ class Thermostat(ClimateEntity): return self.thermostat["runtime"]["desiredHumidity"] return None - @property - def min_humidity(self) -> int: - """Return the minimum humidity.""" - return DEFAULT_MIN_HUMIDITY - - @property - def max_humidity(self) -> int: - """Return the maximum humidity.""" - return DEFAULT_MAX_HUMIDITY - @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" @@ -465,11 +457,6 @@ class Thermostat(ClimateEntity): """Return the fan setting.""" return self.thermostat["runtime"]["desiredFanMode"] - @property - def fan_modes(self): - """Return the available fan modes.""" - return self._fan_modes - @property def preset_mode(self): """Return current preset mode.""" @@ -498,11 +485,6 @@ class Thermostat(ClimateEntity): """Return current operation.""" return ECOBEE_HVAC_TO_HASS[self.settings["hvacMode"]] - @property - def hvac_modes(self): - """Return the operation modes list.""" - return self._operation_list - @property def current_humidity(self) -> int | None: """Return the current humidity.""" diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index 25a2a56ba93..d8ebd3d77d8 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -44,6 +44,10 @@ class EcobeeHumidifier(HumidifierEntity): """A humidifier class for an ecobee thermostat with humidifier attached.""" _attr_supported_features = HumidifierEntityFeature.MODES + _attr_available_modes = [MODE_OFF, MODE_AUTO, MODE_MANUAL] + _attr_device_class = HumidifierDeviceClass.HUMIDIFIER + _attr_min_humidity = DEFAULT_MIN_HUMIDITY + _attr_max_humidity = DEFAULT_MAX_HUMIDITY _attr_has_entity_name = True _attr_name = None @@ -90,31 +94,11 @@ class EcobeeHumidifier(HumidifierEntity): if self.mode != MODE_OFF: self._last_humidifier_on_mode = self.mode - @property - def available_modes(self): - """Return the list of available modes.""" - return [MODE_OFF, MODE_AUTO, MODE_MANUAL] - - @property - def device_class(self): - """Return the device class type.""" - return HumidifierDeviceClass.HUMIDIFIER - @property def is_on(self): """Return True if the humidifier is on.""" return self.mode != MODE_OFF - @property - def max_humidity(self): - """Return the maximum humidity.""" - return DEFAULT_MAX_HUMIDITY - - @property - def min_humidity(self): - """Return the minimum humidity.""" - return DEFAULT_MIN_HUMIDITY - @property def mode(self): """Return the current mode, e.g., off, auto, manual.""" From 15de221c3e247d6d1e87befcb6356e8efa96cc35 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Aug 2023 12:17:26 +0200 Subject: [PATCH 1045/1151] Trigger full CI run if assist_pipeline is modified (#99319) --- .core_files.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.core_files.yaml b/.core_files.yaml index 6fbfdf90a4b..0817d5c8261 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -55,6 +55,7 @@ base_platforms: &base_platforms components: &components - homeassistant/components/alexa/** - homeassistant/components/application_credentials/** + - homeassistant/components/assist_pipeline/** - homeassistant/components/auth/** - homeassistant/components/automation/** - homeassistant/components/backup/** From fb42042402c1ffb3bc323fbdb1a27726ae40f5cd Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 30 Aug 2023 12:22:10 +0200 Subject: [PATCH 1046/1151] Use snapshot assertion for nextdns diagnostics test (#99157) --- tests/components/nextdns/__init__.py | 1 + .../components/nextdns/fixtures/settings.json | 77 ---------- .../nextdns/snapshots/test_diagnostics.ambr | 136 ++++++++++++++++++ tests/components/nextdns/test_diagnostics.py | 65 +-------- 4 files changed, 143 insertions(+), 136 deletions(-) delete mode 100644 tests/components/nextdns/fixtures/settings.json create mode 100644 tests/components/nextdns/snapshots/test_diagnostics.ambr diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 3ef09cae6c8..a175bffbb75 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -119,6 +119,7 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: title="Fake Profile", unique_id="xyz12", data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, + entry_id="d9aa37407ddac7b964a99e86312288d6", ) with patch( diff --git a/tests/components/nextdns/fixtures/settings.json b/tests/components/nextdns/fixtures/settings.json deleted file mode 100644 index 57ed97cfb19..00000000000 --- a/tests/components/nextdns/fixtures/settings.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "block_page": false, - "cache_boost": true, - "cname_flattening": true, - "anonymized_ecs": true, - "logs": true, - "logs_location": "ch", - "logs_retention": 720, - "web3": true, - "allow_affiliate": true, - "block_disguised_trackers": true, - "ai_threat_detection": true, - "block_csam": true, - "block_ddns": true, - "block_nrd": true, - "block_parked_domains": true, - "cryptojacking_protection": true, - "dga_protection": true, - "dns_rebinding_protection": true, - "google_safe_browsing": false, - "idn_homograph_attacks_protection": true, - "threat_intelligence_feeds": true, - "typosquatting_protection": true, - "block_bypass_methods": true, - "safesearch": false, - "youtube_restricted_mode": false, - "block_9gag": true, - "block_amazon": true, - "block_bereal": true, - "block_blizzard": true, - "block_chatgpt": true, - "block_dailymotion": true, - "block_discord": true, - "block_disneyplus": true, - "block_ebay": true, - "block_facebook": true, - "block_fortnite": true, - "block_google_chat": true, - "block_hbomax": true, - "block_hulu": true, - "block_imgur": true, - "block_instagram": true, - "block_leagueoflegends": true, - "block_mastodon": true, - "block_messenger": true, - "block_minecraft": true, - "block_netflix": true, - "block_pinterest": true, - "block_playstation_network": true, - "block_primevideo": true, - "block_reddit": true, - "block_roblox": true, - "block_signal": true, - "block_skype": true, - "block_snapchat": true, - "block_spotify": true, - "block_steam": true, - "block_telegram": true, - "block_tiktok": true, - "block_tinder": true, - "block_tumblr": true, - "block_twitch": true, - "block_twitter": true, - "block_vimeo": true, - "block_vk": true, - "block_whatsapp": true, - "block_xboxlive": true, - "block_youtube": true, - "block_zoom": true, - "block_dating": true, - "block_gambling": true, - "block_online_gaming": true, - "block_piracy": true, - "block_porn": true, - "block_social_networks": true, - "block_video_streaming": true -} diff --git a/tests/components/nextdns/snapshots/test_diagnostics.ambr b/tests/components/nextdns/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..071d14f183b --- /dev/null +++ b/tests/components/nextdns/snapshots/test_diagnostics.ambr @@ -0,0 +1,136 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config_entry': dict({ + 'data': dict({ + 'api_key': '**REDACTED**', + 'profile_id': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'nextdns', + 'entry_id': 'd9aa37407ddac7b964a99e86312288d6', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Fake Profile', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + 'dnssec_coordinator_data': dict({ + 'not_validated_queries': 25, + 'validated_queries': 75, + 'validated_queries_ratio': 75.0, + }), + 'encryption_coordinator_data': dict({ + 'encrypted_queries': 60, + 'encrypted_queries_ratio': 60.0, + 'unencrypted_queries': 40, + }), + 'ip_versions_coordinator_data': dict({ + 'ipv4_queries': 90, + 'ipv6_queries': 10, + 'ipv6_queries_ratio': 10.0, + }), + 'protocols_coordinator_data': dict({ + 'doh3_queries': 15, + 'doh3_queries_ratio': 13.0, + 'doh_queries': 20, + 'doh_queries_ratio': 17.4, + 'doq_queries': 10, + 'doq_queries_ratio': 8.7, + 'dot_queries': 30, + 'dot_queries_ratio': 26.1, + 'tcp_queries': 0, + 'tcp_queries_ratio': 0.0, + 'udp_queries': 40, + 'udp_queries_ratio': 34.8, + }), + 'settings_coordinator_data': dict({ + 'ai_threat_detection': True, + 'allow_affiliate': True, + 'anonymized_ecs': True, + 'block_9gag': True, + 'block_amazon': True, + 'block_bereal': True, + 'block_blizzard': True, + 'block_bypass_methods': True, + 'block_chatgpt': True, + 'block_csam': True, + 'block_dailymotion': True, + 'block_dating': True, + 'block_ddns': True, + 'block_discord': True, + 'block_disguised_trackers': True, + 'block_disneyplus': True, + 'block_ebay': True, + 'block_facebook': True, + 'block_fortnite': True, + 'block_gambling': True, + 'block_google_chat': True, + 'block_hbomax': True, + 'block_hulu': True, + 'block_imgur': True, + 'block_instagram': True, + 'block_leagueoflegends': True, + 'block_mastodon': True, + 'block_messenger': True, + 'block_minecraft': True, + 'block_netflix': True, + 'block_nrd': True, + 'block_online_gaming': True, + 'block_page': False, + 'block_parked_domains': True, + 'block_pinterest': True, + 'block_piracy': True, + 'block_playstation_network': True, + 'block_porn': True, + 'block_primevideo': True, + 'block_reddit': True, + 'block_roblox': True, + 'block_signal': True, + 'block_skype': True, + 'block_snapchat': True, + 'block_social_networks': True, + 'block_spotify': True, + 'block_steam': True, + 'block_telegram': True, + 'block_tiktok': True, + 'block_tinder': True, + 'block_tumblr': True, + 'block_twitch': True, + 'block_twitter': True, + 'block_video_streaming': True, + 'block_vimeo': True, + 'block_vk': True, + 'block_whatsapp': True, + 'block_xboxlive': True, + 'block_youtube': True, + 'block_zoom': True, + 'cache_boost': True, + 'cname_flattening': True, + 'cryptojacking_protection': True, + 'dga_protection': True, + 'dns_rebinding_protection': True, + 'google_safe_browsing': False, + 'idn_homograph_attacks_protection': True, + 'logs': True, + 'logs_location': 'ch', + 'logs_retention': 720, + 'safesearch': False, + 'threat_intelligence_feeds': True, + 'typosquatting_protection': True, + 'web3': True, + 'youtube_restricted_mode': False, + }), + 'status_coordinator_data': dict({ + 'all_queries': 100, + 'allowed_queries': 30, + 'blocked_queries': 20, + 'blocked_queries_ratio': 20.0, + 'default_queries': 40, + 'relayed_queries': 10, + }), + }) +# --- diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 1da52b26e3f..7652bc4f03e 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -1,74 +1,21 @@ """Test NextDNS diagnostics.""" -import json -from homeassistant.components.diagnostics import REDACTED +from syrupy import SnapshotAssertion + from homeassistant.core import HomeAssistant from . import init_integration -from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - settings = json.loads(load_fixture("settings.json", "nextdns")) - entry = await init_integration(hass) - result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - - assert result["config_entry"] == { - "entry_id": entry.entry_id, - "version": 1, - "domain": "nextdns", - "title": "Fake Profile", - "data": {"profile_id": REDACTED, "api_key": REDACTED}, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - } - assert result["dnssec_coordinator_data"] == { - "not_validated_queries": 25, - "validated_queries": 75, - "validated_queries_ratio": 75.0, - } - assert result["encryption_coordinator_data"] == { - "encrypted_queries": 60, - "unencrypted_queries": 40, - "encrypted_queries_ratio": 60.0, - } - assert result["ip_versions_coordinator_data"] == { - "ipv6_queries": 10, - "ipv4_queries": 90, - "ipv6_queries_ratio": 10.0, - } - assert result["protocols_coordinator_data"] == { - "doh_queries": 20, - "doh3_queries": 15, - "doq_queries": 10, - "dot_queries": 30, - "tcp_queries": 0, - "udp_queries": 40, - "doh_queries_ratio": 17.4, - "doh3_queries_ratio": 13.0, - "doq_queries_ratio": 8.7, - "dot_queries_ratio": 26.1, - "tcp_queries_ratio": 0.0, - "udp_queries_ratio": 34.8, - } - assert result["settings_coordinator_data"] == settings - assert result["status_coordinator_data"] == { - "all_queries": 100, - "allowed_queries": 30, - "blocked_queries": 20, - "default_queries": 40, - "relayed_queries": 10, - "blocked_queries_ratio": 20.0, - } + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot From 027ce55fa653ec6fe6a44caf9889c5d731f7abee Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 30 Aug 2023 12:25:06 +0200 Subject: [PATCH 1047/1151] Use snapshot assertion for google assistant diagnostics test (#99167) --- .../snapshots/test_diagnostics.ambr | 110 ++++++++++++++++++ .../google_assistant/test_diagnostics.py | 92 ++------------- 2 files changed, 119 insertions(+), 83 deletions(-) create mode 100644 tests/components/google_assistant/snapshots/test_diagnostics.ambr diff --git a/tests/components/google_assistant/snapshots/test_diagnostics.ambr b/tests/components/google_assistant/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..8d425ae0648 --- /dev/null +++ b/tests/components/google_assistant/snapshots/test_diagnostics.ambr @@ -0,0 +1,110 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'config_entry': dict({ + 'data': dict({ + 'project_id': '1234', + }), + 'disabled_by': None, + 'domain': 'google_assistant', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'import', + 'title': '1234', + 'unique_id': '1234', + 'version': 1, + }), + 'query': dict({ + 'devices': dict({ + 'switch.ac': dict({ + 'on': False, + 'online': True, + }), + 'switch.decorative_lights': dict({ + 'on': True, + 'online': True, + }), + }), + }), + 'sync': dict({ + 'agentUserId': '**REDACTED**', + 'devices': list([ + dict({ + 'attributes': dict({ + 'commandOnlyOnOff': True, + }), + 'customData': dict({ + 'httpPort': 8123, + 'uuid': '**REDACTED**', + 'webhookId': None, + }), + 'id': 'switch.decorative_lights', + 'name': dict({ + 'name': 'Decorative Lights', + }), + 'otherDeviceIds': list([ + dict({ + 'deviceId': 'switch.decorative_lights', + }), + ]), + 'traits': list([ + 'action.devices.traits.OnOff', + ]), + 'type': 'action.devices.types.SWITCH', + 'willReportState': False, + }), + dict({ + 'attributes': dict({ + }), + 'customData': dict({ + 'httpPort': 8123, + 'uuid': '**REDACTED**', + 'webhookId': None, + }), + 'id': 'switch.ac', + 'name': dict({ + 'name': 'AC', + }), + 'otherDeviceIds': list([ + dict({ + 'deviceId': 'switch.ac', + }), + ]), + 'traits': list([ + 'action.devices.traits.OnOff', + ]), + 'type': 'action.devices.types.OUTLET', + 'willReportState': False, + }), + ]), + }), + 'yaml_config': dict({ + 'expose_by_default': True, + 'exposed_domains': list([ + 'alarm_control_panel', + 'binary_sensor', + 'climate', + 'cover', + 'fan', + 'group', + 'humidifier', + 'input_boolean', + 'input_select', + 'light', + 'lock', + 'media_player', + 'scene', + 'script', + 'select', + 'sensor', + 'switch', + 'vacuum', + ]), + 'project_id': '1234', + 'report_state': False, + 'service_account': '**REDACTED**', + }), + }) +# --- diff --git a/tests/components/google_assistant/test_diagnostics.py b/tests/components/google_assistant/test_diagnostics.py index fde7e99025f..df8221b5053 100644 --- a/tests/components/google_assistant/test_diagnostics.py +++ b/tests/components/google_assistant/test_diagnostics.py @@ -1,7 +1,9 @@ """Test diagnostics.""" -from unittest.mock import ANY, patch +from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion +from syrupy.filters import props from homeassistant import setup from homeassistant.components import google_assistant as ga, switch @@ -26,7 +28,9 @@ async def switch_only() -> None: async def test_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics v1.""" @@ -42,84 +46,6 @@ async def test_diagnostics( ) config_entry = hass.config_entries.async_entries("google_assistant")[0] - result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - assert result == { - "config_entry": { - "data": {"project_id": "1234"}, - "disabled_by": None, - "domain": "google_assistant", - "entry_id": ANY, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "import", - "title": "1234", - "unique_id": "1234", - "version": 1, - }, - "sync": { - "agentUserId": "**REDACTED**", - "devices": [ - { - "attributes": {"commandOnlyOnOff": True}, - "id": "switch.decorative_lights", - "otherDeviceIds": [{"deviceId": "switch.decorative_lights"}], - "name": {"name": "Decorative Lights"}, - "traits": ["action.devices.traits.OnOff"], - "type": "action.devices.types.SWITCH", - "willReportState": False, - "customData": { - "httpPort": 8123, - "uuid": "**REDACTED**", - "webhookId": None, - }, - }, - { - "attributes": {}, - "id": "switch.ac", - "otherDeviceIds": [{"deviceId": "switch.ac"}], - "name": {"name": "AC"}, - "traits": ["action.devices.traits.OnOff"], - "type": "action.devices.types.OUTLET", - "willReportState": False, - "customData": { - "httpPort": 8123, - "uuid": "**REDACTED**", - "webhookId": None, - }, - }, - ], - }, - "query": { - "devices": { - "switch.ac": {"on": False, "online": True}, - "switch.decorative_lights": {"on": True, "online": True}, - } - }, - "yaml_config": { - "expose_by_default": True, - "exposed_domains": [ - "alarm_control_panel", - "binary_sensor", - "climate", - "cover", - "fan", - "group", - "humidifier", - "input_boolean", - "input_select", - "light", - "lock", - "media_player", - "scene", - "script", - "select", - "sensor", - "switch", - "vacuum", - ], - "project_id": "1234", - "report_state": False, - "service_account": "**REDACTED**", - }, - } + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("entry_id")) From 6e5f4566d528bbd88796244331454511ce6b8115 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 30 Aug 2023 06:50:23 -0400 Subject: [PATCH 1048/1151] Add zwave_js controller status sensor (#99252) * Add zwave_js controller status sensor * Also update network status command * fix tests * Remove WS command since we have a sensor entity * Update sensor.py Co-authored-by: Martin Hjelmare * move driver assertion out of closures * store state in tests --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 9 +- homeassistant/components/zwave_js/api.py | 1 + homeassistant/components/zwave_js/sensor.py | 98 +++++++++++++++++-- .../zwave_js/fixtures/controller_state.json | 3 +- tests/components/zwave_js/test_discovery.py | 4 +- tests/components/zwave_js/test_init.py | 10 +- tests/components/zwave_js/test_sensor.py | 51 ++++++++++ 7 files changed, 158 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index c86c4ae5688..316459bdb23 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -353,8 +353,13 @@ class ControllerEvents: ) ) - # No need for a ping button or node status sensor for controller nodes - if not node.is_controller_node: + if node.is_controller_node: + # Create a controller status sensor for each device + async_dispatcher_send( + self.hass, + f"{DOMAIN}_{self.config_entry.entry_id}_add_controller_status_sensor", + ) + else: # Create a node status sensor for each device async_dispatcher_send( self.hass, diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index fdf1b83cc6c..d93745f7a66 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -514,6 +514,7 @@ async def websocket_network_status( "is_heal_network_active": controller.is_heal_network_active, "inclusion_state": controller.inclusion_state, "rf_region": controller.rf_region, + "status": controller.status, "nodes": [node_status(node) for node in driver.controller.nodes.values()], }, } diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 468d8f0cbda..3c22288a1d6 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -6,7 +6,7 @@ from typing import cast import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import CommandClass, NodeStatus +from zwave_js_server.const import CommandClass, ControllerStatus, NodeStatus from zwave_js_server.const.command_class.meter import ( RESET_METER_OPTION_TARGET_VALUE, RESET_METER_OPTION_TYPE, @@ -91,7 +91,13 @@ from .helpers import get_device_info, get_valueless_base_unique_id PARALLEL_UPDATES = 0 -STATUS_ICON: dict[NodeStatus, str] = { +CONTROLLER_STATUS_ICON: dict[ControllerStatus, str] = { + ControllerStatus.READY: "mdi:check", + ControllerStatus.UNRESPONSIVE: "mdi:bell-off", + ControllerStatus.JAMMED: "mdi:lock", +} + +NODE_STATUS_ICON: dict[NodeStatus, str] = { NodeStatus.ALIVE: "mdi:heart-pulse", NodeStatus.ASLEEP: "mdi:sleep", NodeStatus.AWAKE: "mdi:eye", @@ -485,12 +491,12 @@ async def async_setup_entry( ) -> None: """Set up Z-Wave sensor from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. @callback def async_add_sensor(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Sensor.""" - driver = client.driver - assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] if info.platform_data: @@ -529,18 +535,19 @@ async def async_setup_entry( async_add_entities(entities) + @callback + def async_add_controller_status_sensor() -> None: + """Add controller status sensor.""" + async_add_entities([ZWaveControllerStatusSensor(config_entry, driver)]) + @callback def async_add_node_status_sensor(node: ZwaveNode) -> None: """Add node status sensor.""" - driver = client.driver - assert driver is not None # Driver is ready before platforms are loaded. async_add_entities([ZWaveNodeStatusSensor(config_entry, driver, node)]) @callback def async_add_statistics_sensors(node: ZwaveNode) -> None: """Add statistics sensors.""" - driver = client.driver - assert driver is not None # Driver is ready before platforms are loaded. async_add_entities( [ ZWaveStatisticsSensor( @@ -565,6 +572,14 @@ async def async_setup_entry( ) ) + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_controller_status_sensor", + async_add_controller_status_sensor, + ) + ) + config_entry.async_on_unload( async_dispatcher_connect( hass, @@ -828,7 +843,7 @@ class ZWaveNodeStatusSensor(SensorEntity): @property def icon(self) -> str | None: """Icon of the entity.""" - return STATUS_ICON[self.node.status] + return NODE_STATUS_ICON[self.node.status] async def async_added_to_hass(self) -> None: """Call when entity is added.""" @@ -856,6 +871,71 @@ class ZWaveNodeStatusSensor(SensorEntity): self.async_write_ha_state() +class ZWaveControllerStatusSensor(SensorEntity): + """Representation of a controller status sensor.""" + + _attr_should_poll = False + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True + + def __init__(self, config_entry: ConfigEntry, driver: Driver) -> None: + """Initialize a generic Z-Wave device entity.""" + self.config_entry = config_entry + self.controller = driver.controller + node = self.controller.own_node + assert node + + # Entity class attributes + self._attr_name = "Status" + self._base_unique_id = get_valueless_base_unique_id(driver, node) + self._attr_unique_id = f"{self._base_unique_id}.controller_status" + # device may not be precreated in main handler yet + self._attr_device_info = get_device_info(driver, node) + + async def async_poll_value(self, _: bool) -> None: + """Poll a value.""" + # We log an error instead of raising an exception because this service call occurs + # in a separate task since it is called via the dispatcher and we don't want to + # raise the exception in that separate task because it is confusing to the user. + LOGGER.error( + "There is no value to refresh for this entity so the zwave_js.refresh_value" + " service won't work for it" + ) + + @callback + def _status_changed(self, _: dict) -> None: + """Call when status event is received.""" + self._attr_native_value = self.controller.status.name.lower() + self.async_write_ha_state() + + @property + def icon(self) -> str | None: + """Icon of the entity.""" + return CONTROLLER_STATUS_ICON[self.controller.status] + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + # Add value_changed callbacks. + self.async_on_remove(self.controller.on("status changed", self._status_changed)) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self.unique_id}_poll_value", + self.async_poll_value, + ) + ) + # we don't listen for `remove_entity_on_ready_node` signal because this is not + # a regular node + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self._base_unique_id}_remove_entity", + self.async_remove, + ) + ) + self._attr_native_value: str = self.controller.status.name.lower() + + class ZWaveStatisticsSensor(SensorEntity): """Representation of a node/controller statistics sensor.""" diff --git a/tests/components/zwave_js/fixtures/controller_state.json b/tests/components/zwave_js/fixtures/controller_state.json index 566ad3b6f2b..d6d9dcacd9e 100644 --- a/tests/components/zwave_js/fixtures/controller_state.json +++ b/tests/components/zwave_js/fixtures/controller_state.json @@ -24,7 +24,8 @@ "sucNodeId": 1, "supportsTimers": false, "isHealNetworkActive": false, - "inclusionState": 0 + "inclusionState": 0, + "status": 0 }, "nodes": [] } diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 99a46eaadf9..cbaa27c2a91 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -227,7 +227,9 @@ async def test_indicator_test( assert len(hass.states.async_entity_ids(NUMBER_DOMAIN)) == 0 assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 # only ping assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1 - assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 2 # include node status + assert ( + len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 3 + ) # include node + controller status assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 entity_id = "binary_sensor.this_is_a_fake_device_binary_sensor" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 212ac9d751e..6985a7bf252 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -205,7 +205,7 @@ async def test_on_node_added_not_ready( dev_reg = dr.async_get(hass) device_id = f"{client.driver.controller.home_id}-{zp3111_not_ready_state['nodeId']}" - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all()) == 1 assert len(dev_reg.devices) == 1 node_state = deepcopy(zp3111_not_ready_state) @@ -224,7 +224,7 @@ async def test_on_node_added_not_ready( await hass.async_block_till_done() # the only entities are the node status sensor and ping button - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device @@ -326,7 +326,7 @@ async def test_existing_node_not_ready( assert not device.sw_version # the only entities are the node status sensor and ping button - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 device = dev_reg.async_get_device(identifiers={(DOMAIN, device_id)}) assert device @@ -964,7 +964,7 @@ async def test_removed_device( # Check how many entities there are ent_reg = er.async_get(hass) entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) - assert len(entity_entries) == 91 + assert len(entity_entries) == 92 # Remove a node and reload the entry old_node = driver.controller.nodes.pop(13) @@ -976,7 +976,7 @@ async def test_removed_device( device_entries = dr.async_entries_for_config_entry(dev_reg, integration.entry_id) assert len(device_entries) == 2 entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) - assert len(entity_entries) == 60 + assert len(entity_entries) == 61 assert ( dev_reg.async_get_device(identifiers={get_device_id(driver, old_node)}) is None ) diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index d809d52821c..d452f28b3bf 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -261,6 +261,47 @@ async def test_config_parameter_sensor( await hass.async_block_till_done() +async def test_controller_status_sensor( + hass: HomeAssistant, client, integration +) -> None: + """Test controller status sensor is created and gets updated on controller state changes.""" + entity_id = "sensor.z_stick_gen5_usb_controller_status" + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(entity_id) + + assert not entity_entry.disabled + assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC + state = hass.states.get(entity_id) + assert state + assert state.state == "ready" + assert state.attributes[ATTR_ICON] == "mdi:check" + + event = Event( + "status changed", + data={"source": "controller", "event": "status changed", "status": 1}, + ) + client.driver.controller.receive_event(event) + state = hass.states.get(entity_id) + assert state + assert state.state == "unresponsive" + assert state.attributes[ATTR_ICON] == "mdi:bell-off" + + # Test transitions work + event = Event( + "status changed", + data={"source": "controller", "event": "status changed", "status": 2}, + ) + client.driver.controller.receive_event(event) + state = hass.states.get(entity_id) + assert state + assert state.state == "jammed" + assert state.attributes[ATTR_ICON] == "mdi:lock" + + # Disconnect the client and make sure the entity is still available + await client.disconnect() + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE + + async def test_node_status_sensor( hass: HomeAssistant, client, lock_id_lock_as_id150, integration ) -> None: @@ -325,6 +366,16 @@ async def test_node_status_sensor( is None ) + # Assert a controller status sensor entity is not created for a node + assert ( + ent_reg.async_get_entity_id( + DOMAIN, + "sensor", + f"{get_valueless_base_unique_id(driver, node)}.controller_status", + ) + is None + ) + async def test_node_status_sensor_not_ready( hass: HomeAssistant, From 5c8e5a7af2fce45b921750f0485457183e9dc527 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 30 Aug 2023 13:14:30 +0200 Subject: [PATCH 1049/1151] Split bsblan coordinator and randomize update interval (#99269) * Split out bsblan coordinator and randomize update interval * Use logger const * Add randomising update interval for following updates * Implement review comments * Re-add config_entry * Remove line --- homeassistant/components/bsblan/__init__.py | 11 ++-- .../components/bsblan/coordinator.py | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/bsblan/coordinator.py diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index 0ef3ed159a6..224cb479dda 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -15,7 +15,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_PASSKEY, DOMAIN, LOGGER, SCAN_INTERVAL +from .const import CONF_PASSKEY, DOMAIN +from .coordinator import BSBLanUpdateCoordinator PLATFORMS = [Platform.CLIMATE] @@ -44,13 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, ) - coordinator: DataUpdateCoordinator[State] = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{DOMAIN}_{entry.data[CONF_HOST]}", - update_interval=SCAN_INTERVAL, - update_method=bsblan.state, - ) + coordinator = BSBLanUpdateCoordinator(hass, entry, bsblan) await coordinator.async_config_entry_first_refresh() device = await bsblan.device() diff --git a/homeassistant/components/bsblan/coordinator.py b/homeassistant/components/bsblan/coordinator.py new file mode 100644 index 00000000000..9344500a118 --- /dev/null +++ b/homeassistant/components/bsblan/coordinator.py @@ -0,0 +1,54 @@ +"""DataUpdateCoordinator for the BSB-Lan integration.""" +from __future__ import annotations + +from datetime import timedelta +from random import randint + +from bsblan import BSBLAN, BSBLANConnectionError +from bsblan.models import State + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + + +class BSBLanUpdateCoordinator(DataUpdateCoordinator[State]): + """The BSB-Lan update coordinator.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + client: BSBLAN, + ) -> None: + """Initialize the BSB-Lan coordinator.""" + + self.client = client + + super().__init__( + hass, + LOGGER, + name=f"{DOMAIN}_{config_entry.data[CONF_HOST]}", + # use the default scan interval and add a random number of seconds to avoid timeouts when + # the BSB-Lan device is already/still busy retrieving data, e.g. for MQTT or internal logging. + update_interval=SCAN_INTERVAL + timedelta(seconds=randint(1, 8)), + ) + + async def _async_update_data(self) -> State: + """Get state from BSB-Lan device.""" + + # use the default scan interval and add a random number of seconds to avoid timeouts when + # the BSB-Lan device is already/still busy retrieving data, e.g. for MQTT or internal logging. + self.update_interval = SCAN_INTERVAL + timedelta(seconds=randint(1, 8)) + + try: + return await self.client.state() + except BSBLANConnectionError as err: + raise UpdateFailed( + f"Error while establishing connection with BSB-Lan device at {self.config_entry.data[CONF_HOST]}" + ) from err From 71a6db0c9db2aad04519dde1d4e7bf66f5b2c5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 30 Aug 2023 14:23:17 +0200 Subject: [PATCH 1050/1151] Update AEMET-OpenData to v0.4.3 (#99312) --- homeassistant/components/aemet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index 6e1989d4f20..c43e7a0b402 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.4.2"] + "requirements": ["AEMET-OpenData==0.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9705e49d6c8..872bc5d9630 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.2 +AEMET-OpenData==0.4.3 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9bcaf2761a..59f3a1393e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.2 +AEMET-OpenData==0.4.3 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 From 7170f5d36c79889f529b8ee84b22d756c952e4b9 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 30 Aug 2023 14:26:58 +0200 Subject: [PATCH 1051/1151] Bump pyduotecno to 2023.8.4 (#99328) --- homeassistant/components/duotecno/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index 69490b6b5aa..d26d4fce61e 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/duotecno", "iot_class": "local_push", - "requirements": ["pyduotecno==2023.8.3"] + "requirements": ["pyduotecno==2023.8.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 872bc5d9630..573c46c0020 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1656,7 +1656,7 @@ pydrawise==2023.8.0 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.8.3 +pyduotecno==2023.8.4 # homeassistant.components.ebox pyebox==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59f3a1393e5..6449ed1ab3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1226,7 +1226,7 @@ pydiscovergy==2.0.3 pydroid-ipcam==2.0.0 # homeassistant.components.duotecno -pyduotecno==2023.8.3 +pyduotecno==2023.8.4 # homeassistant.components.econet pyeconet==0.1.20 From 66ad605d3e38865c11e66e70060bf3fa9a59e3d6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 15:09:02 +0200 Subject: [PATCH 1052/1151] Use shorthand attribute in Google Travel Time (#99331) --- .../components/google_travel_time/sensor.py | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 65db01cde59..06a50dab854 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -70,15 +70,21 @@ class GoogleTravelTimeSensor(SensorEntity): """Representation of a Google travel time sensor.""" _attr_attribution = ATTRIBUTION + _attr_native_unit_of_measurement = UnitOfTime.MINUTES def __init__(self, config_entry, name, api_key, origin, destination, client): """Initialize the sensor.""" - self._name = name + self._attr_name = name + self._attr_unique_id = config_entry.entry_id + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, api_key)}, + name=DOMAIN, + ) + self._config_entry = config_entry - self._unit_of_measurement = UnitOfTime.MINUTES self._matrix = None self._api_key = api_key - self._unique_id = config_entry.entry_id self._client = client self._origin = origin self._destination = destination @@ -107,25 +113,6 @@ class GoogleTravelTimeSensor(SensorEntity): return round(_data["duration"]["value"] / 60) return None - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes.""" - return DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, self._api_key)}, - name=DOMAIN, - ) - - @property - def unique_id(self) -> str: - """Return unique ID of entity.""" - return self._unique_id - - @property - def name(self): - """Get the name of the sensor.""" - return self._name - @property def extra_state_attributes(self): """Return the state attributes.""" @@ -147,11 +134,6 @@ class GoogleTravelTimeSensor(SensorEntity): res["destination"] = self._resolved_destination return res - @property - def native_unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - async def first_update(self, _=None): """Run the first update and write the state.""" await self.hass.async_add_executor_job(self.update) From 587928223afc3d36d2de5e96a72eb15dbe7d757e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 15:16:45 +0200 Subject: [PATCH 1053/1151] Use shorthand attributes in Gree (#99332) --- homeassistant/components/gree/climate.py | 98 ++++++------------------ homeassistant/components/gree/entity.py | 28 ++----- 2 files changed, 33 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 87c3fcf7eed..17d915feadb 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -115,40 +115,33 @@ class GreeClimateEntity(CoordinatorEntity[DeviceDataUpdateCoordinator], ClimateE | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.SWING_MODE ) + _attr_target_temperature_step = TARGET_TEMPERATURE_STEP + _attr_hvac_modes = [*HVAC_MODES_REVERSE, HVACMode.OFF] + _attr_preset_modes = PRESET_MODES + _attr_fan_modes = [*FAN_MODES_REVERSE] + _attr_swing_modes = SWING_MODES def __init__(self, coordinator: DeviceDataUpdateCoordinator) -> None: """Initialize the Gree device.""" super().__init__(coordinator) - self._name = coordinator.device.device_info.name - self._mac = coordinator.device.device_info.mac - - @property - def name(self) -> str: - """Return the name of the device.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique id for the device.""" - return self._mac - - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes.""" - return DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, self._mac)}, - identifiers={(DOMAIN, self._mac)}, + self._attr_name = coordinator.device.device_info.name + mac = coordinator.device.device_info.mac + self._attr_unique_id = mac + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, mac)}, + identifiers={(DOMAIN, mac)}, manufacturer="Gree", - name=self._name, + name=self._attr_name, ) - - @property - def temperature_unit(self) -> str: - """Return the temperature units for the device.""" units = self.coordinator.device.temperature_units if units == TemperatureUnits.C: - return UnitOfTemperature.CELSIUS - return UnitOfTemperature.FAHRENHEIT + self._attr_temperature_unit = UnitOfTemperature.CELSIUS + self._attr_min_temp = TEMP_MIN + self._attr_max_temp = TEMP_MAX + else: + self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT + self._attr_min_temp = TEMP_MIN_F + self._attr_max_temp = TEMP_MAX_F @property def current_temperature(self) -> float: @@ -169,32 +162,13 @@ class GreeClimateEntity(CoordinatorEntity[DeviceDataUpdateCoordinator], ClimateE _LOGGER.debug( "Setting temperature to %d for %s", temperature, - self._name, + self._attr_name, ) self.coordinator.device.target_temperature = round(temperature) await self.coordinator.push_state_update() self.async_write_ha_state() - @property - def min_temp(self) -> float: - """Return the minimum temperature supported by the device.""" - if self.temperature_unit == UnitOfTemperature.CELSIUS: - return TEMP_MIN - return TEMP_MIN_F - - @property - def max_temp(self) -> float: - """Return the maximum temperature supported by the device.""" - if self.temperature_unit == UnitOfTemperature.CELSIUS: - return TEMP_MAX - return TEMP_MAX_F - - @property - def target_temperature_step(self) -> float: - """Return the target temperature step support by the device.""" - return TARGET_TEMPERATURE_STEP - @property def hvac_mode(self) -> HVACMode | None: """Return the current HVAC mode for the device.""" @@ -211,7 +185,7 @@ class GreeClimateEntity(CoordinatorEntity[DeviceDataUpdateCoordinator], ClimateE _LOGGER.debug( "Setting HVAC mode to %s for device %s", hvac_mode, - self._name, + self._attr_name, ) if hvac_mode == HVACMode.OFF: @@ -229,7 +203,7 @@ class GreeClimateEntity(CoordinatorEntity[DeviceDataUpdateCoordinator], ClimateE async def async_turn_on(self) -> None: """Turn on the device.""" - _LOGGER.debug("Turning on HVAC for device %s", self._name) + _LOGGER.debug("Turning on HVAC for device %s", self._attr_name) self.coordinator.device.power = True await self.coordinator.push_state_update() @@ -237,19 +211,12 @@ class GreeClimateEntity(CoordinatorEntity[DeviceDataUpdateCoordinator], ClimateE async def async_turn_off(self) -> None: """Turn off the device.""" - _LOGGER.debug("Turning off HVAC for device %s", self._name) + _LOGGER.debug("Turning off HVAC for device %s", self._attr_name) self.coordinator.device.power = False await self.coordinator.push_state_update() self.async_write_ha_state() - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the HVAC modes support by the device.""" - modes = [*HVAC_MODES_REVERSE] - modes.append(HVACMode.OFF) - return modes - @property def preset_mode(self) -> str: """Return the current preset mode for the device.""" @@ -271,7 +238,7 @@ class GreeClimateEntity(CoordinatorEntity[DeviceDataUpdateCoordinator], ClimateE _LOGGER.debug( "Setting preset mode to %s for device %s", preset_mode, - self._name, + self._attr_name, ) self.coordinator.device.steady_heat = False @@ -291,11 +258,6 @@ class GreeClimateEntity(CoordinatorEntity[DeviceDataUpdateCoordinator], ClimateE await self.coordinator.push_state_update() self.async_write_ha_state() - @property - def preset_modes(self) -> list[str]: - """Return the preset modes support by the device.""" - return PRESET_MODES - @property def fan_mode(self) -> str | None: """Return the current fan mode for the device.""" @@ -311,11 +273,6 @@ class GreeClimateEntity(CoordinatorEntity[DeviceDataUpdateCoordinator], ClimateE await self.coordinator.push_state_update() self.async_write_ha_state() - @property - def fan_modes(self) -> list[str]: - """Return the fan modes support by the device.""" - return [*FAN_MODES_REVERSE] - @property def swing_mode(self) -> str: """Return the current swing mode for the device.""" @@ -338,7 +295,7 @@ class GreeClimateEntity(CoordinatorEntity[DeviceDataUpdateCoordinator], ClimateE _LOGGER.debug( "Setting swing mode to %s for device %s", swing_mode, - self._name, + self._attr_name, ) self.coordinator.device.horizontal_swing = HorizontalSwing.Center @@ -350,8 +307,3 @@ class GreeClimateEntity(CoordinatorEntity[DeviceDataUpdateCoordinator], ClimateE await self.coordinator.push_state_update() self.async_write_ha_state() - - @property - def swing_modes(self) -> list[str]: - """Return the swing modes currently supported for this device.""" - return SWING_MODES diff --git a/homeassistant/components/gree/entity.py b/homeassistant/components/gree/entity.py index ea3aa28ac13..fd1b80ef90d 100644 --- a/homeassistant/components/gree/entity.py +++ b/homeassistant/components/gree/entity.py @@ -13,25 +13,13 @@ class GreeEntity(CoordinatorEntity[DeviceDataUpdateCoordinator]): """Initialize the entity.""" super().__init__(coordinator) self._desc = desc - self._name = f"{coordinator.device.device_info.name}" - self._mac = coordinator.device.device_info.mac - - @property - def name(self): - """Return the name of the node.""" - return f"{self._name} {self._desc}" - - @property - def unique_id(self): - """Return the unique id based for the node.""" - return f"{self._mac}_{self._desc}" - - @property - def device_info(self) -> DeviceInfo: - """Return info about the device.""" - return DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, self._mac)}, - identifiers={(DOMAIN, self._mac)}, + name = coordinator.device.device_info.name + mac = coordinator.device.device_info.mac + self._attr_name = f"{name} {desc}" + self._attr_unique_id = f"{mac}_{desc}" + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, mac)}, + identifiers={(DOMAIN, mac)}, manufacturer="Gree", - name=self._name, + name=name, ) From e56db78b2714e07f5b361cd31514796aec45232b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 15:33:05 +0200 Subject: [PATCH 1054/1151] Use shorthand attributes for Freebox (#99327) --- homeassistant/components/freebox/button.py | 7 +------ .../components/freebox/device_tracker.py | 18 ++++-------------- homeassistant/components/freebox/sensor.py | 1 - homeassistant/components/freebox/switch.py | 4 ++-- 4 files changed, 7 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/freebox/button.py b/homeassistant/components/freebox/button.py index bc40d9560d6..e3a206b43a8 100644 --- a/homeassistant/components/freebox/button.py +++ b/homeassistant/components/freebox/button.py @@ -12,7 +12,6 @@ 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 DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -72,13 +71,9 @@ class FreeboxButton(ButtonEntity): """Initialize a Freebox button.""" self.entity_description = description self._router = router + self._attr_device_info = router.device_info self._attr_unique_id = f"{router.mac} {description.name}" - @property - def device_info(self) -> DeviceInfo: - """Return the device information.""" - return self._router.device_info - async def async_press(self) -> None: """Press the button.""" await self.entity_description.async_press(self._router) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 7232f16696e..42e028b881e 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -61,9 +61,9 @@ class FreeboxDevice(ScannerEntity): self._name = device["primary_name"].strip() or DEFAULT_DEVICE_NAME self._mac = device["l2ident"]["id"] self._manufacturer = device["vendor_name"] - self._icon = icon_for_freebox_device(device) + self._attr_icon = icon_for_freebox_device(device) self._active = False - self._attrs: dict[str, Any] = {} + self._attr_extra_state_attributes: dict[str, Any] = {} @callback def async_update_state(self) -> None: @@ -72,7 +72,7 @@ class FreeboxDevice(ScannerEntity): self._active = device["active"] if device.get("attrs") is None: # device - self._attrs = { + self._attr_extra_state_attributes = { "last_time_reachable": datetime.fromtimestamp( device["last_time_reachable"] ), @@ -80,7 +80,7 @@ class FreeboxDevice(ScannerEntity): } else: # router - self._attrs = device["attrs"] + self._attr_extra_state_attributes = device["attrs"] @property def mac_address(self) -> str: @@ -102,16 +102,6 @@ class FreeboxDevice(ScannerEntity): """Return the source type.""" return SourceType.ROUTER - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the attributes.""" - return self._attrs - @callback def async_on_demand_update(self): """Update state.""" diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index d065907a914..901bfc63199 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -197,7 +197,6 @@ class FreeboxDiskSensor(FreeboxSensor): ) -> None: """Initialize a Freebox disk sensor.""" super().__init__(router, description) - self._disk = disk self._partition = partition self._attr_name = f"{partition['label']} {description.name}" self._attr_unique_id = ( diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index a0298a8bbd4..e7547b97d4e 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -48,8 +48,8 @@ class FreeboxSwitch(SwitchEntity): """Initialize the switch.""" self.entity_description = entity_description self._router = router - self._attr_device_info = self._router.device_info - self._attr_unique_id = f"{self._router.mac} {self.entity_description.name}" + self._attr_device_info = router.device_info + self._attr_unique_id = f"{router.mac} {entity_description.name}" async def _async_set_state(self, enabled: bool): """Turn the switch on or off.""" From 5f05d0d7e95b52eb9952a07511373069e030efbc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 15:33:38 +0200 Subject: [PATCH 1055/1151] Map abode units to HA units (#99323) --- homeassistant/components/abode/sensor.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index d964655384b..bceed215428 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -14,13 +14,18 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import LIGHT_LUX +from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AbodeDevice, AbodeSystem from .const import DOMAIN +ABODE_TEMPERATURE_UNIT_HA_UNIT = { + CONST.UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT, + CONST.UNIT_CELSIUS: UnitOfTemperature.CELSIUS, +} + @dataclass class AbodeSensorDescriptionMixin: @@ -39,13 +44,15 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = ( AbodeSensorDescription( key=CONST.TEMP_STATUS_KEY, device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement_fn=lambda device: device.temp_unit, + native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[ + device.temp_unit + ], value_fn=lambda device: cast(float, device.temp), ), AbodeSensorDescription( key=CONST.HUMI_STATUS_KEY, device_class=SensorDeviceClass.HUMIDITY, - native_unit_of_measurement_fn=lambda device: device.humidity_unit, + native_unit_of_measurement_fn=lambda _: PERCENTAGE, value_fn=lambda device: cast(float, device.humidity), ), AbodeSensorDescription( From fb6d19d08f419f1842c57b011f4d8d76c9cfe317 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 15:53:30 +0200 Subject: [PATCH 1056/1151] Add pressure device class to Telldus live (#99337) --- homeassistant/components/tellduslive/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index 2c3ae3588ab..d52fded3932 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -118,6 +118,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key=SENSOR_TYPE_BAROMETRIC_PRESSURE, name="Barometric Pressure", native_unit_of_measurement=UnitOfPressure.KPA, + device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), } From a1b2b9a78c351f989da178d7ed40d4ca8f59c44e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 16:03:13 +0200 Subject: [PATCH 1057/1151] Add entity translations to Tellduslive (#98963) --- .../components/tellduslive/binary_sensor.py | 2 ++ homeassistant/components/tellduslive/cover.py | 2 ++ homeassistant/components/tellduslive/entry.py | 10 +------ homeassistant/components/tellduslive/light.py | 1 + .../components/tellduslive/sensor.py | 27 +++++-------------- .../components/tellduslive/strings.json | 19 +++++++++++++ .../components/tellduslive/switch.py | 2 ++ 7 files changed, 34 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/tellduslive/binary_sensor.py b/homeassistant/components/tellduslive/binary_sensor.py index 1e7a30d6174..4abe1dfd174 100644 --- a/homeassistant/components/tellduslive/binary_sensor.py +++ b/homeassistant/components/tellduslive/binary_sensor.py @@ -34,6 +34,8 @@ async def async_setup_entry( class TelldusLiveSensor(TelldusLiveEntity, BinarySensorEntity): """Representation of a Tellstick sensor.""" + _attr_name = None + @property def is_on(self): """Return true if switch is on.""" diff --git a/homeassistant/components/tellduslive/cover.py b/homeassistant/components/tellduslive/cover.py index 4934bf811af..2a32756aa1b 100644 --- a/homeassistant/components/tellduslive/cover.py +++ b/homeassistant/components/tellduslive/cover.py @@ -35,6 +35,8 @@ async def async_setup_entry( class TelldusLiveCover(TelldusLiveEntity, CoverEntity): """Representation of a cover.""" + _attr_name = None + @property def is_closed(self) -> bool: """Return the current position of the cover.""" diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index ce9c5222fd5..fdacc02bfca 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -9,7 +9,6 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_VIA_DEVICE, - DEVICE_DEFAULT_NAME, ) from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo @@ -27,12 +26,12 @@ class TelldusLiveEntity(Entity): """Base class for all Telldus Live entities.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, client, device_id): """Initialize the entity.""" self._id = device_id self._client = client - self._name = self.device.name self._async_unsub_dispatcher_connect = None async def async_added_to_hass(self): @@ -50,8 +49,6 @@ class TelldusLiveEntity(Entity): @callback def _update_callback(self): """Return the property of the device might have changed.""" - if self.device.name: - self._name = self.device.name self.async_write_ha_state() @property @@ -74,11 +71,6 @@ class TelldusLiveEntity(Entity): """Return true if unable to access real state of entity.""" return True - @property - def name(self): - """Return name of device.""" - return self._name or DEVICE_DEFAULT_NAME - @property def available(self): """Return true if device is not offline.""" diff --git a/homeassistant/components/tellduslive/light.py b/homeassistant/components/tellduslive/light.py index 3b69b58966c..8284b386250 100644 --- a/homeassistant/components/tellduslive/light.py +++ b/homeassistant/components/tellduslive/light.py @@ -37,6 +37,7 @@ async def async_setup_entry( class TelldusLiveLight(TelldusLiveEntity, LightEntity): """Representation of a Tellstick Net light.""" + _attr_name = None _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index d52fded3932..e15f89888b1 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -43,80 +43,73 @@ SENSOR_TYPE_BAROMETRIC_PRESSURE = "barpress" SENSOR_TYPES: dict[str, SensorEntityDescription] = { SENSOR_TYPE_TEMPERATURE: SensorEntityDescription( key=SENSOR_TYPE_TEMPERATURE, - name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_HUMIDITY: SensorEntityDescription( key=SENSOR_TYPE_HUMIDITY, - name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_RAINRATE: SensorEntityDescription( key=SENSOR_TYPE_RAINRATE, - name="Rain rate", native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, ), SENSOR_TYPE_RAINTOTAL: SensorEntityDescription( key=SENSOR_TYPE_RAINTOTAL, - name="Rain total", native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.TOTAL_INCREASING, ), SENSOR_TYPE_WINDDIRECTION: SensorEntityDescription( key=SENSOR_TYPE_WINDDIRECTION, - name="Wind direction", + translation_key="wind_direction", ), SENSOR_TYPE_WINDAVERAGE: SensorEntityDescription( key=SENSOR_TYPE_WINDAVERAGE, - name="Wind average", + translation_key="wind_average", native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_WINDGUST: SensorEntityDescription( key=SENSOR_TYPE_WINDGUST, - name="Wind gust", + translation_key="wind_gust", native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_UV: SensorEntityDescription( key=SENSOR_TYPE_UV, - name="UV", + translation_key="uv", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_WATT: SensorEntityDescription( key=SENSOR_TYPE_WATT, - name="Power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_LUMINANCE: SensorEntityDescription( key=SENSOR_TYPE_LUMINANCE, - name="Luminance", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_DEW_POINT: SensorEntityDescription( key=SENSOR_TYPE_DEW_POINT, - name="Dew Point", + translation_key="dew_point", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SENSOR_TYPE_BAROMETRIC_PRESSURE: SensorEntityDescription( key=SENSOR_TYPE_BAROMETRIC_PRESSURE, - name="Barometric Pressure", native_unit_of_measurement=UnitOfPressure.KPA, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, @@ -151,6 +144,8 @@ class TelldusLiveSensor(TelldusLiveEntity, SensorEntity): super().__init__(client, device_id) if desc := SENSOR_TYPES.get(self._type): self.entity_description = desc + else: + self._attr_name = None @property def device_id(self): @@ -182,14 +177,6 @@ class TelldusLiveSensor(TelldusLiveEntity, SensorEntity): """Return the value as humidity.""" return int(round(float(self._value))) - @property - def name(self): - """Return the name of the sensor.""" - quantity_name = ( - self.entity_description.name if hasattr(self, "entity_description") else "" - ) - return "{} {}".format(super().name, quantity_name or "").strip() - @property def native_value(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/tellduslive/strings.json b/homeassistant/components/tellduslive/strings.json index 27e74d6d938..1dbea7a0e6c 100644 --- a/homeassistant/components/tellduslive/strings.json +++ b/homeassistant/components/tellduslive/strings.json @@ -21,5 +21,24 @@ "title": "Pick endpoint." } } + }, + "entity": { + "sensor": { + "wind_direction": { + "name": "Wind direction" + }, + "wind_average": { + "name": "Wind average" + }, + "wind_gust": { + "name": "Wind gust" + }, + "uv": { + "name": "UV" + }, + "dew_point": { + "name": "Dew point" + } + } } } diff --git a/homeassistant/components/tellduslive/switch.py b/homeassistant/components/tellduslive/switch.py index fbecda2e775..5ae5a904689 100644 --- a/homeassistant/components/tellduslive/switch.py +++ b/homeassistant/components/tellduslive/switch.py @@ -34,6 +34,8 @@ async def async_setup_entry( class TelldusLiveSwitch(TelldusLiveEntity, SwitchEntity): """Representation of a Tellstick switch.""" + _attr_name = None + @property def is_on(self): """Return true if switch is on.""" From bc5f934f3584e6e2adf5a4508242e3fa5ddc4f50 Mon Sep 17 00:00:00 2001 From: Mike Woudenberg Date: Wed, 30 Aug 2023 16:21:52 +0200 Subject: [PATCH 1058/1151] Correct loqed token URL to production server (#99316) * Corrects token URL to production server * Update homeassistant/components/loqed/strings.json --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/loqed/config_flow.py | 4 ++-- homeassistant/components/loqed/strings.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/loqed/config_flow.py b/homeassistant/components/loqed/config_flow.py index 5eecc0b3f59..911ccb0ff5b 100644 --- a/homeassistant/components/loqed/config_flow.py +++ b/homeassistant/components/loqed/config_flow.py @@ -123,7 +123,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=user_data_schema, description_placeholders={ - "config_url": "https://integrations.production.loqed.com/personal-access-tokens", + "config_url": "https://integrations.loqed.com/personal-access-tokens", }, ) @@ -156,7 +156,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data_schema=user_data_schema, errors=errors, description_placeholders={ - "config_url": "https://integrations.production.loqed.com/personal-access-tokens", + "config_url": "https://integrations.loqed.com/personal-access-tokens", }, ) diff --git a/homeassistant/components/loqed/strings.json b/homeassistant/components/loqed/strings.json index 59b91fea195..e4cd4b71045 100644 --- a/homeassistant/components/loqed/strings.json +++ b/homeassistant/components/loqed/strings.json @@ -3,7 +3,7 @@ "flow_title": "LOQED Touch Smartlock setup", "step": { "user": { - "description": "Login at {config_url} and: \n* Create an API-key by clicking 'Create' \n* Copy the created access token.", + "description": "Login at LOQED's [personal access tokens portal]({config_url}) and: \n* Create an API-key by clicking 'Create' \n* Copy the created access token.", "data": { "name": "Name of your lock in the LOQED app.", "api_token": "[%key:common::config_flow::data::api_token%]" From 63c538b024fe3e5d0cad8739f714a547af1990ec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Aug 2023 16:22:52 +0200 Subject: [PATCH 1059/1151] Add config flow for template sensor (#98970) * Add config flow for template sensor * Tweak error reporting * Improve validation * Fix test * Rename translation strings * Improve validation * Fix sensor async_setup_entry * Avoid duplicating sensor device class translations * Avoid duplicating sensor device class translations * Add config flow tests * Include all units from DEVICE_CLASS_UNITS in unit_of_measurement select * Address review comments --- homeassistant/components/template/__init__.py | 22 + .../template/alarm_control_panel.py | 7 +- .../components/template/binary_sensor.py | 8 +- .../components/template/config_flow.py | 344 +++++++++++ homeassistant/components/template/cover.py | 7 +- homeassistant/components/template/fan.py | 7 +- homeassistant/components/template/image.py | 7 +- homeassistant/components/template/light.py | 7 +- homeassistant/components/template/lock.py | 7 +- .../components/template/manifest.json | 2 + homeassistant/components/template/number.py | 9 +- homeassistant/components/template/select.py | 9 +- homeassistant/components/template/sensor.py | 30 +- .../components/template/strings.json | 103 ++++ homeassistant/components/template/switch.py | 9 +- .../components/template/template_entity.py | 51 +- homeassistant/components/template/vacuum.py | 7 +- homeassistant/components/template/weather.py | 7 +- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 12 +- tests/components/template/test_config_flow.py | 569 ++++++++++++++++++ tests/components/template/test_sensor.py | 3 +- 22 files changed, 1174 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/template/config_flow.py create mode 100644 tests/components/template/test_config_flow.py diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 47b51853bcd..e9ced060491 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -6,6 +6,7 @@ from collections.abc import Callable import logging from homeassistant import config as conf_util +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_UNIQUE_ID, EVENT_HOMEASSISTANT_START, @@ -60,6 +61,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + await hass.config_entries.async_forward_entry_setups( + entry, (entry.options["template_type"],) + ) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (entry.options["template_type"],) + ) + + async def _process_config(hass: HomeAssistant, hass_config: ConfigType) -> None: """Process config.""" coordinators: list[TriggerUpdateCoordinator] | None = hass.data.pop(DOMAIN, None) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 8f164142212..af2e432c61e 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -254,13 +254,14 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): ) self._state = None - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" if self._template: self.add_template_attribute( "_state", self._template, None, self._update_state ) - await super().async_added_to_hass() + super()._async_setup_templates() async def _async_alarm_arm(self, state, script, code): """Arm the panel to specified state with supplied script.""" diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 61df78307f0..202ca0d9e4b 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -224,14 +224,18 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): self._delay_off_raw = config.get(CONF_DELAY_OFF) async def async_added_to_hass(self) -> None: - """Restore state and register callbacks.""" + """Restore state.""" if ( (self._delay_on_raw is not None or self._delay_off_raw is not None) and (last_state := await self.async_get_last_state()) is not None and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) ): self._state = last_state.state == STATE_ON + await super().async_added_to_hass() + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" self.add_template_attribute("_state", self._template, None, self._update_state) if self._delay_on_raw is not None: @@ -250,7 +254,7 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): "_delay_off", self._delay_off_raw, cv.positive_time_period ) - await super().async_added_to_hass() + super()._async_setup_templates() @callback def _update_state(self, result): diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py new file mode 100644 index 00000000000..1d87e8d89e8 --- /dev/null +++ b/homeassistant/components/template/config_flow.py @@ -0,0 +1,344 @@ +"""Config flow for the Template integration.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine, Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + DEVICE_CLASS_STATE_CLASSES, + DEVICE_CLASS_UNITS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_NAME, + CONF_STATE, + CONF_UNIT_OF_MEASUREMENT, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import selector +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, +) + +from .const import DOMAIN +from .sensor import async_create_preview_sensor +from .template_entity import TemplateEntity + +NONE_SENTINEL = "none" + + +def generate_schema(domain: str) -> dict[vol.Marker, Any]: + """Generate schema.""" + schema: dict[vol.Marker, Any] = {} + + if domain == Platform.SENSOR: + schema = { + vol.Optional( + CONF_UNIT_OF_MEASUREMENT, default=NONE_SENTINEL + ): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + NONE_SENTINEL, + *sorted( + { + str(unit) + for units in DEVICE_CLASS_UNITS.values() + for unit in units + if unit is not None + }, + key=str.casefold, + ), + ], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="sensor_unit_of_measurement", + custom_value=True, + ), + ), + vol.Optional( + CONF_DEVICE_CLASS, default=NONE_SENTINEL + ): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + NONE_SENTINEL, + *sorted( + [ + cls.value + for cls in SensorDeviceClass + if cls != SensorDeviceClass.ENUM + ], + key=str.casefold, + ), + ], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="sensor_device_class", + ), + ), + vol.Optional( + CONF_STATE_CLASS, default=NONE_SENTINEL + ): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + NONE_SENTINEL, + *sorted([cls.value for cls in SensorStateClass]), + ], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="sensor_state_class", + ), + ), + } + + return schema + + +def options_schema(domain: str) -> vol.Schema: + """Generate options schema.""" + return vol.Schema( + {vol.Required(CONF_STATE): selector.TemplateSelector()} + | generate_schema(domain), + ) + + +def config_schema(domain: str) -> vol.Schema: + """Generate config schema.""" + return vol.Schema( + { + vol.Required(CONF_NAME): selector.TextSelector(), + vol.Required(CONF_STATE): selector.TemplateSelector(), + } + | generate_schema(domain), + ) + + +async def choose_options_step(options: dict[str, Any]) -> str: + """Return next step_id for options flow according to template_type.""" + return cast(str, options["template_type"]) + + +def _strip_sentinel(options: dict[str, Any]) -> None: + """Convert sentinel to None.""" + for key in (CONF_DEVICE_CLASS, CONF_STATE_CLASS, CONF_UNIT_OF_MEASUREMENT): + if key not in options: + continue + if options[key] == NONE_SENTINEL: + options.pop(key) + + +def _validate_unit(options: dict[str, Any]) -> None: + """Validate unit of measurement.""" + if ( + (device_class := options.get(CONF_DEVICE_CLASS)) + and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None + and (unit := options.get(CONF_UNIT_OF_MEASUREMENT)) not in units + ): + units_string = sorted( + [str(unit) if unit else "no unit of measurement" for unit in units], + key=str.casefold, + ) + + raise vol.Invalid( + f"'{unit}' is not a valid unit for device class '{device_class}'; " + f"expected one of {', '.join(units_string)}" + ) + + +def _validate_state_class(options: dict[str, Any]) -> None: + """Validate state class.""" + if ( + (device_class := options.get(CONF_DEVICE_CLASS)) + and (state_classes := DEVICE_CLASS_STATE_CLASSES.get(device_class)) is not None + and (state_class := options.get(CONF_STATE_CLASS)) not in state_classes + ): + state_classes_string = sorted( + [str(state_class) for state_class in state_classes], + key=str.casefold, + ) + + raise vol.Invalid( + f"'{state_class}' is not a valid state class for device class " + f"'{device_class}'; expected one of {', '.join(state_classes_string)}" + ) + + +def validate_user_input( + template_type: str, +) -> Callable[ + [SchemaCommonFlowHandler, dict[str, Any]], + Coroutine[Any, Any, dict[str, Any]], +]: + """Do post validation of user input. + + For sensors: Strip none-sentinels and validate unit of measurement. + For all domaines: Set template type. + """ + + async def _validate_user_input( + _: SchemaCommonFlowHandler, + user_input: dict[str, Any], + ) -> dict[str, Any]: + """Add template type to user input.""" + if template_type == Platform.SENSOR: + _strip_sentinel(user_input) + _validate_unit(user_input) + _validate_state_class(user_input) + return {"template_type": template_type} | user_input + + return _validate_user_input + + +TEMPLATE_TYPES = [ + "sensor", +] + +CONFIG_FLOW = { + "user": SchemaFlowMenuStep(TEMPLATE_TYPES), + Platform.SENSOR: SchemaFlowFormStep( + config_schema(Platform.SENSOR), + preview="template", + validate_user_input=validate_user_input(Platform.SENSOR), + ), +} + + +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(next_step=choose_options_step), + Platform.SENSOR: SchemaFlowFormStep( + options_schema(Platform.SENSOR), + preview="template", + validate_user_input=validate_user_input(Platform.SENSOR), + ), +} + +CREATE_PREVIEW_ENTITY: dict[ + str, + Callable[[HomeAssistant, str, dict[str, Any]], TemplateEntity], +] = { + "sensor": async_create_preview_sensor, +} + + +class TemplateConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle config flow for template helper.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + @callback + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) + + @staticmethod + async def async_setup_preview(hass: HomeAssistant) -> None: + """Set up preview WS API.""" + websocket_api.async_register_command(hass, ws_start_preview) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "template/start_preview", + vol.Required("flow_id"): str, + vol.Required("flow_type"): vol.Any("config_flow", "options_flow"), + vol.Required("user_input"): dict, + } +) +@callback +def ws_start_preview( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Generate a preview.""" + + def _validate(schema: vol.Schema, domain: str, user_input: dict[str, Any]) -> Any: + errors = {} + key: vol.Marker + for key, validator in schema.schema.items(): + if key.schema not in user_input: + continue + try: + validator(user_input[key.schema]) + except vol.Invalid as ex: + errors[key.schema] = str(ex.msg) + + if domain == Platform.SENSOR: + _strip_sentinel(user_input) + try: + _validate_unit(user_input) + except vol.Invalid as ex: + errors[CONF_UNIT_OF_MEASUREMENT] = str(ex.msg) + try: + _validate_state_class(user_input) + except vol.Invalid as ex: + errors[CONF_STATE_CLASS] = str(ex.msg) + + return errors + + if msg["flow_type"] == "config_flow": + flow_status = hass.config_entries.flow.async_get(msg["flow_id"]) + template_type = flow_status["step_id"] + form_step = cast(SchemaFlowFormStep, CONFIG_FLOW[template_type]) + schema = cast(vol.Schema, form_step.schema) + name = msg["user_input"]["name"] + else: + flow_status = hass.config_entries.options.async_get(msg["flow_id"]) + config_entry = hass.config_entries.async_get_entry(flow_status["handler"]) + if not config_entry: + raise HomeAssistantError + template_type = config_entry.options["template_type"] + name = config_entry.options["name"] + schema = cast(vol.Schema, OPTIONS_FLOW[template_type].schema) + + errors = _validate(schema, template_type, msg["user_input"]) + + @callback + def async_preview_updated( + state: str | None, + attributes: Mapping[str, Any] | None, + error: str | None, + ) -> None: + """Forward config entry state events to websocket.""" + if error is not None: + connection.send_message( + websocket_api.event_message( + msg["id"], + {"error": error}, + ) + ) + return + connection.send_message( + websocket_api.event_message( + msg["id"], + {"attributes": attributes, "state": state}, + ) + ) + + if errors: + connection.send_message( + { + "id": msg["id"], + "type": websocket_api.const.TYPE_RESULT, + "success": False, + "error": {"code": "invalid_user_input", "message": errors}, + } + ) + return + + _strip_sentinel(msg["user_input"]) + preview_entity = CREATE_PREVIEW_ENTITY[template_type](hass, name, msg["user_input"]) + preview_entity.hass = hass + + connection.send_result(msg["id"]) + connection.subscriptions[msg["id"]] = preview_entity.async_start_preview( + async_preview_updated + ) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 256773b714b..3a8e536f7f5 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -182,8 +182,9 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._is_closing = False self._tilt_value = None - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" if self._template: self.add_template_attribute( "_position", self._template, None, self._update_state @@ -204,7 +205,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._update_tilt, none_on_template_error=True, ) - await super().async_added_to_hass() + super()._async_setup_templates() @callback def _update_state(self, result): diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 88309810ad2..c07c680887b 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -351,8 +351,9 @@ class TemplateFan(TemplateEntity, FanEntity): self._state = False - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" if self._template: self.add_template_attribute( "_state", self._template, None, self._update_state @@ -390,7 +391,7 @@ class TemplateFan(TemplateEntity, FanEntity): self._update_direction, none_on_template_error=True, ) - await super().async_added_to_hass() + super()._async_setup_templates() @callback def _update_percentage(self, percentage): diff --git a/homeassistant/components/template/image.py b/homeassistant/components/template/image.py index da0fbd68bc0..55a0e2fb72d 100644 --- a/homeassistant/components/template/image.py +++ b/homeassistant/components/template/image.py @@ -108,10 +108,11 @@ class StateImageEntity(TemplateEntity, ImageEntity): self._cached_image = None self._attr_image_url = result - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" self.add_template_attribute("_url", self._url_template, None, self._update_url) - await super().async_added_to_hass() + super()._async_setup_templates() class TriggerImageEntity(TriggerEntity, ImageEntity): diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index a3dd1fd1ef3..09f5054ed51 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -268,8 +268,9 @@ class LightTemplate(TemplateEntity, LightEntity): """Return true if device is on.""" return self._state - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" if self._template: self.add_template_attribute( "_state", self._template, None, self._update_state @@ -338,7 +339,7 @@ class LightTemplate(TemplateEntity, LightEntity): self._update_supports_transition, none_on_template_error=True, ) - await super().async_added_to_hass() + super()._async_setup_templates() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index da8be80d8a4..d8c7127f0e6 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -133,12 +133,13 @@ class TemplateLock(TemplateEntity, LockEntity): self._state = None - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" self.add_template_attribute( "_state", self._state_template, None, self._update_state ) - await super().async_added_to_hass() + super()._async_setup_templates() async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index 6fe6bfb9db4..4112ca7a73f 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -3,7 +3,9 @@ "name": "Template", "after_dependencies": ["group"], "codeowners": ["@PhracturedBlue", "@tetienne", "@home-assistant/core"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/template", + "integration_type": "helper", "iot_class": "local_push", "quality_scale": "internal" } diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 4e74b469984..988cebf08ab 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -18,7 +18,7 @@ from homeassistant.components.number import ( NumberEntity, ) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script @@ -124,8 +124,9 @@ class TemplateNumber(TemplateEntity, NumberEntity): self._attr_native_min_value = DEFAULT_MIN_VALUE self._attr_native_max_value = DEFAULT_MAX_VALUE - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" self.add_template_attribute( "_attr_native_value", self._value_template, @@ -152,7 +153,7 @@ class TemplateNumber(TemplateEntity, NumberEntity): validator=vol.Coerce(float), none_on_template_error=True, ) - await super().async_added_to_hass() + super()._async_setup_templates() async def async_set_native_value(self, value: float) -> None: """Set value of the number.""" diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index 7871410a694..fea972a5d6f 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -13,7 +13,7 @@ from homeassistant.components.select import ( SelectEntity, ) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script @@ -114,8 +114,9 @@ class TemplateSelect(TemplateEntity, SelectEntity): self._attr_options = [] self._attr_current_option = None - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" self.add_template_attribute( "_attr_current_option", self._value_template, @@ -128,7 +129,7 @@ class TemplateSelect(TemplateEntity, SelectEntity): validator=vol.All(cv.ensure_list, [cv.string]), none_on_template_error=True, ) - await super().async_added_to_hass() + super()._async_setup_templates() async def async_select_option(self, option: str) -> None: """Change the selected option.""" diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 36e54eaabc9..cdd14921bc1 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_CLASS, @@ -195,6 +196,28 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize config entry.""" + _options = dict(config_entry.options) + _options.pop("template_type") + validated_config = SENSOR_SCHEMA(_options) + async_add_entities([SensorTemplate(hass, validated_config, config_entry.entry_id)]) + + +@callback +def async_create_preview_sensor( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> SensorTemplate: + """Create a preview sensor.""" + validated_config = SENSOR_SCHEMA(config | {CONF_NAME: name}) + entity = SensorTemplate(hass, validated_config, None) + return entity + + class SensorTemplate(TemplateEntity, SensorEntity): """Representation of a Template Sensor.""" @@ -217,13 +240,14 @@ class SensorTemplate(TemplateEntity, SensorEntity): ENTITY_ID_FORMAT, object_id, hass=hass ) - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" self.add_template_attribute( "_attr_native_value", self._template, None, self._update_state ) - await super().async_added_to_hass() + super()._async_setup_templates() @callback def _update_state(self, result): diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index fce7129353e..6ceb4b495ef 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -1,4 +1,107 @@ { + "config": { + "step": { + "sensor": { + "data": { + "device_class": "Device class", + "name": "[%key:common::config_flow::data::name%]", + "state_class": "[%key:component::sensor::entity_component::_::state_attributes::state_class::name%]", + "state_template": "State template", + "unit_of_measurement": "Unit of measurement" + }, + "title": "Template sensor" + }, + "user": { + "description": "This helper allow you to create helper entities that define their state using a template.", + "menu_options": { + "sensor": "Template a sensor" + }, + "title": "Template helper" + } + } + }, + "options": { + "step": { + "sensor": { + "data": { + "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", + "state_class": "[%key:component::template::config::step::sensor::data::state_class%]", + "state_template": "[%key:component::template::config::step::sensor::data::state_template%]", + "unit_of_measurement": "[%key:component::template::config::step::sensor::data::unit_of_measurement%]" + }, + "title": "[%key:component::template::config::step::sensor::title%]" + } + } + }, + "selector": { + "sensor_device_class": { + "options": { + "none": "No device class", + "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", + "aqi": "[%key:component::sensor::entity_component::aqi::name%]", + "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", + "battery": "[%key:component::sensor::entity_component::battery::name%]", + "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", + "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", + "current": "[%key:component::sensor::entity_component::current::name%]", + "data_rate": "[%key:component::sensor::entity_component::data_rate::name%]", + "data_size": "[%key:component::sensor::entity_component::data_size::name%]", + "date": "[%key:component::sensor::entity_component::date::name%]", + "distance": "[%key:component::sensor::entity_component::distance::name%]", + "duration": "[%key:component::sensor::entity_component::duration::name%]", + "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", + "frequency": "[%key:component::sensor::entity_component::frequency::name%]", + "gas": "[%key:component::sensor::entity_component::gas::name%]", + "humidity": "[%key:component::sensor::entity_component::humidity::name%]", + "illuminance": "[%key:component::sensor::entity_component::illuminance::name%]", + "irradiance": "[%key:component::sensor::entity_component::irradiance::name%]", + "moisture": "[%key:component::sensor::entity_component::moisture::name%]", + "monetary": "[%key:component::sensor::entity_component::monetary::name%]", + "nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]", + "nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]", + "nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]", + "ozone": "[%key:component::sensor::entity_component::ozone::name%]", + "ph": "[%key:component::sensor::entity_component::ph::name%]", + "pm1": "[%key:component::sensor::entity_component::pm1::name%]", + "pm10": "[%key:component::sensor::entity_component::pm10::name%]", + "pm25": "[%key:component::sensor::entity_component::pm25::name%]", + "power": "[%key:component::sensor::entity_component::power::name%]", + "power_factor": "[%key:component::sensor::entity_component::power_factor::name%]", + "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", + "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", + "pressure": "[%key:component::sensor::entity_component::pressure::name%]", + "reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]", + "signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]", + "sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]", + "speed": "[%key:component::sensor::entity_component::speed::name%]", + "sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]", + "temperature": "[%key:component::sensor::entity_component::temperature::name%]", + "timestamp": "[%key:component::sensor::entity_component::timestamp::name%]", + "volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]", + "volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]", + "voltage": "[%key:component::sensor::entity_component::voltage::name%]", + "volume": "[%key:component::sensor::entity_component::volume::name%]", + "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", + "water": "[%key:component::sensor::entity_component::water::name%]", + "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" + } + }, + "sensor_state_class": { + "options": { + "none": "No state class", + "measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]", + "total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]", + "total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]" + } + }, + "sensor_unit_of_measurement": { + "options": { + "none": "No unit of measurement" + } + } + }, "services": { "reload": { "name": "[%key:common::action::reload%]", diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index b21e02e4074..39270d3fc6d 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -138,14 +138,17 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): await super().async_added_to_hass() if state := await self.async_get_last_state(): self._state = state.state == STATE_ON + await super().async_added_to_hass() - # no need to listen for events - else: + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" + if self._template is not None: self.add_template_attribute( "_state", self._template, None, self._update_state ) - await super().async_added_to_hass() + super()._async_setup_templates() @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 64112b0d3d4..ac06e2c8734 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -1,7 +1,7 @@ """TemplateEntity utility class.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping import contextlib import itertools import logging @@ -18,7 +18,14 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, ) -from homeassistant.core import Context, CoreState, HomeAssistant, State, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Context, + CoreState, + HomeAssistant, + State, + callback, +) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -256,6 +263,9 @@ class TemplateEntity(Entity): self._attr_extra_state_attributes = {} self._self_ref_update_count = 0 self._attr_unique_id = unique_id + self._preview_callback: Callable[ + [str | None, dict[str, Any] | None, str | None], None + ] | None = None if config is None: self._attribute_templates = attribute_templates self._availability_template = availability_template @@ -408,9 +418,14 @@ class TemplateEntity(Entity): event, update.template, update.last_result, update.result ) - self.async_write_ha_state() + if not self._preview_callback: + self.async_write_ha_state() + return - async def _async_template_startup(self, *_: Any) -> None: + self._preview_callback(*self._async_generate_attributes(), None) + + @callback + def _async_template_startup(self, *_: Any) -> None: template_var_tups: list[TrackTemplate] = [] has_availability_template = False @@ -441,8 +456,9 @@ class TemplateEntity(Entity): self._async_update = result_info.async_refresh result_info.async_refresh() - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" if self._availability_template is not None: self.add_template_attribute( "_attr_available", @@ -467,8 +483,29 @@ class TemplateEntity(Entity): ): self.add_template_attribute("_attr_name", self._friendly_name_template) + @callback + def async_start_preview( + self, + preview_callback: Callable[ + [str | None, Mapping[str, Any] | None, str | None], None + ], + ) -> CALLBACK_TYPE: + """Render a preview.""" + + self._preview_callback = preview_callback + self._async_setup_templates() + try: + self._async_template_startup() + except Exception as err: # pylint: disable=broad-exception-caught + preview_callback(None, None, str(err)) + return self._call_on_remove_callbacks + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self._async_setup_templates() + if self.hass.state == CoreState.running: - await self._async_template_startup() + self._async_template_startup() return self.hass.bus.async_listen_once( diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index c5705c34076..4b693c8070c 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -264,8 +264,9 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._attr_fan_speed_list, ) - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" if self._template is not None: self.add_template_attribute( "_state", self._template, None, self._update_state @@ -285,7 +286,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._update_battery_level, none_on_template_error=True, ) - await super().async_added_to_hass() + super()._async_setup_templates() @callback def _update_state(self, result): diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 85f2f82c213..a04fc7a641d 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -301,8 +301,9 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): return "Powered by Home Assistant" return self._attribution - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" if self._condition_template: self.add_template_attribute( @@ -398,7 +399,7 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): validator=partial(self._validate_forecast, "twice_daily"), ) - await super().async_added_to_hass() + super()._async_setup_templates() @callback def _update_forecast( diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index fe23ae9697f..7d84dc87cbe 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -10,6 +10,7 @@ FLOWS = { "integration", "min_max", "switch_as_x", + "template", "threshold", "tod", "utility_meter", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 325b9333c26..ef496e7b58b 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5669,12 +5669,6 @@ "config_flow": false, "iot_class": "local_polling" }, - "template": { - "name": "Template", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_push" - }, "tensorflow": { "name": "TensorFlow", "integration_type": "hub", @@ -6717,6 +6711,12 @@ "config_flow": true, "iot_class": "calculated" }, + "template": { + "name": "Template", + "integration_type": "helper", + "config_flow": true, + "iot_class": "local_push" + }, "threshold": { "integration_type": "helper", "config_flow": true, diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py new file mode 100644 index 00000000000..dbaa23fae11 --- /dev/null +++ b/tests/components/template/test_config_flow.py @@ -0,0 +1,569 @@ +"""Test the Switch config flow.""" +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.template import DOMAIN, async_setup_entry +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry +from tests.typing import WebSocketGenerator + + +@pytest.mark.parametrize( + ( + "template_type", + "state_template", + "template_state", + "input_states", + "input_attributes", + "extra_input", + "extra_options", + "extra_attrs", + ), + ( + ( + "sensor", + "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", + "50.0", + {"one": "30.0", "two": "20.0"}, + {}, + {}, + {}, + {}, + ), + ), +) +async def test_config_flow( + hass: HomeAssistant, + template_type, + state_template, + template_state, + input_states, + input_attributes, + extra_input, + extra_options, + extra_attrs, +) -> None: + """Test the config flow.""" + input_entities = ["one", "two"] + for input_entity in input_entities: + hass.states.async_set( + f"{template_type}.{input_entity}", + input_states[input_entity], + input_attributes.get(input_entity, {}), + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": template_type}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == template_type + + with patch( + "homeassistant.components.template.async_setup_entry", wraps=async_setup_entry + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "name": "My template", + "state": state_template, + **extra_input, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "My template" + assert result["data"] == {} + assert result["options"] == { + "name": "My template", + "state": state_template, + "template_type": template_type, + **extra_options, + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "name": "My template", + "state": state_template, + "template_type": template_type, + **extra_options, + } + + state = hass.states.get(f"{template_type}.my_template") + assert state.state == template_state + for key in extra_attrs: + assert state.attributes[key] == extra_attrs[key] + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema: + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize( + ( + "template_type", + "old_state_template", + "new_state_template", + "input_states", + "extra_options", + "options_options", + ), + ( + ( + "sensor", + "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", + "{{ float(states('sensor.one')) - float(states('sensor.two')) }}", + {"one": "30.0", "two": "20.0"}, + {}, + {}, + ), + ), +) +async def test_options( + hass: HomeAssistant, + template_type, + old_state_template, + new_state_template, + input_states, + extra_options, + options_options, +) -> None: + """Test reconfiguring.""" + input_entities = ["one", "two"] + + for input_entity in input_entities: + hass.states.async_set( + f"{template_type}.{input_entity}", input_states[input_entity], {} + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My template", + "state": old_state_template, + "template_type": template_type, + **extra_options, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{template_type}.my_template") + assert state.state == "50.0" + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == template_type + assert get_suggested(result["data_schema"].schema, "state") == old_state_template + assert "name" not in result["data_schema"].schema + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"state": new_state_template, **options_options}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "name": "My template", + "state": new_state_template, + "template_type": template_type, + **extra_options, + } + assert config_entry.data == {} + assert config_entry.options == { + "name": "My template", + "state": new_state_template, + "template_type": template_type, + **extra_options, + } + assert config_entry.title == "My template" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + state = hass.states.get(f"{template_type}.my_template") + assert state.state == "10.0" + + # Check we don't get suggestions from another entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": template_type}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == template_type + + assert get_suggested(result["data_schema"].schema, "name") is None + assert get_suggested(result["data_schema"].schema, "state") is None + + +@pytest.mark.parametrize( + ( + "template_type", + "state_template", + "extra_user_input", + "input_states", + "template_state", + "extra_attributes", + ), + ( + ( + "sensor", + "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", + {}, + {"one": "30.0", "two": "20.0"}, + "50.0", + [{}, {}], + ), + ), +) +async def test_config_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + template_type: str, + state_template: str, + extra_user_input: dict[str, Any], + input_states: list[str], + template_state: str, + extra_attributes: list[dict[str, Any]], +) -> None: + """Test the config flow preview.""" + client = await hass_ws_client(hass) + + input_entities = ["one", "two"] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": template_type}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == template_type + assert result["errors"] is None + assert result["preview"] == "template" + + await client.send_json_auto_id( + { + "type": "template/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": {"name": "My template", "state": state_template} + | extra_user_input, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + msg = await client.receive_json() + assert msg["event"] == { + "attributes": {"friendly_name": "My template"} | extra_attributes[0], + "state": "unavailable", + } + + for input_entity in input_entities: + hass.states.async_set( + f"{template_type}.{input_entity}", input_states[input_entity], {} + ) + + msg = await client.receive_json() + assert msg["event"] == { + "attributes": {"friendly_name": "My template"} + | extra_attributes[0] + | extra_attributes[1], + "state": template_state, + } + assert len(hass.states.async_all()) == 2 + + +EARLY_END_ERROR = "invalid template (TemplateSyntaxError: unexpected 'end of template')" + + +@pytest.mark.parametrize( + ("template_type", "state_template", "extra_user_input", "error"), + [ + ("sensor", "{{", {}, {"state": EARLY_END_ERROR}), + ( + "sensor", + "", + {"device_class": "temperature", "unit_of_measurement": "cats"}, + { + "state_class": ( + "'None' is not a valid state class for device class 'temperature'; " + "expected one of measurement" + ), + "unit_of_measurement": ( + "'cats' is not a valid unit for device class 'temperature'; " + "expected one of K, °C, °F" + ), + }, + ), + ], +) +async def test_config_flow_preview_bad_input( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + template_type: str, + state_template: str, + extra_user_input: dict[str, str], + error: str, +) -> None: + """Test the config flow preview.""" + client = await hass_ws_client(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": template_type}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == template_type + assert result["errors"] is None + assert result["preview"] == "template" + + await client.send_json_auto_id( + { + "type": "template/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": {"name": "My template", "state": state_template} + | extra_user_input, + } + ) + msg = await client.receive_json() + assert not msg["success"] + assert msg["error"] == { + "code": "invalid_user_input", + "message": error, + } + + +@pytest.mark.parametrize( + ( + "template_type", + "state_template", + "extra_user_input", + ), + ( + ( + "sensor", + "{{ states('sensor.one') }}", + {"unit_of_measurement": "°C"}, + ), + ), +) +async def test_config_flow_preview_bad_state( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + template_type: str, + state_template: str, + extra_user_input: dict[str, Any], +) -> None: + """Test the config flow preview.""" + client = await hass_ws_client(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": template_type}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == template_type + assert result["errors"] is None + assert result["preview"] == "template" + + await client.send_json_auto_id( + { + "type": "template/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": {"name": "My template", "state": state_template} + | extra_user_input, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + msg = await client.receive_json() + assert msg["event"] == { + "error": ( + "Sensor None has device class 'None', state class 'None' unit '°C' " + "and suggested precision 'None' thus indicating it has a numeric " + "value; however, it has the non-numeric value: 'unknown' ()" + ), + } + + +@pytest.mark.parametrize( + ( + "template_type", + "old_state_template", + "new_state_template", + "extra_config_flow_data", + "extra_user_input", + "input_states", + "template_state", + "extra_attributes", + ), + [ + ( + "sensor", + "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", + "{{ float(states('sensor.one')) - float(states('sensor.two')) }}", + {}, + {}, + {"one": "30.0", "two": "20.0"}, + "10.0", + {}, + ), + ], +) +async def test_option_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + template_type: str, + old_state_template: str, + new_state_template: str, + extra_config_flow_data: dict[str, Any], + extra_user_input: dict[str, Any], + input_states: list[str], + template_state: str, + extra_attributes: dict[str, Any], +) -> None: + """Test the option flow preview.""" + client = await hass_ws_client(hass) + + input_entities = input_entities = ["one", "two"] + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My template", + "state": old_state_template, + "template_type": template_type, + } + | extra_config_flow_data, + title="My template", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + assert result["preview"] == "template" + + for input_entity in input_entities: + hass.states.async_set( + f"{template_type}.{input_entity}", input_states[input_entity], {} + ) + + await client.send_json_auto_id( + { + "type": "template/start_preview", + "flow_id": result["flow_id"], + "flow_type": "options_flow", + "user_input": {"state": new_state_template} | extra_user_input, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + msg = await client.receive_json() + assert msg["event"] == { + "attributes": {"friendly_name": "My template"} | extra_attributes, + "state": template_state, + } + assert len(hass.states.async_all()) == 3 + + +async def test_option_flow_sensor_preview_config_entry_removed( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test the option flow preview where the config entry is removed.""" + client = await hass_ws_client(hass) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My template", + "state": "Hello!", + "template_type": "sensor", + }, + title="My template", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + assert result["preview"] == "template" + + await hass.config_entries.async_remove(config_entry.entry_id) + + await client.send_json_auto_id( + { + "type": "template/start_preview", + "flow_id": result["flow_id"], + "flow_type": "options_flow", + "user_input": {"state": "Goodbye!"}, + } + ) + msg = await client.receive_json() + assert not msg["success"] + assert msg["error"] == {"code": "unknown_error", "message": "Unknown error"} diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index d3e3ebf5812..5eca8330789 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -725,8 +725,9 @@ async def test_this_variable_early_hass_not_running( # Signal hass started hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() + await hass.async_block_till_done() - # Re-render icon, name, pciture + other templates now rendered + # icon, name, picture + other templates now re-rendered state = hass.states.get(entity_id) assert state.state == "sensor.none_false: sensor.none_false" assert state.attributes == { From f9b2e10f72efd5927c25c7a7342ba6e2af019c8a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Aug 2023 16:37:13 +0200 Subject: [PATCH 1060/1151] Add new board type (#99334) --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/hassio/__init__.py | 1 + .../homeassistant_green/__init__.py | 27 ++++++ .../homeassistant_green/config_flow.py | 22 +++++ .../components/homeassistant_green/const.py | 3 + .../homeassistant_green/hardware.py | 44 +++++++++ .../homeassistant_green/manifest.json | 9 ++ mypy.ini | 10 ++ script/hassfest/manifest.py | 1 + tests/components/hassio/test_init.py | 1 + .../homeassistant_green/__init__.py | 1 + .../homeassistant_green/test_config_flow.py | 58 +++++++++++ .../homeassistant_green/test_hardware.py | 96 +++++++++++++++++++ .../homeassistant_green/test_init.py | 75 +++++++++++++++ 15 files changed, 351 insertions(+) create mode 100644 homeassistant/components/homeassistant_green/__init__.py create mode 100644 homeassistant/components/homeassistant_green/config_flow.py create mode 100644 homeassistant/components/homeassistant_green/const.py create mode 100644 homeassistant/components/homeassistant_green/hardware.py create mode 100644 homeassistant/components/homeassistant_green/manifest.json create mode 100644 tests/components/homeassistant_green/__init__.py create mode 100644 tests/components/homeassistant_green/test_config_flow.py create mode 100644 tests/components/homeassistant_green/test_hardware.py create mode 100644 tests/components/homeassistant_green/test_init.py diff --git a/.strict-typing b/.strict-typing index b8dc93d6780..e8bca0a1abd 100644 --- a/.strict-typing +++ b/.strict-typing @@ -149,6 +149,7 @@ homeassistant.components.history.* homeassistant.components.homeassistant.exposed_entities homeassistant.components.homeassistant.triggers.event homeassistant.components.homeassistant_alerts.* +homeassistant.components.homeassistant_green.* homeassistant.components.homeassistant_hardware.* homeassistant.components.homeassistant_sky_connect.* homeassistant.components.homeassistant_yellow.* diff --git a/CODEOWNERS b/CODEOWNERS index f33d4052304..2d28671fce5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -521,6 +521,8 @@ build.json @home-assistant/supervisor /tests/components/homeassistant/ @home-assistant/core /homeassistant/components/homeassistant_alerts/ @home-assistant/core /tests/components/homeassistant_alerts/ @home-assistant/core +/homeassistant/components/homeassistant_green/ @home-assistant/core +/tests/components/homeassistant_green/ @home-assistant/core /homeassistant/components/homeassistant_hardware/ @home-assistant/core /tests/components/homeassistant_hardware/ @home-assistant/core /homeassistant/components/homeassistant_sky_connect/ @home-assistant/core diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 0e0d42149fc..72fb5ce5110 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -254,6 +254,7 @@ MAP_SERVICE_API = { } HARDWARE_INTEGRATIONS = { + "green": "homeassistant_green", "odroid-c2": "hardkernel", "odroid-c4": "hardkernel", "odroid-m1": "hardkernel", diff --git a/homeassistant/components/homeassistant_green/__init__.py b/homeassistant/components/homeassistant_green/__init__.py new file mode 100644 index 00000000000..fbcd2093778 --- /dev/null +++ b/homeassistant/components/homeassistant_green/__init__.py @@ -0,0 +1,27 @@ +"""The Home Assistant Green integration.""" +from __future__ import annotations + +from homeassistant.components.hassio import get_os_info +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Home Assistant Green config entry.""" + if (os_info := get_os_info(hass)) is None: + # The hassio integration has not yet fetched data from the supervisor + raise ConfigEntryNotReady + + board: str | None + if (board := os_info.get("board")) is None or board != "green": + # Not running on a Home Assistant Green, Home Assistant may have been migrated + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return True diff --git a/homeassistant/components/homeassistant_green/config_flow.py b/homeassistant/components/homeassistant_green/config_flow.py new file mode 100644 index 00000000000..17ba9aacbc5 --- /dev/null +++ b/homeassistant/components/homeassistant_green/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow for the Home Assistant Green 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 HomeAssistantGreenConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Home Assistant Green.""" + + VERSION = 1 + + async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return self.async_create_entry(title="Home Assistant Green", data={}) diff --git a/homeassistant/components/homeassistant_green/const.py b/homeassistant/components/homeassistant_green/const.py new file mode 100644 index 00000000000..9046a44c12b --- /dev/null +++ b/homeassistant/components/homeassistant_green/const.py @@ -0,0 +1,3 @@ +"""Constants for the Home Assistant Green integration.""" + +DOMAIN = "homeassistant_green" diff --git a/homeassistant/components/homeassistant_green/hardware.py b/homeassistant/components/homeassistant_green/hardware.py new file mode 100644 index 00000000000..2b5268f8d03 --- /dev/null +++ b/homeassistant/components/homeassistant_green/hardware.py @@ -0,0 +1,44 @@ +"""The Home Assistant Green hardware platform.""" +from __future__ import annotations + +from homeassistant.components.hardware.models import BoardInfo, HardwareInfo +from homeassistant.components.hassio import get_os_info +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +BOARD_NAME = "Home Assistant Green" +MANUFACTURER = "homeassistant" +MODEL = "green" + + +@callback +def async_info(hass: HomeAssistant) -> list[HardwareInfo]: + """Return board info.""" + if (os_info := get_os_info(hass)) is None: + raise HomeAssistantError + board: str | None + if (board := os_info.get("board")) is None: + raise HomeAssistantError + if not board == "green": + raise HomeAssistantError + + config_entries = [ + entry.entry_id for entry in hass.config_entries.async_entries(DOMAIN) + ] + + return [ + HardwareInfo( + board=BoardInfo( + hassio_board_id=board, + manufacturer=MANUFACTURER, + model=MODEL, + revision=None, + ), + config_entries=config_entries, + dongle=None, + name=BOARD_NAME, + url=None, + ) + ] diff --git a/homeassistant/components/homeassistant_green/manifest.json b/homeassistant/components/homeassistant_green/manifest.json new file mode 100644 index 00000000000..7c9dd0322ec --- /dev/null +++ b/homeassistant/components/homeassistant_green/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "homeassistant_green", + "name": "Home Assistant Green", + "codeowners": ["@home-assistant/core"], + "config_flow": false, + "dependencies": ["hardware", "hassio", "homeassistant_hardware"], + "documentation": "https://www.home-assistant.io/integrations/homeassistant_green", + "integration_type": "hardware" +} diff --git a/mypy.ini b/mypy.ini index 8278a19465c..82cce328c6a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1252,6 +1252,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homeassistant_green.*] +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.homeassistant_hardware.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 65e37aa515d..9323b8e86c0 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -71,6 +71,7 @@ NO_IOT_CLASS = [ "history", "homeassistant", "homeassistant_alerts", + "homeassistant_green", "homeassistant_hardware", "homeassistant_sky_connect", "homeassistant_yellow", diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 4b10c58036e..31ee73013da 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -891,6 +891,7 @@ async def test_coordinator_updates( @pytest.mark.parametrize( ("extra_os_info", "integration"), [ + ({"board": "green"}, "homeassistant_green"), ({"board": "odroid-c2"}, "hardkernel"), ({"board": "odroid-c4"}, "hardkernel"), ({"board": "odroid-n2"}, "hardkernel"), diff --git a/tests/components/homeassistant_green/__init__.py b/tests/components/homeassistant_green/__init__.py new file mode 100644 index 00000000000..a84e076d9c9 --- /dev/null +++ b/tests/components/homeassistant_green/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Assistant Green integration.""" diff --git a/tests/components/homeassistant_green/test_config_flow.py b/tests/components/homeassistant_green/test_config_flow.py new file mode 100644 index 00000000000..2eb7389af55 --- /dev/null +++ b/tests/components/homeassistant_green/test_config_flow.py @@ -0,0 +1,58 @@ +"""Test the Home Assistant Green config flow.""" +from unittest.mock import patch + +from homeassistant.components.homeassistant_green.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_config_flow(hass: HomeAssistant) -> None: + """Test the config flow.""" + mock_integration(hass, MockModule("hassio")) + + with patch( + "homeassistant.components.homeassistant_green.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Home Assistant Green" + assert result["data"] == {} + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == {} + assert config_entry.title == "Home Assistant Green" + + +async def test_config_flow_single_entry(hass: HomeAssistant) -> None: + """Test only a single entry is allowed.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_green.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + mock_setup_entry.assert_not_called() diff --git a/tests/components/homeassistant_green/test_hardware.py b/tests/components/homeassistant_green/test_hardware.py new file mode 100644 index 00000000000..8aacf09978d --- /dev/null +++ b/tests/components/homeassistant_green/test_hardware.py @@ -0,0 +1,96 @@ +"""Test the Home Assistant Green hardware platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.homeassistant_green.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, MockModule, mock_integration +from tests.typing import WebSocketGenerator + + +async def test_hardware_info( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test we can get the board info.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_green.get_os_info", + return_value={"board": "green"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.homeassistant_green.hardware.get_os_info", + return_value={"board": "green"}, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == { + "hardware": [ + { + "board": { + "hassio_board_id": "green", + "manufacturer": "homeassistant", + "model": "green", + "revision": None, + }, + "config_entries": [config_entry.entry_id], + "dongle": None, + "name": "Home Assistant Green", + "url": None, + } + ] + } + + +@pytest.mark.parametrize("os_info", [None, {"board": None}, {"board": "other"}]) +async def test_hardware_info_fail( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator, os_info +) -> None: + """Test async_info raises if os_info is not as expected.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_green.get_os_info", + return_value={"board": "green"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.homeassistant_green.hardware.get_os_info", + return_value=os_info, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == {"hardware": []} diff --git a/tests/components/homeassistant_green/test_init.py b/tests/components/homeassistant_green/test_init.py new file mode 100644 index 00000000000..f48aea3fdfb --- /dev/null +++ b/tests/components/homeassistant_green/test_init.py @@ -0,0 +1,75 @@ +"""Test the Home Assistant Green integration.""" +from unittest.mock import patch + +from homeassistant.components.homeassistant_green.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_setup_entry(hass: HomeAssistant) -> None: + """Test setup of a config entry.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_green.get_os_info", + return_value={"board": "green"}, + ) as mock_get_os_info: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + # Test unloading the config entry + assert await hass.config_entries.async_unload(config_entry.entry_id) + + +async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: + """Test setup of a config entry with wrong board type.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_green.get_os_info", + return_value={"board": "generic-x86-64"}, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + +async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: + """Test setup of a config entry when hassio has not fetched os_info.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Green", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_green.get_os_info", + return_value=None, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + assert config_entry.state == ConfigEntryState.SETUP_RETRY From 549399cca6c091081f1cb09b9c580c15e2f9bc01 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 16:37:59 +0200 Subject: [PATCH 1061/1151] Remove unneeded variable in Flo (#99322) --- homeassistant/components/flo/entity.py | 3 --- homeassistant/components/flo/sensor.py | 7 ------- homeassistant/components/flo/switch.py | 13 ++++--------- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index ef8a04440d1..066ffef6a05 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -1,8 +1,6 @@ """Base entity class for Flo entities.""" from __future__ import annotations -from typing import Any - from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity import Entity @@ -27,7 +25,6 @@ class FloEntity(Entity): self._attr_unique_id = f"{device.mac_address}_{entity_type}" self._device: FloDeviceDataUpdateCoordinator = device - self._state: Any = None @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index f0aca366cfb..b2a0afdcb13 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -68,7 +68,6 @@ class FloDailyUsageSensor(FloEntity, SensorEntity): def __init__(self, device): """Initialize the daily water usage sensor.""" super().__init__("daily_consumption", device) - self._state: float = None @property def native_value(self) -> float | None: @@ -86,7 +85,6 @@ class FloSystemModeSensor(FloEntity, SensorEntity): def __init__(self, device): """Initialize the system mode sensor.""" super().__init__("current_system_mode", device) - self._state: str = None @property def native_value(self) -> str | None: @@ -107,7 +105,6 @@ class FloCurrentFlowRateSensor(FloEntity, SensorEntity): def __init__(self, device): """Initialize the flow rate sensor.""" super().__init__("current_flow_rate", device) - self._state: float = None @property def native_value(self) -> float | None: @@ -129,7 +126,6 @@ class FloTemperatureSensor(FloEntity, SensorEntity): super().__init__("temperature", device) if is_water: self._attr_translation_key = "water_temperature" - self._state: float = None @property def native_value(self) -> float | None: @@ -149,7 +145,6 @@ class FloHumiditySensor(FloEntity, SensorEntity): def __init__(self, device): """Initialize the humidity sensor.""" super().__init__("humidity", device) - self._state: float = None @property def native_value(self) -> float | None: @@ -170,7 +165,6 @@ class FloPressureSensor(FloEntity, SensorEntity): def __init__(self, device): """Initialize the pressure sensor.""" super().__init__("water_pressure", device) - self._state: float = None @property def native_value(self) -> float | None: @@ -190,7 +184,6 @@ class FloBatterySensor(FloEntity, SensorEntity): def __init__(self, device): """Initialize the battery sensor.""" super().__init__("battery", device) - self._state: float = None @property def native_value(self) -> float | None: diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py index cd522ed177d..18a4341db57 100644 --- a/homeassistant/components/flo/switch.py +++ b/homeassistant/components/flo/switch.py @@ -73,12 +73,7 @@ class FloSwitch(FloEntity, SwitchEntity): def __init__(self, device: FloDeviceDataUpdateCoordinator) -> None: """Initialize the Flo switch.""" super().__init__("shutoff_valve", device) - self._state = self._device.last_known_valve_state == "open" - - @property - def is_on(self) -> bool: - """Return True if the valve is open.""" - return self._state + self._attr_is_on = device.last_known_valve_state == "open" @property def icon(self): @@ -90,19 +85,19 @@ class FloSwitch(FloEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Open the valve.""" await self._device.api_client.device.open_valve(self._device.id) - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Close the valve.""" await self._device.api_client.device.close_valve(self._device.id) - self._state = False + self._attr_is_on = False self.async_write_ha_state() @callback def async_update_state(self) -> None: """Retrieve the latest valve state and update the state machine.""" - self._state = self._device.last_known_valve_state == "open" + self._attr_is_on = self._device.last_known_valve_state == "open" self.async_write_ha_state() async def async_added_to_hass(self) -> None: From 2ee55f50866210916230b0b315faf96d9942790e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Aug 2023 16:42:04 +0200 Subject: [PATCH 1062/1151] Update frontend to 20230830.0 (#99340) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 986dfd6ba52..06b6da85e19 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230802.1"] + "requirements": ["home-assistant-frontend==20230830.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c8c3de858a7..949181c7ddd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230802.1 +home-assistant-frontend==20230830.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 573c46c0020..35394fafa95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230802.1 +home-assistant-frontend==20230830.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6449ed1ab3b..40b6c4ace56 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230802.1 +home-assistant-frontend==20230830.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From a5dcc25aaba041b7f92b27f85a7cf59339ca0653 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 30 Aug 2023 17:11:55 +0200 Subject: [PATCH 1063/1151] Add snapshot assertion to Airzone (#98760) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Álvaro Fernández Rojas --- .../airzone/snapshots/test_diagnostics.ambr | 687 ++++++++++++++++++ tests/components/airzone/test_climate.py | 5 +- tests/components/airzone/test_diagnostics.py | 78 +- tests/components/airzone/test_sensor.py | 3 +- tests/components/airzone/util.py | 1 + 5 files changed, 700 insertions(+), 74 deletions(-) create mode 100644 tests/components/airzone/snapshots/test_diagnostics.ambr diff --git a/tests/components/airzone/snapshots/test_diagnostics.ambr b/tests/components/airzone/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..b9ab7198148 --- /dev/null +++ b/tests/components/airzone/snapshots/test_diagnostics.ambr @@ -0,0 +1,687 @@ +# serializer version: 1 +# name: test_config_entry_diagnostics + dict({ + 'api_data': dict({ + 'hvac': dict({ + 'systems': list([ + dict({ + 'data': list([ + dict({ + 'air_demand': 0, + 'coldStage': 1, + 'coldStages': 1, + 'coldangle': 0, + 'errors': list([ + ]), + 'floor_demand': 0, + 'heatStage': 1, + 'heatStages': 1, + 'heatangle': 0, + 'humidity': 34, + 'maxTemp': 30, + 'minTemp': 15, + 'mode': 3, + 'modes': list([ + 1, + 4, + 2, + 3, + 5, + ]), + 'name': 'Salon', + 'on': 0, + 'roomTemp': 19.6, + 'setpoint': 19.1, + 'sleep': 0, + 'speed': 0, + 'speeds': 3, + 'systemID': 1, + 'thermos_firmware': '3.51', + 'thermos_radio': 0, + 'thermos_type': 2, + 'units': 0, + 'zoneID': 1, + }), + dict({ + 'air_demand': 1, + 'coldStage': 1, + 'coldStages': 1, + 'coldangle': 2, + 'errors': list([ + ]), + 'floor_demand': 1, + 'heatStage': 3, + 'heatStages': 3, + 'heatangle': 1, + 'humidity': 39, + 'maxTemp': 30, + 'minTemp': 15, + 'mode': 3, + 'name': 'Dorm Ppal', + 'on': 1, + 'roomTemp': 21.1, + 'setpoint': 19.2, + 'sleep': 30, + 'speed': 0, + 'speeds': 2, + 'systemID': 1, + 'thermos_firmware': '3.33', + 'thermos_radio': 1, + 'thermos_type': 4, + 'units': 0, + 'zoneID': 2, + }), + dict({ + 'air_demand': 0, + 'coldStage': 1, + 'coldStages': 1, + 'coldangle': 0, + 'errors': list([ + ]), + 'floor_demand': 0, + 'heatStage': 2, + 'heatStages': 2, + 'heatangle': 0, + 'humidity': 35, + 'maxTemp': 30, + 'minTemp': 15, + 'mode': 3, + 'name': 'Dorm #1', + 'on': 1, + 'roomTemp': 20.8, + 'setpoint': 19.3, + 'sleep': 0, + 'systemID': 1, + 'thermos_firmware': '3.33', + 'thermos_radio': 1, + 'thermos_type': 4, + 'units': 0, + 'zoneID': 3, + }), + dict({ + 'air_demand': 0, + 'coldStage': 1, + 'coldStages': 1, + 'coldangle': 0, + 'errors': list([ + dict({ + 'Zone': 'Low battery', + }), + ]), + 'floor_demand': 0, + 'heatStage': 1, + 'heatStages': 1, + 'heatangle': 0, + 'humidity': 36, + 'maxTemp': 86, + 'minTemp': 59, + 'mode': 3, + 'name': 'Despacho', + 'on': 0, + 'roomTemp': 70.16, + 'setpoint': 66.92, + 'sleep': 0, + 'systemID': 1, + 'thermos_firmware': '3.33', + 'thermos_radio': 1, + 'thermos_type': 4, + 'units': 1, + 'zoneID': 4, + }), + dict({ + 'air_demand': 0, + 'coldStage': 1, + 'coldStages': 1, + 'coldangle': 0, + 'errors': list([ + ]), + 'floor_demand': 0, + 'heatStage': 1, + 'heatStages': 1, + 'heatangle': 0, + 'humidity': 40, + 'maxTemp': 30, + 'minTemp': 15, + 'mode': 3, + 'name': 'Dorm #2', + 'on': 0, + 'roomTemp': 20.5, + 'setpoint': 19.5, + 'sleep': 0, + 'systemID': 1, + 'thermos_firmware': '3.33', + 'thermos_radio': 1, + 'thermos_type': 4, + 'units': 0, + 'zoneID': 5, + }), + ]), + }), + dict({ + 'data': list([ + dict({ + 'coldStage': 1, + 'coldStages': 1, + 'errors': list([ + ]), + 'heatStage': 1, + 'heatStages': 1, + 'humidity': 62, + 'maxTemp': 30, + 'minTemp': 15, + 'on': 0, + 'roomTemp': 22.299999, + 'setpoint': 19, + 'speed': 0, + 'speeds': 4, + 'systemID': 2, + 'units': 0, + 'zoneID': 1, + }), + ]), + }), + dict({ + 'data': list([ + dict({ + 'air_demand': 1, + 'coldStage': 0, + 'coldStages': 0, + 'coolmaxtemp': 90, + 'coolmintemp': 64, + 'coolsetpoint': 73, + 'errors': list([ + ]), + 'floor_demand': 0, + 'heatStage': 0, + 'heatStages': 0, + 'heatmaxtemp': 86, + 'heatmintemp': 50, + 'heatsetpoint': 77, + 'humidity': 0, + 'maxTemp': 90, + 'minTemp': 64, + 'mode': 7, + 'modes': list([ + 4, + 2, + 3, + 5, + 7, + ]), + 'name': 'DKN Plus', + 'on': 1, + 'roomTemp': 71, + 'setpoint': 73, + 'speed': 2, + 'speeds': 5, + 'systemID': 3, + 'units': 1, + 'zoneID': 1, + }), + ]), + }), + ]), + }), + 'version': dict({ + 'version': '1.62', + }), + 'webserver': dict({ + 'mac': '**REDACTED**', + 'wifi_channel': 6, + 'wifi_rssi': -42, + }), + }), + 'config_entry': dict({ + 'data': dict({ + 'host': '192.168.1.100', + 'port': 3000, + }), + 'disabled_by': None, + 'domain': 'airzone', + 'entry_id': '6e7a0798c1734ba81d26ced0e690eaec', + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + 'coord_data': dict({ + 'hot-water': dict({ + 'name': 'Airzone DHW', + 'on': True, + 'operation': 1, + 'operations': list([ + 0, + 1, + 2, + ]), + 'power-mode': False, + 'temp': 43, + 'temp-max': 75, + 'temp-min': 30, + 'temp-set': 45, + 'temp-unit': 0, + }), + 'new-systems': list([ + ]), + 'new-zones': list([ + ]), + 'num-systems': 3, + 'num-zones': 7, + 'systems': dict({ + '1': dict({ + 'available': True, + 'firmware': '3.31', + 'full-name': 'Airzone [1] System', + 'id': 1, + 'master-system-zone': '1:1', + 'master-zone': 1, + 'mode': 3, + 'model': 'C6', + 'modes': list([ + 1, + 4, + 2, + 3, + 5, + ]), + 'problems': False, + }), + '2': dict({ + 'available': True, + 'full-name': 'Airzone [2] System', + 'id': 2, + 'master-system-zone': '2:1', + 'master-zone': 1, + 'mode': 7, + 'modes': list([ + 7, + 1, + ]), + 'problems': False, + }), + '3': dict({ + 'available': True, + 'full-name': 'Airzone [3] System', + 'id': 3, + 'master-system-zone': '3:1', + 'master-zone': 1, + 'mode': 7, + 'modes': list([ + 4, + 2, + 3, + 5, + 7, + 1, + ]), + 'problems': False, + }), + }), + 'version': '1.62', + 'webserver': dict({ + 'mac': '**REDACTED**', + 'wifi-channel': 6, + 'wifi-rssi': -42, + }), + 'zones': dict({ + '1:1': dict({ + 'absolute-temp-max': 30.0, + 'absolute-temp-min': 15.0, + 'action': 6, + 'air-demand': False, + 'available': True, + 'cold-angle': 0, + 'cold-stage': 1, + 'cold-stages': list([ + 0, + 1, + ]), + 'demand': False, + 'double-set-point': False, + 'full-name': 'Airzone [1:1] Salon', + 'heat-angle': 0, + 'heat-stage': 1, + 'heat-stages': list([ + 0, + 1, + ]), + 'humidity': 34, + 'id': 1, + 'master': True, + 'mode': 3, + 'modes': list([ + 1, + 4, + 2, + 3, + 5, + ]), + 'name': 'Salon', + 'on': False, + 'problems': False, + 'sleep': 0, + 'speed': 0, + 'speeds': list([ + 0, + 1, + 2, + 3, + ]), + 'system': 1, + 'temp': 19.6, + 'temp-max': 30.0, + 'temp-min': 15.0, + 'temp-set': 19.1, + 'temp-step': 0.5, + 'temp-unit': 0, + 'thermostat-fw': '3.51', + 'thermostat-model': 'Blueface Zero', + 'thermostat-radio': False, + }), + '1:2': dict({ + 'absolute-temp-max': 30.0, + 'absolute-temp-min': 15.0, + 'action': 4, + 'air-demand': True, + 'available': True, + 'battery-low': False, + 'cold-angle': 2, + 'cold-stage': 1, + 'cold-stages': list([ + 0, + 1, + ]), + 'demand': True, + 'double-set-point': False, + 'floor-demand': True, + 'full-name': 'Airzone [1:2] Dorm Ppal', + 'heat-angle': 1, + 'heat-stage': 3, + 'heat-stages': list([ + 0, + 1, + 2, + 3, + ]), + 'humidity': 39, + 'id': 2, + 'master': False, + 'mode': 3, + 'modes': list([ + 1, + 4, + 2, + 3, + 5, + ]), + 'name': 'Dorm Ppal', + 'on': True, + 'problems': False, + 'sleep': 30, + 'speed': 0, + 'speeds': list([ + 0, + 1, + 2, + ]), + 'system': 1, + 'temp': 21.1, + 'temp-max': 30.0, + 'temp-min': 15.0, + 'temp-set': 19.2, + 'temp-step': 0.5, + 'temp-unit': 0, + 'thermostat-fw': '3.33', + 'thermostat-model': 'Think (Radio)', + 'thermostat-radio': True, + }), + '1:3': dict({ + 'absolute-temp-max': 30.0, + 'absolute-temp-min': 15.0, + 'action': 5, + 'air-demand': False, + 'available': True, + 'battery-low': False, + 'cold-angle': 0, + 'cold-stage': 1, + 'cold-stages': list([ + 0, + 1, + ]), + 'demand': False, + 'double-set-point': False, + 'floor-demand': False, + 'full-name': 'Airzone [1:3] Dorm #1', + 'heat-angle': 0, + 'heat-stage': 2, + 'heat-stages': list([ + 0, + 2, + ]), + 'humidity': 35, + 'id': 3, + 'master': False, + 'mode': 3, + 'modes': list([ + 1, + 4, + 2, + 3, + 5, + ]), + 'name': 'Dorm #1', + 'on': True, + 'problems': False, + 'sleep': 0, + 'system': 1, + 'temp': 20.8, + 'temp-max': 30.0, + 'temp-min': 15.0, + 'temp-set': 19.3, + 'temp-step': 0.5, + 'temp-unit': 0, + 'thermostat-fw': '3.33', + 'thermostat-model': 'Think (Radio)', + 'thermostat-radio': True, + }), + '1:4': dict({ + 'absolute-temp-max': 86.0, + 'absolute-temp-min': 59.0, + 'action': 6, + 'air-demand': False, + 'available': True, + 'battery-low': True, + 'cold-angle': 0, + 'cold-stage': 1, + 'cold-stages': list([ + 0, + 1, + ]), + 'demand': False, + 'double-set-point': False, + 'errors': list([ + 'Low battery', + ]), + 'full-name': 'Airzone [1:4] Despacho', + 'heat-angle': 0, + 'heat-stage': 1, + 'heat-stages': list([ + 0, + 1, + ]), + 'humidity': 36, + 'id': 4, + 'master': False, + 'mode': 3, + 'modes': list([ + 1, + 4, + 2, + 3, + 5, + ]), + 'name': 'Despacho', + 'on': False, + 'problems': True, + 'sleep': 0, + 'system': 1, + 'temp': 70.16, + 'temp-max': 86.0, + 'temp-min': 59.0, + 'temp-set': 66.9, + 'temp-step': 1.0, + 'temp-unit': 1, + 'thermostat-fw': '3.33', + 'thermostat-model': 'Think (Radio)', + 'thermostat-radio': True, + }), + '1:5': dict({ + 'absolute-temp-max': 30.0, + 'absolute-temp-min': 15.0, + 'action': 6, + 'air-demand': False, + 'available': True, + 'battery-low': False, + 'cold-angle': 0, + 'cold-stage': 1, + 'cold-stages': list([ + 0, + 1, + ]), + 'demand': False, + 'double-set-point': False, + 'full-name': 'Airzone [1:5] Dorm #2', + 'heat-angle': 0, + 'heat-stage': 1, + 'heat-stages': list([ + 0, + 1, + ]), + 'humidity': 40, + 'id': 5, + 'master': False, + 'mode': 3, + 'modes': list([ + 1, + 4, + 2, + 3, + 5, + ]), + 'name': 'Dorm #2', + 'on': False, + 'problems': False, + 'sleep': 0, + 'system': 1, + 'temp': 20.5, + 'temp-max': 30.0, + 'temp-min': 15.0, + 'temp-set': 19.5, + 'temp-step': 0.5, + 'temp-unit': 0, + 'thermostat-fw': '3.33', + 'thermostat-model': 'Think (Radio)', + 'thermostat-radio': True, + }), + '2:1': dict({ + 'absolute-temp-max': 30.0, + 'absolute-temp-min': 15.0, + 'action': 6, + 'available': True, + 'cold-stage': 1, + 'cold-stages': list([ + 0, + 1, + ]), + 'demand': False, + 'double-set-point': True, + 'full-name': 'Airzone [2:1] Airzone 2:1', + 'heat-stage': 1, + 'heat-stages': list([ + 0, + 1, + ]), + 'humidity': 62, + 'id': 1, + 'master': True, + 'mode': 7, + 'modes': list([ + 7, + 1, + ]), + 'name': 'Airzone 2:1', + 'on': False, + 'problems': False, + 'speed': 0, + 'speeds': list([ + 0, + 1, + 2, + 3, + 4, + ]), + 'system': 2, + 'temp': 22.3, + 'temp-max': 30.0, + 'temp-min': 15.0, + 'temp-set': 19.0, + 'temp-step': 0.5, + 'temp-unit': 0, + }), + '3:1': dict({ + 'absolute-temp-max': 90.0, + 'absolute-temp-min': 50.0, + 'action': 1, + 'air-demand': True, + 'available': True, + 'cold-stage': 0, + 'cool-temp-max': 90.0, + 'cool-temp-min': 64.0, + 'cool-temp-set': 73.0, + 'demand': True, + 'double-set-point': True, + 'floor-demand': False, + 'full-name': 'Airzone [3:1] DKN Plus', + 'heat-stage': 0, + 'heat-temp-max': 86.0, + 'heat-temp-min': 50.0, + 'heat-temp-set': 77.0, + 'id': 1, + 'master': True, + 'mode': 7, + 'modes': list([ + 4, + 2, + 3, + 5, + 7, + 1, + ]), + 'name': 'DKN Plus', + 'on': True, + 'problems': False, + 'speed': 2, + 'speeds': list([ + 0, + 1, + 2, + 3, + 4, + 5, + ]), + 'system': 3, + 'temp': 71.0, + 'temp-max': 90.0, + 'temp-min': 64.0, + 'temp-set': 73.0, + 'temp-step': 1.0, + 'temp-unit': 1, + }), + }), + }), + }) +# --- diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index 1f8667d0344..591584da10b 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -1,4 +1,5 @@ """The climate tests for the Airzone platform.""" +import copy from unittest.mock import patch from aioairzone.const import ( @@ -222,7 +223,7 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP assert state.attributes.get(ATTR_TEMPERATURE) == 22.8 - HVAC_MOCK_CHANGED = {**HVAC_MOCK} + HVAC_MOCK_CHANGED = copy.deepcopy(HVAC_MOCK) HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MAX_TEMP] = 25 HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MIN_TEMP] = 10 @@ -437,7 +438,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: state = hass.states.get("climate.salon") assert state.state == HVACMode.FAN_ONLY - HVAC_MOCK_NO_SET_POINT = {**HVAC_MOCK} + HVAC_MOCK_NO_SET_POINT = copy.deepcopy(HVAC_MOCK) del HVAC_MOCK_NO_SET_POINT[API_SYSTEMS][0][API_DATA][0][API_SET_POINT] with patch( diff --git a/tests/components/airzone/test_diagnostics.py b/tests/components/airzone/test_diagnostics.py index 33f0175bdb7..b64f346f27e 100644 --- a/tests/components/airzone/test_diagnostics.py +++ b/tests/components/airzone/test_diagnostics.py @@ -2,30 +2,13 @@ from unittest.mock import patch -from aioairzone.const import ( - API_DATA, - API_MAC, - API_SYSTEM_ID, - API_SYSTEMS, - API_VERSION, - API_WIFI_RSSI, - AZD_ID, - AZD_MASTER, - AZD_SYSTEM, - AZD_SYSTEMS, - AZD_ZONES, - RAW_HVAC, - RAW_VERSION, - RAW_WEBSERVER, -) +from aioairzone.const import RAW_HVAC, RAW_VERSION, RAW_WEBSERVER +from syrupy import SnapshotAssertion from homeassistant.components.airzone.const import DOMAIN -from homeassistant.components.diagnostics import REDACTED -from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from .util import ( - CONFIG, HVAC_MOCK, HVAC_VERSION_MOCK, HVAC_WEBSERVER_MOCK, @@ -37,7 +20,9 @@ from tests.typing import ClientSessionGenerator async def test_config_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" await async_init_integration(hass) @@ -52,54 +37,5 @@ async def test_config_entry_diagnostics( RAW_WEBSERVER: HVAC_WEBSERVER_MOCK, }, ): - diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - - assert ( - diag["api_data"][RAW_HVAC][API_SYSTEMS][0][API_DATA][0].items() - >= { - API_SYSTEM_ID: HVAC_MOCK[API_SYSTEMS][0][API_DATA][0][API_SYSTEM_ID], - }.items() - ) - - assert ( - diag["api_data"][RAW_VERSION].items() - >= { - API_VERSION: HVAC_VERSION_MOCK[API_VERSION], - }.items() - ) - - assert ( - diag["api_data"][RAW_WEBSERVER].items() - >= { - API_MAC: REDACTED, - API_WIFI_RSSI: HVAC_WEBSERVER_MOCK[API_WIFI_RSSI], - }.items() - ) - - assert ( - diag["config_entry"].items() - >= { - "data": { - CONF_HOST: CONFIG[CONF_HOST], - CONF_PORT: CONFIG[CONF_PORT], - }, - "domain": DOMAIN, - "unique_id": REDACTED, - }.items() - ) - - assert ( - diag["coord_data"][AZD_SYSTEMS]["1"].items() - >= { - AZD_ID: 1, - }.items() - ) - - assert ( - diag["coord_data"][AZD_ZONES]["1:1"].items() - >= { - AZD_ID: 1, - AZD_MASTER: True, - AZD_SYSTEM: 1, - }.items() - ) + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert result == snapshot diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py index 4de1cae7555..6d94defa004 100644 --- a/tests/components/airzone/test_sensor.py +++ b/tests/components/airzone/test_sensor.py @@ -1,5 +1,6 @@ """The sensor tests for the Airzone platform.""" +import copy from unittest.mock import patch from aioairzone.const import API_DATA, API_SYSTEMS @@ -87,7 +88,7 @@ async def test_airzone_sensors_availability( await async_init_integration(hass) - HVAC_MOCK_UNAVAILABLE_ZONE = {**HVAC_MOCK} + HVAC_MOCK_UNAVAILABLE_ZONE = copy.deepcopy(HVAC_MOCK) del HVAC_MOCK_UNAVAILABLE_ZONE[API_SYSTEMS][0][API_DATA][1] with patch( diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index 74cda7c8017..eb687731eb7 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -313,6 +313,7 @@ async def async_init_integration( config_entry = MockConfigEntry( data=CONFIG, + entry_id="6e7a0798c1734ba81d26ced0e690eaec", domain=DOMAIN, unique_id="airzone_unique_id", ) From 501d5db3750bb8c2afcc485d1ee25302cb690b71 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Aug 2023 17:28:56 +0200 Subject: [PATCH 1064/1151] Add config flow for template binary sensor (#99339) --- .../components/template/binary_sensor.py | 24 ++++++++ .../components/template/config_flow.py | 31 +++++++++++ .../components/template/strings.json | 48 ++++++++++++++++ tests/components/template/test_config_flow.py | 55 ++++++++++++++++--- 4 files changed, 151 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 202ca0d9e4b..ca0ed583d86 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -194,6 +195,29 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize config entry.""" + _options = dict(config_entry.options) + _options.pop("template_type") + validated_config = BINARY_SENSOR_SCHEMA(_options) + async_add_entities( + [BinarySensorTemplate(hass, validated_config, config_entry.entry_id)] + ) + + +@callback +def async_create_preview_binary_sensor( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> BinarySensorTemplate: + """Create a preview sensor.""" + validated_config = BINARY_SENSOR_SCHEMA(config | {CONF_NAME: name}) + return BinarySensorTemplate(hass, validated_config, None) + + class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): """A virtual binary sensor that triggers from another sensor.""" diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 1d87e8d89e8..b89b3cbc91d 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -7,6 +7,7 @@ from typing import Any, cast import voluptuous as vol from homeassistant.components import websocket_api +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASS_STATE_CLASSES, @@ -31,6 +32,7 @@ from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowMenuStep, ) +from .binary_sensor import async_create_preview_binary_sensor from .const import DOMAIN from .sensor import async_create_preview_sensor from .template_entity import TemplateEntity @@ -42,6 +44,23 @@ def generate_schema(domain: str) -> dict[vol.Marker, Any]: """Generate schema.""" schema: dict[vol.Marker, Any] = {} + if domain == Platform.BINARY_SENSOR: + schema = { + vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[ + NONE_SENTINEL, + *sorted( + [cls.value for cls in BinarySensorDeviceClass], + key=str.casefold, + ), + ], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="binary_sensor_device_class", + ), + ) + } + if domain == Platform.SENSOR: schema = { vol.Optional( @@ -197,11 +216,17 @@ def validate_user_input( TEMPLATE_TYPES = [ + "binary_sensor", "sensor", ] CONFIG_FLOW = { "user": SchemaFlowMenuStep(TEMPLATE_TYPES), + Platform.BINARY_SENSOR: SchemaFlowFormStep( + config_schema(Platform.BINARY_SENSOR), + preview="template", + validate_user_input=validate_user_input(Platform.BINARY_SENSOR), + ), Platform.SENSOR: SchemaFlowFormStep( config_schema(Platform.SENSOR), preview="template", @@ -212,6 +237,11 @@ CONFIG_FLOW = { OPTIONS_FLOW = { "init": SchemaFlowFormStep(next_step=choose_options_step), + Platform.BINARY_SENSOR: SchemaFlowFormStep( + options_schema(Platform.BINARY_SENSOR), + preview="template", + validate_user_input=validate_user_input(Platform.BINARY_SENSOR), + ), Platform.SENSOR: SchemaFlowFormStep( options_schema(Platform.SENSOR), preview="template", @@ -223,6 +253,7 @@ CREATE_PREVIEW_ENTITY: dict[ str, Callable[[HomeAssistant, str, dict[str, Any]], TemplateEntity], ] = { + "binary_sensor": async_create_preview_binary_sensor, "sensor": async_create_preview_sensor, } diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 6ceb4b495ef..482682d0ce1 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -1,6 +1,14 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", + "name": "[%key:common::config_flow::data::name%]", + "state_template": "[%key:component::template::config::step::sensor::data::state_template%]" + }, + "title": "Template binary sensor" + }, "sensor": { "data": { "device_class": "Device class", @@ -14,6 +22,7 @@ "user": { "description": "This helper allow you to create helper entities that define their state using a template.", "menu_options": { + "binary_sensor": "Template a binary sensor", "sensor": "Template a sensor" }, "title": "Template helper" @@ -22,6 +31,13 @@ }, "options": { "step": { + "binary_sensor": { + "data": { + "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", + "state_template": "[%key:component::template::config::step::sensor::data::state_template%]" + }, + "title": "[%key:component::template::config::step::binary_sensor::title%]" + }, "sensor": { "data": { "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", @@ -34,6 +50,38 @@ } }, "selector": { + "binary_sensor_device_class": { + "options": { + "none": "[%key:component::template::selector::sensor_device_class::options::none%]", + "battery": "[%key:component::binary_sensor::entity_component::battery::name%]", + "battery_charging": "[%key:component::binary_sensor::entity_component::battery_charging::name%]", + "carbon_monoxide": "[%key:component::binary_sensor::entity_component::carbon_monoxide::name%]", + "cold": "[%key:component::binary_sensor::entity_component::cold::name%]", + "connectivity": "[%key:component::binary_sensor::entity_component::connectivity::name%]", + "door": "[%key:component::binary_sensor::entity_component::door::name%]", + "garage_door": "[%key:component::binary_sensor::entity_component::garage_door::name%]", + "gas": "[%key:component::binary_sensor::entity_component::gas::name%]", + "heat": "[%key:component::binary_sensor::entity_component::heat::name%]", + "light": "[%key:component::binary_sensor::entity_component::light::name%]", + "lock": "[%key:component::binary_sensor::entity_component::lock::name%]", + "moisture": "[%key:component::binary_sensor::entity_component::moisture::name%]", + "motion": "[%key:component::binary_sensor::entity_component::motion::name%]", + "moving": "[%key:component::binary_sensor::entity_component::moving::name%]", + "occupancy": "[%key:component::binary_sensor::entity_component::occupancy::name%]", + "opening": "[%key:component::binary_sensor::entity_component::opening::name%]", + "plug": "[%key:component::binary_sensor::entity_component::plug::name%]", + "power": "[%key:component::binary_sensor::entity_component::power::name%]", + "presence": "[%key:component::binary_sensor::entity_component::presence::name%]", + "problem": "[%key:component::binary_sensor::entity_component::problem::name%]", + "running": "[%key:component::binary_sensor::entity_component::running::name%]", + "safety": "[%key:component::binary_sensor::entity_component::safety::name%]", + "smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]", + "sound": "[%key:component::binary_sensor::entity_component::sound::name%]", + "update": "[%key:component::binary_sensor::entity_component::update::name%]", + "vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]", + "window": "[%key:component::binary_sensor::entity_component::window::name%]" + } + }, "sensor_device_class": { "options": { "none": "No device class", diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index dbaa23fae11..dd283ff9214 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -25,6 +25,16 @@ from tests.typing import WebSocketGenerator "extra_attrs", ), ( + ( + "binary_sensor", + "{{ states('binary_sensor.one') == 'on' or states('binary_sensor.two') == 'on' }}", + "on", + {"one": "on", "two": "off"}, + {}, + {}, + {}, + {}, + ), ( "sensor", "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", @@ -125,15 +135,26 @@ def get_suggested(schema, key): "template_type", "old_state_template", "new_state_template", + "template_state", "input_states", "extra_options", "options_options", ), ( + ( + "binary_sensor", + "{{ states('binary_sensor.one') == 'on' or states('binary_sensor.two') == 'on' }}", + "{{ states('binary_sensor.one') == 'on' and states('binary_sensor.two') == 'on' }}", + ["on", "off"], + {"one": "on", "two": "off"}, + {}, + {}, + ), ( "sensor", "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", "{{ float(states('sensor.one')) - float(states('sensor.two')) }}", + ["50.0", "10.0"], {"one": "30.0", "two": "20.0"}, {}, {}, @@ -145,6 +166,7 @@ async def test_options( template_type, old_state_template, new_state_template, + template_state, input_states, extra_options, options_options, @@ -174,7 +196,7 @@ async def test_options( await hass.async_block_till_done() state = hass.states.get(f"{template_type}.my_template") - assert state.state == "50.0" + assert state.state == template_state[0] config_entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -207,7 +229,7 @@ async def test_options( # Check config entry is reloaded with new options await hass.async_block_till_done() state = hass.states.get(f"{template_type}.my_template") - assert state.state == "10.0" + assert state.state == template_state[1] # Check we don't get suggestions from another entry result = await hass.config_entries.flow.async_init( @@ -233,16 +255,24 @@ async def test_options( "state_template", "extra_user_input", "input_states", - "template_state", + "template_states", "extra_attributes", ), ( + ( + "binary_sensor", + "{{ states.binary_sensor.one.state == 'on' or states.binary_sensor.two.state == 'on' }}", + {}, + {"one": "on", "two": "off"}, + ["off", "on"], + [{}, {}], + ), ( "sensor", "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", {}, {"one": "30.0", "two": "20.0"}, - "50.0", + ["unavailable", "50.0"], [{}, {}], ), ), @@ -254,7 +284,7 @@ async def test_config_flow_preview( state_template: str, extra_user_input: dict[str, Any], input_states: list[str], - template_state: str, + template_states: str, extra_attributes: list[dict[str, Any]], ) -> None: """Test the config flow preview.""" @@ -293,7 +323,7 @@ async def test_config_flow_preview( msg = await client.receive_json() assert msg["event"] == { "attributes": {"friendly_name": "My template"} | extra_attributes[0], - "state": "unavailable", + "state": template_states[0], } for input_entity in input_entities: @@ -306,7 +336,7 @@ async def test_config_flow_preview( "attributes": {"friendly_name": "My template"} | extra_attributes[0] | extra_attributes[1], - "state": template_state, + "state": template_states[1], } assert len(hass.states.async_all()) == 2 @@ -317,6 +347,7 @@ EARLY_END_ERROR = "invalid template (TemplateSyntaxError: unexpected 'end of tem @pytest.mark.parametrize( ("template_type", "state_template", "extra_user_input", "error"), [ + ("binary_sensor", "{{", {}, {"state": EARLY_END_ERROR}), ("sensor", "{{", {}, {"state": EARLY_END_ERROR}), ( "sensor", @@ -453,6 +484,16 @@ async def test_config_flow_preview_bad_state( "extra_attributes", ), [ + ( + "binary_sensor", + "{{ states('binary_sensor.one') == 'on' or states('binary_sensor.two') == 'on' }}", + "{{ states('binary_sensor.one') == 'on' and states('binary_sensor.two') == 'on' }}", + {}, + {}, + {"one": "on", "two": "off"}, + "off", + {}, + ), ( "sensor", "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", From 867e9b73bbcad4f681f8996833d65690b4765527 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 30 Aug 2023 11:29:22 -0400 Subject: [PATCH 1065/1151] Add zwave_js device config file change fix/repair (#99314) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 20 +++ .../components/zwave_js/manifest.json | 2 +- homeassistant/components/zwave_js/repairs.py | 50 ++++++ .../components/zwave_js/strings.json | 11 ++ tests/components/zwave_js/conftest.py | 10 +- tests/components/zwave_js/test_number.py | 2 +- tests/components/zwave_js/test_repairs.py | 158 ++++++++++++++++++ tests/components/zwave_js/test_update.py | 26 +-- 8 files changed, 263 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/zwave_js/repairs.py create mode 100644 tests/components/zwave_js/test_repairs.py diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 316459bdb23..2d158f47e44 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -596,6 +596,26 @@ class NodeEvents: node, ) + # After ensuring the node is set up in HA, we should check if the node's + # device config has changed, and if so, issue a repair registry entry for a + # possible reinterview + if not node.is_controller_node and await node.async_has_device_config_changed(): + async_create_issue( + self.hass, + DOMAIN, + f"device_config_file_changed.{device.id}", + data={"device_id": device.id}, + is_fixable=True, + is_persistent=False, + translation_key="device_config_file_changed", + translation_placeholders={ + "device_name": device.name_by_user + or device.name + or "Unnamed device" + }, + severity=IssueSeverity.WARNING, + ) + async def async_handle_discovery_info( self, device: dr.DeviceEntry, diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 7371a7a8896..73fa41a8cca 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "codeowners": ["@home-assistant/z-wave"], "config_flow": true, - "dependencies": ["usb", "http", "websocket_api"], + "dependencies": ["usb", "http", "repairs", "websocket_api"], "documentation": "https://www.home-assistant.io/integrations/zwave_js", "integration_type": "hub", "iot_class": "local_push", diff --git a/homeassistant/components/zwave_js/repairs.py b/homeassistant/components/zwave_js/repairs.py new file mode 100644 index 00000000000..58781941b09 --- /dev/null +++ b/homeassistant/components/zwave_js/repairs.py @@ -0,0 +1,50 @@ +"""Repairs for Z-Wave JS.""" +from __future__ import annotations + +from typing import cast + +import voluptuous as vol +from zwave_js_server.model.node import Node + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.core import HomeAssistant + +from .helpers import async_get_node_from_device_id + + +class DeviceConfigFileChangedFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + def __init__(self, node: Node) -> None: + """Initialize.""" + self.node = node + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + self.hass.async_create_task(self.node.async_refresh_info()) + return self.async_create_entry(title="", data={}) + + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: + """Create flow.""" + if issue_id.split(".")[0] == "device_config_file_changed": + return DeviceConfigFileChangedFlow( + async_get_node_from_device_id(hass, cast(dict, data)["device_id"]) + ) + return ConfirmRepairFlow() diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 934307947d8..6435c6b7a54 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -161,6 +161,17 @@ } } } + }, + "device_config_file_changed": { + "title": "Z-Wave device configuration file changed: {device_name}", + "fix_flow": { + "step": { + "confirm": { + "title": "Z-Wave device configuration file changed: {device_name}", + "description": "Z-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you'd like to proceed, click on SUBMIT below. The re-interview will take place in the background." + } + } + } } }, "services": { diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 8bb55e3949b..dcd847a6e12 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -3,7 +3,7 @@ import asyncio import copy import io import json -from unittest.mock import AsyncMock, patch +from unittest.mock import DEFAULT, AsyncMock, patch import pytest from zwave_js_server.event import Event @@ -687,9 +687,17 @@ def mock_client_fixture( client.version = VersionInfo.from_message(version_state) client.ws_server_url = "ws://test:3000/zjs" + + async def async_send_command_side_effect(message, require_schema=None): + """Return the command response.""" + if message["command"] == "node.has_device_config_changed": + return {"changed": False} + return DEFAULT + client.async_send_command.return_value = { "result": {"success": True, "status": 255} } + client.async_send_command.side_effect = async_send_command_side_effect yield client diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py index 7229d10ebad..7a3ffbda589 100644 --- a/tests/components/zwave_js/test_number.py +++ b/tests/components/zwave_js/test_number.py @@ -124,7 +124,7 @@ async def test_number_writeable( blocking=True, ) - assert len(client.async_send_command.call_args_list) == 1 + assert len(client.async_send_command.call_args_list) == 2 args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 4 diff --git a/tests/components/zwave_js/test_repairs.py b/tests/components/zwave_js/test_repairs.py new file mode 100644 index 00000000000..b1702900d7c --- /dev/null +++ b/tests/components/zwave_js/test_repairs.py @@ -0,0 +1,158 @@ +"""Test the Z-Wave JS repairs module.""" +from copy import deepcopy +from http import HTTPStatus +from unittest.mock import patch + +from zwave_js_server.event import Event +from zwave_js_server.model.node import Node + +from homeassistant.components.repairs.issue_handler import ( + async_process_repairs_platforms, +) +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.components.zwave_js import DOMAIN +from homeassistant.components.zwave_js.helpers import get_device_id +from homeassistant.core import HomeAssistant +import homeassistant.helpers.device_registry as dr +import homeassistant.helpers.issue_registry as ir + +from tests.typing import ClientSessionGenerator, WebSocketGenerator + + +async def test_device_config_file_changed( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, + client, + multisensor_6_state, + integration, +) -> None: + """Test the device_config_file_changed issue.""" + dev_reg = dr.async_get(hass) + # Create a node + node_state = deepcopy(multisensor_6_state) + node = Node(client, node_state) + event = Event( + "node added", + { + "source": "controller", + "event": "node added", + "node": node_state, + "result": "", + }, + ) + with patch( + "zwave_js_server.model.node.Node.async_has_device_config_changed", + return_value=True, + ): + client.driver.controller.receive_event(event) + await hass.async_block_till_done() + + client.async_send_command_no_wait.reset_mock() + + device = dev_reg.async_get_device(identifiers={get_device_id(client.driver, node)}) + assert device + issue_id = f"device_config_file_changed.{device.id}" + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + http_client = await hass_client() + + # Assert the issue is present + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + issue = msg["result"]["issues"][0] + assert issue["issue_id"] == issue_id + assert issue["translation_placeholders"] == {"device_name": device.name} + + url = RepairsFlowIndexView.url + resp = await http_client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + + # Apply fix + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await http_client.post(url) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + + await hass.async_block_till_done() + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + assert client.async_send_command_no_wait.call_args[0][0] == { + "command": "node.refresh_info", + "nodeId": node.node_id, + } + + # Assert the issue is resolved + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 0 + + +async def test_invalid_issue( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, + integration, +) -> None: + """Test the invalid issue.""" + ir.async_create_issue( + hass, + DOMAIN, + "invalid_issue_id", + is_fixable=True, + severity=ir.IssueSeverity.ERROR, + translation_key="invalid_issue", + ) + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + http_client = await hass_client() + + # Assert the issue is present + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + issue = msg["result"]["issues"][0] + assert issue["issue_id"] == "invalid_issue_id" + + url = RepairsFlowIndexView.url + resp = await http_client.post( + url, json={"handler": DOMAIN, "issue_id": "invalid_issue_id"} + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + + # Apply fix + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await http_client.post(url) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + + await hass.async_block_till_done() + + # Assert the issue is resolved + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 0 diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 5234460bb51..9314b9155f5 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -271,12 +271,12 @@ async def test_update_entity_ha_not_running( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 0 + assert len(client.async_send_command.call_args_list) == 1 await hass.async_start() await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 0 + assert len(client.async_send_command.call_args_list) == 1 # Update should be delayed by a day because HA is not running hass.state = CoreState.starting @@ -284,15 +284,15 @@ async def test_update_entity_ha_not_running( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 0 + assert len(client.async_send_command.call_args_list) == 1 hass.state = CoreState.running async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1)) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args_list[0][0][0] + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[1][0][0] assert args["command"] == "controller.get_available_firmware_updates" assert args["nodeId"] == zen_31.node_id @@ -591,26 +591,26 @@ async def test_update_entity_delay( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 0 + assert len(client.async_send_command.call_args_list) == 2 await hass.async_start() await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 0 + assert len(client.async_send_command.call_args_list) == 2 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args_list[0][0][0] + assert len(client.async_send_command.call_args_list) == 3 + args = client.async_send_command.call_args_list[2][0][0] assert args["command"] == "controller.get_available_firmware_updates" assert args["nodeId"] == ge_in_wall_dimmer_switch.node_id async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() - assert len(client.async_send_command.call_args_list) == 2 - args = client.async_send_command.call_args_list[1][0][0] + assert len(client.async_send_command.call_args_list) == 4 + args = client.async_send_command.call_args_list[3][0][0] assert args["command"] == "controller.get_available_firmware_updates" assert args["nodeId"] == zen_31.node_id @@ -741,8 +741,8 @@ async def test_update_entity_full_restore_data_update_available( attrs = state.attributes assert attrs[ATTR_IN_PROGRESS] is True - assert len(client.async_send_command.call_args_list) == 1 - assert client.async_send_command.call_args_list[0][0][0] == { + assert len(client.async_send_command.call_args_list) == 2 + assert client.async_send_command.call_args_list[1][0][0] == { "command": "controller.firmware_update_ota", "nodeId": climate_radio_thermostat_ct100_plus_different_endpoints.node_id, "updates": [{"target": 0, "url": "https://example2.com", "integrity": "sha2"}], From 1018e82725bd222f49094956f22fb343d1e6d642 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 30 Aug 2023 18:17:32 +0200 Subject: [PATCH 1066/1151] Bump version to 2023.9.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 66d05f0bd4f..30b6e4a29cb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 375aa7e5088..575c96234bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0.dev0" +version = "2023.9.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From cb33d82c249735db5831abb998b2ed15cfd1a225 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 31 Aug 2023 17:45:44 +1000 Subject: [PATCH 1067/1151] Patch service validation in Aussie Broadband (#99077) * Bump pyAussieBB * rolling back to previous version * patching the pydantic 2.x issue in aussie_broadband integration * adding test for validate_service_type * adding test for validate_service_type * fixing tests, again * adding additional test * doing fixes for live tests * Implement Feedback * Add test to detect pydantic2 * Update test_init.py * Update docstring --------- Co-authored-by: James Hodgkinson --- .../components/aussie_broadband/__init__.py | 19 +++++++++++++++- .../components/aussie_broadband/test_init.py | 22 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index ae4bc78580c..1bdb0579976 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -3,10 +3,11 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from aiohttp import ClientError from aussiebb.asyncio import AussieBB -from aussiebb.const import FETCH_TYPES +from aussiebb.const import FETCH_TYPES, NBN_TYPES, PHONE_TYPES from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType from homeassistant.config_entries import ConfigEntry @@ -22,6 +23,19 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] +# Backport for the pyaussiebb=0.0.15 validate_service_type method +def validate_service_type(service: dict[str, Any]) -> None: + """Check the service types against known types.""" + + if "type" not in service: + raise ValueError("Field 'type' not found in service data") + if service["type"] not in NBN_TYPES + PHONE_TYPES + ["Hardware"]: + raise UnrecognisedServiceType( + f"Service type {service['type']=} {service['name']=} - not recognised - ", + "please report this at https://github.com/yaleman/aussiebb/issues/new", + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Aussie Broadband from a config entry.""" # Login to the Aussie Broadband API and retrieve the current service list @@ -30,6 +44,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PASSWORD], async_get_clientsession(hass), ) + # Overwrite the pyaussiebb=0.0.15 validate_service_type method with backport + # Required until pydantic 2.x is supported + client.validate_service_type = validate_service_type try: await client.login() services = await client.get_services(drop_types=FETCH_TYPES) diff --git a/tests/components/aussie_broadband/test_init.py b/tests/components/aussie_broadband/test_init.py index 3eb1972011c..dc32212ee87 100644 --- a/tests/components/aussie_broadband/test_init.py +++ b/tests/components/aussie_broadband/test_init.py @@ -3,8 +3,11 @@ from unittest.mock import patch from aiohttp import ClientConnectionError from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType +import pydantic +import pytest from homeassistant import data_entry_flow +from homeassistant.components.aussie_broadband import validate_service_type from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -19,6 +22,19 @@ async def test_unload(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.NOT_LOADED +async def test_validate_service_type() -> None: + """Testing the validation function.""" + test_service = {"type": "Hardware", "name": "test service"} + validate_service_type(test_service) + + with pytest.raises(ValueError): + test_service = {"name": "test service"} + validate_service_type(test_service) + with pytest.raises(UnrecognisedServiceType): + test_service = {"type": "FunkyBob", "name": "test service"} + validate_service_type(test_service) + + async def test_auth_failure(hass: HomeAssistant) -> None: """Test init with an authentication failure.""" with patch( @@ -39,3 +55,9 @@ async def test_service_failure(hass: HomeAssistant) -> None: """Test init with a invalid service.""" entry = await setup_platform(hass, usage_effect=UnrecognisedServiceType()) assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_not_pydantic2() -> None: + """Test that Home Assistant still does not support Pydantic 2.""" + """For PR#99077 and validate_service_type backport""" + assert pydantic.__version__ < "2" From 794071449abe949ae99221d90c8003c5f06ca098 Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 30 Aug 2023 21:36:07 -0700 Subject: [PATCH 1068/1151] Opower MFA fixes (#99317) opower mfa fixes --- .../components/opower/config_flow.py | 24 +++++++++---------- .../components/opower/coordinator.py | 2 +- homeassistant/components/opower/manifest.json | 2 +- homeassistant/components/opower/strings.json | 11 ++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/opower/test_config_flow.py | 4 ++-- 7 files changed, 25 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py index 9f2ec56423d..d456fc536e5 100644 --- a/homeassistant/components/opower/config_flow.py +++ b/homeassistant/components/opower/config_flow.py @@ -5,7 +5,13 @@ from collections.abc import Mapping import logging from typing import Any -from opower import CannotConnect, InvalidAuth, Opower, get_supported_utility_names +from opower import ( + CannotConnect, + InvalidAuth, + Opower, + get_supported_utility_names, + select_utility, +) import voluptuous as vol from homeassistant import config_entries @@ -20,9 +26,7 @@ _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_UTILITY): vol.In( - get_supported_utility_names(supports_mfa=True) - ), + vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names()), vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } @@ -38,7 +42,7 @@ async def _validate_login( login_data[CONF_UTILITY], login_data[CONF_USERNAME], login_data[CONF_PASSWORD], - login_data.get(CONF_TOTP_SECRET, None), + login_data.get(CONF_TOTP_SECRET), ) errors: dict[str, str] = {} try: @@ -50,12 +54,6 @@ async def _validate_login( return errors -@callback -def _supports_mfa(utility: str) -> bool: - """Return whether the utility supports MFA.""" - return utility not in get_supported_utility_names(supports_mfa=False) - - class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Opower.""" @@ -78,7 +76,7 @@ class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_USERNAME: user_input[CONF_USERNAME], } ) - if _supports_mfa(user_input[CONF_UTILITY]): + if select_utility(user_input[CONF_UTILITY]).accepts_mfa(): self.utility_info = user_input return await self.async_step_mfa() @@ -154,7 +152,7 @@ class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_USERNAME): self.reauth_entry.data[CONF_USERNAME], vol.Required(CONF_PASSWORD): str, } - if _supports_mfa(self.reauth_entry.data[CONF_UTILITY]): + if select_utility(self.reauth_entry.data[CONF_UTILITY]).accepts_mfa(): schema[vol.Optional(CONF_TOTP_SECRET)] = str return self.async_show_form( step_id="reauth_confirm", diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index 1410b62b7b6..5ce35e949af 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -55,7 +55,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): entry_data[CONF_UTILITY], entry_data[CONF_USERNAME], entry_data[CONF_PASSWORD], - entry_data.get(CONF_TOTP_SECRET, None), + entry_data.get(CONF_TOTP_SECRET), ) async def _async_update_data( diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index fb4ff5153ec..05e89ea96d4 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.0.32"] + "requirements": ["opower==0.0.33"] } diff --git a/homeassistant/components/opower/strings.json b/homeassistant/components/opower/strings.json index ac931bf9308..362e6cd7596 100644 --- a/homeassistant/components/opower/strings.json +++ b/homeassistant/components/opower/strings.json @@ -5,8 +5,13 @@ "data": { "utility": "Utility name", "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]", - "totp_secret": "TOTP Secret (only for some utilities, see documentation)" + "password": "[%key:common::config_flow::data::password%]" + } + }, + "mfa": { + "description": "The TOTP secret below is not one of the 6 digit time-based numeric codes. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation.", + "data": { + "totp_secret": "TOTP Secret" } }, "reauth_confirm": { @@ -14,7 +19,7 @@ "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "totp_secret": "TOTP Secret (only for some utilities, see documentation)" + "totp_secret": "TOTP Secret" } } }, diff --git a/requirements_all.txt b/requirements_all.txt index 35394fafa95..56f75109423 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1374,7 +1374,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.32 +opower==0.0.33 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40b6c4ace56..69e6a9043a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1040,7 +1040,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.32 +opower==0.0.33 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index 0391e42ca16..f9ae457a80e 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -300,7 +300,7 @@ async def test_form_valid_reauth( assert result["reason"] == "reauth_successful" await hass.async_block_till_done() - assert hass.config_entries.async_entries(DOMAIN)[0].data == { + assert mock_config_entry.data == { "utility": "Pacific Gas and Electric Company (PG&E)", "username": "test-username", "password": "test-password2", @@ -350,7 +350,7 @@ async def test_form_valid_reauth_with_mfa( assert result["reason"] == "reauth_successful" await hass.async_block_till_done() - assert hass.config_entries.async_entries(DOMAIN)[0].data == { + assert mock_config_entry.data == { "utility": "Consolidated Edison (ConEd)", "username": "test-username", "password": "test-password2", From 52f8dbf25bc3d698b5a163e17a8777cda8553a70 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Aug 2023 16:50:53 +0200 Subject: [PATCH 1069/1151] Add documentation URL for homeassistant_yellow (#99336) * Add documentation URL for homeassistant_yellow * Fix test * Tweak --- homeassistant/components/homeassistant_yellow/hardware.py | 3 ++- tests/components/homeassistant_yellow/test_hardware.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant_yellow/hardware.py b/homeassistant/components/homeassistant_yellow/hardware.py index b67eb50ff2c..0749ca8edc6 100644 --- a/homeassistant/components/homeassistant_yellow/hardware.py +++ b/homeassistant/components/homeassistant_yellow/hardware.py @@ -9,6 +9,7 @@ from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN BOARD_NAME = "Home Assistant Yellow" +DOCUMENTATION_URL = "https://yellow.home-assistant.io/documentation/" MANUFACTURER = "homeassistant" MODEL = "yellow" @@ -39,6 +40,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: config_entries=config_entries, dongle=None, name=BOARD_NAME, - url=None, + url=DOCUMENTATION_URL, ) ] diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index 5fa0e73d82c..5fb662471aa 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -54,7 +54,7 @@ async def test_hardware_info( "config_entries": [config_entry.entry_id], "dongle": None, "name": "Home Assistant Yellow", - "url": None, + "url": "https://yellow.home-assistant.io/documentation/", } ] } From 3066d70809ed0918dfa215fd7770760b5f87281c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 30 Aug 2023 14:56:19 -0400 Subject: [PATCH 1070/1151] Bump ZHA dependencies (#99341) * Bump ZHA dependencies * Include bellows as well --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4f23945b105..cd0dc2db5ae 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -20,12 +20,12 @@ "zigpy_znp" ], "requirements": [ - "bellows==0.35.9", + "bellows==0.36.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.102", + "zha-quirks==0.0.103", "zigpy-deconz==0.21.0", - "zigpy==0.56.4", + "zigpy==0.57.0", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", "zigpy-znp==0.11.4" diff --git a/requirements_all.txt b/requirements_all.txt index 56f75109423..264404dd225 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -509,7 +509,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.35.9 +bellows==0.36.1 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -2769,7 +2769,7 @@ zeroconf==0.88.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.102 +zha-quirks==0.0.103 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.9 @@ -2790,7 +2790,7 @@ zigpy-zigate==0.11.0 zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.56.4 +zigpy==0.57.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69e6a9043a1..49b8ad76018 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -430,7 +430,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.35.9 +bellows==0.36.1 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -2039,7 +2039,7 @@ zeroconf==0.88.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.102 +zha-quirks==0.0.103 # homeassistant.components.zha zigpy-deconz==0.21.0 @@ -2054,7 +2054,7 @@ zigpy-zigate==0.11.0 zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.56.4 +zigpy==0.57.0 # homeassistant.components.zwave_js zwave-js-server-python==0.51.0 From 316f89beadbde976e6106c3a45f24303bae614dc Mon Sep 17 00:00:00 2001 From: Austin Brunkhorst Date: Thu, 31 Aug 2023 02:15:45 -0700 Subject: [PATCH 1071/1151] Update pysnooz to 0.8.6 (#99368) --- homeassistant/components/snooz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snooz/manifest.json b/homeassistant/components/snooz/manifest.json index cd132d5a175..5b43aa7e92d 100644 --- a/homeassistant/components/snooz/manifest.json +++ b/homeassistant/components/snooz/manifest.json @@ -14,5 +14,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/snooz", "iot_class": "local_push", - "requirements": ["pysnooz==0.8.3"] + "requirements": ["pysnooz==0.8.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 264404dd225..9cf0a7abba1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2035,7 +2035,7 @@ pysml==0.0.12 pysnmplib==5.0.21 # homeassistant.components.snooz -pysnooz==0.8.3 +pysnooz==0.8.6 # homeassistant.components.soma pysoma==0.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49b8ad76018..864a661e72d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1518,7 +1518,7 @@ pysml==0.0.12 pysnmplib==5.0.21 # homeassistant.components.snooz -pysnooz==0.8.3 +pysnooz==0.8.6 # homeassistant.components.soma pysoma==0.0.12 From eb423c39b63f7abdcb8de2b3a9b158c86c37364c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Aug 2023 15:16:32 +0200 Subject: [PATCH 1072/1151] Improve template sensor config flow validation (#99373) --- .../components/template/config_flow.py | 27 +++++++--- tests/components/template/test_config_flow.py | 54 +++++++++++++++++-- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index b89b3cbc91d..b2ccddedad8 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -160,32 +160,43 @@ def _validate_unit(options: dict[str, Any]) -> None: and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None and (unit := options.get(CONF_UNIT_OF_MEASUREMENT)) not in units ): - units_string = sorted( - [str(unit) if unit else "no unit of measurement" for unit in units], + sorted_units = sorted( + [f"'{str(unit)}'" if unit else "no unit of measurement" for unit in units], key=str.casefold, ) + if len(sorted_units) == 1: + units_string = sorted_units[0] + else: + units_string = f"one of {', '.join(sorted_units)}" raise vol.Invalid( f"'{unit}' is not a valid unit for device class '{device_class}'; " - f"expected one of {', '.join(units_string)}" + f"expected {units_string}" ) def _validate_state_class(options: dict[str, Any]) -> None: """Validate state class.""" if ( - (device_class := options.get(CONF_DEVICE_CLASS)) + (state_class := options.get(CONF_STATE_CLASS)) + and (device_class := options.get(CONF_DEVICE_CLASS)) and (state_classes := DEVICE_CLASS_STATE_CLASSES.get(device_class)) is not None - and (state_class := options.get(CONF_STATE_CLASS)) not in state_classes + and state_class not in state_classes ): - state_classes_string = sorted( - [str(state_class) for state_class in state_classes], + sorted_state_classes = sorted( + [f"'{str(state_class)}'" for state_class in state_classes], key=str.casefold, ) + if len(sorted_state_classes) == 0: + state_classes_string = "no state class" + elif len(sorted_state_classes) == 1: + state_classes_string = sorted_state_classes[0] + else: + state_classes_string = f"one of {', '.join(sorted_state_classes)}" raise vol.Invalid( f"'{state_class}' is not a valid state class for device class " - f"'{device_class}'; expected one of {', '.join(state_classes_string)}" + f"'{device_class}'; expected {state_classes_string}" ) diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index dd283ff9214..ba939f3b8d1 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -349,18 +349,62 @@ EARLY_END_ERROR = "invalid template (TemplateSyntaxError: unexpected 'end of tem [ ("binary_sensor", "{{", {}, {"state": EARLY_END_ERROR}), ("sensor", "{{", {}, {"state": EARLY_END_ERROR}), + ( + "sensor", + "", + {"device_class": "aqi", "unit_of_measurement": "cats"}, + { + "unit_of_measurement": ( + "'cats' is not a valid unit for device class 'aqi'; " + "expected no unit of measurement" + ), + }, + ), ( "sensor", "", {"device_class": "temperature", "unit_of_measurement": "cats"}, { - "state_class": ( - "'None' is not a valid state class for device class 'temperature'; " - "expected one of measurement" - ), "unit_of_measurement": ( "'cats' is not a valid unit for device class 'temperature'; " - "expected one of K, °C, °F" + "expected one of 'K', '°C', '°F'" + ), + }, + ), + ( + "sensor", + "", + {"device_class": "timestamp", "state_class": "measurement"}, + { + "state_class": ( + "'measurement' is not a valid state class for device class " + "'timestamp'; expected no state class" + ), + }, + ), + ( + "sensor", + "", + {"device_class": "aqi", "state_class": "total"}, + { + "state_class": ( + "'total' is not a valid state class for device class " + "'aqi'; expected 'measurement'" + ), + }, + ), + ( + "sensor", + "", + {"device_class": "energy", "state_class": "measurement"}, + { + "state_class": ( + "'measurement' is not a valid state class for device class " + "'energy'; expected one of 'total', 'total_increasing'" + ), + "unit_of_measurement": ( + "'None' is not a valid unit for device class 'energy'; " + "expected one of 'GJ', 'kWh', 'MJ', 'MWh', 'Wh'" ), }, ), From a0d03d6bb1100362505ce1aeee9a769b09d3e23e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Aug 2023 03:33:57 -0400 Subject: [PATCH 1073/1151] Revert orjson to 3.9.2 (#99374) * Revert "Update orjson to 3.9.4 (#98108)" This reverts commit 3dd377cb2a0b60593a18767a5e4b032f5630fd78. * Revert "Update orjson to 3.9.3 (#97930)" This reverts commit d993aa59ea097b25084a5fde2730a576eb13b7b5. --- 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 949181c7ddd..0994ca657ba 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.4 +orjson==3.9.2 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.0.0 diff --git a/pyproject.toml b/pyproject.toml index 575c96234bb..c8aa4f7566f 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.4", + "orjson==3.9.2", "packaging>=23.1", "pip>=21.3.1", "python-slugify==4.0.1", diff --git a/requirements.txt b/requirements.txt index 10220697390..e7a3b0fc4c5 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.4 +orjson==3.9.2 packaging>=23.1 pip>=21.3.1 python-slugify==4.0.1 From 97b0815122976a6c54895fc3135f2d09e000b6ad Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 31 Aug 2023 10:39:24 +0200 Subject: [PATCH 1074/1151] Add documentation URL for homeassistant_sky_connect (#99377) --- .../components/homeassistant_sky_connect/hardware.py | 3 ++- tests/components/homeassistant_sky_connect/test_hardware.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index 217a6e57543..bd752278397 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -6,6 +6,7 @@ from homeassistant.core import HomeAssistant, callback from .const import DOMAIN +DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/" DONGLE_NAME = "Home Assistant SkyConnect" @@ -26,7 +27,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: description=entry.data["description"], ), name=DONGLE_NAME, - url=None, + url=DOCUMENTATION_URL, ) for entry in entries ] diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 5ddddfc637b..ca9a7887040 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -78,7 +78,7 @@ async def test_hardware_info( "description": "bla_description", }, "name": "Home Assistant SkyConnect", - "url": None, + "url": "https://skyconnect.home-assistant.io/documentation/", }, { "board": None, @@ -91,7 +91,7 @@ async def test_hardware_info( "description": "bla_description_2", }, "name": "Home Assistant SkyConnect", - "url": None, + "url": "https://skyconnect.home-assistant.io/documentation/", }, ] } From d1c154fc0dd3fa412754aeb5ee510710a9ae456f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 31 Aug 2023 10:24:03 +0200 Subject: [PATCH 1075/1151] Revert "Sonos add yaml config issue" (#99379) Revert "Sonos add yaml config issue (#97365)" This reverts commit 2299430dbeb470ff8b5a62fae1fa80fbfc3f014f. --- homeassistant/components/sonos/__init__.py | 23 +--------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 259a9f54044..e6b328cbcb0 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -25,13 +25,7 @@ from homeassistant.components import ssdp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import ( - CALLBACK_TYPE, - DOMAIN as HOMEASSISTANT_DOMAIN, - Event, - HomeAssistant, - callback, -) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -39,7 +33,6 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later, async_track_time_interval -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .alarms import SonosAlarms @@ -132,20 +125,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) ) - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.2.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Sonos", - }, - ) return True From db8980246bf4ca04b96fc8af0245b254fa5a3ffe Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 31 Aug 2023 14:29:24 +0200 Subject: [PATCH 1076/1151] Add entity component translation for water heater away mode attribute (#99394) --- homeassistant/components/water_heater/strings.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index 5ddb61d28b0..6991d371bd3 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -16,6 +16,15 @@ "high_demand": "High Demand", "heat_pump": "Heat Pump", "performance": "Performance" + }, + "state_attributes": { + "away_mode": { + "name": "Away mode", + "state": { + "off": "Off", + "on": "On" + } + } } } }, From 9836d17c92f8c86cf539e9547cab2a44d8a737dd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 31 Aug 2023 15:32:37 +0200 Subject: [PATCH 1077/1151] Update frontend to 20230831.0 (#99405) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 06b6da85e19..a31faaf362e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230830.0"] + "requirements": ["home-assistant-frontend==20230831.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0994ca657ba..3dccb80d11e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230830.0 +home-assistant-frontend==20230831.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9cf0a7abba1..9084b181383 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230830.0 +home-assistant-frontend==20230831.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 864a661e72d..88a25dffed0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230830.0 +home-assistant-frontend==20230831.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From a603a99bd6e6a3accdb65d9e355348d8fa3a8c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 31 Aug 2023 16:43:32 +0200 Subject: [PATCH 1078/1151] Add remote alias to connection info response (#99410) --- homeassistant/components/cloud/client.py | 1 + tests/components/cloud/test_client.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 6fbcfc30f69..c216ec85c5c 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -221,6 +221,7 @@ class CloudClient(Interface): "connected": self.cloud.remote.is_connected, "enabled": self._prefs.remote_enabled, "instance_domain": self.cloud.remote.instance_domain, + "alias": self.cloud.remote.alias, }, "version": HA_VERSION, } diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 50cfce3f9a9..e205ba5f6e8 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -365,6 +365,11 @@ async def test_cloud_connection_info(hass: HomeAssistant) -> None: response = await cloud.client.async_cloud_connection_info({}) assert response == { - "remote": {"connected": False, "enabled": False, "instance_domain": None}, + "remote": { + "connected": False, + "enabled": False, + "instance_domain": None, + "alias": None, + }, "version": HA_VERSION, } From 8284c288bf637c94ffa38c8dcdb9862c55061868 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 31 Aug 2023 17:19:10 +0200 Subject: [PATCH 1079/1151] Bump version to 2023.9.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 30b6e4a29cb..0b1cc1c5ea0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index c8aa4f7566f..210c2973d4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b0" +version = "2023.9.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0bae0824b42ee4f5e18cff36ce3b3ddc99487bc0 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 31 Aug 2023 12:09:46 -0400 Subject: [PATCH 1080/1151] Initialize ZHA device database before connecting to the radio (#98082) * Create ZHA entities before attempting to connect to the coordinator * Delete the ZHA gateway object when unloading the config entry * Only load ZHA groups if the coordinator device info is known offline * Do not create a coordinator ZHA device until it is ready * [WIP] begin fixing unit tests * [WIP] Fix existing unit tests (one failure left) * Fix remaining unit test --- homeassistant/components/zha/__init__.py | 20 +---- homeassistant/components/zha/core/const.py | 1 - homeassistant/components/zha/core/gateway.py | 53 ++++++++++--- homeassistant/components/zha/core/helpers.py | 6 +- tests/components/zha/common.py | 10 +-- tests/components/zha/conftest.py | 26 ++++++- tests/components/zha/test_api.py | 2 +- tests/components/zha/test_gateway.py | 79 +++++++++++++------- tests/components/zha/test_websocket_api.py | 11 ++- 9 files changed, 133 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index a51d6f387e1..e48f8ce2096 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -10,7 +10,7 @@ from zhaquirks import setup as setup_quirks from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TYPE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv @@ -33,7 +33,6 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_CONFIG, DATA_ZHA_GATEWAY, - DATA_ZHA_SHUTDOWN_TASK, DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, @@ -137,6 +136,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() + config_entry.async_on_unload(zha_gateway.shutdown) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -149,15 +150,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b websocket_api.async_load_api(hass) - async def async_zha_shutdown(event): - """Handle shutdown tasks.""" - zha_gateway: ZHAGateway = zha_data[DATA_ZHA_GATEWAY] - await zha_gateway.shutdown() - - zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_zha_shutdown - ) - await zha_gateway.async_initialize_devices_and_entities() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) @@ -167,12 +159,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload ZHA config entry.""" try: - zha_gateway: ZHAGateway = hass.data[DATA_ZHA].pop(DATA_ZHA_GATEWAY) + del hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] except KeyError: return False - await zha_gateway.shutdown() - GROUP_PROBE.cleanup() websocket_api.async_unload_api(hass) @@ -184,8 +174,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) ) - hass.data[DATA_ZHA][DATA_ZHA_SHUTDOWN_TASK]() - return True diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 7aab6112ab0..63b59e9d8d4 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -187,7 +187,6 @@ DATA_ZHA_CONFIG = "config" DATA_ZHA_BRIDGE_ID = "zha_bridge_id" DATA_ZHA_CORE_EVENTS = "zha_core_events" DATA_ZHA_GATEWAY = "zha_gateway" -DATA_ZHA_SHUTDOWN_TASK = "zha_shutdown_task" DEBUG_COMP_BELLOWS = "bellows" DEBUG_COMP_ZHA = "homeassistant.components.zha" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 1320e77ba3c..3abf1274f98 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -148,7 +148,6 @@ class ZHAGateway: self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] - self.initialized: bool = False def get_application_controller_data(self) -> tuple[ControllerApplication, dict]: """Get an uninitialized instance of a zigpy `ControllerApplication`.""" @@ -199,12 +198,32 @@ class ZHAGateway: self.ha_entity_registry = er.async_get(self._hass) app_controller_cls, app_config = self.get_application_controller_data() + self.application_controller = await app_controller_cls.new( + config=app_config, + auto_form=False, + start_radio=False, + ) + + self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self + + self.async_load_devices() + + # Groups are attached to the coordinator device so we need to load it early + coordinator = self._find_coordinator_device() + loaded_groups = False + + # We can only load groups early if the coordinator's model info has been stored + # in the zigpy database + if coordinator.model is not None: + self.coordinator_zha_device = self._async_get_or_create_device( + coordinator, restored=True + ) + self.async_load_groups() + loaded_groups = True for attempt in range(STARTUP_RETRIES): try: - self.application_controller = await app_controller_cls.new( - app_config, auto_form=True, start_radio=True - ) + await self.application_controller.startup(auto_form=True) except zigpy.exceptions.TransientConnectionError as exc: raise ConfigEntryNotReady from exc except Exception as exc: # pylint: disable=broad-except @@ -223,21 +242,33 @@ class ZHAGateway: else: break + self.coordinator_zha_device = self._async_get_or_create_device( + self._find_coordinator_device(), restored=True + ) + self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) + + # If ZHA groups could not load early, we can safely load them now + if not loaded_groups: + self.async_load_groups() + self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) - self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self - self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) - self.async_load_devices() - self.async_load_groups() - self.initialized = True + + def _find_coordinator_device(self) -> zigpy.device.Device: + if last_backup := self.application_controller.backups.most_recent_backup(): + zigpy_coordinator = self.application_controller.get_device( + ieee=last_backup.node_info.ieee + ) + else: + zigpy_coordinator = self.application_controller.get_device(nwk=0x0000) + + return zigpy_coordinator @callback def async_load_devices(self) -> None: """Restore ZHA devices from zigpy application state.""" for zigpy_device in self.application_controller.devices.values(): zha_device = self._async_get_or_create_device(zigpy_device, restored=True) - if zha_device.ieee == self.coordinator_ieee: - self.coordinator_zha_device = zha_device delta_msg = "not known" if zha_device.last_seen is not None: delta = round(time.time() - zha_device.last_seen) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index ac7c15d3ecd..7b0d062738b 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -27,7 +27,6 @@ import zigpy.zdo.types as zdo_types from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback -from homeassistant.exceptions import IntegrationError from homeassistant.helpers import config_validation as cv, device_registry as dr from .const import ( @@ -246,11 +245,8 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: _LOGGER.error("Device id `%s` not found in registry", device_id) raise KeyError(f"Device id `{device_id}` not found in registry.") zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - if not zha_gateway.initialized: - _LOGGER.error("Attempting to get a ZHA device when ZHA is not initialized") - raise IntegrationError("ZHA is not initialized yet") try: - ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee_address = list(registry_device.identifiers)[0][1] ieee = zigpy.types.EUI64.convert(ieee_address) except (IndexError, ValueError) as ex: _LOGGER.error( diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 01206c432e6..db1da3721ee 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -87,10 +87,7 @@ def update_attribute_cache(cluster): def get_zha_gateway(hass): """Return ZHA gateway from hass.data.""" - try: - return hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] - except KeyError: - return None + return hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] def make_attribute(attrid, value, status=0): @@ -167,12 +164,9 @@ def find_entity_ids(domain, zha_device, hass): def async_find_group_entity_id(hass, domain, group): """Find the group entity id under test.""" - entity_id = ( - f"{domain}.fakemanufacturer_fakemodel_{group.name.lower().replace(' ', '_')}" - ) + entity_id = f"{domain}.coordinator_manufacturer_coordinator_model_{group.name.lower().replace(' ', '_')}" entity_ids = hass.states.async_entity_ids(domain) - assert entity_id in entity_ids return entity_id diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index dd2c200973c..f690a5152fc 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -16,6 +16,8 @@ import zigpy.profiles import zigpy.quirks import zigpy.types import zigpy.util +from zigpy.zcl.clusters.general import Basic, Groups +from zigpy.zcl.foundation import Status import zigpy.zdo.types as zdo_t import homeassistant.components.zha.core.const as zha_const @@ -116,6 +118,9 @@ def zigpy_app_controller(): { zigpy.config.CONF_DATABASE: None, zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: "/dev/null"}, + zigpy.config.CONF_STARTUP_ENERGY_SCAN: False, + zigpy.config.CONF_NWK_BACKUP_ENABLED: False, + zigpy.config.CONF_TOPO_SCAN_ENABLED: False, } ) @@ -128,9 +133,24 @@ def zigpy_app_controller(): app.state.network_info.channel = 15 app.state.network_info.network_key.key = zigpy.types.KeyData(range(16)) - with patch("zigpy.device.Device.request"), patch.object( - app, "permit", autospec=True - ), patch.object(app, "permit_with_key", autospec=True): + # Create a fake coordinator device + dev = app.add_device(nwk=app.state.node_info.nwk, ieee=app.state.node_info.ieee) + dev.node_desc = zdo_t.NodeDescriptor() + dev.node_desc.logical_type = zdo_t.LogicalType.Coordinator + dev.manufacturer = "Coordinator Manufacturer" + dev.model = "Coordinator Model" + + ep = dev.add_endpoint(1) + ep.add_input_cluster(Basic.cluster_id) + ep.add_input_cluster(Groups.cluster_id) + + with patch( + "zigpy.device.Device.request", return_value=[Status.SUCCESS] + ), patch.object(app, "permit", autospec=True), patch.object( + app, "startup", wraps=app.startup + ), patch.object( + app, "permit_with_key", autospec=True + ): yield app diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 85f85cc0437..c2cb16efcc8 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -71,7 +71,7 @@ async def test_async_get_network_settings_missing( await setup_zha() gateway = api._get_gateway(hass) - await zha.async_unload_entry(hass, gateway.config_entry) + await gateway.config_entry.async_unload(hass) # Network settings were never loaded for whatever reason zigpy_app_controller.state.network_info = zigpy.state.NetworkInfo() diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index b9fcd4b6932..0f791a08955 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,9 +1,9 @@ """Test ZHA Gateway.""" import asyncio -from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest +from zigpy.application import ControllerApplication import zigpy.exceptions import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general @@ -232,68 +232,89 @@ async def test_gateway_create_group_with_id( ) @patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) @pytest.mark.parametrize( - "startup", + "startup_effect", [ - [asyncio.TimeoutError(), FileNotFoundError(), MagicMock()], - [asyncio.TimeoutError(), MagicMock()], - [MagicMock()], + [asyncio.TimeoutError(), FileNotFoundError(), None], + [asyncio.TimeoutError(), None], + [None], ], ) async def test_gateway_initialize_success( - startup: list[Any], + startup_effect: list[Exception | None], hass: HomeAssistant, device_light_1: ZHADevice, coordinator: ZHADevice, + zigpy_app_controller: ControllerApplication, ) -> None: """Test ZHA initializing the gateway successfully.""" zha_gateway = get_zha_gateway(hass) assert zha_gateway is not None - zha_gateway.shutdown = AsyncMock() + zigpy_app_controller.startup.side_effect = startup_effect + zigpy_app_controller.startup.reset_mock() with patch( - "bellows.zigbee.application.ControllerApplication.new", side_effect=startup - ) as mock_new: + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ): await zha_gateway.async_initialize() - assert mock_new.call_count == len(startup) - + assert zigpy_app_controller.startup.call_count == len(startup_effect) device_light_1.async_cleanup_handles() @patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) async def test_gateway_initialize_failure( - hass: HomeAssistant, device_light_1, coordinator + hass: HomeAssistant, + device_light_1: ZHADevice, + coordinator: ZHADevice, + zigpy_app_controller: ControllerApplication, ) -> None: """Test ZHA failing to initialize the gateway.""" zha_gateway = get_zha_gateway(hass) assert zha_gateway is not None + zigpy_app_controller.startup.side_effect = [ + asyncio.TimeoutError(), + RuntimeError(), + FileNotFoundError(), + ] + zigpy_app_controller.startup.reset_mock() + with patch( "bellows.zigbee.application.ControllerApplication.new", - side_effect=[asyncio.TimeoutError(), FileNotFoundError(), RuntimeError()], - ) as mock_new, pytest.raises(RuntimeError): + return_value=zigpy_app_controller, + ), pytest.raises(FileNotFoundError): await zha_gateway.async_initialize() - assert mock_new.call_count == 3 + assert zigpy_app_controller.startup.call_count == 3 @patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) async def test_gateway_initialize_failure_transient( - hass: HomeAssistant, device_light_1, coordinator + hass: HomeAssistant, + device_light_1: ZHADevice, + coordinator: ZHADevice, + zigpy_app_controller: ControllerApplication, ) -> None: """Test ZHA failing to initialize the gateway but with a transient error.""" zha_gateway = get_zha_gateway(hass) assert zha_gateway is not None + zigpy_app_controller.startup.side_effect = [ + RuntimeError(), + zigpy.exceptions.TransientConnectionError(), + ] + zigpy_app_controller.startup.reset_mock() + with patch( "bellows.zigbee.application.ControllerApplication.new", - side_effect=[RuntimeError(), zigpy.exceptions.TransientConnectionError()], - ) as mock_new, pytest.raises(ConfigEntryNotReady): + return_value=zigpy_app_controller, + ), pytest.raises(ConfigEntryNotReady): await zha_gateway.async_initialize() # Initialization immediately stops and is retried after TransientConnectionError - assert mock_new.call_count == 2 + assert zigpy_app_controller.startup.call_count == 2 @patch( @@ -313,7 +334,12 @@ async def test_gateway_initialize_failure_transient( ], ) async def test_gateway_initialize_bellows_thread( - device_path, thread_state, config_override, hass: HomeAssistant, coordinator + device_path: str, + thread_state: bool, + config_override: dict, + hass: HomeAssistant, + coordinator: ZHADevice, + zigpy_app_controller: ControllerApplication, ) -> None: """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" zha_gateway = get_zha_gateway(hass) @@ -324,15 +350,12 @@ async def test_gateway_initialize_bellows_thread( zha_gateway._config.setdefault("zigpy_config", {}).update(config_override) with patch( - "bellows.zigbee.application.ControllerApplication.new" - ) as controller_app_mock: - mock = AsyncMock() - mock.add_listener = MagicMock() - mock.groups = MagicMock() - controller_app_mock.return_value = mock + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ) as mock_new: await zha_gateway.async_initialize() - assert controller_app_mock.mock_calls[0].args[0]["use_thread"] is thread_state + assert mock_new.mock_calls[0].kwargs["config"]["use_thread"] is thread_state @pytest.mark.parametrize( diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index 0904fc1f685..740ffd6c06c 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -13,6 +13,7 @@ import zigpy.profiles.zha import zigpy.types from zigpy.types.named import EUI64 import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters.general import Groups import zigpy.zcl.clusters.security as security import zigpy.zdo.types as zdo_types @@ -233,7 +234,7 @@ async def test_list_devices(zha_client) -> None: msg = await zha_client.receive_json() devices = msg["result"] - assert len(devices) == 2 + assert len(devices) == 2 + 1 # the coordinator is included as well msg_id = 100 for device in devices: @@ -371,8 +372,13 @@ async def test_get_group_not_found(zha_client) -> None: assert msg["error"]["code"] == const.ERR_NOT_FOUND -async def test_list_groupable_devices(zha_client, device_groupable) -> None: +async def test_list_groupable_devices( + zha_client, device_groupable, zigpy_app_controller +) -> None: """Test getting ZHA devices that have a group cluster.""" + # Ensure the coordinator doesn't have a group cluster + coordinator = zigpy_app_controller.get_device(nwk=0x0000) + del coordinator.endpoints[1].in_clusters[Groups.cluster_id] await zha_client.send_json({ID: 10, TYPE: "zha/devices/groupable"}) @@ -479,6 +485,7 @@ async def app_controller( ) -> ControllerApplication: """Fixture for zigpy Application Controller.""" await setup_zha() + zigpy_app_controller.permit.reset_mock() return zigpy_app_controller From 2ec9abfd24eaf4f9047d8901356a5ca5e054dbbc Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:05:45 -0400 Subject: [PATCH 1081/1151] Create a ZHA repair when directly accessing a radio with multi-PAN firmware (#98275) * Add the SiLabs flasher as a dependency * Create a repair if the wrong firmware is detected on an EZSP device * Update homeassistant/components/zha/strings.json Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> * Provide the ZHA config entry as a reusable fixture * Create a separate repair when using non-Nabu Casa hardware * Add unit tests * Drop extraneous `config_entry.add_to_hass` added in 021def44 * Fully unit test all edge cases * Move `socket://`-ignoring logic into repair function * Open a repair from ZHA flows when the wrong firmware is running * Fix existing unit tests * Link to the flashing section in the documentation * Reduce repair severity to `ERROR` * Make issue persistent * Add unit tests for new radio probing states * Add unit tests for new config flow steps * Handle probing failure raising an exception * Implement review suggestions * Address review comments --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/zha/__init__.py | 21 +- homeassistant/components/zha/config_flow.py | 26 +- homeassistant/components/zha/manifest.json | 6 +- homeassistant/components/zha/radio_manager.py | 23 +- homeassistant/components/zha/repairs.py | 126 ++++++++++ homeassistant/components/zha/strings.json | 16 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/hassfest/dependencies.py | 1 + tests/components/zha/conftest.py | 33 ++- tests/components/zha/test_config_flow.py | 75 ++++-- tests/components/zha/test_diagnostics.py | 6 +- tests/components/zha/test_radio_manager.py | 63 ++++- tests/components/zha/test_repairs.py | 235 ++++++++++++++++++ 14 files changed, 587 insertions(+), 50 deletions(-) create mode 100644 homeassistant/components/zha/repairs.py create mode 100644 tests/components/zha/test_repairs.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index e48f8ce2096..1c4c3e776d0 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -12,13 +12,14 @@ from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import ConfigType -from . import websocket_api +from . import repairs, websocket_api from .core import ZHAGateway from .core.const import ( BAUD_RATES, @@ -134,7 +135,23 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("ZHA storage file does not exist or was already removed") zha_gateway = ZHAGateway(hass, config, config_entry) - await zha_gateway.async_initialize() + + try: + await zha_gateway.async_initialize() + except Exception: # pylint: disable=broad-except + if RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp: + try: + await repairs.warn_on_wrong_silabs_firmware( + hass, config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] + ) + except repairs.AlreadyRunningEZSP as exc: + # If connecting fails but we somehow probe EZSP (e.g. stuck in the + # bootloader), reconnect, it should work + raise ConfigEntryNotReady from exc + + raise + + repairs.async_delete_blocking_issues(hass) config_entry.async_on_unload(zha_gateway.shutdown) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 6ac3a155ed9..1b6bbee5159 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -35,6 +35,7 @@ from .core.const import ( from .radio_manager import ( HARDWARE_DISCOVERY_SCHEMA, RECOMMENDED_RADIOS, + ProbeResult, ZhaRadioManager, ) @@ -60,6 +61,8 @@ OPTIONS_INTENT_RECONFIGURE = "intent_reconfigure" UPLOADED_BACKUP_FILE = "uploaded_backup_file" +REPAIR_MY_URL = "https://my.home-assistant.io/redirect/repairs/" + DEFAULT_ZHA_ZEROCONF_PORT = 6638 ESPHOME_API_PORT = 6053 @@ -187,7 +190,13 @@ class BaseZhaFlow(FlowHandler): port = ports[list_of_ports.index(user_selection)] self._radio_mgr.device_path = port.device - if not await self._radio_mgr.detect_radio_type(): + probe_result = await self._radio_mgr.detect_radio_type() + if probe_result == ProbeResult.WRONG_FIRMWARE_INSTALLED: + return self.async_abort( + reason="wrong_firmware_installed", + description_placeholders={"repair_url": REPAIR_MY_URL}, + ) + if probe_result == ProbeResult.PROBING_FAILED: # Did not autodetect anything, proceed to manual selection return await self.async_step_manual_pick_radio_type() @@ -530,10 +539,17 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN # config flow logic that interacts with hardware. if user_input is not None or not onboarding.async_is_onboarded(self.hass): # Probe the radio type if we don't have one yet - if ( - self._radio_mgr.radio_type is None - and not await self._radio_mgr.detect_radio_type() - ): + if self._radio_mgr.radio_type is None: + probe_result = await self._radio_mgr.detect_radio_type() + else: + probe_result = ProbeResult.RADIO_TYPE_DETECTED + + if probe_result == ProbeResult.WRONG_FIRMWARE_INSTALLED: + return self.async_abort( + reason="wrong_firmware_installed", + description_placeholders={"repair_url": REPAIR_MY_URL}, + ) + if probe_result == ProbeResult.PROBING_FAILED: # This path probably will not happen now that we have # more precise USB matching unless there is a problem # with the device diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index cd0dc2db5ae..809b576defa 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -17,7 +17,8 @@ "zigpy_deconz", "zigpy_xbee", "zigpy_zigate", - "zigpy_znp" + "zigpy_znp", + "universal_silabs_flasher" ], "requirements": [ "bellows==0.36.1", @@ -28,7 +29,8 @@ "zigpy==0.57.0", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", - "zigpy-znp==0.11.4" + "zigpy-znp==0.11.4", + "universal-silabs-flasher==0.0.13" ], "usb": [ { diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 4e70fc2247f..751fea99847 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -5,6 +5,7 @@ import asyncio import contextlib from contextlib import suppress import copy +import enum import logging import os from typing import Any @@ -20,6 +21,7 @@ from homeassistant import config_entries from homeassistant.components import usb from homeassistant.core import HomeAssistant +from . import repairs from .core.const import ( CONF_DATABASE, CONF_RADIO_TYPE, @@ -76,6 +78,14 @@ HARDWARE_MIGRATION_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) +class ProbeResult(enum.StrEnum): + """Radio firmware probing result.""" + + RADIO_TYPE_DETECTED = "radio_type_detected" + WRONG_FIRMWARE_INSTALLED = "wrong_firmware_installed" + PROBING_FAILED = "probing_failed" + + def _allow_overwrite_ezsp_ieee( backup: zigpy.backups.NetworkBackup, ) -> zigpy.backups.NetworkBackup: @@ -171,8 +181,10 @@ class ZhaRadioManager: return RadioType[radio_type] - async def detect_radio_type(self) -> bool: + async def detect_radio_type(self) -> ProbeResult: """Probe all radio types on the current port.""" + assert self.device_path is not None + for radio in AUTOPROBE_RADIOS: _LOGGER.debug("Attempting to probe radio type %s", radio) @@ -191,9 +203,14 @@ class ZhaRadioManager: self.radio_type = radio self.device_settings = dev_config - return True + repairs.async_delete_blocking_issues(self.hass) + return ProbeResult.RADIO_TYPE_DETECTED - return False + with suppress(repairs.AlreadyRunningEZSP): + if await repairs.warn_on_wrong_silabs_firmware(self.hass, self.device_path): + return ProbeResult.WRONG_FIRMWARE_INSTALLED + + return ProbeResult.PROBING_FAILED async def async_load_network_settings( self, *, create_backup: bool = False diff --git a/homeassistant/components/zha/repairs.py b/homeassistant/components/zha/repairs.py new file mode 100644 index 00000000000..ac523f37aa0 --- /dev/null +++ b/homeassistant/components/zha/repairs.py @@ -0,0 +1,126 @@ +"""ZHA repairs for common environmental and device problems.""" +from __future__ import annotations + +import enum +import logging + +from universal_silabs_flasher.const import ApplicationType +from universal_silabs_flasher.flasher import Flasher + +from homeassistant.components.homeassistant_sky_connect import ( + hardware as skyconnect_hardware, +) +from homeassistant.components.homeassistant_yellow import ( + RADIO_DEVICE as YELLOW_RADIO_DEVICE, + hardware as yellow_hardware, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import issue_registry as ir + +from .core.const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class AlreadyRunningEZSP(Exception): + """The device is already running EZSP firmware.""" + + +class HardwareType(enum.StrEnum): + """Detected Zigbee hardware type.""" + + SKYCONNECT = "skyconnect" + YELLOW = "yellow" + OTHER = "other" + + +DISABLE_MULTIPAN_URL = { + HardwareType.YELLOW: ( + "https://yellow.home-assistant.io/guides/disable-multiprotocol/#flash-the-silicon-labs-radio-firmware" + ), + HardwareType.SKYCONNECT: ( + "https://skyconnect.home-assistant.io/procedures/disable-multiprotocol/#step-flash-the-silicon-labs-radio-firmware" + ), + HardwareType.OTHER: None, +} + +ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED = "wrong_silabs_firmware_installed" + + +def _detect_radio_hardware(hass: HomeAssistant, device: str) -> HardwareType: + """Identify the radio hardware with the given serial port.""" + try: + yellow_hardware.async_info(hass) + except HomeAssistantError: + pass + else: + if device == YELLOW_RADIO_DEVICE: + return HardwareType.YELLOW + + try: + info = skyconnect_hardware.async_info(hass) + except HomeAssistantError: + pass + else: + for hardware_info in info: + for entry_id in hardware_info.config_entries or []: + entry = hass.config_entries.async_get_entry(entry_id) + + if entry is not None and entry.data["device"] == device: + return HardwareType.SKYCONNECT + + return HardwareType.OTHER + + +async def probe_silabs_firmware_type(device: str) -> ApplicationType | None: + """Probe the running firmware on a Silabs device.""" + flasher = Flasher(device=device) + + try: + await flasher.probe_app_type() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Failed to probe application type", exc_info=True) + + return flasher.app_type + + +async def warn_on_wrong_silabs_firmware(hass: HomeAssistant, device: str) -> bool: + """Create a repair issue if the wrong type of SiLabs firmware is detected.""" + # Only consider actual serial ports + if device.startswith("socket://"): + return False + + app_type = await probe_silabs_firmware_type(device) + + if app_type is None: + # Failed to probe, we can't tell if the wrong firmware is installed + return False + + if app_type == ApplicationType.EZSP: + # If connecting fails but we somehow probe EZSP (e.g. stuck in bootloader), + # reconnect, it should work + raise AlreadyRunningEZSP() + + hardware_type = _detect_radio_hardware(hass, device) + ir.async_create_issue( + hass, + domain=DOMAIN, + issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + is_fixable=False, + is_persistent=True, + learn_more_url=DISABLE_MULTIPAN_URL[hardware_type], + severity=ir.IssueSeverity.ERROR, + translation_key=( + ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED + + ("_nabucasa" if hardware_type != HardwareType.OTHER else "_other") + ), + translation_placeholders={"firmware_type": app_type.name}, + ) + + return True + + +def async_delete_blocking_issues(hass: HomeAssistant) -> None: + """Delete repair issues that should disappear on a successful startup.""" + ir.async_delete_issue(hass, DOMAIN, ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 3829ee68bb5..87738e821ea 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -75,7 +75,8 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "not_zha_device": "This device is not a zha device", - "usb_probe_failed": "Failed to probe the usb device" + "usb_probe_failed": "Failed to probe the usb device", + "wrong_firmware_installed": "Your device is running the wrong firmware and cannot be used with ZHA until the correct firmware is installed. [A repair has been created]({repair_url}) with more information and instructions for how to fix this." } }, "options": { @@ -168,7 +169,8 @@ "abort": { "single_instance_allowed": "[%key:component::zha::config::abort::single_instance_allowed%]", "not_zha_device": "[%key:component::zha::config::abort::not_zha_device%]", - "usb_probe_failed": "[%key:component::zha::config::abort::usb_probe_failed%]" + "usb_probe_failed": "[%key:component::zha::config::abort::usb_probe_failed%]", + "wrong_firmware_installed": "[%key:component::zha::config::abort::wrong_firmware_installed%]" } }, "config_panel": { @@ -502,5 +504,15 @@ } } } + }, + "issues": { + "wrong_silabs_firmware_installed_nabucasa": { + "title": "Zigbee radio with multiprotocol firmware detected", + "description": "Your Zigbee radio was previously used with multiprotocol (Zigbee and Thread) and still has multiprotocol firmware installed: ({firmware_type}). \n Option 1: To run your radio exclusively with ZHA, you need to install the Zigbee firmware:\n - Open the documentation by selecting the link under \"Learn More\".\n -. Follow the instructions described in the step to flash the Zigbee firmware.\n Option 2: To run your radio with multiprotocol, follow these steps: \n - Go to Settings > System > Hardware, select the device and select Configure. \n - Select the Configure IEEE 802.15.4 radio multiprotocol support option. \n - Select the checkbox and select Submit. \n - Once installed, configure the newly discovered ZHA integration." + }, + "wrong_silabs_firmware_installed_other": { + "title": "[%key:component::zha::issues::wrong_silabs_firmware_installed_nabucasa::title%]", + "description": "Your Zigbee radio was previously used with multiprotocol (Zigbee and Thread) and still has multiprotocol firmware installed: ({firmware_type}). To run your radio exclusively with ZHA, you need to install Zigbee firmware. Follow your Zigbee radio manufacturer's instructions for how to do this." + } } } diff --git a/requirements_all.txt b/requirements_all.txt index 9084b181383..bd372977b95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2611,6 +2611,9 @@ unifi-discovery==1.1.7 # homeassistant.components.unifiled unifiled==0.11 +# homeassistant.components.zha +universal-silabs-flasher==0.0.13 + # homeassistant.components.upb upb-lib==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88a25dffed0..7cc452889b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1908,6 +1908,9 @@ ultraheat-api==0.5.1 # homeassistant.components.unifiprotect unifi-discovery==1.1.7 +# homeassistant.components.zha +universal-silabs-flasher==0.0.13 + # homeassistant.components.upb upb-lib==0.5.4 diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index c0733841ed5..31fd31dfc96 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -149,6 +149,7 @@ IGNORE_VIOLATIONS = { ("http", "network"), # This would be a circular dep ("zha", "homeassistant_hardware"), + ("zha", "homeassistant_sky_connect"), ("zha", "homeassistant_yellow"), # This should become a helper method that integrations can submit data to ("websocket_api", "lovelace"), diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index f690a5152fc..4778f3216da 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,5 +1,5 @@ """Test configuration for the ZHA component.""" -from collections.abc import Callable +from collections.abc import Callable, Generator import itertools import time from unittest.mock import AsyncMock, MagicMock, patch @@ -155,10 +155,10 @@ def zigpy_app_controller(): @pytest.fixture(name="config_entry") -async def config_entry_fixture(hass): +async def config_entry_fixture(hass) -> MockConfigEntry: """Fixture representing a config entry.""" - entry = MockConfigEntry( - version=2, + return MockConfigEntry( + version=3, domain=zha_const.DOMAIN, data={ zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB0"}, @@ -178,23 +178,30 @@ async def config_entry_fixture(hass): } }, ) - entry.add_to_hass(hass) - return entry @pytest.fixture -def setup_zha(hass, config_entry, zigpy_app_controller): +def mock_zigpy_connect( + zigpy_app_controller: ControllerApplication, +) -> Generator[ControllerApplication, None, None]: + """Patch the zigpy radio connection with our mock application.""" + with patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ) as mock_app: + yield mock_app + + +@pytest.fixture +def setup_zha(hass, config_entry: MockConfigEntry, mock_zigpy_connect): """Set up ZHA component.""" zha_config = {zha_const.CONF_ENABLE_QUIRKS: False} - p1 = patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ) - async def _setup(config=None): + config_entry.add_to_hass(hass) config = config or {} - with p1: + + with mock_zigpy_connect: status = await async_setup_component( hass, zha_const.DOMAIN, {zha_const.DOMAIN: {**zha_config, **config}} ) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 8e071247872..77d8a615c72 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -26,6 +26,7 @@ from homeassistant.components.zha.core.const import ( EZSP_OVERWRITE_EUI64, RadioType, ) +from homeassistant.components.zha.radio_manager import ProbeResult from homeassistant.config_entries import ( SOURCE_SSDP, SOURCE_USB, @@ -114,7 +115,10 @@ def backup(make_backup): return make_backup() -def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): +def mock_detect_radio_type( + radio_type: RadioType = RadioType.ezsp, + ret: ProbeResult = ProbeResult.RADIO_TYPE_DETECTED, +): """Mock `detect_radio_type` that just sets the appropriate attributes.""" async def detect(self): @@ -489,8 +493,11 @@ async def test_zigate_discovery_via_usb(probe_mock, hass: HomeAssistant) -> None } -@patch(f"bellows.{PROBE_FUNCTION_PATH}", return_value=False) -async def test_discovery_via_usb_no_radio(probe_mock, hass: HomeAssistant) -> None: +@patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + AsyncMock(return_value=ProbeResult.PROBING_FAILED), +) +async def test_discovery_via_usb_no_radio(hass: HomeAssistant) -> None: """Test usb flow -- no radio detected.""" discovery_info = usb.UsbServiceInfo( device="/dev/null", @@ -759,7 +766,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: @patch( "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", - mock_detect_radio_type(ret=False), + AsyncMock(return_value=ProbeResult.PROBING_FAILED), ) @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) async def test_user_flow_not_detected(hass: HomeAssistant) -> None: @@ -851,6 +858,7 @@ async def test_detect_radio_type_success( handler = config_flow.ZhaConfigFlowHandler() handler._radio_mgr.device_path = "/dev/null" + handler.hass = hass await handler._radio_mgr.detect_radio_type() @@ -879,6 +887,8 @@ async def test_detect_radio_type_success_with_settings( handler = config_flow.ZhaConfigFlowHandler() handler._radio_mgr.device_path = "/dev/null" + handler.hass = hass + await handler._radio_mgr.detect_radio_type() assert handler._radio_mgr.radio_type == RadioType.ezsp @@ -956,22 +966,10 @@ async def test_user_port_config(probe_mock, hass: HomeAssistant) -> None: ], ) async def test_migration_ti_cc_to_znp( - old_type, new_type, hass: HomeAssistant, config_entry + old_type, new_type, hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test zigpy-cc to zigpy-znp config migration.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id=old_type + new_type, - data={ - CONF_RADIO_TYPE: old_type, - CONF_DEVICE: { - CONF_DEVICE_PATH: "/dev/ttyUSB1", - CONF_BAUDRATE: 115200, - CONF_FLOWCONTROL: None, - }, - }, - ) - + config_entry.data = {**config_entry.data, CONF_RADIO_TYPE: old_type} config_entry.version = 2 config_entry.add_to_hass(hass) @@ -1919,3 +1917,44 @@ async def test_config_flow_port_multiprotocol_port_name(hass: HomeAssistant) -> result["data_schema"].schema["path"].container[0] == "socket://core-silabs-multiprotocol:9999 - Multiprotocol add-on - Nabu Casa" ) + + +@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) +async def test_probe_wrong_firmware_installed(hass: HomeAssistant) -> None: + """Test auto-probing failing because the wrong firmware is installed.""" + + with patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + return_value=ProbeResult.WRONG_FIRMWARE_INSTALLED, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: "choose_serial_port"}, + data={ + CONF_DEVICE_PATH: ( + "/dev/ttyUSB1234 - Some serial port, s/n: 1234 - Virtual serial port" + ) + }, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "wrong_firmware_installed" + + +async def test_discovery_wrong_firmware_installed(hass: HomeAssistant) -> None: + """Test auto-probing failing because the wrong firmware is installed.""" + + with patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + return_value=ProbeResult.WRONG_FIRMWARE_INSTALLED, + ), patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: "confirm"}, + data={}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "wrong_firmware_installed" diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 0bb06ea723b..6bcb321ab14 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -15,6 +15,7 @@ from homeassistant.helpers.device_registry import async_get from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE +from tests.common import MockConfigEntry from tests.components.diagnostics import ( get_diagnostics_for_config_entry, get_diagnostics_for_device, @@ -57,7 +58,7 @@ def zigpy_device(zigpy_device_mock): async def test_diagnostics_for_config_entry( hass: HomeAssistant, hass_client: ClientSessionGenerator, - config_entry, + config_entry: MockConfigEntry, zha_device_joined, zigpy_device, ) -> None: @@ -86,12 +87,11 @@ async def test_diagnostics_for_config_entry( async def test_diagnostics_for_device( hass: HomeAssistant, hass_client: ClientSessionGenerator, - config_entry, + config_entry: MockConfigEntry, zha_device_joined, zigpy_device, ) -> None: """Test diagnostics for device.""" - zha_device: ZHADevice = await zha_device_joined(zigpy_device) dev_reg = async_get(hass) device = dev_reg.async_get_device(identifiers={("zha", str(zha_device.ieee))}) diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index c507db3e6ab..7acf9219d67 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -14,6 +14,7 @@ from homeassistant import config_entries from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.zha import radio_manager from homeassistant.components.zha.core.const import DOMAIN, RadioType +from homeassistant.components.zha.radio_manager import ProbeResult, ZhaRadioManager from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -59,10 +60,13 @@ def backup(): return backup -def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): +def mock_detect_radio_type( + radio_type: RadioType = RadioType.ezsp, + ret: ProbeResult = ProbeResult.RADIO_TYPE_DETECTED, +): """Mock `detect_radio_type` that just sets the appropriate attributes.""" - async def detect(self): + async def detect(self) -> ProbeResult: self.radio_type = radio_type self.device_settings = radio_type.controller.SCHEMA_DEVICE( {CONF_DEVICE_PATH: self.device_path} @@ -421,3 +425,58 @@ async def test_migrate_initiate_failure( await migration_helper.async_initiate_migration(migration_data) assert len(mock_load_info.mock_calls) == radio_manager.BACKUP_RETRIES + + +@pytest.fixture(name="radio_manager") +def zha_radio_manager(hass: HomeAssistant) -> ZhaRadioManager: + """Fixture for an instance of `ZhaRadioManager`.""" + radio_manager = ZhaRadioManager() + radio_manager.hass = hass + radio_manager.device_path = "/dev/ttyZigbee" + return radio_manager + + +async def test_detect_radio_type_success(radio_manager: ZhaRadioManager) -> None: + """Test radio type detection, success.""" + with patch( + "bellows.zigbee.application.ControllerApplication.probe", return_value=False + ), patch( + # Intentionally probe only the second radio type + "zigpy_znp.zigbee.application.ControllerApplication.probe", + return_value=True, + ): + assert ( + await radio_manager.detect_radio_type() == ProbeResult.RADIO_TYPE_DETECTED + ) + assert radio_manager.radio_type == RadioType.znp + + +async def test_detect_radio_type_failure_wrong_firmware( + radio_manager: ZhaRadioManager, +) -> None: + """Test radio type detection, wrong firmware.""" + with patch( + "homeassistant.components.zha.radio_manager.AUTOPROBE_RADIOS", () + ), patch( + "homeassistant.components.zha.radio_manager.repairs.warn_on_wrong_silabs_firmware", + return_value=True, + ): + assert ( + await radio_manager.detect_radio_type() + == ProbeResult.WRONG_FIRMWARE_INSTALLED + ) + assert radio_manager.radio_type is None + + +async def test_detect_radio_type_failure_no_detect( + radio_manager: ZhaRadioManager, +) -> None: + """Test radio type detection, no firmware detected.""" + with patch( + "homeassistant.components.zha.radio_manager.AUTOPROBE_RADIOS", () + ), patch( + "homeassistant.components.zha.radio_manager.repairs.warn_on_wrong_silabs_firmware", + return_value=False, + ): + assert await radio_manager.detect_radio_type() == ProbeResult.PROBING_FAILED + assert radio_manager.radio_type is None diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py new file mode 100644 index 00000000000..18705168a3f --- /dev/null +++ b/tests/components/zha/test_repairs.py @@ -0,0 +1,235 @@ +"""Test ZHA repairs.""" +from collections.abc import Callable +import logging +from unittest.mock import patch + +import pytest +from universal_silabs_flasher.const import ApplicationType +from universal_silabs_flasher.flasher import Flasher + +from homeassistant.components.homeassistant_sky_connect import ( + DOMAIN as SKYCONNECT_DOMAIN, +) +from homeassistant.components.zha.core.const import DOMAIN +from homeassistant.components.zha.repairs import ( + DISABLE_MULTIPAN_URL, + ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + HardwareType, + _detect_radio_hardware, + probe_silabs_firmware_type, + warn_on_wrong_silabs_firmware, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + +SKYCONNECT_DEVICE = "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0" + + +def set_flasher_app_type(app_type: ApplicationType) -> Callable[[Flasher], None]: + """Set the app type on the flasher.""" + + def replacement(self: Flasher) -> None: + self.app_type = app_type + + return replacement + + +def test_detect_radio_hardware(hass: HomeAssistant) -> None: + """Test logic to detect radio hardware.""" + skyconnect_config_entry = MockConfigEntry( + data={ + "device": SKYCONNECT_DEVICE, + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "description": "SkyConnect v1.0", + }, + domain=SKYCONNECT_DOMAIN, + options={}, + title="Home Assistant SkyConnect", + ) + skyconnect_config_entry.add_to_hass(hass) + + assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.SKYCONNECT + assert ( + _detect_radio_hardware(hass, SKYCONNECT_DEVICE + "_foo") == HardwareType.OTHER + ) + assert _detect_radio_hardware(hass, "/dev/ttyAMA1") == HardwareType.OTHER + + with patch( + "homeassistant.components.homeassistant_yellow.hardware.get_os_info", + return_value={"board": "yellow"}, + ): + assert _detect_radio_hardware(hass, "/dev/ttyAMA1") == HardwareType.YELLOW + assert _detect_radio_hardware(hass, "/dev/ttyAMA2") == HardwareType.OTHER + assert ( + _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.SKYCONNECT + ) + + +def test_detect_radio_hardware_failure(hass: HomeAssistant) -> None: + """Test radio hardware detection failure.""" + + with patch( + "homeassistant.components.homeassistant_yellow.hardware.async_info", + side_effect=HomeAssistantError(), + ), patch( + "homeassistant.components.homeassistant_sky_connect.hardware.async_info", + side_effect=HomeAssistantError(), + ): + assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.OTHER + + +@pytest.mark.parametrize( + ("detected_hardware", "expected_learn_more_url"), + [ + (HardwareType.SKYCONNECT, DISABLE_MULTIPAN_URL[HardwareType.SKYCONNECT]), + (HardwareType.YELLOW, DISABLE_MULTIPAN_URL[HardwareType.YELLOW]), + (HardwareType.OTHER, None), + ], +) +async def test_multipan_firmware_repair( + hass: HomeAssistant, + detected_hardware: HardwareType, + expected_learn_more_url: str, + config_entry: MockConfigEntry, + mock_zigpy_connect, +) -> None: + """Test creating a repair when multi-PAN firmware is installed and probed.""" + + config_entry.add_to_hass(hass) + + # ZHA fails to set up + with patch( + "homeassistant.components.zha.repairs.Flasher.probe_app_type", + side_effect=set_flasher_app_type(ApplicationType.CPC), + autospec=True, + ), patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + side_effect=RuntimeError(), + ), patch( + "homeassistant.components.zha.repairs._detect_radio_hardware", + return_value=detected_hardware, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_ERROR + + await hass.config_entries.async_unload(config_entry.entry_id) + + issue_registry = ir.async_get(hass) + + issue = issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + ) + + # The issue is created when we fail to probe + assert issue is not None + assert issue.translation_placeholders["firmware_type"] == "CPC" + assert issue.learn_more_url == expected_learn_more_url + + # If ZHA manages to start up normally after this, the issue will be deleted + with mock_zigpy_connect: + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + issue = issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + ) + assert issue is None + + +async def test_multipan_firmware_no_repair_on_probe_failure( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test that a repair is not created when multi-PAN firmware cannot be probed.""" + + config_entry.add_to_hass(hass) + + # ZHA fails to set up + with patch( + "homeassistant.components.zha.repairs.Flasher.probe_app_type", + side_effect=set_flasher_app_type(None), + autospec=True, + ), patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + side_effect=RuntimeError(), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_ERROR + + await hass.config_entries.async_unload(config_entry.entry_id) + + # No repair is created + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + ) + assert issue is None + + +async def test_multipan_firmware_retry_on_probe_ezsp( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_zigpy_connect, +) -> None: + """Test that ZHA is reloaded when EZSP firmware is probed.""" + + config_entry.add_to_hass(hass) + + # ZHA fails to set up + with patch( + "homeassistant.components.zha.repairs.Flasher.probe_app_type", + side_effect=set_flasher_app_type(ApplicationType.EZSP), + autospec=True, + ), patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + side_effect=RuntimeError(), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # The config entry state is `SETUP_RETRY`, not `SETUP_ERROR`! + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + await hass.config_entries.async_unload(config_entry.entry_id) + + # No repair is created + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, + ) + assert issue is None + + +async def test_no_warn_on_socket(hass: HomeAssistant) -> None: + """Test that no warning is issued when the device is a socket.""" + with patch( + "homeassistant.components.zha.repairs.probe_silabs_firmware_type", autospec=True + ) as mock_probe: + await warn_on_wrong_silabs_firmware(hass, device="socket://1.2.3.4:5678") + + mock_probe.assert_not_called() + + +async def test_probe_failure_exception_handling(caplog) -> None: + """Test that probe failures are handled gracefully.""" + with patch( + "homeassistant.components.zha.repairs.Flasher.probe_app_type", + side_effect=RuntimeError(), + ), caplog.at_level(logging.DEBUG): + await probe_silabs_firmware_type("/dev/ttyZigbee") + + assert "Failed to probe application type" in caplog.text From 057daa5fdbc8321c5a7ed737a5f33ffe5d04e5d3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 31 Aug 2023 20:35:11 +0200 Subject: [PATCH 1082/1151] Address late review for Nextcloud (#99226) --- .../components/nextcloud/__init__.py | 2 +- homeassistant/components/nextcloud/entity.py | 2 +- homeassistant/components/nextcloud/sensor.py | 196 ++++++++++-------- 3 files changed, 106 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 27c9b8b6078..9cfe4aa7f70 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) for entity in entities: old_uid_start = f"{entry.data[CONF_URL]}#nextcloud_" - new_uid_start = f"{entry.data[CONF_URL]}#" + new_uid_start = f"{entry.entry_id}#" if entity.unique_id.startswith(old_uid_start): new_uid = entity.unique_id.replace(old_uid_start, new_uid_start) _LOGGER.debug("migrate unique id '%s' to '%s'", entity.unique_id, new_uid) diff --git a/homeassistant/components/nextcloud/entity.py b/homeassistant/components/nextcloud/entity.py index 92ba65a134b..b9dab9179c1 100644 --- a/homeassistant/components/nextcloud/entity.py +++ b/homeassistant/components/nextcloud/entity.py @@ -23,7 +23,7 @@ class NextcloudEntity(CoordinatorEntity[NextcloudDataUpdateCoordinator]): ) -> None: """Initialize the Nextcloud sensor.""" super().__init__(coordinator) - self._attr_unique_id = f"{coordinator.url}#{description.key}" + self._attr_unique_id = f"{entry.entry_id}#{description.key}" self._attr_device_info = DeviceInfo( configuration_url=coordinator.url, identifiers={(DOMAIN, entry.entry_id)}, diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index 0cf30cee000..0133a9e7f76 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -1,8 +1,10 @@ """Summary data from Nextcoud.""" from __future__ import annotations -from datetime import UTC, datetime -from typing import Final, cast +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime +from typing import Final from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType +from homeassistant.util.dt import utc_from_timestamp from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator @@ -26,32 +28,42 @@ from .entity import NextcloudEntity UNIT_OF_LOAD: Final[str] = "load" -SENSORS: Final[list[SensorEntityDescription]] = [ - SensorEntityDescription( + +@dataclass +class NextcloudSensorEntityDescription(SensorEntityDescription): + """Describes Nextcloud sensor entity.""" + + value_fn: Callable[ + [str | int | float], str | int | float | datetime + ] = lambda value: value + + +SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ + NextcloudSensorEntityDescription( key="activeUsers_last1hour", translation_key="nextcloud_activeusers_last1hour", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:account-multiple", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="activeUsers_last24hours", translation_key="nextcloud_activeusers_last24hours", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:account-multiple", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="activeUsers_last5minutes", translation_key="nextcloud_activeusers_last5minutes", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:account-multiple", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_expunges", translation_key="nextcloud_cache_expunges", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_mem_size", translation_key="nextcloud_cache_mem_size", device_class=SensorDeviceClass.DATA_SIZE, @@ -60,56 +72,57 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_memory_type", translation_key="nextcloud_cache_memory_type", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_num_entries", translation_key="nextcloud_cache_num_entries", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_num_hits", translation_key="nextcloud_cache_num_hits", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_num_inserts", translation_key="nextcloud_cache_num_inserts", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_num_misses", translation_key="nextcloud_cache_num_misses", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_num_slots", translation_key="nextcloud_cache_num_slots", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_start_time", translation_key="nextcloud_cache_start_time", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, + value_fn=lambda val: utc_from_timestamp(float(val)), ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="cache_ttl", translation_key="nextcloud_cache_ttl", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="database_size", translation_key="nextcloud_database_size", device_class=SensorDeviceClass.DATA_SIZE, @@ -118,19 +131,19 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="database_type", translation_key="nextcloud_database_type", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:database", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="database_version", translation_key="nextcloud_database_version", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:database", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="interned_strings_usage_buffer_size", translation_key="nextcloud_interned_strings_usage_buffer_size", device_class=SensorDeviceClass.DATA_SIZE, @@ -140,7 +153,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="interned_strings_usage_free_memory", translation_key="nextcloud_interned_strings_usage_free_memory", device_class=SensorDeviceClass.DATA_SIZE, @@ -150,13 +163,13 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="interned_strings_usage_number_of_strings", translation_key="nextcloud_interned_strings_usage_number_of_strings", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="interned_strings_usage_used_memory", translation_key="nextcloud_interned_strings_usage_used_memory", device_class=SensorDeviceClass.DATA_SIZE, @@ -166,7 +179,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="jit_buffer_free", translation_key="nextcloud_jit_buffer_free", device_class=SensorDeviceClass.DATA_SIZE, @@ -176,7 +189,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="jit_buffer_size", translation_key="nextcloud_jit_buffer_size", device_class=SensorDeviceClass.DATA_SIZE, @@ -186,93 +199,94 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="jit_kind", translation_key="nextcloud_jit_kind", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="jit_opt_flags", translation_key="nextcloud_jit_opt_flags", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="jit_opt_level", translation_key="nextcloud_jit_opt_level", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_blacklist_miss_ratio", translation_key="nextcloud_opcache_statistics_blacklist_miss_ratio", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, native_unit_of_measurement=PERCENTAGE, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_blacklist_misses", translation_key="nextcloud_opcache_statistics_blacklist_misses", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_hash_restarts", translation_key="nextcloud_opcache_statistics_hash_restarts", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_hits", translation_key="nextcloud_opcache_statistics_hits", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_last_restart_time", translation_key="nextcloud_opcache_statistics_last_restart_time", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, + value_fn=lambda val: utc_from_timestamp(float(val)), ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_manual_restarts", translation_key="nextcloud_opcache_statistics_manual_restarts", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_max_cached_keys", translation_key="nextcloud_opcache_statistics_max_cached_keys", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_misses", translation_key="nextcloud_opcache_statistics_misses", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_num_cached_keys", translation_key="nextcloud_opcache_statistics_num_cached_keys", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_num_cached_scripts", translation_key="nextcloud_opcache_statistics_num_cached_scripts", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_oom_restarts", translation_key="nextcloud_opcache_statistics_oom_restarts", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_opcache_hit_rate", translation_key="nextcloud_opcache_statistics_opcache_hit_rate", entity_category=EntityCategory.DIAGNOSTIC, @@ -280,14 +294,15 @@ SENSORS: Final[list[SensorEntityDescription]] = [ native_unit_of_measurement=PERCENTAGE, suggested_display_precision=1, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="opcache_statistics_start_time", translation_key="nextcloud_opcache_statistics_start_time", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, + value_fn=lambda val: utc_from_timestamp(float(val)), ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_opcache_memory_usage_current_wasted_percentage", translation_key="nextcloud_server_php_opcache_memory_usage_current_wasted_percentage", entity_category=EntityCategory.DIAGNOSTIC, @@ -296,7 +311,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ native_unit_of_measurement=PERCENTAGE, suggested_display_precision=1, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_opcache_memory_usage_free_memory", translation_key="nextcloud_server_php_opcache_memory_usage_free_memory", device_class=SensorDeviceClass.DATA_SIZE, @@ -307,7 +322,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_opcache_memory_usage_used_memory", translation_key="nextcloud_server_php_opcache_memory_usage_used_memory", device_class=SensorDeviceClass.DATA_SIZE, @@ -318,7 +333,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_opcache_memory_usage_wasted_memory", translation_key="nextcloud_server_php_opcache_memory_usage_wasted_memory", device_class=SensorDeviceClass.DATA_SIZE, @@ -329,7 +344,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_max_execution_time", translation_key="nextcloud_server_php_max_execution_time", device_class=SensorDeviceClass.DURATION, @@ -337,7 +352,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ icon="mdi:language-php", native_unit_of_measurement=UnitOfTime.SECONDS, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_memory_limit", translation_key="nextcloud_server_php_memory_limit", device_class=SensorDeviceClass.DATA_SIZE, @@ -347,7 +362,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_upload_max_filesize", translation_key="nextcloud_server_php_upload_max_filesize", device_class=SensorDeviceClass.DATA_SIZE, @@ -357,62 +372,62 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_php_version", translation_key="nextcloud_server_php_version", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:language-php", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="server_webserver", translation_key="nextcloud_server_webserver", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_fed_shares_sent", translation_key="nextcloud_shares_num_fed_shares_sent", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_fed_shares_received", translation_key="nextcloud_shares_num_fed_shares_received", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares", translation_key="nextcloud_shares_num_shares", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares_groups", translation_key="nextcloud_shares_num_shares_groups", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares_link", translation_key="nextcloud_shares_num_shares_link", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares_link_no_password", translation_key="nextcloud_shares_num_shares_link_no_password", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares_mail", translation_key="nextcloud_shares_num_shares_mail", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="shares_num_shares_room", translation_key="nextcloud_shares_num_shares_room", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( - key="server_num_shares_user", + NextcloudSensorEntityDescription( + key="shares_num_shares_user", translation_key="nextcloud_shares_num_shares_user", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="sma_avail_mem", translation_key="nextcloud_sma_avail_mem", device_class=SensorDeviceClass.DATA_SIZE, @@ -422,13 +437,13 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="sma_num_seg", translation_key="nextcloud_sma_num_seg", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="sma_seg_size", translation_key="nextcloud_sma_seg_size", device_class=SensorDeviceClass.DATA_SIZE, @@ -438,64 +453,64 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_files", translation_key="nextcloud_storage_num_files", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_storages", translation_key="nextcloud_storage_num_storages", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_storages_home", translation_key="nextcloud_storage_num_storages_home", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_storages_local", translation_key="nextcloud_storage_num_storages_local", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_storages_other", translation_key="nextcloud_storage_num_storages_other", entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="storage_num_users", translation_key="nextcloud_storage_num_users", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_apps_num_installed", translation_key="nextcloud_system_apps_num_installed", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_apps_num_updates_available", translation_key="nextcloud_system_apps_num_updates_available", icon="mdi:update", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_cpuload_1", translation_key="nextcloud_system_cpuload_1", native_unit_of_measurement=UNIT_OF_LOAD, icon="mdi:chip", suggested_display_precision=2, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_cpuload_5", translation_key="nextcloud_system_cpuload_5", native_unit_of_measurement=UNIT_OF_LOAD, icon="mdi:chip", suggested_display_precision=2, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_cpuload_15", translation_key="nextcloud_system_cpuload_15", native_unit_of_measurement=UNIT_OF_LOAD, icon="mdi:chip", suggested_display_precision=2, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_freespace", translation_key="nextcloud_system_freespace", device_class=SensorDeviceClass.DATA_SIZE, @@ -504,7 +519,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_mem_free", translation_key="nextcloud_system_mem_free", device_class=SensorDeviceClass.DATA_SIZE, @@ -513,7 +528,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_mem_total", translation_key="nextcloud_system_mem_total", device_class=SensorDeviceClass.DATA_SIZE, @@ -522,25 +537,25 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_memcache.distributed", translation_key="nextcloud_system_memcache_distributed", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_memcache.local", translation_key="nextcloud_system_memcache_local", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_memcache.locking", translation_key="nextcloud_system_memcache_locking", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_swap_total", translation_key="nextcloud_system_swap_total", device_class=SensorDeviceClass.DATA_SIZE, @@ -549,7 +564,7 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_swap_free", translation_key="nextcloud_system_swap_free", device_class=SensorDeviceClass.DATA_SIZE, @@ -558,11 +573,11 @@ SENSORS: Final[list[SensorEntityDescription]] = [ suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_theme", translation_key="nextcloud_system_theme", ), - SensorEntityDescription( + NextcloudSensorEntityDescription( key="system_version", translation_key="nextcloud_system_version", ), @@ -586,13 +601,10 @@ async def async_setup_entry( class NextcloudSensor(NextcloudEntity, SensorEntity): """Represents a Nextcloud sensor.""" + entity_description: NextcloudSensorEntityDescription + @property - def native_value(self) -> StateType | datetime: + def native_value(self) -> str | int | float | datetime: """Return the state for this sensor.""" val = self.coordinator.data.get(self.entity_description.key) - if ( - getattr(self.entity_description, "device_class", None) - == SensorDeviceClass.TIMESTAMP - ): - return datetime.fromtimestamp(cast(int, val), tz=UTC) - return val + return self.entity_description.value_fn(val) # type: ignore[arg-type] From ebf42ad342309439500e3863a1993b6ba2e7fe8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Aug 2023 13:47:01 -0400 Subject: [PATCH 1083/1151] Significantly reduce overhead to filter event triggers (#99376) * fast * cleanups * cleanups * cleanups * comment * comment * add more cover * comment * pull more examples from forums to validate cover --- .../homeassistant/triggers/event.py | 66 +++++++++---- .../homeassistant/triggers/test_event.py | 93 ++++++++++++++++++- 2 files changed, 138 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index d0e74d5b04e..a4266a70add 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -1,6 +1,7 @@ """Offer event listening automation rules.""" from __future__ import annotations +from collections.abc import ItemsView from typing import Any import voluptuous as vol @@ -47,9 +48,8 @@ async def async_attach_trigger( event_types = template.render_complex( config[CONF_EVENT_TYPE], variables, limited=True ) - removes = [] - - event_data_schema = None + event_data_schema: vol.Schema | None = None + event_data_items: ItemsView | None = None if CONF_EVENT_DATA in config: # Render the schema input template.attach(hass, config[CONF_EVENT_DATA]) @@ -57,13 +57,21 @@ async def async_attach_trigger( event_data.update( template.render_complex(config[CONF_EVENT_DATA], variables, limited=True) ) - # Build the schema - event_data_schema = vol.Schema( - {vol.Required(key): value for key, value in event_data.items()}, - extra=vol.ALLOW_EXTRA, - ) + # Build the schema or a an items view if the schema is simple + # and does not contain sub-dicts. We explicitly do not check for + # list like the context data below since lists are a special case + # only for context data. (see test test_event_data_with_list) + if any(isinstance(value, dict) for value in event_data.values()): + event_data_schema = vol.Schema( + {vol.Required(key): value for key, value in event_data.items()}, + extra=vol.ALLOW_EXTRA, + ) + else: + # Use a simple items comparison if possible + event_data_items = event_data.items() - event_context_schema = None + event_context_schema: vol.Schema | None = None + event_context_items: ItemsView | None = None if CONF_EVENT_CONTEXT in config: # Render the schema input template.attach(hass, config[CONF_EVENT_CONTEXT]) @@ -71,14 +79,23 @@ async def async_attach_trigger( event_context.update( template.render_complex(config[CONF_EVENT_CONTEXT], variables, limited=True) ) - # Build the schema - event_context_schema = vol.Schema( - { - vol.Required(key): _schema_value(value) - for key, value in event_context.items() - }, - extra=vol.ALLOW_EXTRA, - ) + # Build the schema or a an items view if the schema is simple + # and does not contain lists. Lists are a special case to support + # matching events by user_id. (see test test_if_fires_on_multiple_user_ids) + # This can likely be optimized further in the future to handle the + # multiple user_id case without requiring expensive schema + # validation. + if any(isinstance(value, list) for value in event_context.values()): + event_context_schema = vol.Schema( + { + vol.Required(key): _schema_value(value) + for key, value in event_context.items() + }, + extra=vol.ALLOW_EXTRA, + ) + else: + # Use a simple items comparison if possible + event_context_items = event_context.items() job = HassJob(action, f"event trigger {trigger_info}") @@ -88,9 +105,20 @@ async def async_attach_trigger( try: # Check that the event data and context match the configured # schema if one was provided - if event_data_schema: + if event_data_items: + # Fast path for simple items comparison + if not (event.data.items() >= event_data_items): + return False + elif event_data_schema: + # Slow path for schema validation event_data_schema(event.data) - if event_context_schema: + + if event_context_items: + # Fast path for simple items comparison + if not (event.context.as_dict().items() >= event_context_items): + return False + elif event_context_schema: + # Slow path for schema validation event_context_schema(dict(event.context.as_dict())) except vol.Invalid: # If event doesn't match, skip event diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index 6fc7e5055ed..d996cd74da7 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -288,7 +288,11 @@ async def test_if_fires_on_event_with_empty_data_and_context_config( async def test_if_fires_on_event_with_nested_data(hass: HomeAssistant, calls) -> None: - """Test the firing of events with nested data.""" + """Test the firing of events with nested data. + + This test exercises the slow path of using vol.Schema to validate + matching event data. + """ assert await async_setup_component( hass, automation.DOMAIN, @@ -311,6 +315,87 @@ async def test_if_fires_on_event_with_nested_data(hass: HomeAssistant, calls) -> assert len(calls) == 1 +async def test_if_fires_on_event_with_empty_data(hass: HomeAssistant, calls) -> None: + """Test the firing of events with empty data. + + This test exercises the fast path to validate matching event data. + """ + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "event", + "event_type": "test_event", + "event_data": {}, + }, + "action": {"service": "test.automation"}, + } + }, + ) + hass.bus.async_fire("test_event", {"any_attr": {}}) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_if_fires_on_sample_zha_event(hass: HomeAssistant, calls) -> None: + """Test the firing of events with a sample zha event. + + This test exercises the fast path to validate matching event data. + """ + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "event", + "event_type": "zha_event", + "event_data": { + "device_ieee": "00:15:8d:00:02:93:04:11", + "command": "attribute_updated", + "args": { + "attribute_id": 0, + "attribute_name": "on_off", + "value": True, + }, + }, + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.bus.async_fire( + "zha_event", + { + "device_ieee": "00:15:8d:00:02:93:04:11", + "unique_id": "00:15:8d:00:02:93:04:11:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "command": "attribute_updated", + "args": {"attribute_id": 0, "attribute_name": "on_off", "value": True}, + }, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.bus.async_fire( + "zha_event", + { + "device_ieee": "00:15:8d:00:02:93:04:11", + "unique_id": "00:15:8d:00:02:93:04:11:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "command": "attribute_updated", + "args": {"attribute_id": 0, "attribute_name": "on_off", "value": False}, + }, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_not_fires_if_event_data_not_matches( hass: HomeAssistant, calls ) -> None: @@ -362,7 +447,11 @@ async def test_if_not_fires_if_event_context_not_matches( async def test_if_fires_on_multiple_user_ids( hass: HomeAssistant, calls, context_with_user ) -> None: - """Test the firing of event when the trigger has multiple user ids.""" + """Test the firing of event when the trigger has multiple user ids. + + This test exercises the slow path of using vol.Schema to validate + matching event context. + """ assert await async_setup_component( hass, automation.DOMAIN, From a95691f30645850d4df265532ce2da4e9db74dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 31 Aug 2023 21:59:01 +0200 Subject: [PATCH 1084/1151] Update AEMET-OpenData to v0.4.4 (#99418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/aemet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index c43e7a0b402..1c65572a64e 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aemet", "iot_class": "cloud_polling", "loggers": ["aemet_opendata"], - "requirements": ["AEMET-OpenData==0.4.3"] + "requirements": ["AEMET-OpenData==0.4.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index bd372977b95..8b22f051591 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.3 +AEMET-OpenData==0.4.4 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7cc452889b7..03a52733d51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.4.3 +AEMET-OpenData==0.4.4 # homeassistant.components.aladdin_connect AIOAladdinConnect==0.1.57 From 469a72a5f90e703a0b3740dcdab5b14f56386573 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 1 Sep 2023 15:58:01 +0200 Subject: [PATCH 1085/1151] Use common key for away mode state translations (#99425) --- homeassistant/components/water_heater/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/water_heater/strings.json b/homeassistant/components/water_heater/strings.json index 6991d371bd3..1b3af02610c 100644 --- a/homeassistant/components/water_heater/strings.json +++ b/homeassistant/components/water_heater/strings.json @@ -21,8 +21,8 @@ "away_mode": { "name": "Away mode", "state": { - "off": "Off", - "on": "On" + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" } } } From 987a959b1943c879eb3d2094d56f95b82fdf1c81 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Fri, 1 Sep 2023 02:24:13 -0400 Subject: [PATCH 1086/1151] Update asynsleepiq library to 1.3.7 (#99431) --- homeassistant/components/sleepiq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 3d757e2328d..874ae90ec4a 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/sleepiq", "iot_class": "cloud_polling", "loggers": ["asyncsleepiq"], - "requirements": ["asyncsleepiq==1.3.5"] + "requirements": ["asyncsleepiq==1.3.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8b22f051591..3fc82ea3316 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,7 +464,7 @@ asyncinotify==4.0.2 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.3.5 +asyncsleepiq==1.3.7 # homeassistant.components.aten_pe # atenpdu==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03a52733d51..191a845eb94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -409,7 +409,7 @@ async-upnp-client==0.35.0 async_interrupt==1.1.1 # homeassistant.components.sleepiq -asyncsleepiq==1.3.5 +asyncsleepiq==1.3.7 # homeassistant.components.aurora auroranoaa==0.0.3 From eb8d375e359186c0841b1d88a8c91d66ea69c37d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Sep 2023 17:14:42 +0200 Subject: [PATCH 1087/1151] Fix template helper strings (#99456) --- homeassistant/components/template/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 482682d0ce1..7e5e56a26d6 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -5,7 +5,7 @@ "data": { "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", "name": "[%key:common::config_flow::data::name%]", - "state_template": "[%key:component::template::config::step::sensor::data::state_template%]" + "state": "[%key:component::template::config::step::sensor::data::state%]" }, "title": "Template binary sensor" }, @@ -14,7 +14,7 @@ "device_class": "Device class", "name": "[%key:common::config_flow::data::name%]", "state_class": "[%key:component::sensor::entity_component::_::state_attributes::state_class::name%]", - "state_template": "State template", + "state": "State template", "unit_of_measurement": "Unit of measurement" }, "title": "Template sensor" @@ -34,7 +34,7 @@ "binary_sensor": { "data": { "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", - "state_template": "[%key:component::template::config::step::sensor::data::state_template%]" + "state": "[%key:component::template::config::step::sensor::data::state%]" }, "title": "[%key:component::template::config::step::binary_sensor::title%]" }, @@ -42,7 +42,7 @@ "data": { "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", "state_class": "[%key:component::template::config::step::sensor::data::state_class%]", - "state_template": "[%key:component::template::config::step::sensor::data::state_template%]", + "state": "[%key:component::template::config::step::sensor::data::state%]", "unit_of_measurement": "[%key:component::template::config::step::sensor::data::unit_of_measurement%]" }, "title": "[%key:component::template::config::step::sensor::title%]" From 528e8c0fe75bd9a47c6f9f40ffe55fb9cac1abbb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 1 Sep 2023 17:28:52 +0200 Subject: [PATCH 1088/1151] Update frontend to 20230901.0 (#99464) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index a31faaf362e..3b46f568d3e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230831.0"] + "requirements": ["home-assistant-frontend==20230901.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3dccb80d11e..19169de83f6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230831.0 +home-assistant-frontend==20230901.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3fc82ea3316..ae8066dd6d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230831.0 +home-assistant-frontend==20230901.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 191a845eb94..dde4bf062f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230831.0 +home-assistant-frontend==20230901.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From 1f0b3c4e339d09d765f11fb96217d1444ddaf94d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 1 Sep 2023 18:01:07 +0200 Subject: [PATCH 1089/1151] Bump version to 2023.9.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0b1cc1c5ea0..19370552139 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 210c2973d4b..0248bd43af1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b1" +version = "2023.9.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f573c1e27c3a4252b6da18f3254c302fc56df5a2 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Sat, 2 Sep 2023 01:20:36 -0700 Subject: [PATCH 1090/1151] Handle timestamp sensors in Prometheus integration (#98001) --- homeassistant/components/prometheus/__init__.py | 16 +++++++++++++++- tests/components/prometheus/test_init.py | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index e5d7f6cb060..adc5225b286 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -19,6 +19,7 @@ from homeassistant.components.climate import ( from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_DEVICE_CLASS, @@ -44,6 +45,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.typing import ConfigType +from homeassistant.util.dt import as_timestamp from homeassistant.util.unit_conversion import TemperatureConverter _LOGGER = logging.getLogger(__name__) @@ -147,6 +149,7 @@ class PrometheusMetrics: self._sensor_metric_handlers = [ self._sensor_override_component_metric, self._sensor_override_metric, + self._sensor_timestamp_metric, self._sensor_attribute_metric, self._sensor_default_metric, self._sensor_fallback_metric, @@ -292,7 +295,10 @@ class PrometheusMetrics: def state_as_number(state): """Return a state casted to a float.""" try: - value = state_helper.state_as_number(state) + if state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP: + value = as_timestamp(state.state) + else: + value = state_helper.state_as_number(state) except ValueError: _LOGGER.debug("Could not convert %s to float", state) value = 0 @@ -576,6 +582,14 @@ class PrometheusMetrics: return f"sensor_{metric}_{unit}" return None + @staticmethod + def _sensor_timestamp_metric(state, unit): + """Get metric for timestamp sensors, which have no unit of measurement attribute.""" + metric = state.attributes.get(ATTR_DEVICE_CLASS) + if metric == SensorDeviceClass.TIMESTAMP: + return f"sensor_{metric}_seconds" + return None + def _sensor_override_metric(self, state, unit): """Get metric from override in configuration.""" if self._override_metric: diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 446666c4a6a..82a205eb259 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -232,6 +232,12 @@ async def test_sensor_device_class(client, sensor_entities) -> None: 'friendly_name="Radio Energy"} 14.0' in body ) + assert ( + 'sensor_timestamp_seconds{domain="sensor",' + 'entity="sensor.timestamp",' + 'friendly_name="Timestamp"} 1.691445808136036e+09' in body + ) + @pytest.mark.parametrize("namespace", [""]) async def test_input_number(client, input_number_entities) -> None: @@ -1049,6 +1055,16 @@ async def sensor_fixture( set_state_with_entry(hass, sensor_11, 50) data["sensor_11"] = sensor_11 + sensor_12 = entity_registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_12", + original_device_class=SensorDeviceClass.TIMESTAMP, + suggested_object_id="Timestamp", + original_name="Timestamp", + ) + set_state_with_entry(hass, sensor_12, "2023-08-07T15:03:28.136036-0700") + data["sensor_12"] = sensor_12 await hass.async_block_till_done() return data From f0878addcaa43ec3ef8c7dbe990db56744ea0f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 2 Sep 2023 15:08:49 +0200 Subject: [PATCH 1091/1151] Update Tibber library to 0.28.2 (#99115) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index c668430914f..1d8120a4321 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["tibber"], "quality_scale": "silver", - "requirements": ["pyTibber==0.28.0"] + "requirements": ["pyTibber==0.28.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index ae8066dd6d9..6909ae3b924 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1547,7 +1547,7 @@ pyRFXtrx==0.30.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.28.0 +pyTibber==0.28.2 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dde4bf062f3..f4570ea0a32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1159,7 +1159,7 @@ pyElectra==1.2.0 pyRFXtrx==0.30.1 # homeassistant.components.tibber -pyTibber==0.28.0 +pyTibber==0.28.2 # homeassistant.components.dlink pyW215==0.7.0 From 270be19e1a44ca960f530643187ccd78e8523ab1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 2 Sep 2023 21:00:33 +0200 Subject: [PATCH 1092/1151] Check new IP of Reolink camera from DHCP (#99381) Co-authored-by: J. Nick Koston --- .../components/reolink/config_flow.py | 44 +++++++++- homeassistant/components/reolink/util.py | 23 +++++ tests/components/reolink/test_config_flow.py | 85 ++++++++++++++++--- 3 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/reolink/util.py diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index d24fd8d1f14..d924f395c50 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -12,13 +12,14 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import format_mac from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN from .exceptions import ReolinkException, ReolinkWebhookException, UserNotAdmin from .host import ReolinkHost +from .util import has_connection_problem _LOGGER = logging.getLogger(__name__) @@ -96,7 +97,46 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" mac_address = format_mac(discovery_info.macaddress) - await self.async_set_unique_id(mac_address) + existing_entry = await self.async_set_unique_id(mac_address) + if ( + existing_entry + and CONF_PASSWORD in existing_entry.data + and existing_entry.data[CONF_HOST] != discovery_info.ip + ): + if has_connection_problem(self.hass, existing_entry): + _LOGGER.debug( + "Reolink DHCP reported new IP '%s', " + "but connection to camera seems to be okay, so sticking to IP '%s'", + discovery_info.ip, + existing_entry.data[CONF_HOST], + ) + raise AbortFlow("already_configured") + + # check if the camera is reachable at the new IP + host = ReolinkHost(self.hass, existing_entry.data, existing_entry.options) + try: + await host.api.get_state("GetLocalLink") + await host.api.logout() + except ReolinkError as err: + _LOGGER.debug( + "Reolink DHCP reported new IP '%s', " + "but got error '%s' trying to connect, so sticking to IP '%s'", + discovery_info.ip, + str(err), + existing_entry.data[CONF_HOST], + ) + raise AbortFlow("already_configured") from err + if format_mac(host.api.mac_address) != mac_address: + _LOGGER.debug( + "Reolink mac address '%s' at new IP '%s' from DHCP, " + "does not match mac '%s' of config entry, so sticking to IP '%s'", + format_mac(host.api.mac_address), + discovery_info.ip, + mac_address, + existing_entry.data[CONF_HOST], + ) + raise AbortFlow("already_configured") + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) self.context["title_placeholders"] = { diff --git a/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py new file mode 100644 index 00000000000..2ab625647a7 --- /dev/null +++ b/homeassistant/components/reolink/util.py @@ -0,0 +1,23 @@ +"""Utility functions for the Reolink component.""" +from __future__ import annotations + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant + +from . import ReolinkData +from .const import DOMAIN + + +def has_connection_problem( + hass: HomeAssistant, config_entry: config_entries.ConfigEntry +) -> bool: + """Check if a existing entry has a connection problem.""" + reolink_data: ReolinkData | None = hass.data.get(DOMAIN, {}).get( + config_entry.entry_id + ) + connection_problem = ( + reolink_data is not None + and config_entry.state == config_entries.ConfigEntryState.LOADED + and reolink_data.device_coordinator.last_update_success + ) + return connection_problem diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 048b48d9576..1a4bf999cce 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -1,18 +1,22 @@ """Test the Reolink config flow.""" +from datetime import timedelta import json -from unittest.mock import MagicMock +from typing import Any +from unittest.mock import AsyncMock, MagicMock import pytest from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp -from homeassistant.components.reolink import const +from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL, const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL from homeassistant.components.reolink.exceptions import ReolinkWebhookException +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac +from homeassistant.util.dt import utcnow from .conftest import ( TEST_HOST, @@ -27,12 +31,14 @@ from .conftest import ( TEST_USERNAME2, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed -pytestmark = pytest.mark.usefixtures("mock_setup_entry", "reolink_connect") +pytestmark = pytest.mark.usefixtures("reolink_connect") -async def test_config_flow_manual_success(hass: HomeAssistant) -> None: +async def test_config_flow_manual_success( + hass: HomeAssistant, mock_setup_entry: MagicMock +) -> None: """Successful flow manually initialized by the user.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -66,7 +72,7 @@ async def test_config_flow_manual_success(hass: HomeAssistant) -> None: async def test_config_flow_errors( - hass: HomeAssistant, reolink_connect: MagicMock + hass: HomeAssistant, reolink_connect: MagicMock, mock_setup_entry: MagicMock ) -> None: """Successful flow manually initialized by the user after some errors.""" result = await hass.config_entries.flow.async_init( @@ -192,7 +198,7 @@ async def test_config_flow_errors( } -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: """Test specifying non default settings using options flow.""" config_entry = MockConfigEntry( domain=const.DOMAIN, @@ -230,7 +236,9 @@ async def test_options_flow(hass: HomeAssistant) -> None: } -async def test_change_connection_settings(hass: HomeAssistant) -> None: +async def test_change_connection_settings( + hass: HomeAssistant, mock_setup_entry: MagicMock +) -> None: """Test changing connection settings by issuing a second user config flow.""" config_entry = MockConfigEntry( domain=const.DOMAIN, @@ -273,7 +281,7 @@ async def test_change_connection_settings(hass: HomeAssistant) -> None: assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 -async def test_reauth(hass: HomeAssistant) -> None: +async def test_reauth(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: """Test a reauth flow.""" config_entry = MockConfigEntry( domain=const.DOMAIN, @@ -333,7 +341,7 @@ async def test_reauth(hass: HomeAssistant) -> None: assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 -async def test_dhcp_flow(hass: HomeAssistant) -> None: +async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: """Successful flow from DHCP discovery.""" dhcp_data = dhcp.DhcpServiceInfo( ip=TEST_HOST, @@ -371,8 +379,44 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: } -async def test_dhcp_abort_flow(hass: HomeAssistant) -> None: - """Test dhcp discovery aborts if already configured.""" +@pytest.mark.parametrize( + ("last_update_success", "attr", "value", "expected"), + [ + ( + False, + None, + None, + TEST_HOST2, + ), + ( + True, + None, + None, + TEST_HOST, + ), + ( + False, + "get_state", + AsyncMock(side_effect=ReolinkError("Test error")), + TEST_HOST, + ), + ( + False, + "mac_address", + "aa:aa:aa:aa:aa:aa", + TEST_HOST, + ), + ], +) +async def test_dhcp_ip_update( + hass: HomeAssistant, + reolink_connect: MagicMock, + last_update_success: bool, + attr: str, + value: Any, + expected: str, +) -> None: + """Test dhcp discovery aborts if already configured where the IP is updated if appropriate.""" config_entry = MockConfigEntry( domain=const.DOMAIN, unique_id=format_mac(TEST_MAC), @@ -392,16 +436,31 @@ async def test_dhcp_abort_flow(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + + if not last_update_success: + # ensure the last_update_succes is False for the device_coordinator. + reolink_connect.get_states = AsyncMock(side_effect=ReolinkError("Test error")) + async_fire_time_changed( + hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(minutes=1) + ) + await hass.async_block_till_done() dhcp_data = dhcp.DhcpServiceInfo( - ip=TEST_HOST, + ip=TEST_HOST2, hostname="Reolink", macaddress=TEST_MAC, ) + if attr is not None: + setattr(reolink_connect, attr, value) + result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) assert result["type"] is data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" + + await hass.async_block_till_done() + assert config_entry.data[CONF_HOST] == expected From 8dcc96c083a35079c03eae6b018933f52e6cd174 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:06:37 -0400 Subject: [PATCH 1093/1151] Fix device name in zwave_js repair flow (#99414) --- homeassistant/components/zwave_js/__init__.py | 9 +++------ homeassistant/components/zwave_js/repairs.py | 17 +++++++++++------ tests/components/zwave_js/test_repairs.py | 1 + 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 2d158f47e44..b56298e36ba 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -600,19 +600,16 @@ class NodeEvents: # device config has changed, and if so, issue a repair registry entry for a # possible reinterview if not node.is_controller_node and await node.async_has_device_config_changed(): + device_name = device.name_by_user or device.name or "Unnamed device" async_create_issue( self.hass, DOMAIN, f"device_config_file_changed.{device.id}", - data={"device_id": device.id}, + data={"device_id": device.id, "device_name": device_name}, is_fixable=True, is_persistent=False, translation_key="device_config_file_changed", - translation_placeholders={ - "device_name": device.name_by_user - or device.name - or "Unnamed device" - }, + translation_placeholders={"device_name": device_name}, severity=IssueSeverity.WARNING, ) diff --git a/homeassistant/components/zwave_js/repairs.py b/homeassistant/components/zwave_js/repairs.py index 58781941b09..89f51dddb88 100644 --- a/homeassistant/components/zwave_js/repairs.py +++ b/homeassistant/components/zwave_js/repairs.py @@ -1,8 +1,6 @@ """Repairs for Z-Wave JS.""" from __future__ import annotations -from typing import cast - import voluptuous as vol from zwave_js_server.model.node import Node @@ -16,9 +14,10 @@ from .helpers import async_get_node_from_device_id class DeviceConfigFileChangedFlow(RepairsFlow): """Handler for an issue fixing flow.""" - def __init__(self, node: Node) -> None: + def __init__(self, node: Node, device_name: str) -> None: """Initialize.""" self.node = node + self.device_name = device_name async def async_step_init( self, user_input: dict[str, str] | None = None @@ -34,17 +33,23 @@ class DeviceConfigFileChangedFlow(RepairsFlow): self.hass.async_create_task(self.node.async_refresh_info()) return self.async_create_entry(title="", data={}) - return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders={"device_name": self.device_name}, + ) async def async_create_fix_flow( hass: HomeAssistant, issue_id: str, - data: dict[str, str | int | float | None] | None, + data: dict[str, str] | None, ) -> RepairsFlow: """Create flow.""" + if issue_id.split(".")[0] == "device_config_file_changed": + assert data return DeviceConfigFileChangedFlow( - async_get_node_from_device_id(hass, cast(dict, data)["device_id"]) + async_get_node_from_device_id(hass, data["device_id"]), data["device_name"] ) return ConfirmRepairFlow() diff --git a/tests/components/zwave_js/test_repairs.py b/tests/components/zwave_js/test_repairs.py index b1702900d7c..07371a299ef 100644 --- a/tests/components/zwave_js/test_repairs.py +++ b/tests/components/zwave_js/test_repairs.py @@ -77,6 +77,7 @@ async def test_device_config_file_changed( flow_id = data["flow_id"] assert data["step_id"] == "confirm" + assert data["description_placeholders"] == {"device_name": device.name} # Apply fix url = RepairsFlowResourceView.url.format(flow_id=flow_id) From 9fec0be17328d7243e6b2ba12eb57e21e67f8a46 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 2 Sep 2023 16:51:06 +0200 Subject: [PATCH 1094/1151] Log unexpected exceptions causing recorder shutdown (#99445) --- homeassistant/components/recorder/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index ffdc3807039..bbaff24ff77 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -692,6 +692,10 @@ class Recorder(threading.Thread): """Run the recorder thread.""" try: self._run() + except Exception: # pylint: disable=broad-exception-caught + _LOGGER.exception( + "Recorder._run threw unexpected exception, recorder shutting down" + ) finally: # Ensure shutdown happens cleanly if # anything goes wrong in the run loop From caaba5d422fba47a87d56c1e637845ecbfb98227 Mon Sep 17 00:00:00 2001 From: jimmyd-be <34766203+jimmyd-be@users.noreply.github.com> Date: Sat, 2 Sep 2023 10:55:12 +0200 Subject: [PATCH 1095/1151] Fix translation bug Renson sensors (#99461) * Fix translation bug * Revert "Fix translation bug" This reverts commit 84b5e90dac1e75a4c9f6d890865ac42044858682. * Fixed translation of Renson sensor --- homeassistant/components/renson/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/renson/sensor.py b/homeassistant/components/renson/sensor.py index c8a355a0f7c..661ab82f373 100644 --- a/homeassistant/components/renson/sensor.py +++ b/homeassistant/components/renson/sensor.py @@ -266,6 +266,8 @@ SENSORS: tuple[RensonSensorEntityDescription, ...] = ( class RensonSensor(RensonEntity, SensorEntity): """Get a sensor data from the Renson API and store it in the state of the class.""" + _attr_has_entity_name = True + def __init__( self, description: RensonSensorEntityDescription, From cb1267477b7ab8e392ced0cf61347a1025165ceb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 2 Sep 2023 09:47:59 +0200 Subject: [PATCH 1096/1151] Fix default language in Workday (#99463) Workday fix default language --- homeassistant/components/workday/binary_sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 6b6dfbffa5d..ad18c8863d6 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -129,7 +129,13 @@ async def async_setup_entry( workdays: list[str] = entry.options[CONF_WORKDAYS] year: int = (dt_util.now() + timedelta(days=days_offset)).year - obj_holidays: HolidayBase = country_holidays(country, subdiv=province, years=year) + cls: HolidayBase = country_holidays(country, subdiv=province, years=year) + obj_holidays: HolidayBase = country_holidays( + country, + subdiv=province, + years=year, + language=cls.default_language, + ) # Add custom holidays try: From e726b49adb3643ea399044fdae8449e26eab7e87 Mon Sep 17 00:00:00 2001 From: Andrew Onyshchuk Date: Fri, 1 Sep 2023 13:28:53 -0700 Subject: [PATCH 1097/1151] Update aiotractive to 0.5.6 (#99477) --- homeassistant/components/tractive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tractive/manifest.json b/homeassistant/components/tractive/manifest.json index 9e448d1fd26..75ddf065bd7 100644 --- a/homeassistant/components/tractive/manifest.json +++ b/homeassistant/components/tractive/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_push", "loggers": ["aiotractive"], - "requirements": ["aiotractive==0.5.5"] + "requirements": ["aiotractive==0.5.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6909ae3b924..942fa631902 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -360,7 +360,7 @@ aioswitcher==3.3.0 aiosyncthing==0.5.1 # homeassistant.components.tractive -aiotractive==0.5.5 +aiotractive==0.5.6 # homeassistant.components.unifi aiounifi==58 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4570ea0a32..2802d0a37f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ aioswitcher==3.3.0 aiosyncthing==0.5.1 # homeassistant.components.tractive -aiotractive==0.5.5 +aiotractive==0.5.6 # homeassistant.components.unifi aiounifi==58 From 46343bc261f6849876f72a2f9d704fd0587af019 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 2 Sep 2023 14:04:13 -0500 Subject: [PATCH 1098/1151] Bump zeroconf to 0.91.1 (#99490) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 79b7e514f51..26577bd0bbe 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.88.0"] + "requirements": ["zeroconf==0.91.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 19169de83f6..cb114b1504d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtcvad==2.0.10 yarl==1.9.2 -zeroconf==0.88.0 +zeroconf==0.91.1 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 942fa631902..e53d9d8fd0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2766,7 +2766,7 @@ zamg==0.2.4 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.88.0 +zeroconf==0.91.1 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2802d0a37f5..ec3b4a62052 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2036,7 +2036,7 @@ youtubeaio==1.1.5 zamg==0.2.4 # homeassistant.components.zeroconf -zeroconf==0.88.0 +zeroconf==0.91.1 # homeassistant.components.zeversolar zeversolar==0.3.1 From 7b50316b3e4e61e4fa5b73c66888256b059e2625 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 2 Sep 2023 21:09:52 +0200 Subject: [PATCH 1099/1151] Bump version to 2023.9.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 19370552139..12a12aea631 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 0248bd43af1..dc9d314fe4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b2" +version = "2023.9.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 269bcac982db1d58da21b1c8fc0dca4761ed3351 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 2 Sep 2023 16:19:45 -0700 Subject: [PATCH 1100/1151] Extend template entities with a script section (#96175) * Extend template entities with a script section This allows making a trigger entity that triggers a few times a day, and allows collecting data from a service resopnse which can be fed into a template entity. The current alternatives are to publish and subscribe to events or to store data in input entities. * Make variables set in actions accessible to templates * Format code --------- Co-authored-by: Erik --- homeassistant/components/script/__init__.py | 3 +- homeassistant/components/template/__init__.py | 19 ++++++-- homeassistant/components/template/config.py | 3 +- homeassistant/components/template/const.py | 1 + .../components/websocket_api/commands.py | 4 +- homeassistant/helpers/script.py | 15 +++++-- tests/components/template/test_sensor.py | 44 +++++++++++++++++++ 7 files changed, 79 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 8530aa3b04c..13b25a00053 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -563,7 +563,8 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): ) coro = self._async_run(variables, context) if wait: - return await coro + script_result = await coro + return script_result.service_response if script_result else None # Caller does not want to wait for called script to finish so let script run in # separate Task. Make a new empty script stack; scripts are allowed to diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index e9ced060491..c4ba7081f5a 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -20,11 +20,12 @@ from homeassistant.helpers import ( update_coordinator, ) from homeassistant.helpers.reload import async_reload_integration_platforms +from homeassistant.helpers.script import Script from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration -from .const import CONF_TRIGGER, DOMAIN, PLATFORMS +from .const import CONF_ACTION, CONF_TRIGGER, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -133,6 +134,7 @@ class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): self.config = config self._unsub_start: Callable[[], None] | None = None self._unsub_trigger: Callable[[], None] | None = None + self._script: Script | None = None @property def unique_id(self) -> str | None: @@ -170,6 +172,14 @@ class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): async def _attach_triggers(self, start_event=None) -> None: """Attach the triggers.""" + if CONF_ACTION in self.config: + self._script = Script( + self.hass, + self.config[CONF_ACTION], + self.name, + DOMAIN, + ) + if start_event is not None: self._unsub_start = None @@ -183,8 +193,11 @@ class TriggerUpdateCoordinator(update_coordinator.DataUpdateCoordinator): start_event is not None, ) - @callback - def _handle_triggered(self, run_variables, context=None): + async def _handle_triggered(self, run_variables, context=None): + if self._script: + script_result = await self._script.async_run(run_variables, context) + if script_result: + run_variables = script_result.variables self.async_set_updated_data( {"run_variables": run_variables, "context": context} ) diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 2261bde2659..54c82d88c74 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -22,7 +22,7 @@ from . import ( select as select_platform, sensor as sensor_platform, ) -from .const import CONF_TRIGGER, DOMAIN +from .const import CONF_ACTION, CONF_TRIGGER, DOMAIN PACKAGE_MERGE_HINT = "list" @@ -30,6 +30,7 @@ CONFIG_SECTION_SCHEMA = vol.Schema( { vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_TRIGGER): cv.TRIGGER_SCHEMA, + vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(NUMBER_DOMAIN): vol.All( cv.ensure_list, [number_platform.NUMBER_SCHEMA] ), diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 9b371125750..6805c0ad812 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -2,6 +2,7 @@ from homeassistant.const import Platform +CONF_ACTION = "action" CONF_AVAILABILITY_TEMPLATE = "availability_template" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" CONF_TRIGGER = "trigger" diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index bbcbfa6ecb8..c6564967a39 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -713,12 +713,12 @@ async def handle_execute_script( context = connection.context(msg) script_obj = Script(hass, script_config, f"{const.DOMAIN} script", const.DOMAIN) - response = await script_obj.async_run(msg.get("variables"), context=context) + script_result = await script_obj.async_run(msg.get("variables"), context=context) connection.send_result( msg["id"], { "context": context, - "response": response, + "response": script_result.service_response if script_result else None, }, ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 4035d55b325..c9d8de23b96 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -6,6 +6,7 @@ from collections.abc import Callable, Mapping, Sequence from contextlib import asynccontextmanager, suppress from contextvars import ContextVar from copy import copy +from dataclasses import dataclass from datetime import datetime, timedelta from functools import partial import itertools @@ -401,7 +402,7 @@ class _ScriptRun: ) self._log("Executing step %s%s", self._script.last_action, _timeout) - async def async_run(self) -> ServiceResponse: + async def async_run(self) -> ScriptRunResult | None: """Run script.""" # Push the script to the script execution stack if (script_stack := script_stack_cv.get()) is None: @@ -443,7 +444,7 @@ class _ScriptRun: script_stack.pop() self._finish() - return response + return ScriptRunResult(response, self._variables) async def _async_step(self, log_exceptions): continue_on_error = self._action.get(CONF_CONTINUE_ON_ERROR, False) @@ -1189,6 +1190,14 @@ class _IfData(TypedDict): if_else: Script | None +@dataclass +class ScriptRunResult: + """Container with the result of a script run.""" + + service_response: ServiceResponse + variables: dict + + class Script: """Representation of a script.""" @@ -1480,7 +1489,7 @@ class Script: run_variables: _VarsType | None = None, context: Context | None = None, started_action: Callable[..., Any] | None = None, - ) -> ServiceResponse: + ) -> ScriptRunResult | None: """Run script.""" if context is None: self._log( diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 5eca8330789..1bd1e797c05 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1530,3 +1530,47 @@ async def test_trigger_entity_restore_state( assert state.attributes["entity_picture"] == "/local/dogs.png" assert state.attributes["plus_one"] == 3 assert state.attributes["another"] == 1 + + +@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": [ + { + "unique_id": "listening-test-event", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [ + { + "variables": { + "my_variable": "{{ trigger.event.data.beer + 1 }}" + }, + }, + ], + "sensor": [ + { + "name": "Hello Name", + "state": "{{ my_variable + 1 }}", + } + ], + }, + ], + }, + ], +) +async def test_trigger_action( + hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry +) -> None: + """Test trigger entity with an action works.""" + state = hass.states.get("sensor.hello_name") + assert state is not None + assert state.state == STATE_UNKNOWN + + context = Context() + hass.bus.async_fire("test_event", {"beer": 1}, context=context) + await hass.async_block_till_done() + + state = hass.states.get("sensor.hello_name") + assert state.state == "3" + assert state.context is context From 4524b38b8043f58d7cbff7382c1746fda70000db Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 4 Sep 2023 20:23:46 +0200 Subject: [PATCH 1101/1151] Mark AVM Fritz!Smarthome as Gold integration (#97086) set quality scale to gold --- homeassistant/components/fritzbox/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 35b78e91f81..fdf38d88439 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -7,6 +7,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["pyfritzhome"], + "quality_scale": "gold", "requirements": ["pyfritzhome==0.6.9"], "ssdp": [ { From e0594bffa14242d0d31da5424f7bff9ee1d743a6 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Mon, 4 Sep 2023 00:30:56 -0700 Subject: [PATCH 1102/1151] Enumerate available states in Prometheus startup (#97993) --- .../components/prometheus/__init__.py | 24 +++++++++++----- tests/components/prometheus/test_init.py | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index adc5225b286..1818f308239 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -120,10 +120,15 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: default_metric, ) - hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_state_changed) + hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_state_changed_event) hass.bus.listen( EVENT_ENTITY_REGISTRY_UPDATED, metrics.handle_entity_registry_updated ) + + for state in hass.states.all(): + if entity_filter(state.entity_id): + metrics.handle_state(state) + return True @@ -162,16 +167,13 @@ class PrometheusMetrics: self._metrics = {} self._climate_units = climate_units - def handle_state_changed(self, event): - """Listen for new messages on the bus, and add them to Prometheus.""" + def handle_state_changed_event(self, event): + """Handle new messages from the bus.""" if (state := event.data.get("new_state")) is None: return - entity_id = state.entity_id - _LOGGER.debug("Handling state update for %s", entity_id) - domain, _ = hacore.split_entity_id(entity_id) - if not self._filter(state.entity_id): + _LOGGER.debug("Filtered out entity %s", state.entity_id) return if (old_state := event.data.get("old_state")) is not None and ( @@ -179,6 +181,14 @@ class PrometheusMetrics: ) != state.attributes.get(ATTR_FRIENDLY_NAME): self._remove_labelsets(old_state.entity_id, old_friendly_name) + self.handle_state(state) + + def handle_state(self, state): + """Add/update a state in Prometheus.""" + entity_id = state.entity_id + _LOGGER.debug("Handling state update for %s", entity_id) + domain, _ = hacore.split_entity_id(entity_id) + ignored_states = (STATE_UNAVAILABLE, STATE_UNKNOWN) handler = f"_handle_{domain}" diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 82a205eb259..07a666946fb 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -107,6 +107,34 @@ async def generate_latest_metrics(client): return body +@pytest.mark.parametrize("namespace", [""]) +async def test_setup_enumeration(hass, hass_client, entity_registry, namespace): + """Test that setup enumerates existing states/entities.""" + + # The order of when things are created must be carefully controlled in + # this test, so we don't use fixtures. + + sensor_1 = entity_registry.async_get_or_create( + domain=sensor.DOMAIN, + platform="test", + unique_id="sensor_1", + unit_of_measurement=UnitOfTemperature.CELSIUS, + original_device_class=SensorDeviceClass.TEMPERATURE, + suggested_object_id="outside_temperature", + original_name="Outside Temperature", + ) + set_state_with_entry(hass, sensor_1, 12.3, {}) + assert await async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: {}}) + + client = await hass_client() + body = await generate_latest_metrics(client) + assert ( + 'homeassistant_sensor_temperature_celsius{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 12.3' in body + ) + + @pytest.mark.parametrize("namespace", [""]) async def test_view_empty_namespace(client, sensor_entities) -> None: """Test prometheus metrics view.""" From 1c2c13c9380481269679a25e54b5860bed1c2721 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 14:08:38 +0200 Subject: [PATCH 1103/1151] Don't set assumed_state in cover groups (#99391) --- homeassistant/components/group/cover.py | 20 +------------------- tests/components/group/test_cover.py | 13 +++++++------ 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index dbb49222bb0..d22184c0922 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -17,7 +17,6 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, @@ -44,7 +43,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import GroupEntity -from .util import attribute_equal, reduce_attribute +from .util import reduce_attribute KEY_OPEN_CLOSE = "open_close" KEY_STOP = "stop" @@ -116,7 +115,6 @@ class CoverGroup(GroupEntity, CoverEntity): _attr_is_opening: bool | None = False _attr_is_closing: bool | None = False _attr_current_cover_position: int | None = 100 - _attr_assumed_state: bool = True def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a CoverGroup entity.""" @@ -251,8 +249,6 @@ class CoverGroup(GroupEntity, CoverEntity): @callback def async_update_group_state(self) -> None: """Update state and attributes.""" - self._attr_assumed_state = False - states = [ state.state for entity_id in self._entity_ids @@ -293,9 +289,6 @@ class CoverGroup(GroupEntity, CoverEntity): self._attr_current_cover_position = reduce_attribute( position_states, ATTR_CURRENT_POSITION ) - self._attr_assumed_state |= not attribute_equal( - position_states, ATTR_CURRENT_POSITION - ) tilt_covers = self._tilts[KEY_POSITION] all_tilt_states = [self.hass.states.get(x) for x in tilt_covers] @@ -303,9 +296,6 @@ class CoverGroup(GroupEntity, CoverEntity): self._attr_current_cover_tilt_position = reduce_attribute( tilt_states, ATTR_CURRENT_TILT_POSITION ) - self._attr_assumed_state |= not attribute_equal( - tilt_states, ATTR_CURRENT_TILT_POSITION - ) supported_features = CoverEntityFeature(0) if self._covers[KEY_OPEN_CLOSE]: @@ -322,11 +312,3 @@ class CoverGroup(GroupEntity, CoverEntity): if self._tilts[KEY_POSITION]: supported_features |= CoverEntityFeature.SET_TILT_POSITION self._attr_supported_features = supported_features - - if not self._attr_assumed_state: - for entity_id in self._entity_ids: - if (state := self.hass.states.get(entity_id)) is None: - continue - if state and state.attributes.get(ATTR_ASSUMED_STATE): - self._attr_assumed_state = True - break diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 84ccba2ff66..4e0ddc19a31 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -346,10 +346,10 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_CURRENT_POSITION] == 70 assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 60 - # ### Test assumed state ### + # ### Test state when group members have different states ### # ########################## - # For covers - assumed state set true if position differ + # Covers hass.states.async_set( DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100} ) @@ -357,7 +357,7 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: state = hass.states.get(COVER_GROUP) assert state.state == STATE_OPEN - assert state.attributes[ATTR_ASSUMED_STATE] is True + assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_SUPPORTED_FEATURES] == 244 assert state.attributes[ATTR_CURRENT_POSITION] == 85 # (70 + 100) / 2 assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 60 @@ -373,7 +373,7 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: assert ATTR_CURRENT_POSITION not in state.attributes assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 60 - # For tilts - assumed state set true if tilt position differ + # Tilts hass.states.async_set( DEMO_TILT, STATE_OPEN, @@ -383,7 +383,7 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: state = hass.states.get(COVER_GROUP) assert state.state == STATE_OPEN - assert state.attributes[ATTR_ASSUMED_STATE] is True + assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 assert ATTR_CURRENT_POSITION not in state.attributes assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 80 # (60 + 100) / 2 @@ -399,11 +399,12 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: assert ATTR_CURRENT_POSITION not in state.attributes assert ATTR_CURRENT_TILT_POSITION not in state.attributes + # Group member has set assumed_state hass.states.async_set(DEMO_TILT, STATE_CLOSED, {ATTR_ASSUMED_STATE: True}) await hass.async_block_till_done() state = hass.states.get(COVER_GROUP) - assert state.attributes[ATTR_ASSUMED_STATE] is True + assert ATTR_ASSUMED_STATE not in state.attributes # Test entity registry integration entity_registry = er.async_get(hass) From 2a5f8ee4a7aa0e24de80101d6b9a57127bb968a5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 14:08:50 +0200 Subject: [PATCH 1104/1151] Don't set assumed_state in fan groups (#99399) --- homeassistant/components/group/fan.py | 18 +----------------- tests/components/group/test_config_flow.py | 2 +- tests/components/group/test_fan.py | 17 ++++------------- 3 files changed, 6 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index 4ee788c8402..4e3bb824266 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -25,7 +25,6 @@ from homeassistant.components.fan import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, @@ -41,12 +40,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import GroupEntity -from .util import ( - attribute_equal, - most_frequent_attribute, - reduce_attribute, - states_equal, -) +from .util import attribute_equal, most_frequent_attribute, reduce_attribute SUPPORTED_FLAGS = { FanEntityFeature.SET_SPEED, @@ -110,7 +104,6 @@ class FanGroup(GroupEntity, FanEntity): """Representation of a FanGroup.""" _attr_available: bool = False - _attr_assumed_state: bool = True def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a FanGroup entity.""" @@ -243,19 +236,16 @@ class FanGroup(GroupEntity, FanEntity): """Set an attribute based on most frequent supported entities attributes.""" states = self._async_states_by_support_flag(flag) setattr(self, attr, most_frequent_attribute(states, entity_attr)) - self._attr_assumed_state |= not attribute_equal(states, entity_attr) @callback def async_update_group_state(self) -> None: """Update state and attributes.""" - self._attr_assumed_state = False states = [ state for entity_id in self._entity_ids if (state := self.hass.states.get(entity_id)) is not None ] - self._attr_assumed_state |= not states_equal(states) # Set group as unavailable if all members are unavailable or missing self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) @@ -274,9 +264,6 @@ class FanGroup(GroupEntity, FanEntity): FanEntityFeature.SET_SPEED ) self._percentage = reduce_attribute(percentage_states, ATTR_PERCENTAGE) - self._attr_assumed_state |= not attribute_equal( - percentage_states, ATTR_PERCENTAGE - ) if ( percentage_states and percentage_states[0].attributes.get(ATTR_PERCENTAGE_STEP) @@ -301,6 +288,3 @@ class FanGroup(GroupEntity, FanEntity): ior, [feature for feature in SUPPORTED_FLAGS if self._fans[feature]], 0 ) ) - self._attr_assumed_state |= any( - state.attributes.get(ATTR_ASSUMED_STATE) for state in states - ) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index d0e90fe61bd..1c8275c7f2d 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -468,7 +468,7 @@ async def test_options_flow_hides_members( COVER_ATTRS = [{"supported_features": 0}, {}] EVENT_ATTRS = [{"event_types": []}, {"event_type": None}] -FAN_ATTRS = [{"supported_features": 0}, {"assumed_state": True}] +FAN_ATTRS = [{"supported_features": 0}, {}] LIGHT_ATTRS = [ { "icon": "mdi:lightbulb-group", diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index 6269df3fed7..2272a29f6ed 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -247,11 +247,7 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_PERCENTAGE] == 50 assert ATTR_ASSUMED_STATE not in state.attributes - # Add Entity that supports - # ### Test assumed state ### - # ########################## - - # Add Entity with a different speed should set assumed state + # Add Entity with a different speed should not set assumed state hass.states.async_set( PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_ON, @@ -264,7 +260,7 @@ async def test_attributes(hass: HomeAssistant, setup_comp) -> None: state = hass.states.get(FAN_GROUP) assert state.state == STATE_ON - assert state.attributes[ATTR_ASSUMED_STATE] is True + assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_PERCENTAGE] == int((50 + 75) / 2) @@ -306,11 +302,7 @@ async def test_direction_oscillating(hass: HomeAssistant, setup_comp) -> None: assert state.attributes[ATTR_DIRECTION] == DIRECTION_FORWARD assert ATTR_ASSUMED_STATE not in state.attributes - # Add Entity that supports - # ### Test assumed state ### - # ########################## - - # Add Entity with a different direction should set assumed state + # Add Entity with a different direction should not set assumed state hass.states.async_set( PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_ON, @@ -325,11 +317,10 @@ async def test_direction_oscillating(hass: HomeAssistant, setup_comp) -> None: state = hass.states.get(FAN_GROUP) assert state.state == STATE_ON - assert state.attributes[ATTR_ASSUMED_STATE] is True + assert ATTR_ASSUMED_STATE not in state.attributes assert ATTR_PERCENTAGE in state.attributes assert state.attributes[ATTR_PERCENTAGE] == 50 assert state.attributes[ATTR_OSCILLATING] is True - assert ATTR_ASSUMED_STATE in state.attributes # Now that everything is the same, no longer assumed state From 852589f02549853d512e88a0eb330c5bbdd2856c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiit=20R=C3=A4tsep?= Date: Mon, 4 Sep 2023 11:03:58 +0300 Subject: [PATCH 1105/1151] Fix battery reading in SOMA API (#99403) Co-authored-by: Robert Resch --- homeassistant/components/soma/sensor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index 6472f6934e0..d1c0de188a0 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -43,11 +43,12 @@ class SomaSensor(SomaEntity, SensorEntity): async def async_update(self) -> None: """Update the sensor with the latest data.""" response = await self.get_battery_level_from_api() - - # https://support.somasmarthome.com/hc/en-us/articles/360026064234-HTTP-API - # battery_level response is expected to be min = 360, max 410 for - # 0-100% levels above 410 are consider 100% and below 360, 0% as the - # device considers 360 the minimum to move the motor. - _battery = round(2 * (response["battery_level"] - 360)) + _battery = response.get("battery_percentage") + if _battery is None: + # https://support.somasmarthome.com/hc/en-us/articles/360026064234-HTTP-API + # battery_level response is expected to be min = 360, max 410 for + # 0-100% levels above 410 are consider 100% and below 360, 0% as the + # device considers 360 the minimum to move the motor. + _battery = round(2 * (response["battery_level"] - 360)) battery = max(min(100, _battery), 0) self.battery_state = battery From bdc39e1d52aa73a23a7c6d68e3f5653bb0d4a439 Mon Sep 17 00:00:00 2001 From: Mike O'Driscoll Date: Sun, 3 Sep 2023 11:08:17 -0400 Subject: [PATCH 1106/1151] Fix recollect_waste month time boundary issue (#99429) --- .../components/recollect_waste/__init__.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 21cf574d548..076067312eb 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1,7 +1,7 @@ """The ReCollect Waste integration.""" from __future__ import annotations -from datetime import timedelta +from datetime import date, timedelta from typing import Any from aiorecollect.client import Client, PickupEvent @@ -31,7 +31,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_get_pickup_events() -> list[PickupEvent]: """Get the next pickup.""" try: - return await client.async_get_pickup_events() + # Retrieve today through to 35 days in the future, to get + # coverage across a full two months boundary so that no + # upcoming pickups are missed. The api.recollect.net base API + # call returns only the current month when no dates are passed. + # This ensures that data about when the next pickup is will be + # returned when the next pickup is the first day of the next month. + # Ex: Today is August 31st, tomorrow is a pickup on September 1st. + today = date.today() + return await client.async_get_pickup_events( + start_date=today, + end_date=today + timedelta(days=35), + ) except RecollectError as err: raise UpdateFailed( f"Error while requesting data from ReCollect: {err}" From e5fd6961a8ef10a2c656c891a625f11dc253e54d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 14:09:51 +0200 Subject: [PATCH 1107/1151] Set state of entity with invalid state to unknown (#99452) * Set state of entity with invalid state to unknown * Add test * Apply suggestions from code review Co-authored-by: Robert Resch * Update test_entity.py --------- Co-authored-by: Robert Resch --- homeassistant/helpers/entity.py | 16 ++++++++++++++-- tests/helpers/test_entity.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 29a944874ab..e946c41d3b8 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -35,7 +35,11 @@ from homeassistant.const import ( EntityCategory, ) from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError +from homeassistant.exceptions import ( + HomeAssistantError, + InvalidStateError, + NoEntitySpecifiedError, +) from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util, ensure_unique_string, slugify @@ -848,7 +852,15 @@ class Entity(ABC): self._context = None self._context_set = None - hass.states.async_set(entity_id, state, attr, self.force_update, self._context) + try: + hass.states.async_set( + entity_id, state, attr, self.force_update, self._context + ) + except InvalidStateError: + _LOGGER.exception("Failed to set state, fall back to %s", STATE_UNKNOWN) + hass.states.async_set( + entity_id, STATE_UNKNOWN, {}, self.force_update, self._context + ) def schedule_update_ha_state(self, force_refresh: bool = False) -> None: """Schedule an update ha state change task. diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 200b0230adb..20bea6a98eb 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import Iterable import dataclasses from datetime import timedelta +import logging import threading from typing import Any from unittest.mock import MagicMock, PropertyMock, patch @@ -1477,3 +1478,30 @@ async def test_warn_no_platform( caplog.clear() ent.async_write_ha_state() assert error_message not in caplog.text + + +async def test_invalid_state( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the entity helper catches InvalidState and sets state to unknown.""" + ent = entity.Entity() + ent.entity_id = "test.test" + ent.hass = hass + + ent._attr_state = "x" * 255 + ent.async_write_ha_state() + assert hass.states.get("test.test").state == "x" * 255 + + caplog.clear() + ent._attr_state = "x" * 256 + ent.async_write_ha_state() + assert hass.states.get("test.test").state == STATE_UNKNOWN + assert ( + "homeassistant.helpers.entity", + logging.ERROR, + f"Failed to set state, fall back to {STATE_UNKNOWN}", + ) in caplog.record_tuples + + ent._attr_state = "x" * 255 + ent.async_write_ha_state() + assert hass.states.get("test.test").state == "x" * 255 From 423e8fbde3e5130ff4efa132a16ed1d9d2b5be52 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 14:10:43 +0200 Subject: [PATCH 1108/1151] Validate state in template helper preview (#99455) * Validate state in template helper preview * Deduplicate state validation --- .../components/template/template_entity.py | 13 ++++++++++--- homeassistant/core.py | 16 +++++++++++----- tests/test_core.py | 7 +++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index ac06e2c8734..c33674fa86f 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -25,6 +25,7 @@ from homeassistant.core import ( HomeAssistant, State, callback, + validate_state, ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -413,8 +414,8 @@ class TemplateEntity(Entity): return for update in updates: - for attr in self._template_attrs[update.template]: - attr.handle_result( + for template_attr in self._template_attrs[update.template]: + template_attr.handle_result( event, update.template, update.last_result, update.result ) @@ -422,7 +423,13 @@ class TemplateEntity(Entity): self.async_write_ha_state() return - self._preview_callback(*self._async_generate_attributes(), None) + try: + state, attrs = self._async_generate_attributes() + validate_state(state) + except Exception as err: # pylint: disable=broad-exception-caught + self._preview_callback(None, None, str(err)) + else: + self._preview_callback(state, attrs, None) @callback def _async_template_startup(self, *_: Any) -> None: diff --git a/homeassistant/core.py b/homeassistant/core.py index 18c5c355ae9..f2921e244ab 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -174,6 +174,16 @@ def valid_entity_id(entity_id: str) -> bool: return VALID_ENTITY_ID.match(entity_id) is not None +def validate_state(state: str) -> str: + """Validate a state, raise if it not valid.""" + if len(state) > MAX_LENGTH_STATE_STATE: + raise InvalidStateError( + f"Invalid state with length {len(state)}. " + "State max length is 255 characters." + ) + return state + + def callback(func: _CallableT) -> _CallableT: """Annotation to mark method as safe to call from within the event loop.""" setattr(func, "_hass_callback", True) @@ -1251,11 +1261,7 @@ class State: "Format should be ." ) - if len(state) > MAX_LENGTH_STATE_STATE: - raise InvalidStateError( - f"Invalid state encountered for entity ID: {entity_id}. " - "State max length is 255 characters." - ) + validate_state(state) self.entity_id = entity_id.lower() self.state = state diff --git a/tests/test_core.py b/tests/test_core.py index 4f7916e757b..8ec4dad2ebd 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2464,3 +2464,10 @@ async def test_cancellable_hassjob(hass: HomeAssistant) -> None: # Cleanup timer2.cancel() + + +async def test_validate_state(hass: HomeAssistant) -> None: + """Test validate_state.""" + assert ha.validate_state("test") == "test" + with pytest.raises(InvalidStateError): + ha.validate_state("t" * 256) From 2bc08d1fccac71a6b230d8c3c7d874feb95a25a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Sep 2023 13:19:10 -0500 Subject: [PATCH 1109/1151] Fix module check in _async_get_flow_handler (#99509) We should have been checking for the module in hass.data[DATA_COMPONENTS] and not hass.config.components as the check was ineffective if there were no existing integrations instances for the domain which is the case for discovery or when the integration is ignored --- homeassistant/config_entries.py | 4 +++- homeassistant/loader.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a3b03407a14..02117c3ac5a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2055,7 +2055,9 @@ async def _async_get_flow_handler( """Get a flow handler for specified domain.""" # First check if there is a handler registered for the domain - if domain in hass.config.components and (handler := HANDLERS.get(domain)): + if loader.is_component_module_loaded(hass, f"{domain}.config_flow") and ( + handler := HANDLERS.get(domain) + ): return handler await _load_integration(hass, domain, hass_config) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 40161bd3be9..37e470c1178 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -1162,3 +1162,8 @@ def _lookup_path(hass: HomeAssistant) -> list[str]: if hass.config.safe_mode: return [PACKAGE_BUILTIN] return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] + + +def is_component_module_loaded(hass: HomeAssistant, module: str) -> bool: + """Test if a component module is loaded.""" + return module in hass.data[DATA_COMPONENTS] From 70b460f52bc86168b28f59a7d0fd2ddcf34d20fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 2 Sep 2023 15:25:42 -0500 Subject: [PATCH 1110/1151] Bump aiohomekit to 3.0.2 (#99514) --- 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 83852f38d52..9567ff83cea 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==3.0.1"], + "requirements": ["aiohomekit==3.0.2"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index e53d9d8fd0f..06f5cea0169 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==3.0.1 +aiohomekit==3.0.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec3b4a62052..93779c21efd 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==3.0.1 +aiohomekit==3.0.2 # homeassistant.components.emulated_hue # homeassistant.components.http From 453c5f90f8fa823119715a18f9784989dc6c11a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Sep 2023 08:22:03 -0500 Subject: [PATCH 1111/1151] Bump bleak to 0.21.0 (#99520) Co-authored-by: Martin Hjelmare --- .../components/bluetooth/manifest.json | 2 +- homeassistant/components/bluetooth/wrappers.py | 6 ++++-- .../components/esphome/bluetooth/client.py | 18 +++++++++++------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 59a87f4dfbb..8ddf0a38c1d 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -14,7 +14,7 @@ ], "quality_scale": "internal", "requirements": [ - "bleak==0.20.2", + "bleak==0.21.0", "bleak-retry-connector==3.1.1", "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index 3a0abc855b5..97f253f8825 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -120,15 +120,17 @@ class HaBleakScannerWrapper(BaseBleakScanner): def register_detection_callback( self, callback: AdvertisementDataCallback | None - ) -> None: + ) -> Callable[[], None]: """Register a detection callback. The callback is called when a device is discovered or has a property changed. - This method takes the callback and registers it with the long running sscanner. + This method takes the callback and registers it with the long running scanner. """ self._advertisement_data_callback = callback self._setup_detection_callback() + assert self._detection_cancel is not None + return self._detection_cancel def _setup_detection_callback(self) -> None: """Set up the detection callback.""" diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index ad43ca5df7d..411a5b989a3 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -7,9 +7,15 @@ import contextlib from dataclasses import dataclass, field from functools import partial import logging +import sys from typing import Any, TypeVar, cast import uuid +if sys.version_info < (3, 12): + from typing_extensions import Buffer +else: + from collections.abc import Buffer + from aioesphomeapi import ( ESP_CONNECTION_ERROR_DESCRIPTION, ESPHOME_GATT_ERRORS, @@ -620,14 +626,14 @@ class ESPHomeClient(BaseBleakClient): @api_error_as_bleak_error async def write_gatt_char( self, - char_specifier: BleakGATTCharacteristic | int | str | uuid.UUID, - data: bytes | bytearray | memoryview, + characteristic: BleakGATTCharacteristic | int | str | uuid.UUID, + data: Buffer, response: bool = False, ) -> None: """Perform a write operation of the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): + characteristic (BleakGATTCharacteristic, int, str or UUID): The characteristic to write to, specified by either integer handle, UUID or directly by the BleakGATTCharacteristic object representing it. @@ -635,16 +641,14 @@ class ESPHomeClient(BaseBleakClient): response (bool): If write-with-response operation should be done. Defaults to `False`. """ - characteristic = self._resolve_characteristic(char_specifier) + characteristic = self._resolve_characteristic(characteristic) await self._client.bluetooth_gatt_write( self._address_as_int, characteristic.handle, bytes(data), response ) @verify_connected @api_error_as_bleak_error - async def write_gatt_descriptor( - self, handle: int, data: bytes | bytearray | memoryview - ) -> None: + async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None: """Perform a write operation on the specified GATT descriptor. Args: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cb114b1504d..c169c2ab3b1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ attrs==23.1.0 awesomeversion==22.9.0 bcrypt==4.0.1 bleak-retry-connector==3.1.1 -bleak==0.20.2 +bleak==0.21.0 bluetooth-adapters==0.16.0 bluetooth-auto-recovery==1.2.1 bluetooth-data-tools==1.9.1 diff --git a/requirements_all.txt b/requirements_all.txt index 06f5cea0169..719e9d6f0ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -521,7 +521,7 @@ bizkaibus==0.1.1 bleak-retry-connector==3.1.1 # homeassistant.components.bluetooth -bleak==0.20.2 +bleak==0.21.0 # homeassistant.components.blebox blebox-uniapi==2.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93779c21efd..e06beb16ed1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -439,7 +439,7 @@ bimmer-connected==0.14.0 bleak-retry-connector==3.1.1 # homeassistant.components.bluetooth -bleak==0.20.2 +bleak==0.21.0 # homeassistant.components.blebox blebox-uniapi==2.1.4 From c677dae9c1b5bca91fabfc271ffa8feb04f910fe Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 3 Sep 2023 17:48:25 +0200 Subject: [PATCH 1112/1151] Modbus switch, allow restore "unknown" (#99533) --- homeassistant/components/modbus/base_platform.py | 6 +++++- tests/components/modbus/test_switch.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 7c3fcd78b05..65cfa1b49ba 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_SLAVE, CONF_STRUCTURE, CONF_UNIQUE_ID, + STATE_OFF, STATE_ON, ) from homeassistant.core import callback @@ -311,7 +312,10 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): """Handle entity which will be added.""" await self.async_base_added_to_hass() if state := await self.async_get_last_state(): - self._attr_is_on = state.state == STATE_ON + if state.state == STATE_ON: + self._attr_is_on = True + elif state.state == STATE_OFF: + self._attr_is_on = False async def async_turn(self, command: int) -> None: """Evaluate switch result.""" diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index dce4588d606..7a79e19869a 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -250,7 +250,7 @@ async def test_lazy_error_switch( @pytest.mark.parametrize( "mock_test_state", - [(State(ENTITY_ID, STATE_ON),)], + [(State(ENTITY_ID, STATE_ON),), (State(ENTITY_ID, STATE_OFF),)], indirect=True, ) @pytest.mark.parametrize( From a60e23caf2b5f0385a5c37fea7b14428a7757e3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Sep 2023 09:13:21 -0500 Subject: [PATCH 1113/1151] Bump bleak-retry-connector to 3.1.2 (#99540) --- 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 8ddf0a38c1d..c643df542d8 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.21.0", - "bleak-retry-connector==3.1.1", + "bleak-retry-connector==3.1.2", "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", "bluetooth-data-tools==1.9.1", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c169c2ab3b1..4e6338aa30d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ atomicwrites-homeassistant==1.4.1 attrs==23.1.0 awesomeversion==22.9.0 bcrypt==4.0.1 -bleak-retry-connector==3.1.1 +bleak-retry-connector==3.1.2 bleak==0.21.0 bluetooth-adapters==0.16.0 bluetooth-auto-recovery==1.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index 719e9d6f0ed..04ffad53cbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ bimmer-connected==0.14.0 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==3.1.1 +bleak-retry-connector==3.1.2 # homeassistant.components.bluetooth bleak==0.21.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e06beb16ed1..a99e9bbb765 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -436,7 +436,7 @@ bellows==0.36.1 bimmer-connected==0.14.0 # homeassistant.components.bluetooth -bleak-retry-connector==3.1.1 +bleak-retry-connector==3.1.2 # homeassistant.components.bluetooth bleak==0.21.0 From 8ccd2b6457dccee7608e1134decaae471ab3febd Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 1 Sep 2023 23:33:19 +0100 Subject: [PATCH 1114/1151] Update bluetooth-data-tools to 1.11.0 (#99485) --- homeassistant/components/bluetooth/manifest.json | 2 +- 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 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index c643df542d8..e1a5ee41324 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak-retry-connector==3.1.2", "bluetooth-adapters==0.16.0", "bluetooth-auto-recovery==1.2.1", - "bluetooth-data-tools==1.9.1", + "bluetooth-data-tools==1.11.0", "dbus-fast==1.94.1" ] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d0ab27656c2..7d552f340f0 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "requirements": [ "async_interrupt==1.1.1", "aioesphomeapi==16.0.3", - "bluetooth-data-tools==1.9.1", + "bluetooth-data-tools==1.11.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 0c77e0e2ef5..798a80147de 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.9.1", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.11.0", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 36e3b7355ff..da5b4b0a4ee 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.9.1", "led-ble==1.0.0"] + "requirements": ["bluetooth-data-tools==1.11.0", "led-ble==1.0.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4e6338aa30d..598c2625f78 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ bleak-retry-connector==3.1.2 bleak==0.21.0 bluetooth-adapters==0.16.0 bluetooth-auto-recovery==1.2.1 -bluetooth-data-tools==1.9.1 +bluetooth-data-tools==1.11.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 04ffad53cbf..4dc721b679b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -549,7 +549,7 @@ bluetooth-auto-recovery==1.2.1 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.9.1 +bluetooth-data-tools==1.11.0 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a99e9bbb765..19d4295649d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -460,7 +460,7 @@ bluetooth-auto-recovery==1.2.1 # homeassistant.components.esphome # homeassistant.components.ld2410_ble # homeassistant.components.led_ble -bluetooth-data-tools==1.9.1 +bluetooth-data-tools==1.11.0 # homeassistant.components.bond bond-async==0.2.1 From c791ddc937b828977643f4afba441746424e1651 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 4 Sep 2023 07:33:46 +0200 Subject: [PATCH 1115/1151] Fix loading filesize coordinator from wrong place (#99547) * Fix loading filesize coordinator from wrong place * aboslute in executor * combine into executor --- homeassistant/components/filesize/__init__.py | 14 ++++-- .../components/filesize/coordinator.py | 48 +++++++++++++++++++ homeassistant/components/filesize/sensor.py | 45 ++--------------- 3 files changed, 62 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/filesize/coordinator.py diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index 73f060e79b7..9d7cc99421f 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -9,10 +9,11 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .const import PLATFORMS +from .coordinator import FileSizeCoordinator -def _check_path(hass: HomeAssistant, path: str) -> None: - """Check if path is valid and allowed.""" +def _get_full_path(hass: HomeAssistant, path: str) -> str: + """Check if path is valid, allowed and return full path.""" get_path = pathlib.Path(path) if not get_path.exists() or not get_path.is_file(): raise ConfigEntryNotReady(f"Can not access file {path}") @@ -20,10 +21,17 @@ def _check_path(hass: HomeAssistant, path: str) -> None: if not hass.config.is_allowed_path(path): raise ConfigEntryNotReady(f"Filepath {path} is not valid or allowed") + return str(get_path.absolute()) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - await hass.async_add_executor_job(_check_path, hass, entry.data[CONF_FILE_PATH]) + full_path = await hass.async_add_executor_job( + _get_full_path, hass, entry.data[CONF_FILE_PATH] + ) + coordinator = FileSizeCoordinator(hass, full_path) + await coordinator.async_config_entry_first_refresh() + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/filesize/coordinator.py b/homeassistant/components/filesize/coordinator.py new file mode 100644 index 00000000000..75411f84975 --- /dev/null +++ b/homeassistant/components/filesize/coordinator.py @@ -0,0 +1,48 @@ +"""Coordinator for monitoring the size of a file.""" +from __future__ import annotations + +from datetime import datetime, timedelta +import logging +import os + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +import homeassistant.util.dt as dt_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class FileSizeCoordinator(DataUpdateCoordinator[dict[str, int | float | datetime]]): + """Filesize coordinator.""" + + def __init__(self, hass: HomeAssistant, path: str) -> None: + """Initialize filesize coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=60), + always_update=False, + ) + self._path = path + + async def _async_update_data(self) -> dict[str, float | int | datetime]: + """Fetch file information.""" + try: + statinfo = await self.hass.async_add_executor_job(os.stat, self._path) + except OSError as error: + raise UpdateFailed(f"Can not retrieve file statistics {error}") from error + + size = statinfo.st_size + last_updated = dt_util.utc_from_timestamp(statinfo.st_mtime) + + _LOGGER.debug("size %s, last updated %s", size, last_updated) + data: dict[str, int | float | datetime] = { + "file": round(size / 1e6, 2), + "bytes": size, + "last_updated": last_updated, + } + + return data diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 0e600363640..c8e5dae5892 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -1,9 +1,8 @@ """Sensor for monitoring the size of a file.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime import logging -import os import pathlib from homeassistant.components.sensor import ( @@ -17,14 +16,10 @@ from homeassistant.const import CONF_FILE_PATH, EntityCategory, UnitOfInformatio from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) -import homeassistant.util.dt as dt_util +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN +from .coordinator import FileSizeCoordinator _LOGGER = logging.getLogger(__name__) @@ -80,40 +75,6 @@ async def async_setup_entry( ) -class FileSizeCoordinator(DataUpdateCoordinator): - """Filesize coordinator.""" - - def __init__(self, hass: HomeAssistant, path: str) -> None: - """Initialize filesize coordinator.""" - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=timedelta(seconds=60), - always_update=False, - ) - self._path = path - - async def _async_update_data(self) -> dict[str, float | int | datetime]: - """Fetch file information.""" - try: - statinfo = await self.hass.async_add_executor_job(os.stat, self._path) - except OSError as error: - raise UpdateFailed(f"Can not retrieve file statistics {error}") from error - - size = statinfo.st_size - last_updated = dt_util.utc_from_timestamp(statinfo.st_mtime) - - _LOGGER.debug("size %s, last updated %s", size, last_updated) - data: dict[str, int | float | datetime] = { - "file": round(size / 1e6, 2), - "bytes": size, - "last_updated": last_updated, - } - - return data - - class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): """Filesize sensor.""" From 5c42ea57b3cba7e0ceb1dee1cf2c193ea8d65e23 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 3 Sep 2023 21:31:25 +0200 Subject: [PATCH 1116/1151] Bump aiounifi to v60 (#99548) --- 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 363313bf878..cb1c8f1c0dc 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==58"], + "requirements": ["aiounifi==60"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 4dc721b679b..988413b7f8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -363,7 +363,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==58 +aiounifi==60 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19d4295649d..db79ab36403 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==58 +aiounifi==60 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From a7efeb2c878f5e7e49606689bc364c37a2ce2c8e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 4 Sep 2023 09:26:14 -0400 Subject: [PATCH 1117/1151] Bump ZHA dependencies (#99561) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 809b576defa..7352487a318 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,12 +21,12 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.36.1", + "bellows==0.36.2", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.103", "zigpy-deconz==0.21.0", - "zigpy==0.57.0", + "zigpy==0.57.1", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", "zigpy-znp==0.11.4", diff --git a/requirements_all.txt b/requirements_all.txt index 988413b7f8e..1c634186c3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -509,7 +509,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.36.1 +bellows==0.36.2 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -2793,7 +2793,7 @@ zigpy-zigate==0.11.0 zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.57.0 +zigpy==0.57.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db79ab36403..c63c5ca7783 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -430,7 +430,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.36.1 +bellows==0.36.2 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.0 @@ -2057,7 +2057,7 @@ zigpy-zigate==0.11.0 zigpy-znp==0.11.4 # homeassistant.components.zha -zigpy==0.57.0 +zigpy==0.57.1 # homeassistant.components.zwave_js zwave-js-server-python==0.51.0 From 37f8f911712c9999b61edb4f2b15f56db7b5a3ec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 20:17:30 +0200 Subject: [PATCH 1118/1151] Small cleanup of WS command render_template (#99562) --- homeassistant/components/websocket_api/commands.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index c6564967a39..84c7567a40e 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -516,7 +516,6 @@ async def handle_render_template( template_obj = _cached_template(template_str, hass) variables = msg.get("variables") timeout = msg.get("timeout") - info = None if timeout: try: @@ -540,7 +539,6 @@ async def handle_render_template( event: EventType[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: - nonlocal info track_template_result = updates.pop() result = track_template_result.result if isinstance(result, TemplateError): @@ -549,7 +547,7 @@ async def handle_render_template( connection.send_message( messages.event_message( - msg["id"], {"result": result, "listeners": info.listeners} # type: ignore[attr-defined] + msg["id"], {"result": result, "listeners": info.listeners} ) ) From 30c65652058ed41180446bfb933b059e32e9f138 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Sep 2023 13:39:24 +0200 Subject: [PATCH 1119/1151] Bump pyenphase to 1.9.1 (#99574) --- 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 540c121bb17..a45f4f01e49 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.8.1"], + "requirements": ["pyenphase==1.9.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 1c634186c3c..c180b2db67d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1671,7 +1671,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.8.1 +pyenphase==1.9.1 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c63c5ca7783..7965cea85c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1235,7 +1235,7 @@ pyeconet==0.1.20 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.8.1 +pyenphase==1.9.1 # homeassistant.components.everlights pyeverlights==0.1.0 From 89e280e1367c9225e5808569477d756db1ba8ca4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 4 Sep 2023 15:29:30 +0200 Subject: [PATCH 1120/1151] Remove unneeded name property from Logi Circle (#99604) --- homeassistant/components/logi_circle/camera.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 77c0f2f24c8..5c27d2a08ae 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -122,11 +122,6 @@ class LogiCam(Camera): """Return a unique ID.""" return self._id - @property - def name(self): - """Return the name of this camera.""" - return self._name - @property def device_info(self) -> DeviceInfo: """Return information about the device.""" From 0520cadfa6e8665563db4feee760af31591d5049 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 20:10:16 +0200 Subject: [PATCH 1121/1151] Revert "Deprecate timer start optional duration parameter" (#99613) Revert "Deprecate timer start optional duration parameter (#93471)" This reverts commit 2ce5b08fc36e77a2594a39040e5440d2ca01dff8. --- homeassistant/components/timer/__init__.py | 13 ------------- homeassistant/components/timer/strings.json | 13 ------------- tests/components/timer/test_init.py | 16 ++-------------- 3 files changed, 2 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 1bc8eb8fd5e..228e2071b4a 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -22,7 +22,6 @@ from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service from homeassistant.helpers.storage import Store @@ -304,18 +303,6 @@ class Timer(collection.CollectionEntity, RestoreEntity): @callback def async_start(self, duration: timedelta | None = None): """Start a timer.""" - if duration: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_duration_in_start", - breaks_in_ha_version="2024.3.0", - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_duration_in_start", - ) - if self._listener: self._listener() self._listener = None diff --git a/homeassistant/components/timer/strings.json b/homeassistant/components/timer/strings.json index c85a9f4c55e..56cb46d26b4 100644 --- a/homeassistant/components/timer/strings.json +++ b/homeassistant/components/timer/strings.json @@ -63,18 +63,5 @@ } } } - }, - "issues": { - "deprecated_duration_in_start": { - "title": "The timer start service duration parameter is being removed", - "fix_flow": { - "step": { - "confirm": { - "title": "[%key:component::timer::issues::deprecated_duration_in_start::title%]", - "description": "The timer service `timer.start` optional duration parameter is being removed and use of it has been detected. To change the duration please create a new timer.\n\nPlease remove the use of the `duration` parameter in the `timer.start` service in your automations and scripts and select **submit** to close this issue." - } - } - } - } } } diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 7bc2df87f35..eabc5e04e0b 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -46,11 +46,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, CoreState, HomeAssistant, State from homeassistant.exceptions import HomeAssistantError, Unauthorized -from homeassistant.helpers import ( - config_validation as cv, - entity_registry as er, - issue_registry as ir, -) +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.restore_state import StoredState, async_get from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -270,9 +266,7 @@ async def test_methods_and_events(hass: HomeAssistant) -> None: @pytest.mark.freeze_time("2023-06-05 17:47:50") -async def test_start_service( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: +async def test_start_service(hass: HomeAssistant) -> None: """Test the start/stop service.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}}) @@ -317,12 +311,6 @@ async def test_start_service( blocking=True, ) await hass.async_block_till_done() - - # Ensure an issue is raised for the use of this deprecated service - assert issue_registry.async_get_issue( - domain=DOMAIN, issue_id="deprecated_duration_in_start" - ) - state = hass.states.get("timer.test1") assert state assert state.state == STATUS_ACTIVE From 6b988a99d5d395186e2efbbb3f390da57c0fb84c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 4 Sep 2023 20:44:20 +0200 Subject: [PATCH 1122/1151] Update frontend to 20230904.0 (#99636) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 3b46f568d3e..156adfa73d2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230901.0"] + "requirements": ["home-assistant-frontend==20230904.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 598c2625f78..87ed1537205 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230901.0 +home-assistant-frontend==20230904.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c180b2db67d..0960f774960 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230901.0 +home-assistant-frontend==20230904.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7965cea85c7..8a28b186f40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230901.0 +home-assistant-frontend==20230904.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From 4fcb0a840a1b470bb4627dd86648a1123b166539 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 4 Sep 2023 20:50:17 +0200 Subject: [PATCH 1123/1151] Bump version to 2023.9.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 12a12aea631..397b6ea1ab8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index dc9d314fe4d..1e7899afd52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b3" +version = "2023.9.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ba5822bed4775b51d2a84f8187fe2146345f9137 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 3 Sep 2023 10:25:00 +0200 Subject: [PATCH 1124/1151] Bump gardena_bluetooth to 1.4.0 (#99530) --- homeassistant/components/gardena_bluetooth/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json index 5d1c1888586..3e07eb1ad42 100644 --- a/homeassistant/components/gardena_bluetooth/manifest.json +++ b/homeassistant/components/gardena_bluetooth/manifest.json @@ -13,5 +13,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth", "iot_class": "local_polling", - "requirements": ["gardena_bluetooth==1.3.0"] + "requirements": ["gardena_bluetooth==1.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0960f774960..1572d523409 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -835,7 +835,7 @@ fritzconnection[qr]==1.12.2 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena_bluetooth==1.3.0 +gardena_bluetooth==1.4.0 # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a28b186f40..37970b023d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -654,7 +654,7 @@ fritzconnection[qr]==1.12.2 gTTS==2.2.4 # homeassistant.components.gardena_bluetooth -gardena_bluetooth==1.3.0 +gardena_bluetooth==1.4.0 # homeassistant.components.google_assistant_sdk gassist-text==0.0.10 From cab9c9759831f394f66f3223aebc2991c8cb2b28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Sep 2023 09:13:34 -0500 Subject: [PATCH 1125/1151] Bump aioesphomeapi to 16.0.4 (#99541) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 7d552f340f0..fd6fde0cb05 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async_interrupt==1.1.1", - "aioesphomeapi==16.0.3", + "aioesphomeapi==16.0.4", "bluetooth-data-tools==1.11.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 1572d523409..45972fad83b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==16.0.3 +aioesphomeapi==16.0.4 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37970b023d3..cae833a3b5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==16.0.3 +aioesphomeapi==16.0.4 # homeassistant.components.flo aioflo==2021.11.0 From 4c0e4fe74584daafec8af846032a7d7c12cc8b77 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Sep 2023 22:19:40 +0200 Subject: [PATCH 1126/1151] Small cleanup of TemplateEnvironment (#99571) * Small cleanup of TemplateEnvironment * Fix typo --- homeassistant/helpers/template.py | 57 +++++++++++++++++-------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 40d64ba37ae..b5a6a45e97f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -492,7 +492,7 @@ class Template: if ret is None: ret = self.hass.data[wanted_env] = TemplateEnvironment( self.hass, - self._limited, # type: ignore[no-untyped-call] + self._limited, self._strict, ) return ret @@ -2276,7 +2276,12 @@ class HassLoader(jinja2.BaseLoader): class TemplateEnvironment(ImmutableSandboxedEnvironment): """The Home Assistant template environment.""" - def __init__(self, hass, limited=False, strict=False): + def __init__( + self, + hass: HomeAssistant | None, + limited: bool | None = False, + strict: bool | None = False, + ) -> None: """Initialise template environment.""" undefined: type[LoggingUndefined] | type[jinja2.StrictUndefined] if not strict: @@ -2381,6 +2386,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): # can be discarded, we only need to get at the hass object. def hassfunction( func: Callable[Concatenate[HomeAssistant, _P], _R], + jinja_context: Callable[ + [Callable[Concatenate[Any, _P], _R]], + Callable[Concatenate[Any, _P], _R], + ] = pass_context, ) -> Callable[Concatenate[Any, _P], _R]: """Wrap function that depend on hass.""" @@ -2388,42 +2397,40 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): def wrapper(_: Any, *args: _P.args, **kwargs: _P.kwargs) -> _R: return func(hass, *args, **kwargs) - return pass_context(wrapper) + return jinja_context(wrapper) self.globals["device_entities"] = hassfunction(device_entities) - self.filters["device_entities"] = pass_context(self.globals["device_entities"]) + self.filters["device_entities"] = self.globals["device_entities"] self.globals["device_attr"] = hassfunction(device_attr) - self.filters["device_attr"] = pass_context(self.globals["device_attr"]) + self.filters["device_attr"] = self.globals["device_attr"] self.globals["is_device_attr"] = hassfunction(is_device_attr) - self.tests["is_device_attr"] = pass_eval_context(self.globals["is_device_attr"]) + self.tests["is_device_attr"] = hassfunction(is_device_attr, pass_eval_context) self.globals["config_entry_id"] = hassfunction(config_entry_id) - self.filters["config_entry_id"] = pass_context(self.globals["config_entry_id"]) + self.filters["config_entry_id"] = self.globals["config_entry_id"] self.globals["device_id"] = hassfunction(device_id) - self.filters["device_id"] = pass_context(self.globals["device_id"]) + self.filters["device_id"] = self.globals["device_id"] self.globals["areas"] = hassfunction(areas) - self.filters["areas"] = pass_context(self.globals["areas"]) + self.filters["areas"] = self.globals["areas"] self.globals["area_id"] = hassfunction(area_id) - self.filters["area_id"] = pass_context(self.globals["area_id"]) + self.filters["area_id"] = self.globals["area_id"] self.globals["area_name"] = hassfunction(area_name) - self.filters["area_name"] = pass_context(self.globals["area_name"]) + self.filters["area_name"] = self.globals["area_name"] self.globals["area_entities"] = hassfunction(area_entities) - self.filters["area_entities"] = pass_context(self.globals["area_entities"]) + self.filters["area_entities"] = self.globals["area_entities"] self.globals["area_devices"] = hassfunction(area_devices) - self.filters["area_devices"] = pass_context(self.globals["area_devices"]) + self.filters["area_devices"] = self.globals["area_devices"] self.globals["integration_entities"] = hassfunction(integration_entities) - self.filters["integration_entities"] = pass_context( - self.globals["integration_entities"] - ) + self.filters["integration_entities"] = self.globals["integration_entities"] if limited: # Only device_entities is available to limited templates, mark other @@ -2479,25 +2486,25 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return self.globals["expand"] = hassfunction(expand) - self.filters["expand"] = pass_context(self.globals["expand"]) + self.filters["expand"] = self.globals["expand"] self.globals["closest"] = hassfunction(closest) - self.filters["closest"] = pass_context(hassfunction(closest_filter)) + self.filters["closest"] = hassfunction(closest_filter) self.globals["distance"] = hassfunction(distance) self.globals["is_hidden_entity"] = hassfunction(is_hidden_entity) - self.tests["is_hidden_entity"] = pass_eval_context( - self.globals["is_hidden_entity"] + self.tests["is_hidden_entity"] = hassfunction( + is_hidden_entity, pass_eval_context ) self.globals["is_state"] = hassfunction(is_state) - self.tests["is_state"] = pass_eval_context(self.globals["is_state"]) + self.tests["is_state"] = hassfunction(is_state, pass_eval_context) self.globals["is_state_attr"] = hassfunction(is_state_attr) - self.tests["is_state_attr"] = pass_eval_context(self.globals["is_state_attr"]) + self.tests["is_state_attr"] = hassfunction(is_state_attr, pass_eval_context) self.globals["state_attr"] = hassfunction(state_attr) self.filters["state_attr"] = self.globals["state_attr"] self.globals["states"] = AllStates(hass) self.filters["states"] = self.globals["states"] self.globals["has_value"] = hassfunction(has_value) - self.filters["has_value"] = pass_context(self.globals["has_value"]) - self.tests["has_value"] = pass_eval_context(self.globals["has_value"]) + self.filters["has_value"] = self.globals["has_value"] + self.tests["has_value"] = hassfunction(has_value, pass_eval_context) self.globals["utcnow"] = hassfunction(utcnow) self.globals["now"] = hassfunction(now) self.globals["relative_time"] = hassfunction(relative_time) @@ -2575,4 +2582,4 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return cached -_NO_HASS_ENV = TemplateEnvironment(None) # type: ignore[no-untyped-call] +_NO_HASS_ENV = TemplateEnvironment(None) From 2a7c12013fc87d979747f39768f4abd66c70ca2f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 4 Sep 2023 22:46:19 +0200 Subject: [PATCH 1127/1151] Fix not stripping no device class in template helper binary sensor (#99640) Strip none template helper binary sensor --- homeassistant/components/template/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index b2ccddedad8..ccc06989c71 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -208,6 +208,7 @@ def validate_user_input( ]: """Do post validation of user input. + For binary sensors: Strip none-sentinels. For sensors: Strip none-sentinels and validate unit of measurement. For all domaines: Set template type. """ @@ -217,8 +218,9 @@ def validate_user_input( user_input: dict[str, Any], ) -> dict[str, Any]: """Add template type to user input.""" - if template_type == Platform.SENSOR: + if template_type in (Platform.BINARY_SENSOR, Platform.SENSOR): _strip_sentinel(user_input) + if template_type == Platform.SENSOR: _validate_unit(user_input) _validate_state_class(user_input) return {"template_type": template_type} | user_input From f0cf539e15c517755f1047afac7b9cd7b42b8474 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 4 Sep 2023 22:31:53 +0200 Subject: [PATCH 1128/1151] Fix missing unique id in SQL (#99641) --- homeassistant/components/sql/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index f4f44d4f9a4..3fdc6b2c079 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -123,7 +123,7 @@ async def async_setup_entry( value_template.hass = hass name_template = Template(name, hass) - trigger_entity_config = {CONF_NAME: name_template} + trigger_entity_config = {CONF_NAME: name_template, CONF_UNIQUE_ID: entry.entry_id} for key in TRIGGER_ENTITY_OPTIONS: if key not in entry.options: continue From c040672cabb580763990ca383618e7f6291a918f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 5 Sep 2023 19:42:19 +0900 Subject: [PATCH 1129/1151] Update aioairzone to v0.6.8 (#99644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update aioairzone to v0.6.8 Signed-off-by: Álvaro Fernández Rojas * Trigger CI --------- Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index bb1e448c8eb..c0b24b2cc3e 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.7"] + "requirements": ["aioairzone==0.6.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 45972fad83b..ca0f713e423 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -188,7 +188,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.1 # homeassistant.components.airzone -aioairzone==0.6.7 +aioairzone==0.6.8 # homeassistant.components.ambient_station aioambient==2023.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cae833a3b5b..b1c8207303f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ aioairq==0.2.4 aioairzone-cloud==0.2.1 # homeassistant.components.airzone -aioairzone==0.6.7 +aioairzone==0.6.8 # homeassistant.components.ambient_station aioambient==2023.04.0 From 9e03f8a8d6cf56e93e5145c41ef5588487182846 Mon Sep 17 00:00:00 2001 From: itpeters <59966384+itpeters@users.noreply.github.com> Date: Tue, 5 Sep 2023 05:10:14 -0600 Subject: [PATCH 1130/1151] Fix long press event for matter generic switch (#99645) --- homeassistant/components/matter/event.py | 2 +- tests/components/matter/test_event.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/event.py b/homeassistant/components/matter/event.py index 3a1faa6dcbe..84049301296 100644 --- a/homeassistant/components/matter/event.py +++ b/homeassistant/components/matter/event.py @@ -65,7 +65,7 @@ class MatterEventEntity(MatterEntity, EventEntity): if feature_map & SwitchFeature.kMomentarySwitchRelease: event_types.append("short_release") if feature_map & SwitchFeature.kMomentarySwitchLongPress: - event_types.append("long_press_ongoing") + event_types.append("long_press") event_types.append("long_release") if feature_map & SwitchFeature.kMomentarySwitchMultiPress: event_types.append("multi_press_ongoing") diff --git a/tests/components/matter/test_event.py b/tests/components/matter/test_event.py index 0d5891a7778..0aa9385a74c 100644 --- a/tests/components/matter/test_event.py +++ b/tests/components/matter/test_event.py @@ -48,7 +48,7 @@ async def test_generic_switch_node( assert state.attributes[ATTR_EVENT_TYPES] == [ "initial_press", "short_release", - "long_press_ongoing", + "long_press", "long_release", "multi_press_ongoing", "multi_press_complete", @@ -111,7 +111,7 @@ async def test_generic_switch_multi_node( assert state_button_1.attributes[ATTR_EVENT_TYPES] == [ "initial_press", "short_release", - "long_press_ongoing", + "long_press", "long_release", ] # check button 2 From 5d1fe0eb00bfd3bae2d1b7c3a3efaa699f0f5f87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Sep 2023 19:56:34 -0500 Subject: [PATCH 1131/1151] Fix mobile app dispatcher performance (#99647) Fix mobile app thundering heard The mobile_app would setup a dispatcher to listener for updates on every entity and reject the ones that were not for the unique id that it was intrested in. Instead we now register for a signal per unique id since we were previously generating O(entities*sensors*devices) callbacks which was causing the event loop to stall when there were a large number of mobile app users. --- homeassistant/components/mobile_app/entity.py | 13 +++++++------ homeassistant/components/mobile_app/webhook.py | 5 ++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 3a2f038a0af..120014d1d52 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -1,6 +1,8 @@ """A entity class for mobile_app.""" from __future__ import annotations +from typing import Any + from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, STATE_UNAVAILABLE from homeassistant.core import callback @@ -36,7 +38,9 @@ class MobileAppEntity(RestoreEntity): """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( - self.hass, SIGNAL_SENSOR_UPDATE, self._handle_update + self.hass, + f"{SIGNAL_SENSOR_UPDATE}-{self._attr_unique_id}", + self._handle_update, ) ) @@ -96,10 +100,7 @@ class MobileAppEntity(RestoreEntity): return self._config.get(ATTR_SENSOR_STATE) != STATE_UNAVAILABLE @callback - def _handle_update(self, incoming_id, data): + def _handle_update(self, data: dict[str, Any]) -> None: """Handle async event updates.""" - if incoming_id != self._attr_unique_id: - return - - self._config = {**self._config, **data} + self._config.update(data) self.async_write_ha_state() diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 62417b0873a..1a56b13ddc5 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -607,7 +607,7 @@ async def webhook_register_sensor( if changes: entity_registry.async_update_entity(existing_sensor, **changes) - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, unique_store_key, data) + async_dispatcher_send(hass, f"{SIGNAL_SENSOR_UPDATE}-{unique_store_key}", data) else: data[CONF_UNIQUE_ID] = unique_store_key data[ @@ -693,8 +693,7 @@ async def webhook_update_sensor_states( sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] async_dispatcher_send( hass, - SIGNAL_SENSOR_UPDATE, - unique_store_key, + f"{SIGNAL_SENSOR_UPDATE}-{unique_store_key}", sensor, ) From bd0fd9db77b8a798cdf2edb1cb33200c81355b8f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 5 Sep 2023 09:58:32 -0400 Subject: [PATCH 1132/1151] Bump zwave-js-server-python to 0.51.1 (#99652) * Bump zwave-js-server-python to 0.51.1 * Update test --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 5 ++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 73fa41a8cca..080074451bd 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.51.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.51.1"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index ca0f713e423..1cf89dd95fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2799,7 +2799,7 @@ zigpy==0.57.1 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.51.0 +zwave-js-server-python==0.51.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1c8207303f..774d70d08e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2060,7 +2060,7 @@ zigpy-znp==0.11.4 zigpy==0.57.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.51.0 +zwave-js-server-python==0.51.1 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index e686def8883..02ed507cabe 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3679,7 +3679,6 @@ async def test_abort_firmware_update( ws_client = await hass_ws_client(hass) device = get_device(hass, multisensor_6) - client.async_send_command.return_value = {} await ws_client.send_json( { ID: 1, @@ -3690,8 +3689,8 @@ async def test_abort_firmware_update( msg = await ws_client.receive_json() assert msg["success"] - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "node.abort_firmware_update" assert args["nodeId"] == multisensor_6.node_id From fed84ebc4010a2de5b0164c30ec84ed96f2cba8c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 5 Sep 2023 20:12:40 +0200 Subject: [PATCH 1133/1151] Update frontend to 20230905.0 (#99677) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 156adfa73d2..627b36a59b8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230904.0"] + "requirements": ["home-assistant-frontend==20230905.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 87ed1537205..5f939ea9763 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230904.0 +home-assistant-frontend==20230905.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1cf89dd95fe..11f404b6394 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230904.0 +home-assistant-frontend==20230905.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 774d70d08e4..581e8f3d325 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230904.0 +home-assistant-frontend==20230905.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From 6b8027019bb0f57b2c63a390fc46d2df723734e7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 5 Sep 2023 20:25:02 +0200 Subject: [PATCH 1134/1151] Bump version to 2023.9.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 397b6ea1ab8..a851c041398 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 1e7899afd52..d4a71cd6abe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b4" +version = "2023.9.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9d87e8d02b68b96f16d56a9b5673ef9790147cec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 10:03:35 +0200 Subject: [PATCH 1135/1151] Allow specifying a custom log function for template render (#99572) * Allow specifying a custom log function for template render * Bypass template cache when reporting errors + fix tests * Send errors as events * Fix logic for creating new TemplateEnvironment * Add strict mode back * Only send error events if report_errors is True * Force test of websocket_api only * Debug test * Run pytest with higher verbosity * Timeout after 1 minute, enable syslog output * Adjust timeout * Add debug logs * Fix unsafe call to WebSocketHandler._send_message * Remove debug code * Improve test coverage * Revert accidental change * Include severity in error events * Remove redundant information from error events --- .../components/websocket_api/commands.py | 32 +- homeassistant/helpers/event.py | 17 +- homeassistant/helpers/template.py | 113 ++++--- .../components/websocket_api/test_commands.py | 278 ++++++++++++++++-- tests/helpers/test_template.py | 18 +- 5 files changed, 374 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 84c7567a40e..7772bef66f9 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -5,6 +5,7 @@ from collections.abc import Callable import datetime as dt from functools import lru_cache, partial import json +import logging from typing import Any, cast import voluptuous as vol @@ -505,6 +506,7 @@ def _cached_template(template_str: str, hass: HomeAssistant) -> template.Templat vol.Optional("variables"): dict, vol.Optional("timeout"): vol.Coerce(float), vol.Optional("strict", default=False): bool, + vol.Optional("report_errors", default=False): bool, } ) @decorators.async_response @@ -513,14 +515,32 @@ async def handle_render_template( ) -> None: """Handle render_template command.""" template_str = msg["template"] - template_obj = _cached_template(template_str, hass) + report_errors: bool = msg["report_errors"] + if report_errors: + template_obj = template.Template(template_str, hass) + else: + template_obj = _cached_template(template_str, hass) variables = msg.get("variables") timeout = msg.get("timeout") + @callback + def _error_listener(level: int, template_error: str) -> None: + connection.send_message( + messages.event_message( + msg["id"], + {"error": template_error, "level": logging.getLevelName(level)}, + ) + ) + + @callback + def _thread_safe_error_listener(level: int, template_error: str) -> None: + hass.loop.call_soon_threadsafe(_error_listener, level, template_error) + if timeout: try: + log_fn = _thread_safe_error_listener if report_errors else None timed_out = await template_obj.async_render_will_timeout( - timeout, variables, strict=msg["strict"] + timeout, variables, strict=msg["strict"], log_fn=log_fn ) except TemplateError as ex: connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) @@ -542,7 +562,11 @@ async def handle_render_template( track_template_result = updates.pop() result = track_template_result.result if isinstance(result, TemplateError): - connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(result)) + if not report_errors: + return + connection.send_message( + messages.event_message(msg["id"], {"error": str(result)}) + ) return connection.send_message( @@ -552,12 +576,14 @@ async def handle_render_template( ) try: + log_fn = _error_listener if report_errors else None info = async_track_template_result( hass, [TrackTemplate(template_obj, variables)], _template_listener, raise_on_template_error=True, strict=msg["strict"], + log_fn=log_fn, ) except TemplateError as ex: connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex)) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 62a3b91991d..173dd057f96 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -915,7 +915,12 @@ class TrackTemplateResultInfo: """Return the representation.""" return f"" - def async_setup(self, raise_on_template_error: bool, strict: bool = False) -> None: + def async_setup( + self, + raise_on_template_error: bool, + strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, + ) -> None: """Activation of template tracking.""" block_render = False super_template = self._track_templates[0] if self._has_super_template else None @@ -925,7 +930,7 @@ class TrackTemplateResultInfo: template = super_template.template variables = super_template.variables self._info[template] = info = template.async_render_to_info( - variables, strict=strict + variables, strict=strict, log_fn=log_fn ) # If the super template did not render to True, don't update other templates @@ -946,7 +951,7 @@ class TrackTemplateResultInfo: template = track_template_.template variables = track_template_.variables self._info[template] = info = template.async_render_to_info( - variables, strict=strict + variables, strict=strict, log_fn=log_fn ) if info.exception: @@ -1233,6 +1238,7 @@ def async_track_template_result( action: TrackTemplateResultListener, raise_on_template_error: bool = False, strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, has_super_template: bool = False, ) -> TrackTemplateResultInfo: """Add a listener that fires when the result of a template changes. @@ -1264,6 +1270,9 @@ def async_track_template_result( tracking. strict When set to True, raise on undefined variables. + log_fn + If not None, template error messages will logging by calling log_fn + instead of the normal logging facility. has_super_template When set to True, the first template will block rendering of other templates if it doesn't render as True. @@ -1274,7 +1283,7 @@ def async_track_template_result( """ tracker = TrackTemplateResultInfo(hass, track_templates, action, has_super_template) - tracker.async_setup(raise_on_template_error, strict=strict) + tracker.async_setup(raise_on_template_error, strict=strict, log_fn=log_fn) return tracker diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index b5a6a45e97f..9f280db6c98 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -458,6 +458,7 @@ class Template: "_exc_info", "_limited", "_strict", + "_log_fn", "_hash_cache", "_renders", ) @@ -475,6 +476,7 @@ class Template: self._exc_info: sys._OptExcInfo | None = None self._limited: bool | None = None self._strict: bool | None = None + self._log_fn: Callable[[int, str], None] | None = None self._hash_cache: int = hash(self.template) self._renders: int = 0 @@ -482,6 +484,11 @@ class Template: def _env(self) -> TemplateEnvironment: if self.hass is None: return _NO_HASS_ENV + # Bypass cache if a custom log function is specified + if self._log_fn is not None: + return TemplateEnvironment( + self.hass, self._limited, self._strict, self._log_fn + ) if self._limited: wanted_env = _ENVIRONMENT_LIMITED elif self._strict: @@ -491,9 +498,7 @@ class Template: ret: TemplateEnvironment | None = self.hass.data.get(wanted_env) if ret is None: ret = self.hass.data[wanted_env] = TemplateEnvironment( - self.hass, - self._limited, - self._strict, + self.hass, self._limited, self._strict, self._log_fn ) return ret @@ -537,6 +542,7 @@ class Template: parse_result: bool = True, limited: bool = False, strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, **kwargs: Any, ) -> Any: """Render given template. @@ -553,7 +559,7 @@ class Template: return self.template return self._parse_result(self.template) - compiled = self._compiled or self._ensure_compiled(limited, strict) + compiled = self._compiled or self._ensure_compiled(limited, strict, log_fn) if variables is not None: kwargs.update(variables) @@ -608,6 +614,7 @@ class Template: timeout: float, variables: TemplateVarsType = None, strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, **kwargs: Any, ) -> bool: """Check to see if rendering a template will timeout during render. @@ -628,7 +635,7 @@ class Template: if self.is_static: return False - compiled = self._compiled or self._ensure_compiled(strict=strict) + compiled = self._compiled or self._ensure_compiled(strict=strict, log_fn=log_fn) if variables is not None: kwargs.update(variables) @@ -664,7 +671,11 @@ class Template: @callback def async_render_to_info( - self, variables: TemplateVarsType = None, strict: bool = False, **kwargs: Any + self, + variables: TemplateVarsType = None, + strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, + **kwargs: Any, ) -> RenderInfo: """Render the template and collect an entity filter.""" self._renders += 1 @@ -680,7 +691,9 @@ class Template: token = _render_info.set(render_info) try: - render_info._result = self.async_render(variables, strict=strict, **kwargs) + render_info._result = self.async_render( + variables, strict=strict, log_fn=log_fn, **kwargs + ) except TemplateError as ex: render_info.exception = ex finally: @@ -743,7 +756,10 @@ class Template: return value if error_value is _SENTINEL else error_value def _ensure_compiled( - self, limited: bool = False, strict: bool = False + self, + limited: bool = False, + strict: bool = False, + log_fn: Callable[[int, str], None] | None = None, ) -> jinja2.Template: """Bind a template to a specific hass instance.""" self.ensure_valid() @@ -756,10 +772,14 @@ class Template: self._strict is None or self._strict == strict ), "can't change between strict and non strict template" assert not (strict and limited), "can't combine strict and limited template" + assert ( + self._log_fn is None or self._log_fn == log_fn + ), "can't change custom log function" assert self._compiled_code is not None, "template code was not compiled" self._limited = limited self._strict = strict + self._log_fn = log_fn env = self._env self._compiled = jinja2.Template.from_code( @@ -2178,45 +2198,56 @@ def _render_with_context( return template.render(**kwargs) -class LoggingUndefined(jinja2.Undefined): +def make_logging_undefined( + strict: bool | None, log_fn: Callable[[int, str], None] | None +) -> type[jinja2.Undefined]: """Log on undefined variables.""" - def _log_message(self) -> None: + if strict: + return jinja2.StrictUndefined + + def _log_with_logger(level: int, msg: str) -> None: template, action = template_cv.get() or ("", "rendering or compiling") - _LOGGER.warning( - "Template variable warning: %s when %s '%s'", - self._undefined_message, + _LOGGER.log( + level, + "Template variable %s: %s when %s '%s'", + logging.getLevelName(level).lower(), + msg, action, template, ) - def _fail_with_undefined_error(self, *args, **kwargs): - try: - return super()._fail_with_undefined_error(*args, **kwargs) - except self._undefined_exception as ex: - template, action = template_cv.get() or ("", "rendering or compiling") - _LOGGER.error( - "Template variable error: %s when %s '%s'", - self._undefined_message, - action, - template, - ) - raise ex + _log_fn = log_fn or _log_with_logger - def __str__(self) -> str: - """Log undefined __str___.""" - self._log_message() - return super().__str__() + class LoggingUndefined(jinja2.Undefined): + """Log on undefined variables.""" - def __iter__(self): - """Log undefined __iter___.""" - self._log_message() - return super().__iter__() + def _log_message(self) -> None: + _log_fn(logging.WARNING, self._undefined_message) - def __bool__(self) -> bool: - """Log undefined __bool___.""" - self._log_message() - return super().__bool__() + def _fail_with_undefined_error(self, *args, **kwargs): + try: + return super()._fail_with_undefined_error(*args, **kwargs) + except self._undefined_exception as ex: + _log_fn(logging.ERROR, self._undefined_message) + raise ex + + def __str__(self) -> str: + """Log undefined __str___.""" + self._log_message() + return super().__str__() + + def __iter__(self): + """Log undefined __iter___.""" + self._log_message() + return super().__iter__() + + def __bool__(self) -> bool: + """Log undefined __bool___.""" + self._log_message() + return super().__bool__() + + return LoggingUndefined async def async_load_custom_templates(hass: HomeAssistant) -> None: @@ -2281,14 +2312,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): hass: HomeAssistant | None, limited: bool | None = False, strict: bool | None = False, + log_fn: Callable[[int, str], None] | None = None, ) -> None: """Initialise template environment.""" - undefined: type[LoggingUndefined] | type[jinja2.StrictUndefined] - if not strict: - undefined = LoggingUndefined - else: - undefined = jinja2.StrictUndefined - super().__init__(undefined=undefined) + super().__init__(undefined=make_logging_undefined(strict, log_fn)) self.hass = hass self.template_cache: weakref.WeakValueDictionary[ str | jinja2.nodes.Template, CodeType | str | None diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 73baa968ab6..96e79a81716 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -2,6 +2,7 @@ import asyncio from copy import deepcopy import datetime +import logging from unittest.mock import ANY, AsyncMock, Mock, patch import pytest @@ -33,7 +34,11 @@ from tests.common import ( async_mock_service, mock_platform, ) -from tests.typing import ClientSessionGenerator, WebSocketGenerator +from tests.typing import ( + ClientSessionGenerator, + MockHAClientWebSocket, + WebSocketGenerator, +) STATE_KEY_SHORT_NAMES = { "entity_id": "e", @@ -1225,46 +1230,187 @@ async def test_render_template_manual_entity_ids_no_longer_needed( } +EMPTY_LISTENERS = {"all": False, "entities": [], "domains": [], "time": False} + +ERR_MSG = {"type": "result", "success": False} + +VARIABLE_ERROR_UNDEFINED_FUNC = { + "error": "'my_unknown_func' is undefined", + "level": "ERROR", +} +TEMPLATE_ERROR_UNDEFINED_FUNC = { + "code": "template_error", + "message": "UndefinedError: 'my_unknown_func' is undefined", +} + +VARIABLE_WARNING_UNDEFINED_VAR = { + "error": "'my_unknown_var' is undefined", + "level": "WARNING", +} +TEMPLATE_ERROR_UNDEFINED_VAR = { + "code": "template_error", + "message": "UndefinedError: 'my_unknown_var' is undefined", +} + +TEMPLATE_ERROR_UNDEFINED_FILTER = { + "code": "template_error", + "message": "TemplateAssertionError: No filter named 'unknown_filter'.", +} + + @pytest.mark.parametrize( - "template", + ("template", "expected_events"), [ - "{{ my_unknown_func() + 1 }}", - "{{ my_unknown_var }}", - "{{ my_unknown_var + 1 }}", - "{{ now() | unknown_filter }}", + ( + "{{ my_unknown_func() + 1 }}", + [ + {"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC}, + ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}, + ], + ), + ( + "{{ my_unknown_var }}", + [ + {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + { + "type": "event", + "event": {"result": "", "listeners": EMPTY_LISTENERS}, + }, + ], + ), + ( + "{{ my_unknown_var + 1 }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + ), + ( + "{{ now() | unknown_filter }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}], + ), ], ) async def test_render_template_with_error( - hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture, template + hass: HomeAssistant, + websocket_client: MockHAClientWebSocket, + caplog: pytest.LogCaptureFixture, + template: str, + expected_events: list[dict[str, str]], ) -> None: """Test a template with an error.""" + caplog.set_level(logging.INFO) await websocket_client.send_json( - {"id": 5, "type": "render_template", "template": template, "strict": True} + { + "id": 5, + "type": "render_template", + "template": template, + "report_errors": True, + } ) - msg = await websocket_client.receive_json() - assert msg["id"] == 5 - assert msg["type"] == const.TYPE_RESULT - assert not msg["success"] - assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + for expected_event in expected_events: + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + for key, value in expected_event.items(): + assert msg[key] == value assert "Template variable error" not in caplog.text + assert "Template variable warning" not in caplog.text assert "TemplateError" not in caplog.text @pytest.mark.parametrize( - "template", + ("template", "expected_events"), [ - "{{ my_unknown_func() + 1 }}", - "{{ my_unknown_var }}", - "{{ my_unknown_var + 1 }}", - "{{ now() | unknown_filter }}", + ( + "{{ my_unknown_func() + 1 }}", + [ + {"type": "event", "event": VARIABLE_ERROR_UNDEFINED_FUNC}, + ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}, + ], + ), + ( + "{{ my_unknown_var }}", + [ + {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + {"type": "result", "success": True, "result": None}, + {"type": "event", "event": VARIABLE_WARNING_UNDEFINED_VAR}, + { + "type": "event", + "event": {"result": "", "listeners": EMPTY_LISTENERS}, + }, + ], + ), + ( + "{{ my_unknown_var + 1 }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + ), + ( + "{{ now() | unknown_filter }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}], + ), ], ) async def test_render_template_with_timeout_and_error( - hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture, template + hass: HomeAssistant, + websocket_client, + caplog: pytest.LogCaptureFixture, + template: str, + expected_events: list[dict[str, str]], ) -> None: """Test a template with an error with a timeout.""" + caplog.set_level(logging.INFO) + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": template, + "timeout": 5, + "report_errors": True, + } + ) + + for expected_event in expected_events: + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + for key, value in expected_event.items(): + assert msg[key] == value + + assert "Template variable error" not in caplog.text + assert "Template variable warning" not in caplog.text + assert "TemplateError" not in caplog.text + + +@pytest.mark.parametrize( + ("template", "expected_events"), + [ + ( + "{{ my_unknown_func() + 1 }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FUNC}], + ), + ( + "{{ my_unknown_var }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + ), + ( + "{{ my_unknown_var + 1 }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_VAR}], + ), + ( + "{{ now() | unknown_filter }}", + [ERR_MSG | {"error": TEMPLATE_ERROR_UNDEFINED_FILTER}], + ), + ], +) +async def test_render_template_strict_with_timeout_and_error( + hass: HomeAssistant, + websocket_client, + caplog: pytest.LogCaptureFixture, + template: str, + expected_events: list[dict[str, str]], +) -> None: + """Test a template with an error with a timeout.""" + caplog.set_level(logging.INFO) await websocket_client.send_json( { "id": 5, @@ -1275,13 +1421,14 @@ async def test_render_template_with_timeout_and_error( } ) - msg = await websocket_client.receive_json() - assert msg["id"] == 5 - assert msg["type"] == const.TYPE_RESULT - assert not msg["success"] - assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + for expected_event in expected_events: + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + for key, value in expected_event.items(): + assert msg[key] == value assert "Template variable error" not in caplog.text + assert "Template variable warning" not in caplog.text assert "TemplateError" not in caplog.text @@ -1299,13 +1446,19 @@ async def test_render_template_error_in_template_code( assert not msg["success"] assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + assert "Template variable error" not in caplog.text + assert "Template variable warning" not in caplog.text assert "TemplateError" not in caplog.text async def test_render_template_with_delayed_error( hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture ) -> None: - """Test a template with an error that only happens after a state change.""" + """Test a template with an error that only happens after a state change. + + In this test report_errors is enabled. + """ + caplog.set_level(logging.INFO) hass.states.async_set("sensor.test", "on") await hass.async_block_till_done() @@ -1318,12 +1471,16 @@ async def test_render_template_with_delayed_error( """ await websocket_client.send_json( - {"id": 5, "type": "render_template", "template": template_str} + { + "id": 5, + "type": "render_template", + "template": template_str, + "report_errors": True, + } ) await hass.async_block_till_done() msg = await websocket_client.receive_json() - assert msg["id"] == 5 assert msg["type"] == const.TYPE_RESULT assert msg["success"] @@ -1347,13 +1504,74 @@ async def test_render_template_with_delayed_error( msg = await websocket_client.receive_json() assert msg["id"] == 5 - assert msg["type"] == const.TYPE_RESULT - assert not msg["success"] - assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR + assert msg["type"] == "event" + event = msg["event"] + assert event["error"] == "'None' has no attribute 'state'" + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"error": "UndefinedError: 'explode' is undefined"} + + assert "Template variable error" not in caplog.text + assert "Template variable warning" not in caplog.text assert "TemplateError" not in caplog.text +async def test_render_template_with_delayed_error_2( + hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture +) -> None: + """Test a template with an error that only happens after a state change. + + In this test report_errors is disabled. + """ + hass.states.async_set("sensor.test", "on") + await hass.async_block_till_done() + + template_str = """ +{% if states.sensor.test.state %} + on +{% else %} + {{ explode + 1 }} +{% endif %} + """ + + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": template_str, + "report_errors": False, + } + ) + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + hass.states.async_remove("sensor.test") + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == { + "result": "on", + "listeners": { + "all": False, + "domains": [], + "entities": ["sensor.test"], + "time": False, + }, + } + + assert "Template variable warning" in caplog.text + + async def test_render_template_with_timeout( hass: HomeAssistant, websocket_client, caplog: pytest.LogCaptureFixture ) -> None: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index d14496d321e..58e0c730165 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -4466,15 +4466,25 @@ async def test_parse_result(hass: HomeAssistant) -> None: assert template.Template(tpl, hass).async_render() == result -async def test_undefined_variable( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture +@pytest.mark.parametrize( + "template_string", + [ + "{{ no_such_variable }}", + "{{ no_such_variable and True }}", + "{{ no_such_variable | join(', ') }}", + ], +) +async def test_undefined_symbol_warnings( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + template_string: str, ) -> None: """Test a warning is logged on undefined variables.""" - tpl = template.Template("{{ no_such_variable }}", hass) + tpl = template.Template(template_string, hass) assert tpl.async_render() == "" assert ( "Template variable warning: 'no_such_variable' is undefined when rendering " - "'{{ no_such_variable }}'" in caplog.text + f"'{template_string}'" in caplog.text ) From 0cbcacbbf5439f69a37574ca7ae5ab58fb867e1d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 09:49:42 +0200 Subject: [PATCH 1136/1151] Include template listener info in template preview (#99669) --- .../components/template/config_flow.py | 3 +- .../components/template/template_entity.py | 34 ++++++++++++++----- .../helpers/trigger_template_entity.py | 6 ++-- tests/components/template/test_config_flow.py | 27 +++++++++++++++ 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index ccc06989c71..093cbf14098 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -349,6 +349,7 @@ def ws_start_preview( def async_preview_updated( state: str | None, attributes: Mapping[str, Any] | None, + listeners: dict[str, bool | set[str]] | None, error: str | None, ) -> None: """Forward config entry state events to websocket.""" @@ -363,7 +364,7 @@ def ws_start_preview( connection.send_message( websocket_api.event_message( msg["id"], - {"attributes": attributes, "state": state}, + {"attributes": attributes, "listeners": listeners, "state": state}, ) ) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index c33674fa86f..2ce42083117 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -34,6 +34,7 @@ from homeassistant.helpers.event import ( EventStateChangedData, TrackTemplate, TrackTemplateResult, + TrackTemplateResultInfo, async_track_template_result, ) from homeassistant.helpers.script import Script, _VarsType @@ -260,12 +261,18 @@ class TemplateEntity(Entity): ) -> None: """Template Entity.""" self._template_attrs: dict[Template, list[_TemplateAttribute]] = {} - self._async_update: Callable[[], None] | None = None + self._template_result_info: TrackTemplateResultInfo | None = None self._attr_extra_state_attributes = {} self._self_ref_update_count = 0 self._attr_unique_id = unique_id self._preview_callback: Callable[ - [str | None, dict[str, Any] | None, str | None], None + [ + str | None, + dict[str, Any] | None, + dict[str, bool | set[str]] | None, + str | None, + ], + None, ] | None = None if config is None: self._attribute_templates = attribute_templates @@ -427,9 +434,12 @@ class TemplateEntity(Entity): state, attrs = self._async_generate_attributes() validate_state(state) except Exception as err: # pylint: disable=broad-exception-caught - self._preview_callback(None, None, str(err)) + self._preview_callback(None, None, None, str(err)) else: - self._preview_callback(state, attrs, None) + assert self._template_result_info + self._preview_callback( + state, attrs, self._template_result_info.listeners, None + ) @callback def _async_template_startup(self, *_: Any) -> None: @@ -460,7 +470,7 @@ class TemplateEntity(Entity): has_super_template=has_availability_template, ) self.async_on_remove(result_info.async_remove) - self._async_update = result_info.async_refresh + self._template_result_info = result_info result_info.async_refresh() @callback @@ -494,7 +504,13 @@ class TemplateEntity(Entity): def async_start_preview( self, preview_callback: Callable[ - [str | None, Mapping[str, Any] | None, str | None], None + [ + str | None, + Mapping[str, Any] | None, + dict[str, bool | set[str]] | None, + str | None, + ], + None, ], ) -> CALLBACK_TYPE: """Render a preview.""" @@ -504,7 +520,7 @@ class TemplateEntity(Entity): try: self._async_template_startup() except Exception as err: # pylint: disable=broad-exception-caught - preview_callback(None, None, str(err)) + preview_callback(None, None, None, str(err)) return self._call_on_remove_callbacks async def async_added_to_hass(self) -> None: @@ -521,8 +537,8 @@ class TemplateEntity(Entity): async def async_update(self) -> None: """Call for forced update.""" - assert self._async_update - self._async_update() + assert self._template_result_info + self._template_result_info.async_refresh() async def async_run_script( self, diff --git a/homeassistant/helpers/trigger_template_entity.py b/homeassistant/helpers/trigger_template_entity.py index 8fc99f5cb52..0ee653b42bd 100644 --- a/homeassistant/helpers/trigger_template_entity.py +++ b/homeassistant/helpers/trigger_template_entity.py @@ -77,8 +77,8 @@ class TriggerBaseEntity(Entity): """Template Base entity based on trigger data.""" domain: str - extra_template_keys: tuple | None = None - extra_template_keys_complex: tuple | None = None + extra_template_keys: tuple[str, ...] | None = None + extra_template_keys_complex: tuple[str, ...] | None = None _unique_id: str | None def __init__( @@ -94,7 +94,7 @@ class TriggerBaseEntity(Entity): self._config = config self._static_rendered = {} - self._to_render_simple = [] + self._to_render_simple: list[str] = [] self._to_render_complex: list[str] = [] for itm in ( diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index ba939f3b8d1..b8634b68b1c 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -3,6 +3,7 @@ from typing import Any from unittest.mock import patch import pytest +from pytest_unordered import unordered from homeassistant import config_entries from homeassistant.components.template import DOMAIN, async_setup_entry @@ -257,6 +258,7 @@ async def test_options( "input_states", "template_states", "extra_attributes", + "listeners", ), ( ( @@ -266,6 +268,7 @@ async def test_options( {"one": "on", "two": "off"}, ["off", "on"], [{}, {}], + [["one", "two"], ["one"]], ), ( "sensor", @@ -274,6 +277,7 @@ async def test_options( {"one": "30.0", "two": "20.0"}, ["unavailable", "50.0"], [{}, {}], + [["one"], ["one", "two"]], ), ), ) @@ -286,6 +290,7 @@ async def test_config_flow_preview( input_states: list[str], template_states: str, extra_attributes: list[dict[str, Any]], + listeners: list[list[str]], ) -> None: """Test the config flow preview.""" client = await hass_ws_client(hass) @@ -323,6 +328,12 @@ async def test_config_flow_preview( msg = await client.receive_json() assert msg["event"] == { "attributes": {"friendly_name": "My template"} | extra_attributes[0], + "listeners": { + "all": False, + "domains": [], + "entities": unordered([f"{template_type}.{_id}" for _id in listeners[0]]), + "time": False, + }, "state": template_states[0], } @@ -336,6 +347,12 @@ async def test_config_flow_preview( "attributes": {"friendly_name": "My template"} | extra_attributes[0] | extra_attributes[1], + "listeners": { + "all": False, + "domains": [], + "entities": unordered([f"{template_type}.{_id}" for _id in listeners[1]]), + "time": False, + }, "state": template_states[1], } assert len(hass.states.async_all()) == 2 @@ -526,6 +543,7 @@ async def test_config_flow_preview_bad_state( "input_states", "template_state", "extra_attributes", + "listeners", ), [ ( @@ -537,6 +555,7 @@ async def test_config_flow_preview_bad_state( {"one": "on", "two": "off"}, "off", {}, + ["one", "two"], ), ( "sensor", @@ -547,6 +566,7 @@ async def test_config_flow_preview_bad_state( {"one": "30.0", "two": "20.0"}, "10.0", {}, + ["one", "two"], ), ], ) @@ -561,6 +581,7 @@ async def test_option_flow_preview( input_states: list[str], template_state: str, extra_attributes: dict[str, Any], + listeners: list[str], ) -> None: """Test the option flow preview.""" client = await hass_ws_client(hass) @@ -608,6 +629,12 @@ async def test_option_flow_preview( msg = await client.receive_json() assert msg["event"] == { "attributes": {"friendly_name": "My template"} | extra_attributes, + "listeners": { + "all": False, + "domains": [], + "entities": unordered([f"{template_type}.{_id}" for _id in listeners]), + "time": False, + }, "state": template_state, } assert len(hass.states.async_all()) == 3 From aa32b658b2d39c42f6bf5899cf655ff42b02af8f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:30:28 -0400 Subject: [PATCH 1137/1151] Fix ZHA startup creating entities with non-unique IDs (#99679) * Make the ZHAGateway initialization restartable so entities are unique * Add a unit test --- homeassistant/components/zha/__init__.py | 4 +- homeassistant/components/zha/core/gateway.py | 12 ++--- tests/components/zha/test_init.py | 50 +++++++++++++++++++- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 1c4c3e776d0..f9113ebaa90 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -134,7 +134,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b else: _LOGGER.debug("ZHA storage file does not exist or was already removed") - zha_gateway = ZHAGateway(hass, config, config_entry) + # Re-use the gateway object between ZHA reloads + if (zha_gateway := zha_data.get(DATA_ZHA_GATEWAY)) is None: + zha_gateway = ZHAGateway(hass, config, config_entry) try: await zha_gateway.async_initialize() diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 3abf1274f98..353bc6904d7 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -149,6 +149,12 @@ class ZHAGateway: self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] + discovery.PROBE.initialize(self._hass) + discovery.GROUP_PROBE.initialize(self._hass) + + self.ha_device_registry = dr.async_get(self._hass) + self.ha_entity_registry = er.async_get(self._hass) + def get_application_controller_data(self) -> tuple[ControllerApplication, dict]: """Get an uninitialized instance of a zigpy `ControllerApplication`.""" radio_type = self.config_entry.data[CONF_RADIO_TYPE] @@ -191,12 +197,6 @@ class ZHAGateway: async def async_initialize(self) -> None: """Initialize controller and connect radio.""" - discovery.PROBE.initialize(self._hass) - discovery.GROUP_PROBE.initialize(self._hass) - - self.ha_device_registry = dr.async_get(self._hass) - self.ha_entity_registry = er.async_get(self._hass) - app_controller_cls, app_config = self.get_application_controller_data() self.application_controller = await app_controller_cls.new( config=app_config, diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 24ee63fb3d5..63ca10bbf91 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -1,8 +1,10 @@ """Tests for ZHA integration init.""" +import asyncio from unittest.mock import AsyncMock, Mock, patch import pytest from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH +from zigpy.exceptions import TransientConnectionError from homeassistant.components.zha import async_setup_entry from homeassistant.components.zha.core.const import ( @@ -11,10 +13,13 @@ from homeassistant.components.zha.core.const import ( CONF_USB_PATH, DOMAIN, ) -from homeassistant.const import MAJOR_VERSION, MINOR_VERSION +from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_call_later from homeassistant.setup import async_setup_component +from .test_light import LIGHT_ON_OFF + from tests.common import MockConfigEntry DATA_RADIO_TYPE = "deconz" @@ -157,3 +162,46 @@ async def test_setup_with_v3_cleaning_uri( assert config_entry_v3.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE assert config_entry_v3.data[CONF_DEVICE][CONF_DEVICE_PATH] == cleaned_path assert config_entry_v3.version == 3 + + +@patch( + "homeassistant.components.zha.PLATFORMS", + [Platform.LIGHT, Platform.BUTTON, Platform.SENSOR, Platform.SELECT], +) +async def test_zha_retry_unique_ids( + hass: HomeAssistant, + config_entry: MockConfigEntry, + zigpy_device_mock, + mock_zigpy_connect, + caplog, +) -> None: + """Test that ZHA retrying creates unique entity IDs.""" + + config_entry.add_to_hass(hass) + + # Ensure we have some device to try to load + app = mock_zigpy_connect.return_value + light = zigpy_device_mock(LIGHT_ON_OFF) + app.devices[light.ieee] = light + + # Re-try setup but have it fail once, so entities have two chances to be created + with patch.object( + app, + "startup", + side_effect=[TransientConnectionError(), None], + ) as mock_connect: + with patch( + "homeassistant.config_entries.async_call_later", + lambda hass, delay, action: async_call_later(hass, 0, action), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Wait for the config entry setup to retry + await asyncio.sleep(0.1) + + assert len(mock_connect.mock_calls) == 2 + + await hass.config_entries.async_unload(config_entry.entry_id) + + assert "does not generate unique IDs" not in caplog.text From 96e932dad6b41579fa89a426e7c0ec112a7ff45a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 5 Sep 2023 21:21:27 +0200 Subject: [PATCH 1138/1151] Bump reolink_aio to 0.7.9 (#99680) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 3ff25d1e7a0..060490c6e56 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.8"] + "requirements": ["reolink-aio==0.7.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 11f404b6394..06d081594db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2294,7 +2294,7 @@ renault-api==0.2.0 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.8 +reolink-aio==0.7.9 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 581e8f3d325..45775e4374e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1684,7 +1684,7 @@ renault-api==0.2.0 renson-endura-delta==1.5.0 # homeassistant.components.reolink -reolink-aio==0.7.8 +reolink-aio==0.7.9 # homeassistant.components.rflink rflink==0.0.65 From cea1109e25d34455922607a987194f62354ed948 Mon Sep 17 00:00:00 2001 From: Daniel Gangl <31815106+killer0071234@users.noreply.github.com> Date: Tue, 5 Sep 2023 21:18:06 +0200 Subject: [PATCH 1139/1151] Bump zamg to 0.3.0 (#99685) --- homeassistant/components/zamg/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zamg/manifest.json b/homeassistant/components/zamg/manifest.json index 3ff7612d47e..df17672231e 100644 --- a/homeassistant/components/zamg/manifest.json +++ b/homeassistant/components/zamg/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zamg", "iot_class": "cloud_polling", - "requirements": ["zamg==0.2.4"] + "requirements": ["zamg==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 06d081594db..a9568704d71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2760,7 +2760,7 @@ youtubeaio==1.1.5 yt-dlp==2023.7.6 # homeassistant.components.zamg -zamg==0.2.4 +zamg==0.3.0 # homeassistant.components.zengge zengge==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45775e4374e..cec152acba5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2033,7 +2033,7 @@ youless-api==1.0.1 youtubeaio==1.1.5 # homeassistant.components.zamg -zamg==0.2.4 +zamg==0.3.0 # homeassistant.components.zeroconf zeroconf==0.91.1 From e486ad735d41b9a6e4720dd5f8739950128e62b3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 5 Sep 2023 21:13:28 +0200 Subject: [PATCH 1140/1151] Bump aiounifi to v61 (#99686) * Bump aiounifi to v61 * Alter a test to cover the upstream change --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifi/test_sensor.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index cb1c8f1c0dc..f20e5f9e4ac 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==60"], + "requirements": ["aiounifi==61"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a9568704d71..56a34af1fbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -363,7 +363,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==60 +aiounifi==61 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cec152acba5..bb4d90e93ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==60 +aiounifi==61 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index da2c0b46f76..7ed87512f2b 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -336,8 +336,8 @@ async def test_bandwidth_sensors( "mac": "00:00:00:00:00:02", "name": "Wireless client", "oui": "Producer", - "rx_bytes-r": 2345000000, - "tx_bytes-r": 6789000000, + "rx_bytes-r": 2345000000.0, + "tx_bytes-r": 6789000000.0, } options = { CONF_ALLOW_BANDWIDTH_SENSORS: True, From f9ee18352d60c9984782a651e0524bde0503e72f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Sep 2023 20:42:50 -0500 Subject: [PATCH 1141/1151] Bump aioesphomeapi to 16.0.5 (#99698) changelog: https://github.com/esphome/aioesphomeapi/compare/v16.0.4...v16.0.5 fixes `RuntimeError: set changed size during iteration` https://github.com/esphome/aioesphomeapi/pull/538 some added debug logging which may help with https://github.com/home-assistant/core/issues/98221 --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index fd6fde0cb05..1c8da971168 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async_interrupt==1.1.1", - "aioesphomeapi==16.0.4", + "aioesphomeapi==16.0.5", "bluetooth-data-tools==1.11.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 56a34af1fbb..6345387e525 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -231,7 +231,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==16.0.4 +aioesphomeapi==16.0.5 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb4d90e93ab..2afdc1c2f10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==16.0.4 +aioesphomeapi==16.0.5 # homeassistant.components.flo aioflo==2021.11.0 From 7b7fd35af29ae46593097f6baf66daec43a13c7f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 10:35:04 +0200 Subject: [PATCH 1142/1151] Fix unit conversion for gas cost sensor (#99708) --- homeassistant/components/energy/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index ae92ee2de58..e9760a96aa4 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -377,11 +377,10 @@ class EnergyCostSensor(SensorEntity): if energy_price_unit is None: converted_energy_price = energy_price else: - if self._adapter.source_type == "grid": - converter: Callable[ - [float, str, str], float - ] = unit_conversion.EnergyConverter.convert - elif self._adapter.source_type in ("gas", "water"): + converter: Callable[[float, str, str], float] + if energy_unit in VALID_ENERGY_UNITS: + converter = unit_conversion.EnergyConverter.convert + else: converter = unit_conversion.VolumeConverter.convert converted_energy_price = converter( From 800ff489b09da542384310f3d77c4ce332cba10e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 6 Sep 2023 10:40:05 +0200 Subject: [PATCH 1143/1151] Update frontend to 20230906.0 (#99715) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 627b36a59b8..9e0bd3e5de9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230905.0"] + "requirements": ["home-assistant-frontend==20230906.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5f939ea9763..a0736bb427b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230905.0 +home-assistant-frontend==20230906.0 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6345387e525..0a8ecc21ae4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230905.0 +home-assistant-frontend==20230906.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2afdc1c2f10..b7aa90642e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230905.0 +home-assistant-frontend==20230906.0 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From 98896834cdac922e3fc60a20c225f1bd03eff98d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 6 Sep 2023 11:00:01 +0200 Subject: [PATCH 1144/1151] Bump version to 2023.9.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a851c041398..e4d6d55a523 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index d4a71cd6abe..63027052edd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b5" +version = "2023.9.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 107ca83d4284460f5da92df7e86db3403ba46986 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 6 Sep 2023 14:46:24 +0200 Subject: [PATCH 1145/1151] Reolink onvif not supported fix (#99714) * only subscibe to ONVIF if supported * Catch NotSupportedError when ONVIF is not supported * fix styling --- homeassistant/components/reolink/host.py | 47 +++++++++++++++++------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index a679cb34f4b..a43dbce9a7c 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -10,7 +10,7 @@ import aiohttp from aiohttp.web import Request from reolink_aio.api import Host from reolink_aio.enums import SubType -from reolink_aio.exceptions import ReolinkError, SubscriptionError +from reolink_aio.exceptions import NotSupportedError, ReolinkError, SubscriptionError from homeassistant.components import webhook from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -61,6 +61,7 @@ class ReolinkHost: ) self.webhook_id: str | None = None + self._onvif_supported: bool = True self._base_url: str = "" self._webhook_url: str = "" self._webhook_reachable: bool = False @@ -96,6 +97,8 @@ class ReolinkHost: f"'{self._api.user_level}', only admin users can change camera settings" ) + self._onvif_supported = self._api.supported(None, "ONVIF") + enable_rtsp = None enable_onvif = None enable_rtmp = None @@ -106,7 +109,7 @@ class ReolinkHost: ) enable_rtsp = True - if not self._api.onvif_enabled: + if not self._api.onvif_enabled and self._onvif_supported: _LOGGER.debug( "ONVIF is disabled on %s, trying to enable it", self._api.nvr_name ) @@ -154,21 +157,34 @@ class ReolinkHost: self._unique_id = format_mac(self._api.mac_address) - await self.subscribe() - - if self._api.supported(None, "initial_ONVIF_state"): + if self._onvif_supported: + try: + await self.subscribe() + except NotSupportedError: + self._onvif_supported = False + self.unregister_webhook() + await self._api.unsubscribe() + else: + if self._api.supported(None, "initial_ONVIF_state"): + _LOGGER.debug( + "Waiting for initial ONVIF state on webhook '%s'", + self._webhook_url, + ) + else: + _LOGGER.debug( + "Camera model %s most likely does not push its initial state" + " upon ONVIF subscription, do not check", + self._api.model, + ) + self._cancel_onvif_check = async_call_later( + self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif + ) + if not self._onvif_supported: _LOGGER.debug( - "Waiting for initial ONVIF state on webhook '%s'", self._webhook_url - ) - else: - _LOGGER.debug( - "Camera model %s most likely does not push its initial state" - " upon ONVIF subscription, do not check", + "Camera model %s does not support ONVIF, using fast polling instead", self._api.model, ) - self._cancel_onvif_check = async_call_later( - self._hass, FIRST_ONVIF_TIMEOUT, self._async_check_onvif - ) + await self._async_poll_all_motion() if self._api.sw_version_update_required: ir.async_create_issue( @@ -365,6 +381,9 @@ class ReolinkHost: async def renew(self) -> None: """Renew the subscription of motion events (lease time is 15 minutes).""" + if not self._onvif_supported: + return + try: await self._renew(SubType.push) if self._long_poll_task is not None: From 067f9461297e57a000c0f522096a3bd7c55b6024 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 16:07:05 +0200 Subject: [PATCH 1146/1151] Send template render errors to template helper preview (#99716) --- .../components/template/template_entity.py | 23 +-- homeassistant/helpers/event.py | 13 +- tests/components/template/test_config_flow.py | 173 +++++++++++++++++- 3 files changed, 190 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 2ce42083117..8c3554c067e 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -15,13 +15,11 @@ from homeassistant.const import ( CONF_ICON, CONF_ICON_TEMPLATE, CONF_NAME, - EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, ) from homeassistant.core import ( CALLBACK_TYPE, Context, - CoreState, HomeAssistant, State, callback, @@ -38,6 +36,7 @@ from homeassistant.helpers.event import ( async_track_template_result, ) from homeassistant.helpers.script import Script, _VarsType +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.template import ( Template, TemplateStateFromEntityId, @@ -442,7 +441,11 @@ class TemplateEntity(Entity): ) @callback - def _async_template_startup(self, *_: Any) -> None: + def _async_template_startup( + self, + _hass: HomeAssistant | None, + log_fn: Callable[[int, str], None] | None = None, + ) -> None: template_var_tups: list[TrackTemplate] = [] has_availability_template = False @@ -467,6 +470,7 @@ class TemplateEntity(Entity): self.hass, template_var_tups, self._handle_results, + log_fn=log_fn, has_super_template=has_availability_template, ) self.async_on_remove(result_info.async_remove) @@ -515,10 +519,13 @@ class TemplateEntity(Entity): ) -> CALLBACK_TYPE: """Render a preview.""" + def log_template_error(level: int, msg: str) -> None: + preview_callback(None, None, None, msg) + self._preview_callback = preview_callback self._async_setup_templates() try: - self._async_template_startup() + self._async_template_startup(None, log_template_error) except Exception as err: # pylint: disable=broad-exception-caught preview_callback(None, None, None, str(err)) return self._call_on_remove_callbacks @@ -527,13 +534,7 @@ class TemplateEntity(Entity): """Run when entity about to be added to hass.""" self._async_setup_templates() - if self.hass.state == CoreState.running: - self._async_template_startup() - return - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._async_template_startup - ) + async_at_start(self.hass, self._async_template_startup) async def async_update(self) -> None: """Call for forced update.""" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 173dd057f96..51a8f1f1982 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -957,11 +957,14 @@ class TrackTemplateResultInfo: if info.exception: if raise_on_template_error: raise info.exception - _LOGGER.error( - "Error while processing template: %s", - track_template_.template, - exc_info=info.exception, - ) + if not log_fn: + _LOGGER.error( + "Error while processing template: %s", + track_template_.template, + exc_info=info.exception, + ) + else: + log_fn(logging.ERROR, str(info.exception)) self._track_state_changes = async_track_state_change_filtered( self.hass, _render_infos_to_track_states(self._info.values()), self._refresh diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index b8634b68b1c..f4cfe90b9f0 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -272,12 +272,12 @@ async def test_options( ), ( "sensor", - "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", + "{{ float(states('sensor.one'), default='') + float(states('sensor.two'), default='') }}", {}, {"one": "30.0", "two": "20.0"}, - ["unavailable", "50.0"], + ["", "50.0"], [{}, {}], - [["one"], ["one", "two"]], + [["one", "two"], ["one", "two"]], ), ), ) @@ -470,6 +470,173 @@ async def test_config_flow_preview_bad_input( } +@pytest.mark.parametrize( + ( + "template_type", + "state_template", + "input_states", + "template_states", + "error_events", + ), + [ + ( + "sensor", + "{{ float(states('sensor.one')) + float(states('sensor.two')) }}", + {"one": "30.0", "two": "20.0"}, + ["unavailable", "50.0"], + [ + ( + "ValueError: Template error: float got invalid input 'unknown' " + "when rendering template '{{ float(states('sensor.one')) + " + "float(states('sensor.two')) }}' but no default was specified" + ) + ], + ), + ], +) +async def test_config_flow_preview_template_startup_error( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + template_type: str, + state_template: str, + input_states: dict[str, str], + template_states: list[str], + error_events: list[str], +) -> None: + """Test the config flow preview.""" + client = await hass_ws_client(hass) + + input_entities = ["one", "two"] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": template_type}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == template_type + assert result["errors"] is None + assert result["preview"] == "template" + + await client.send_json_auto_id( + { + "type": "template/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": {"name": "My template", "state": state_template}, + } + ) + msg = await client.receive_json() + assert msg["type"] == "result" + assert msg["success"] + + for error_event in error_events: + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"] == {"error": error_event} + + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"]["state"] == template_states[0] + + for input_entity in input_entities: + hass.states.async_set( + f"{template_type}.{input_entity}", input_states[input_entity], {} + ) + + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"]["state"] == template_states[1] + + +@pytest.mark.parametrize( + ( + "template_type", + "state_template", + "input_states", + "template_states", + "error_events", + ), + [ + ( + "sensor", + "{{ float(states('sensor.one')) > 30 and undefined_function() }}", + [{"one": "30.0", "two": "20.0"}, {"one": "35.0", "two": "20.0"}], + ["False", "unavailable"], + ["'undefined_function' is undefined"], + ), + ], +) +async def test_config_flow_preview_template_error( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + template_type: str, + state_template: str, + input_states: list[dict[str, str]], + template_states: list[str], + error_events: list[str], +) -> None: + """Test the config flow preview.""" + client = await hass_ws_client(hass) + + input_entities = ["one", "two"] + + for input_entity in input_entities: + hass.states.async_set( + f"{template_type}.{input_entity}", input_states[0][input_entity], {} + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": template_type}, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == template_type + assert result["errors"] is None + assert result["preview"] == "template" + + await client.send_json_auto_id( + { + "type": "template/start_preview", + "flow_id": result["flow_id"], + "flow_type": "config_flow", + "user_input": {"name": "My template", "state": state_template}, + } + ) + msg = await client.receive_json() + assert msg["type"] == "result" + assert msg["success"] + + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"]["state"] == template_states[0] + + for input_entity in input_entities: + hass.states.async_set( + f"{template_type}.{input_entity}", input_states[1][input_entity], {} + ) + + for error_event in error_events: + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"] == {"error": error_event} + + msg = await client.receive_json() + assert msg["type"] == "event" + assert msg["event"]["state"] == template_states[1] + + @pytest.mark.parametrize( ( "template_type", From 6f6306b39b7d0d9898dffa8176f8ee76f1be9b23 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 15:59:30 +0200 Subject: [PATCH 1147/1151] Don't allow changing device class in template binary sensor options (#99720) --- homeassistant/components/template/config_flow.py | 8 ++++---- homeassistant/components/template/strings.json | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 093cbf14098..15be2c52d91 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -40,11 +40,11 @@ from .template_entity import TemplateEntity NONE_SENTINEL = "none" -def generate_schema(domain: str) -> dict[vol.Marker, Any]: +def generate_schema(domain: str, flow_type: str) -> dict[vol.Marker, Any]: """Generate schema.""" schema: dict[vol.Marker, Any] = {} - if domain == Platform.BINARY_SENSOR: + if domain == Platform.BINARY_SENSOR and flow_type == "config": schema = { vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector( selector.SelectSelectorConfig( @@ -124,7 +124,7 @@ def options_schema(domain: str) -> vol.Schema: """Generate options schema.""" return vol.Schema( {vol.Required(CONF_STATE): selector.TemplateSelector()} - | generate_schema(domain), + | generate_schema(domain, "option"), ) @@ -135,7 +135,7 @@ def config_schema(domain: str) -> vol.Schema: vol.Required(CONF_NAME): selector.TextSelector(), vol.Required(CONF_STATE): selector.TemplateSelector(), } - | generate_schema(domain), + | generate_schema(domain, "config"), ) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 7e5e56a26d6..a0ee31126cd 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -33,7 +33,6 @@ "step": { "binary_sensor": { "data": { - "device_class": "[%key:component::template::config::step::sensor::data::device_class%]", "state": "[%key:component::template::config::step::sensor::data::state%]" }, "title": "[%key:component::template::config::step::binary_sensor::title%]" From d99e5e0c4f293f6edd30e48a05150ecfd376e9ec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 16:06:33 +0200 Subject: [PATCH 1148/1151] Correct state attributes in template helper preview (#99722) --- homeassistant/components/template/config_flow.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 15be2c52d91..c361b4c42cc 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -24,7 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import selector +from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.schema_config_entry_flow import ( SchemaCommonFlowHandler, SchemaConfigFlowHandler, @@ -328,6 +328,7 @@ def ws_start_preview( return errors + entity_registry_entry: er.RegistryEntry | None = None if msg["flow_type"] == "config_flow": flow_status = hass.config_entries.flow.async_get(msg["flow_id"]) template_type = flow_status["step_id"] @@ -342,6 +343,12 @@ def ws_start_preview( template_type = config_entry.options["template_type"] name = config_entry.options["name"] schema = cast(vol.Schema, OPTIONS_FLOW[template_type].schema) + entity_registry = er.async_get(hass) + entries = er.async_entries_for_config_entry( + entity_registry, flow_status["handler"] + ) + if entries: + entity_registry_entry = entries[0] errors = _validate(schema, template_type, msg["user_input"]) @@ -382,6 +389,7 @@ def ws_start_preview( _strip_sentinel(msg["user_input"]) preview_entity = CREATE_PREVIEW_ENTITY[template_type](hass, name, msg["user_input"]) preview_entity.hass = hass + preview_entity.registry_entry = entity_registry_entry connection.send_result(msg["id"]) connection.subscriptions[msg["id"]] = preview_entity.async_start_preview( From b5d9014bbd608e0454120e3d68e06cefd9a783a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Sep 2023 15:59:03 +0200 Subject: [PATCH 1149/1151] Correct state attributes in group helper preview (#99723) --- homeassistant/components/group/config_flow.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 9eb973b9609..93160b0db5b 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -361,6 +361,7 @@ def ws_start_preview( msg: dict[str, Any], ) -> None: """Generate a preview.""" + entity_registry_entry: er.RegistryEntry | None = None if msg["flow_type"] == "config_flow": flow_status = hass.config_entries.flow.async_get(msg["flow_id"]) group_type = flow_status["step_id"] @@ -370,12 +371,17 @@ def ws_start_preview( name = validated["name"] else: flow_status = hass.config_entries.options.async_get(msg["flow_id"]) - config_entry = hass.config_entries.async_get_entry(flow_status["handler"]) + config_entry_id = flow_status["handler"] + config_entry = hass.config_entries.async_get_entry(config_entry_id) if not config_entry: raise HomeAssistantError group_type = config_entry.options["group_type"] name = config_entry.options["name"] validated = PREVIEW_OPTIONS_SCHEMA[group_type](msg["user_input"]) + entity_registry = er.async_get(hass) + entries = er.async_entries_for_config_entry(entity_registry, config_entry_id) + if entries: + entity_registry_entry = entries[0] @callback def async_preview_updated(state: str, attributes: Mapping[str, Any]) -> None: @@ -388,6 +394,7 @@ def ws_start_preview( preview_entity = CREATE_PREVIEW_ENTITY[group_type](name, validated) preview_entity.hass = hass + preview_entity.registry_entry = entity_registry_entry connection.send_result(msg["id"]) connection.subscriptions[msg["id"]] = preview_entity.async_start_preview( From 02fc735c080e05fb3c7a99df7b2eaecb75fd7e88 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 6 Sep 2023 15:00:26 +0200 Subject: [PATCH 1150/1151] Update frontend to 20230906.1 (#99733) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 9e0bd3e5de9..50c557eae89 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230906.0"] + "requirements": ["home-assistant-frontend==20230906.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a0736bb427b..7c48166172f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.70.0 hassil==1.2.5 home-assistant-bluetooth==1.10.3 -home-assistant-frontend==20230906.0 +home-assistant-frontend==20230906.1 home-assistant-intents==2023.8.2 httpx==0.24.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0a8ecc21ae4..d656b0dbb48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230906.0 +home-assistant-frontend==20230906.1 # homeassistant.components.conversation home-assistant-intents==2023.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7aa90642e4..308759108ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20230906.0 +home-assistant-frontend==20230906.1 # homeassistant.components.conversation home-assistant-intents==2023.8.2 From d369a700923292b819ff960af1bf129c3efbf4a9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 6 Sep 2023 16:09:56 +0200 Subject: [PATCH 1151/1151] Bumped version to 2023.9.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e4d6d55a523..cfdb5095128 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 63027052edd..e4403bd7c30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.9.0b6" +version = "2023.9.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"