From 99f3cfdf8a99d9025695d6fd0cfcaf7ce84a3f86 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Dec 2022 16:51:38 +0100 Subject: [PATCH 001/173] Bumped version to 2023.1.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 8136a0f038d..cfafc1ab4d1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index ccc19625330..3da5110d38d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.0.dev0" +version = "2023.1.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b7654c0fcef935b6d3917ed15250070bd3c054d8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 28 Dec 2022 18:29:37 +0100 Subject: [PATCH 002/173] Bump python matter server to 1.0.8 (#84692) Co-authored-by: Martin Hjelmare --- homeassistant/components/matter/__init__.py | 3 +-- homeassistant/components/matter/adapter.py | 11 ++++++++--- .../components/matter/binary_sensor.py | 11 ++++------- homeassistant/components/matter/config_flow.py | 9 +++++++-- homeassistant/components/matter/entity.py | 10 +++++++++- homeassistant/components/matter/light.py | 17 ++++++++++------- homeassistant/components/matter/manifest.json | 2 +- homeassistant/components/matter/switch.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 44 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index b96be6bee45..b1470ecc422 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from typing import cast import async_timeout from matter_server.client import MatterClient @@ -245,7 +244,7 @@ def _async_init_services(hass: HomeAssistant) -> None: # This could be more efficient for node in await matter_client.get_nodes(): if node.unique_id == unique_id: - return cast(int, node.node_id) + return node.node_id return None diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 42e6070d49b..b573ed0a3fc 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -47,8 +47,12 @@ class MatterAdapter: for node in await self.matter_client.get_nodes(): self._setup_node(node) - def node_added_callback(event: EventType, node: MatterNode) -> None: + def node_added_callback(event: EventType, node: MatterNode | None) -> None: """Handle node added event.""" + if node is None: + # We can clean this up when we've improved the typing in the library. + # https://github.com/home-assistant-libs/python-matter-server/pull/153 + raise RuntimeError("Node added event without node") self._setup_node(node) self.config_entry.async_on_unload( @@ -61,8 +65,9 @@ class MatterAdapter: bridge_unique_id: str | None = None - if node.aggregator_device_type_instance is not None: - node_info = node.root_device_type_instance.get_cluster(all_clusters.Basic) + if node.aggregator_device_type_instance is not None and ( + node_info := node.root_device_type_instance.get_cluster(all_clusters.Basic) + ): self._create_device_registry( node_info, node_info.nodeLabel or "Hub device", None ) diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index 8d0b2f08c2a..15ad13d25ad 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -39,9 +39,8 @@ class MatterBinarySensor(MatterEntity, BinarySensorEntity): @callback def _update_from_device(self) -> None: """Update from device.""" - self._attr_is_on = self._device_type_instance.get_cluster( - clusters.BooleanState - ).stateValue + cluster = self._device_type_instance.get_cluster(clusters.BooleanState) + self._attr_is_on = cluster.stateValue if cluster else None class MatterOccupancySensor(MatterBinarySensor): @@ -52,11 +51,9 @@ class MatterOccupancySensor(MatterBinarySensor): @callback def _update_from_device(self) -> None: """Update from device.""" - occupancy = self._device_type_instance.get_cluster( - clusters.OccupancySensing - ).occupancy + cluster = self._device_type_instance.get_cluster(clusters.OccupancySensing) # The first bit = if occupied - self._attr_is_on = occupancy & 1 == 1 + self._attr_is_on = cluster.occupancy & 1 == 1 if cluster else None @dataclass diff --git a/homeassistant/components/matter/config_flow.py b/homeassistant/components/matter/config_flow.py index 98146a6ae01..6e25370d86a 100644 --- a/homeassistant/components/matter/config_flow.py +++ b/homeassistant/components/matter/config_flow.py @@ -20,6 +20,7 @@ from homeassistant.components.hassio import ( from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client from .addon import get_addon_manager @@ -131,7 +132,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await self.start_task - except (CannotConnect, AddonError, AbortFlow) as err: + except (FailedConnect, AddonError, AbortFlow) as err: self.start_task = None LOGGER.error(err) return self.async_show_progress_done(next_step_id="start_failed") @@ -170,7 +171,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: break else: - raise CannotConnect("Failed to start Matter Server add-on: timeout") + raise FailedConnect("Failed to start Matter Server add-on: timeout") finally: # Continue the flow after show progress when the task is done. self.hass.async_create_task( @@ -324,3 +325,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, }, ) + + +class FailedConnect(HomeAssistantError): + """Failed to connect to the Matter Server.""" diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 019631750f4..4f28c1d2369 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -59,7 +59,15 @@ class MatterEntity(Entity): self._unsubscribes: list[Callable] = [] # for fast lookups we create a mapping to the attribute paths self._attributes_map: dict[type, str] = {} - self._attr_unique_id = f"{matter_client.server_info.compressed_fabric_id}-{node.unique_id}-{device_type_instance.endpoint}-{device_type_instance.device_type.device_type}" + server_info = matter_client.server_info + # The server info is set when the client connects to the server. + assert server_info is not None + self._attr_unique_id = ( + f"{server_info.compressed_fabric_id}-" + f"{node.unique_id}-" + f"{device_type_instance.endpoint}-" + f"{device_type_instance.device_type.device_type}" + ) @property def device_info(self) -> DeviceInfo | None: diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index a07b791c64d..0136b74f32e 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -57,6 +57,9 @@ class MatterLight(MatterEntity, LightEntity): return level_control = self._device_type_instance.get_cluster(clusters.LevelControl) + # We check above that the device supports brightness, ie level control. + assert level_control is not None + level = round( renormalize( kwargs[ATTR_BRIGHTNESS], @@ -86,20 +89,20 @@ class MatterLight(MatterEntity, LightEntity): @callback def _update_from_device(self) -> None: """Update from device.""" - if self._attr_supported_color_modes is None: - if self._supports_brightness(): - self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + supports_brigthness = self._supports_brightness() + + if self._attr_supported_color_modes is None and supports_brigthness: + self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} if attr := self.get_matter_attribute(clusters.OnOff.Attributes.OnOff): self._attr_is_on = attr.value - if ( - clusters.LevelControl.Attributes.CurrentLevel - in self.entity_description.subscribe_attributes - ): + if supports_brigthness: level_control = self._device_type_instance.get_cluster( clusters.LevelControl ) + # We check above that the device supports brightness, ie level control. + assert level_control is not None # Convert brightness to Home Assistant = 0..255 self._attr_brightness = round( diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index b94ba5f58ad..129110ba519 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -3,7 +3,7 @@ "name": "Matter (BETA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/matter", - "requirements": ["python-matter-server==1.0.7"], + "requirements": ["python-matter-server==1.0.8"], "dependencies": ["websocket_api"], "codeowners": ["@home-assistant/matter"], "iot_class": "local_push" diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 6968a3d095d..f86a7cbb023 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -56,7 +56,8 @@ class MatterSwitch(MatterEntity, SwitchEntity): @callback def _update_from_device(self) -> None: """Update from device.""" - self._attr_is_on = self._device_type_instance.get_cluster(clusters.OnOff).onOff + cluster = self._device_type_instance.get_cluster(clusters.OnOff) + self._attr_is_on = cluster.onOff if cluster else None @dataclass diff --git a/requirements_all.txt b/requirements_all.txt index 382feba38b6..385fec56a16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2038,7 +2038,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==1.0.7 +python-matter-server==1.0.8 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91c99f7daba..6d8d13f41ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1428,7 +1428,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.matter -python-matter-server==1.0.7 +python-matter-server==1.0.8 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From ed43e1d3a436ab5d202ab7cddad5e6fc093a62cb Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Wed, 28 Dec 2022 22:13:20 +0100 Subject: [PATCH 003/173] Update apprise to 1.2.1 (#84705) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 984ecea50d7..4475f68cd3b 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==1.2.0"], + "requirements": ["apprise==1.2.1"], "codeowners": ["@caronc"], "iot_class": "cloud_push", "loggers": ["apprise"] diff --git a/requirements_all.txt b/requirements_all.txt index 385fec56a16..12b6572ed47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==1.2.0 +apprise==1.2.1 # homeassistant.components.aprs aprslib==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d8d13f41ab..4e32156f0b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -305,7 +305,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==1.2.0 +apprise==1.2.1 # homeassistant.components.aprs aprslib==0.7.0 From 8aa3a6cc15bf4dd03ba758d3657378307f1fcab7 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 28 Dec 2022 22:11:40 +0100 Subject: [PATCH 004/173] Remove deprecated tankerkoenig YAML config (#84711) remove yaml import --- .../components/tankerkoenig/__init__.py | 90 +------------------ .../components/tankerkoenig/config_flow.py | 31 ------- .../tankerkoenig/test_config_flow.py | 45 +--------- 3 files changed, 5 insertions(+), 161 deletions(-) diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 3db67b4c8be..3ffa2ff4576 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -7,110 +7,28 @@ from math import ceil import pytankerkoenig from requests.exceptions import RequestException -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - ATTR_ID, - CONF_API_KEY, - CONF_LATITUDE, - CONF_LOCATION, - CONF_LONGITUDE, - CONF_NAME, - CONF_RADIUS, - CONF_SCAN_INTERVAL, - CONF_SHOW_ON_MAP, - Platform, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ID, CONF_API_KEY, CONF_SHOW_ON_MAP, Platform 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.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, UpdateFailed, ) -from .const import ( - CONF_FUEL_TYPES, - CONF_STATIONS, - DEFAULT_RADIUS, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - FUEL_TYPES, -) +from .const import CONF_FUEL_TYPES, CONF_STATIONS, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - vol.Optional(CONF_FUEL_TYPES, default=FUEL_TYPES): vol.All( - cv.ensure_list, [vol.In(FUEL_TYPES)] - ), - vol.Inclusive( - CONF_LATITUDE, - "coordinates", - "Latitude and longitude must exist together", - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, - "coordinates", - "Latitude and longitude must exist together", - ): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.All( - cv.positive_int, vol.Range(min=1) - ), - vol.Optional(CONF_STATIONS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set the tankerkoenig component up.""" - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_NAME: "Home", - CONF_API_KEY: conf[CONF_API_KEY], - CONF_FUEL_TYPES: conf[CONF_FUEL_TYPES], - CONF_LOCATION: { - "latitude": conf.get(CONF_LATITUDE, hass.config.latitude), - "longitude": conf.get(CONF_LONGITUDE, hass.config.longitude), - }, - CONF_RADIUS: conf[CONF_RADIUS], - CONF_STATIONS: conf[CONF_STATIONS], - CONF_SHOW_ON_MAP: conf[CONF_SHOW_ON_MAP], - }, - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index 5441ddeb52c..79f6349f0cb 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -67,37 +67,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import YAML configuration.""" - await self.async_set_unique_id( - f"{config[CONF_LOCATION][CONF_LATITUDE]}_{config[CONF_LOCATION][CONF_LONGITUDE]}" - ) - self._abort_if_unique_id_configured() - - selected_station_ids: list[str] = [] - # add all nearby stations - nearby_stations = await async_get_nearby_stations(self.hass, config) - for station in nearby_stations.get("stations", []): - selected_station_ids.append(station["id"]) - - # add all manual added stations - for station_id in config[CONF_STATIONS]: - selected_station_ids.append(station_id) - - return self._create_entry( - data={ - CONF_NAME: "Home", - CONF_API_KEY: config[CONF_API_KEY], - CONF_FUEL_TYPES: config[CONF_FUEL_TYPES], - CONF_LOCATION: config[CONF_LOCATION], - CONF_RADIUS: config[CONF_RADIUS], - CONF_STATIONS: selected_station_ids, - }, - options={ - CONF_SHOW_ON_MAP: config[CONF_SHOW_ON_MAP], - }, - ) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index 600bfd98c73..266f9b67376 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.tankerkoenig.const import ( CONF_STATIONS, DOMAIN, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, @@ -47,18 +47,6 @@ MOCK_OPTIONS_DATA = { ], } -MOCK_IMPORT_DATA = { - CONF_API_KEY: "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx", - CONF_FUEL_TYPES: ["e5"], - CONF_LOCATION: {CONF_LATITUDE: 51.0, CONF_LONGITUDE: 13.0}, - CONF_RADIUS: 2.0, - CONF_STATIONS: [ - "3bcd61da-yyyy-yyyy-yyyy-19d5523a7ae8", - "36b4b812-yyyy-yyyy-yyyy-c51735325858", - ], - CONF_SHOW_ON_MAP: True, -} - MOCK_NEARVY_STATIONS_OK = { "ok": True, "stations": [ @@ -187,37 +175,6 @@ async def test_user_no_stations(hass: HomeAssistant): assert result["errors"][CONF_RADIUS] == "no_stations" -async def test_import(hass: HomeAssistant): - """Test starting a flow by import.""" - with patch( - "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True - ) as mock_setup_entry, patch( - "homeassistant.components.tankerkoenig.config_flow.getNearbyStations", - return_value=MOCK_NEARVY_STATIONS_OK, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_DATA - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_NAME] == "Home" - assert result["data"][CONF_API_KEY] == "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx" - assert result["data"][CONF_FUEL_TYPES] == ["e5"] - assert result["data"][CONF_LOCATION] == {"latitude": 51.0, "longitude": 13.0} - assert result["data"][CONF_RADIUS] == 2.0 - assert result["data"][CONF_STATIONS] == [ - "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", - "36b4b812-xxxx-xxxx-xxxx-c51735325858", - "3bcd61da-yyyy-yyyy-yyyy-19d5523a7ae8", - "36b4b812-yyyy-yyyy-yyyy-c51735325858", - ] - assert result["options"][CONF_SHOW_ON_MAP] - - await hass.async_block_till_done() - - assert mock_setup_entry.called - - async def test_reauth(hass: HomeAssistant): """Test starting a flow by user to re-auth.""" From ba4ec8f8c190fa066d26d75085bfbe707729328d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 28 Dec 2022 13:07:51 -0800 Subject: [PATCH 005/173] Gracefully handle caldav event with missing summary (#84719) fixes undefined --- homeassistant/components/caldav/calendar.py | 6 ++++-- tests/components/caldav/test_calendar.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index d510c0c08e7..2d5c7217043 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -184,7 +184,7 @@ class WebDavCalendarData: continue event_list.append( CalendarEvent( - summary=vevent.summary.value, + summary=self.get_attr_value(vevent, "summary") or "", start=vevent.dtstart.value, end=self.get_end_date(vevent), location=self.get_attr_value(vevent, "location"), @@ -264,7 +264,9 @@ class WebDavCalendarData: return # Populate the entity attributes with the event values - (summary, offset) = extract_offset(vevent.summary.value, OFFSET) + (summary, offset) = extract_offset( + self.get_attr_value(vevent, "summary") or "", OFFSET + ) self.event = CalendarEvent( summary=summary, start=vevent.dtstart.value, diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index b936a02db87..e9c58034cbe 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -214,6 +214,18 @@ DESCRIPTION:The bell tolls for thee RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=12 END:VEVENT END:VCALENDAR +""", + """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Global Corp.//CalDAV Client//EN +BEGIN:VEVENT +UID:14 +DTSTAMP:20151125T000000Z +DTSTART:20151127T000000Z +DTEND:20151127T003000Z +RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=12 +END:VEVENT +END:VCALENDAR """, ] @@ -917,7 +929,7 @@ async def test_get_events(hass, calendar, get_api_events): await hass.async_block_till_done() events = await get_api_events("calendar.private") - assert len(events) == 14 + assert len(events) == 15 assert calendar.call From 001bd78bcbd673217a4093a668b10b1c896ebd5b Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Wed, 28 Dec 2022 21:45:24 +0100 Subject: [PATCH 006/173] water_heater: Add unsupported states (#84720) --- homeassistant/components/vicare/water_heater.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index f9f02c1a074..59ed07bdeb2 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -36,7 +36,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) VICARE_MODE_DHW = "dhw" +VICARE_MODE_HEATING = "heating" VICARE_MODE_DHWANDHEATING = "dhwAndHeating" +VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling" VICARE_MODE_FORCEDREDUCED = "forcedReduced" VICARE_MODE_FORCEDNORMAL = "forcedNormal" VICARE_MODE_OFF = "standby" @@ -50,6 +52,8 @@ OPERATION_MODE_OFF = "off" VICARE_TO_HA_HVAC_DHW = { VICARE_MODE_DHW: OPERATION_MODE_ON, VICARE_MODE_DHWANDHEATING: OPERATION_MODE_ON, + VICARE_MODE_DHWANDHEATINGCOOLING: OPERATION_MODE_ON, + VICARE_MODE_HEATING: OPERATION_MODE_OFF, VICARE_MODE_FORCEDREDUCED: OPERATION_MODE_OFF, VICARE_MODE_FORCEDNORMAL: OPERATION_MODE_ON, VICARE_MODE_OFF: OPERATION_MODE_OFF, From b146f523177b53819bc79f214f11927a91253431 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 28 Dec 2022 14:03:49 -0700 Subject: [PATCH 007/173] Remove incorrect unit for AirVisual AQI sensor (#84723) fixes undefined --- homeassistant/components/airvisual/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index c52eb9a2334..cfb9b67ff38 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -69,7 +69,6 @@ GEOGRAPHY_SENSOR_DESCRIPTIONS = ( key=SENSOR_KIND_AQI, name="Air quality index", device_class=SensorDeviceClass.AQI, - native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( From 6ef40866837bb46e5770e16aa056f032c20ce814 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 28 Dec 2022 14:06:44 -0700 Subject: [PATCH 008/173] Fix incorrect values for AirVisual Pro sensors (#84725) --- homeassistant/components/airvisual_pro/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airvisual_pro/sensor.py b/homeassistant/components/airvisual_pro/sensor.py index 06295494d1c..b0e2a947632 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -159,6 +159,6 @@ class AirVisualProSensor(AirVisualProEntity, SensorEntity): elif self.entity_description.key == SENSOR_KIND_BATTERY_LEVEL: self._attr_native_value = self.status["battery"] else: - self._attr_native_value = self.MEASUREMENTS_KEY_TO_VALUE[ - self.entity_description.key + self._attr_native_value = self.measurements[ + self.MEASUREMENTS_KEY_TO_VALUE[self.entity_description.key] ] From 9ce64f89908fafe4e2cd284a70f1a42971ce73ed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 28 Dec 2022 22:18:06 +0100 Subject: [PATCH 009/173] Bumped version to 2023.1.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 cfafc1ab4d1..cc2bac738cc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 3da5110d38d..83d90da2b0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.0b0" +version = "2023.1.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6c0e4a9e8f8df51ffe930d3e570ec36922a03867 Mon Sep 17 00:00:00 2001 From: Tomas Kislan Date: Thu, 29 Dec 2022 14:16:06 +0100 Subject: [PATCH 010/173] Fix and upgrade minio integration (#84545) closes https://github.com/home-assistant/core/issues/79842 --- homeassistant/components/minio/__init__.py | 6 ++---- homeassistant/components/minio/manifest.json | 2 +- homeassistant/components/minio/minio_helper.py | 4 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index 89c5687075f..1f325f3866d 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -136,8 +136,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: file_path = _render_service_value(service, ATTR_FILE_PATH) if not hass.config.is_allowed_path(file_path): - _LOGGER.error("Invalid file_path %s", file_path) - return + raise ValueError(f"Invalid file_path {file_path}") minio_client.fput_object(bucket, key, file_path) @@ -148,8 +147,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: file_path = _render_service_value(service, ATTR_FILE_PATH) if not hass.config.is_allowed_path(file_path): - _LOGGER.error("Invalid file_path %s", file_path) - return + raise ValueError(f"Invalid file_path {file_path}") minio_client.fget_object(bucket, key, file_path) diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json index f89db2346d9..ce3b7f141d9 100644 --- a/homeassistant/components/minio/manifest.json +++ b/homeassistant/components/minio/manifest.json @@ -2,7 +2,7 @@ "domain": "minio", "name": "Minio", "documentation": "https://www.home-assistant.io/integrations/minio", - "requirements": ["minio==5.0.10"], + "requirements": ["minio==7.1.12"], "codeowners": ["@tkislan"], "iot_class": "cloud_push", "loggers": ["minio"] diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index 4f10da10998..75a8d003aeb 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -34,7 +34,9 @@ def create_minio_client( endpoint: str, access_key: str, secret_key: str, secure: bool ) -> Minio: """Create Minio client.""" - return Minio(endpoint, access_key, secret_key, secure) + return Minio( + endpoint=endpoint, access_key=access_key, secret_key=secret_key, secure=secure + ) def get_minio_notification_response( diff --git a/requirements_all.txt b/requirements_all.txt index 12b6572ed47..a159ae9e720 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1110,7 +1110,7 @@ mill-local==0.2.0 millheater==0.10.0 # homeassistant.components.minio -minio==5.0.10 +minio==7.1.12 # homeassistant.components.moat moat-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e32156f0b6..6ce23266fbe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -812,7 +812,7 @@ mill-local==0.2.0 millheater==0.10.0 # homeassistant.components.minio -minio==5.0.10 +minio==7.1.12 # homeassistant.components.moat moat-ble==0.1.1 From 9a29f64128d048e15604d3ae6d6783231cbd8e59 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 29 Dec 2022 01:26:58 +0100 Subject: [PATCH 011/173] Bump pynetgear to 0.10.9 (#84733) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 92b3065147c..4661e39ad53 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.8"], + "requirements": ["pynetgear==0.10.9"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index a159ae9e720..a7a79262139 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1767,7 +1767,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.8 +pynetgear==0.10.9 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ce23266fbe..681aaac592c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1256,7 +1256,7 @@ pymyq==3.1.4 pymysensors==0.24.0 # homeassistant.components.netgear -pynetgear==0.10.8 +pynetgear==0.10.9 # homeassistant.components.nina pynina==0.2.0 From 5787e1506c56a444140dc48863e0073e60f9337c Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 29 Dec 2022 05:40:25 +0000 Subject: [PATCH 012/173] Fix UUID normalisation for vendor extensions in homekit_controller thread transport (#84746) --- 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 4553dc5d1ff..195e3330c7c 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.4.2"], + "requirements": ["aiohomekit==2.4.3"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index a7a79262139..19c1fc8cb4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -177,7 +177,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.2 +aiohomekit==2.4.3 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 681aaac592c..214d5d06ecd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.2 +aiohomekit==2.4.3 # homeassistant.components.emulated_hue # homeassistant.components.http From 31847e3a69ad090a5d07ed0e33c0ce1d2dfad2b7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 29 Dec 2022 02:00:31 -0800 Subject: [PATCH 013/173] Check google calendar API scope to determine if write access is enabled (#84749) * Check google calendar API scope to determine if write access is enabled * Add API scope for calendar service for creating events --- homeassistant/components/google/calendar.py | 12 +++++++++-- tests/components/google/test_calendar.py | 22 ++++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 6f2b571d96b..a96eb6b2ca6 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -63,6 +63,7 @@ from . import ( load_config, update_config, ) +from .api import get_feature_access from .const import ( DATA_SERVICE, DATA_STORE, @@ -74,6 +75,7 @@ from .const import ( EVENT_START_DATE, EVENT_START_DATETIME, EVENT_TYPES_CONF, + FeatureAccess, ) _LOGGER = logging.getLogger(__name__) @@ -213,7 +215,10 @@ async def async_setup_entry( # Prefer calendar sync down of resources when possible. However, sync does not work # for search. Also free-busy calendars denormalize recurring events as individual # events which is not efficient for sync - support_write = calendar_item.access_role.is_writer + support_write = ( + calendar_item.access_role.is_writer + and get_feature_access(hass, config_entry) is FeatureAccess.read_write + ) if ( search := data.get(CONF_SEARCH) or calendar_item.access_role == AccessRole.FREE_BUSY_READER @@ -265,7 +270,10 @@ async def async_setup_entry( await hass.async_add_executor_job(append_calendars_to_config) platform = entity_platform.async_get_current_platform() - if any(calendar_item.access_role.is_writer for calendar_item in result.items): + if ( + any(calendar_item.access_role.is_writer for calendar_item in result.items) + and get_feature_access(hass, config_entry) is FeatureAccess.read_write + ): platform.async_register_entity_service( SERVICE_CREATE_EVENT, CREATE_EVENT_SCHEMA, diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 1ab92ca700e..7a0cd180a1f 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -14,7 +14,7 @@ from aiohttp.client_exceptions import ClientError from gcal_sync.auth import API_BASE_URL import pytest -from homeassistant.components.google.const import DOMAIN +from homeassistant.components.google.const import CONF_CALENDAR_ACCESS, DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -1054,8 +1054,24 @@ async def test_websocket_delete_recurring_event_instance( @pytest.mark.parametrize( - "calendar_access_role", - ["reader"], + "calendar_access_role,token_scopes,config_entry_options", + [ + ( + "reader", + ["https://www.googleapis.com/auth/calendar"], + {CONF_CALENDAR_ACCESS: "read_write"}, + ), + ( + "reader", + ["https://www.googleapis.com/auth/calendar.readonly"], + {CONF_CALENDAR_ACCESS: "read_only"}, + ), + ( + "owner", + ["https://www.googleapis.com/auth/calendar.readonly"], + {CONF_CALENDAR_ACCESS: "read_only"}, + ), + ], ) async def test_readonly_websocket_create( hass: HomeAssistant, From cae386465e08cd5a591b74e1600e17d7440dfc7c Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Thu, 29 Dec 2022 15:28:33 +0100 Subject: [PATCH 014/173] Catch vicare errors when deactivating preset fails (#84778) vicare: catch errors when deactivating preset fails --- homeassistant/components/vicare/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index c8a4781fa0a..9a55da0f219 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -6,6 +6,7 @@ import logging from typing import Any from PyViCare.PyViCareUtils import ( + PyViCareCommandError, PyViCareInvalidDataError, PyViCareNotSupportedFeatureError, PyViCareRateLimitError, @@ -354,7 +355,10 @@ class ViCareClimate(ClimateEntity): _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) if self._current_program != VICARE_PROGRAM_NORMAL: # We can't deactivate "normal" - self._circuit.deactivateProgram(self._current_program) + try: + self._circuit.deactivateProgram(self._current_program) + except PyViCareCommandError: + _LOGGER.debug("Unable to deactivate program %s", self._current_program) if vicare_program != VICARE_PROGRAM_NORMAL: # And we can't explicitly activate normal, either self._circuit.activateProgram(vicare_program) From fbdc7d44bca156c4278ed55ab27c0e7dfe371461 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 29 Dec 2022 21:51:16 +0100 Subject: [PATCH 015/173] Only subscribe to specific UniFi object ID (#84787) --- homeassistant/components/unifi/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index da9ca3828d7..79a80fad73c 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -83,6 +83,7 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): self.async_on_remove( handler.subscribe( self.async_signalling_callback, + id_filter=self._obj_id, ) ) From f1d509be03abf2889ca5e09e54438c52c1be38bd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Dec 2022 10:32:06 -0700 Subject: [PATCH 016/173] Remove `ozone` device class from OpenUV sensor (#84791) fixes undefined --- homeassistant/components/openuv/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 4f02f01a93c..44bde8341a0 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from homeassistant.components.sensor import ( - SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, @@ -51,7 +50,6 @@ SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_CURRENT_OZONE_LEVEL, name="Current ozone level", - device_class=SensorDeviceClass.OZONE, native_unit_of_measurement="du", state_class=SensorStateClass.MEASUREMENT, ), From 3c8a66abbe258f4aa7abae9076a5d01ad9e5000d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Dec 2022 15:57:50 -0700 Subject: [PATCH 017/173] Don't attempt setup on migrated AirVisual Pro in the `airvisual` domain (#84796) fixes undefined --- homeassistant/components/airvisual/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 55763d2dfbb..f8cb3250452 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -207,6 +207,11 @@ def _standardize_geography_config_entry( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AirVisual as config entry.""" + if CONF_API_KEY not in entry.data: + # If this is a migrated AirVisual Pro entry, there's no actual setup to do; + # that will be handled by the `airvisual_pro` domain: + return False + _standardize_geography_config_entry(hass, entry) websession = aiohttp_client.async_get_clientsession(hass) From f0514008faefcee01b3365a03b759a4d63eb1d68 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 30 Dec 2022 02:41:08 +0100 Subject: [PATCH 018/173] Deprecate YAML config in PI-Hole (#84797) create an issue about deprecated yaml config --- homeassistant/components/pi_hole/__init__.py | 25 ++++++++++++++----- homeassistant/components/pi_hole/strings.json | 6 +++++ .../components/pi_hole/translations/de.json | 6 +++++ .../components/pi_hole/translations/en.json | 6 +++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 8f725e9da50..714547ba961 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback 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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -69,14 +70,26 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = {} + if DOMAIN not in config: + return True + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.2.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + # import - if DOMAIN in config: - for conf in config[DOMAIN]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) + for conf in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf ) + ) return True diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json index fbf3c5a627b..e911779d5d7 100644 --- a/homeassistant/components/pi_hole/strings.json +++ b/homeassistant/components/pi_hole/strings.json @@ -25,5 +25,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The PI-Hole YAML configuration is being removed", + "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index 40a5db3c21f..831c1daf03e 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von PI-Hole mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die PI-Hole-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die PI-Hole YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 9053a70c18f..4333838ae64 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -25,5 +25,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The PI-Hole YAML configuration is being removed" + } } } \ No newline at end of file From bd86111dd8f9c1df1012afbba32440736bc15b66 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 29 Dec 2022 12:42:33 -0700 Subject: [PATCH 019/173] Fix AirVisual Pro sensors with incorrect units for their device classes (#84800) --- homeassistant/components/airvisual_pro/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/airvisual_pro/sensor.py b/homeassistant/components/airvisual_pro/sensor.py index b0e2a947632..9fb15c7ac74 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -39,7 +39,6 @@ SENSOR_DESCRIPTIONS = ( key=SENSOR_KIND_AQI, name="Air quality index", device_class=SensorDeviceClass.AQI, - native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( @@ -94,7 +93,7 @@ SENSOR_DESCRIPTIONS = ( key=SENSOR_KIND_VOC, name="VOC", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), ) From cc2b592221ff82701bf70e03c0c237b0a2cc0e50 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 29 Dec 2022 22:34:01 +0100 Subject: [PATCH 020/173] Correct missing alarm reset button on nibe (#84809) fixes undefined --- homeassistant/components/nibe_heatpump/button.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/button.py b/homeassistant/components/nibe_heatpump/button.py index da94ba9fc11..e8bb3026e47 100644 --- a/homeassistant/components/nibe_heatpump/button.py +++ b/homeassistant/components/nibe_heatpump/button.py @@ -24,11 +24,9 @@ async def async_setup_entry( coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] def reset_buttons(): - for entity_description in UNIT_COILGROUPS.get(coordinator.series, {}).get( - "main" - ): + if unit := UNIT_COILGROUPS.get(coordinator.series, {}).get("main"): try: - yield NibeAlarmResetButton(coordinator, entity_description) + yield NibeAlarmResetButton(coordinator, unit) except CoilNotFoundException as exception: LOGGER.debug("Skipping button %r", exception) @@ -46,6 +44,7 @@ class NibeAlarmResetButton(CoordinatorEntity[Coordinator], ButtonEntity): self._reset_coil = coordinator.heatpump.get_coil_by_address(unit.alarm_reset) self._alarm_coil = coordinator.heatpump.get_coil_by_address(unit.alarm) super().__init__(coordinator, {self._alarm_coil.address}) + self._attr_name = self._reset_coil.title self._attr_unique_id = f"{coordinator.unique_id}-alarm_reset" self._attr_device_info = coordinator.device_info From 522477d5a498b7cec6f081e93e017f8e333940aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 29 Dec 2022 15:09:37 -1000 Subject: [PATCH 021/173] Fix thermobeacon WS08 models that identify with manufacturer_id 27 (#84812) fixes #84706 --- homeassistant/components/thermobeacon/manifest.json | 8 +++++++- homeassistant/generated/bluetooth.py | 9 +++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json index 3e105eff138..01e66bbbb94 100644 --- a/homeassistant/components/thermobeacon/manifest.json +++ b/homeassistant/components/thermobeacon/manifest.json @@ -28,9 +28,15 @@ "manufacturer_data_start": [0], "connectable": false }, + { + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 27, + "manufacturer_data_start": [0], + "connectable": false + }, { "local_name": "ThermoBeacon", "connectable": false } ], - "requirements": ["thermobeacon-ble==0.4.0"], + "requirements": ["thermobeacon-ble==0.6.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 6cf107124da..dc50434f63f 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -385,6 +385,15 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "manufacturer_id": 24, "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", }, + { + "connectable": False, + "domain": "thermobeacon", + "manufacturer_data_start": [ + 0, + ], + "manufacturer_id": 27, + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", + }, { "connectable": False, "domain": "thermobeacon", diff --git a/requirements_all.txt b/requirements_all.txt index 19c1fc8cb4d..fa5a7f83b7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2435,7 +2435,7 @@ tesla-wall-connector==1.0.2 # tf-models-official==2.5.0 # homeassistant.components.thermobeacon -thermobeacon-ble==0.4.0 +thermobeacon-ble==0.6.0 # homeassistant.components.thermopro thermopro-ble==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 214d5d06ecd..d3d64c3adc6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1693,7 +1693,7 @@ tesla-powerwall==0.3.18 tesla-wall-connector==1.0.2 # homeassistant.components.thermobeacon -thermobeacon-ble==0.4.0 +thermobeacon-ble==0.6.0 # homeassistant.components.thermopro thermopro-ble==0.4.3 From 46e11c2fa81281316f453286f0c1d5a7fb2e9966 Mon Sep 17 00:00:00 2001 From: shbatm Date: Thu, 29 Dec 2022 19:37:34 -0600 Subject: [PATCH 022/173] ISY994: Bump PyISY to 3.0.10 (#84821) --- homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index baaaf8d5da8..cfc7f5d0e22 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY994", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.9"], + "requirements": ["pyisy==3.0.10"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index fa5a7f83b7c..82872b65b55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1674,7 +1674,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.9 +pyisy==3.0.10 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3d64c3adc6..a553d88133b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1187,7 +1187,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.9 +pyisy==3.0.10 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From d9aff9d7b00a5fffd42c211f04ba022e4435760b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Dec 2022 20:55:27 -0500 Subject: [PATCH 023/173] Bumped version to 2023.1.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 cc2bac738cc..db4132465c6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 83d90da2b0f..295bfed8057 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.0b1" +version = "2023.1.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f05de2b28c51381c84e9cead0d03a6bbf3c8aebf Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 30 Dec 2022 15:30:29 +0100 Subject: [PATCH 024/173] Actually try port when finding next available port for ssdp server (#84206) fixes undefined --- homeassistant/components/ssdp/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 77065818369..18ed063c8bc 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -706,11 +706,12 @@ async def _async_find_next_available_port(source: AddressTupleVXType) -> int: test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) for port in range(UPNP_SERVER_MIN_PORT, UPNP_SERVER_MAX_PORT): + addr = (source[0],) + (port,) + source[2:] try: - test_socket.bind(source) + test_socket.bind(addr) return port except OSError: - if port == UPNP_SERVER_MAX_PORT: + if port == UPNP_SERVER_MAX_PORT - 1: raise raise RuntimeError("unreachable") From 2cb7a80f98f66903d0a9b3673a152772488243ff Mon Sep 17 00:00:00 2001 From: Chris Straffon Date: Fri, 30 Dec 2022 14:33:30 +0000 Subject: [PATCH 025/173] Fix growatt identification issue (#84628) Fixes https://github.com/home-assistant/core/issues/84600 fixes undefined --- homeassistant/components/growatt_server/config_flow.py | 6 +++++- homeassistant/components/growatt_server/sensor.py | 10 ++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/growatt_server/config_flow.py b/homeassistant/components/growatt_server/config_flow.py index 11f082f1eab..a4dcd25173f 100644 --- a/homeassistant/components/growatt_server/config_flow.py +++ b/homeassistant/components/growatt_server/config_flow.py @@ -22,7 +22,7 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialise growatt server flow.""" - self.api = growattServer.GrowattApi() + self.api = None self.user_id = None self.data = {} @@ -46,6 +46,10 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not user_input: return self._async_show_user_form() + # Initialise the library with the username & a random id each time it is started + self.api = growattServer.GrowattApi( + add_random_user_id=True, agent_identifier=user_input[CONF_USERNAME] + ) self.api.server_url = user_input[CONF_URL] login_response = await self.hass.async_add_executor_job( self.api.login, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 57dd7f10fb1..1dea3b3510c 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -80,14 +80,8 @@ async def async_setup_entry( config[CONF_URL] = url hass.config_entries.async_update_entry(config_entry, data=config) - # Initialise the library with a random user id each time it is started, - # also extend the library's default identifier to include 'home-assistant' - api = growattServer.GrowattApi( - add_random_user_id=True, - agent_identifier=( - f"{growattServer.GrowattApi.agent_identifier} - home-assistant" - ), - ) + # Initialise the library with the username & a random id each time it is started + api = growattServer.GrowattApi(add_random_user_id=True, agent_identifier=username) api.server_url = url devices, plant_id = await hass.async_add_executor_job(get_device_list, api, config) From b24c40f2df2de722501dc970abac58a75e3b4104 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 28 Dec 2022 21:49:00 +0100 Subject: [PATCH 026/173] Enable unit conversion for DATA_RATE (#84698) --- homeassistant/components/sensor/__init__.py | 2 + homeassistant/util/unit_conversion.py | 23 +++++++++ tests/util/test_unit_conversion.py | 57 +++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 9adc1f74cac..08386ced6de 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -86,6 +86,7 @@ from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, + DataRateConverter, DistanceConverter, MassConverter, PressureConverter, @@ -466,6 +467,7 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] # Note: this needs to be aligned with frontend: OVERRIDE_SENSOR_UNITS in # `entity-registry-settings.ts` UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { + SensorDeviceClass.DATA_RATE: DataRateConverter, SensorDeviceClass.DISTANCE: DistanceConverter, SensorDeviceClass.GAS: VolumeConverter, SensorDeviceClass.PRECIPITATION: DistanceConverter, diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 1aeda96b6b3..3cfd0a764e0 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, + UnitOfDataRate, UnitOfEnergy, UnitOfLength, UnitOfMass, @@ -86,6 +87,28 @@ class BaseUnitConverter: return cls._UNIT_CONVERSION[from_unit] / cls._UNIT_CONVERSION[to_unit] +class DataRateConverter(BaseUnitConverter): + """Utility to convert data rate values.""" + + UNIT_CLASS = "data_rate" + NORMALIZED_UNIT = UnitOfDataRate.BITS_PER_SECOND + # Units in terms of bits + _UNIT_CONVERSION: dict[str, float] = { + UnitOfDataRate.BITS_PER_SECOND: 1, + UnitOfDataRate.KILOBITS_PER_SECOND: 1 / 1e3, + UnitOfDataRate.MEGABITS_PER_SECOND: 1 / 1e6, + UnitOfDataRate.GIGABITS_PER_SECOND: 1 / 1e9, + UnitOfDataRate.BYTES_PER_SECOND: 1 / 8, + UnitOfDataRate.KILOBYTES_PER_SECOND: 1 / 8e3, + UnitOfDataRate.MEGABYTES_PER_SECOND: 1 / 8e6, + UnitOfDataRate.GIGABYTES_PER_SECOND: 1 / 8e9, + UnitOfDataRate.KIBIBYTES_PER_SECOND: 1 / 2**13, + UnitOfDataRate.MEBIBYTES_PER_SECOND: 1 / 2**23, + UnitOfDataRate.GIBIBYTES_PER_SECOND: 1 / 2**33, + } + VALID_UNITS = set(UnitOfDataRate) + + class DistanceConverter(BaseUnitConverter): """Utility to convert distance values.""" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 0d5cb7143ec..f5c9970ca59 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -2,6 +2,7 @@ import pytest from homeassistant.const import ( + UnitOfDataRate, UnitOfEnergy, UnitOfLength, UnitOfMass, @@ -15,6 +16,7 @@ from homeassistant.const import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.util.unit_conversion import ( BaseUnitConverter, + DataRateConverter, DistanceConverter, EnergyConverter, MassConverter, @@ -31,6 +33,7 @@ INVALID_SYMBOL = "bob" @pytest.mark.parametrize( "converter,valid_unit", [ + (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), (DistanceConverter, UnitOfLength.KILOMETERS), (DistanceConverter, UnitOfLength.METERS), (DistanceConverter, UnitOfLength.CENTIMETERS), @@ -85,6 +88,7 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) @pytest.mark.parametrize( "converter,valid_unit", [ + (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), (DistanceConverter, UnitOfLength.KILOMETERS), (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), (MassConverter, UnitOfMass.GRAMS), @@ -111,6 +115,11 @@ def test_convert_invalid_unit( @pytest.mark.parametrize( "converter,from_unit,to_unit", [ + ( + DataRateConverter, + UnitOfDataRate.BYTES_PER_SECOND, + UnitOfDataRate.BITS_PER_SECOND, + ), (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS), (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR), (MassConverter, UnitOfMass.GRAMS, UnitOfMass.KILOGRAMS), @@ -132,6 +141,12 @@ def test_convert_nonnumeric_value( @pytest.mark.parametrize( "converter,from_unit,to_unit,expected", [ + ( + DataRateConverter, + UnitOfDataRate.BITS_PER_SECOND, + UnitOfDataRate.BYTES_PER_SECOND, + 8, + ), (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS, 1 / 1000), (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000), (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), @@ -168,6 +183,48 @@ def test_get_unit_ratio( assert converter.get_unit_ratio(from_unit, to_unit) == expected +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (8e3, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.KILOBITS_PER_SECOND), + (8e6, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.MEGABITS_PER_SECOND), + (8e9, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.GIGABITS_PER_SECOND), + (8, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.BYTES_PER_SECOND), + (8e3, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.KILOBYTES_PER_SECOND), + (8e6, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.MEGABYTES_PER_SECOND), + (8e9, UnitOfDataRate.BITS_PER_SECOND, 1, UnitOfDataRate.GIGABYTES_PER_SECOND), + ( + 8 * 2**10, + UnitOfDataRate.BITS_PER_SECOND, + 1, + UnitOfDataRate.KIBIBYTES_PER_SECOND, + ), + ( + 8 * 2**20, + UnitOfDataRate.BITS_PER_SECOND, + 1, + UnitOfDataRate.MEBIBYTES_PER_SECOND, + ), + ( + 8 * 2**30, + UnitOfDataRate.BITS_PER_SECOND, + 1, + UnitOfDataRate.GIBIBYTES_PER_SECOND, + ), + ], +) +def test_data_rate_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert DataRateConverter.convert(value, from_unit, to_unit) == pytest.approx( + expected + ) + + @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ From 5901964bf65f16a85d4deb4b5e4921cf9eb3e66c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 29 Dec 2022 09:58:15 +0100 Subject: [PATCH 027/173] Enable unit conversion for DATA_SIZE (#84699) --- homeassistant/components/sensor/__init__.py | 2 + homeassistant/util/unit_conversion.py | 33 +++++++++++++++ tests/util/test_unit_conversion.py | 47 +++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 08386ced6de..12f49766834 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -88,6 +88,7 @@ from homeassistant.util.unit_conversion import ( BaseUnitConverter, DataRateConverter, DistanceConverter, + InformationConverter, MassConverter, PressureConverter, SpeedConverter, @@ -468,6 +469,7 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] # `entity-registry-settings.ts` UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { SensorDeviceClass.DATA_RATE: DataRateConverter, + SensorDeviceClass.DATA_SIZE: InformationConverter, SensorDeviceClass.DISTANCE: DistanceConverter, SensorDeviceClass.GAS: VolumeConverter, SensorDeviceClass.PRECIPITATION: DistanceConverter, diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 3cfd0a764e0..623b70da1a8 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -5,6 +5,7 @@ from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, UnitOfDataRate, UnitOfEnergy, + UnitOfInformation, UnitOfLength, UnitOfMass, UnitOfPower, @@ -155,6 +156,38 @@ class EnergyConverter(BaseUnitConverter): } +class InformationConverter(BaseUnitConverter): + """Utility to convert information values.""" + + UNIT_CLASS = "information" + NORMALIZED_UNIT = UnitOfInformation.BITS + # Units in terms of bits + _UNIT_CONVERSION: dict[str, float] = { + UnitOfInformation.BITS: 1, + UnitOfInformation.KILOBITS: 1 / 1e3, + UnitOfInformation.MEGABITS: 1 / 1e6, + UnitOfInformation.GIGABITS: 1 / 1e9, + UnitOfInformation.BYTES: 1 / 8, + UnitOfInformation.KILOBYTES: 1 / 8e3, + UnitOfInformation.MEGABYTES: 1 / 8e6, + UnitOfInformation.GIGABYTES: 1 / 8e9, + UnitOfInformation.TERABYTES: 1 / 8e12, + UnitOfInformation.PETABYTES: 1 / 8e15, + UnitOfInformation.EXABYTES: 1 / 8e18, + UnitOfInformation.ZETTABYTES: 1 / 8e21, + UnitOfInformation.YOTTABYTES: 1 / 8e24, + UnitOfInformation.KIBIBYTES: 1 / 2**13, + UnitOfInformation.MEBIBYTES: 1 / 2**23, + UnitOfInformation.GIBIBYTES: 1 / 2**33, + UnitOfInformation.TEBIBYTES: 1 / 2**43, + UnitOfInformation.PEBIBYTES: 1 / 2**53, + UnitOfInformation.EXBIBYTES: 1 / 2**63, + UnitOfInformation.ZEBIBYTES: 1 / 2**73, + UnitOfInformation.YOBIBYTES: 1 / 2**83, + } + VALID_UNITS = set(UnitOfInformation) + + class MassConverter(BaseUnitConverter): """Utility to convert mass values.""" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index f5c9970ca59..a0e926ed2a6 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -4,6 +4,7 @@ import pytest from homeassistant.const import ( UnitOfDataRate, UnitOfEnergy, + UnitOfInformation, UnitOfLength, UnitOfMass, UnitOfPower, @@ -19,6 +20,7 @@ from homeassistant.util.unit_conversion import ( DataRateConverter, DistanceConverter, EnergyConverter, + InformationConverter, MassConverter, PowerConverter, PressureConverter, @@ -46,6 +48,7 @@ INVALID_SYMBOL = "bob" (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), (EnergyConverter, UnitOfEnergy.MEGA_WATT_HOUR), (EnergyConverter, UnitOfEnergy.GIGA_JOULE), + (InformationConverter, UnitOfInformation.GIGABYTES), (MassConverter, UnitOfMass.GRAMS), (MassConverter, UnitOfMass.KILOGRAMS), (MassConverter, UnitOfMass.MICROGRAMS), @@ -91,6 +94,7 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), (DistanceConverter, UnitOfLength.KILOMETERS), (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), + (InformationConverter, UnitOfInformation.GIBIBYTES), (MassConverter, UnitOfMass.GRAMS), (PowerConverter, UnitOfPower.WATT), (PressureConverter, UnitOfPressure.PA), @@ -122,6 +126,11 @@ def test_convert_invalid_unit( ), (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS), (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR), + ( + InformationConverter, + UnitOfInformation.GIBIBYTES, + UnitOfInformation.GIGABYTES, + ), (MassConverter, UnitOfMass.GRAMS, UnitOfMass.KILOGRAMS), (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT), (PressureConverter, UnitOfPressure.HPA, UnitOfPressure.INHG), @@ -149,6 +158,7 @@ def test_convert_nonnumeric_value( ), (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS, 1 / 1000), (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000), + (InformationConverter, UnitOfInformation.BITS, UnitOfInformation.BYTES, 8), (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), ( PressureConverter, @@ -364,6 +374,43 @@ def test_energy_convert( assert EnergyConverter.convert(value, from_unit, to_unit) == expected +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (8e3, UnitOfInformation.BITS, 8, UnitOfInformation.KILOBITS), + (8e6, UnitOfInformation.BITS, 8, UnitOfInformation.MEGABITS), + (8e9, UnitOfInformation.BITS, 8, UnitOfInformation.GIGABITS), + (8, UnitOfInformation.BITS, 1, UnitOfInformation.BYTES), + (8e3, UnitOfInformation.BITS, 1, UnitOfInformation.KILOBYTES), + (8e6, UnitOfInformation.BITS, 1, UnitOfInformation.MEGABYTES), + (8e9, UnitOfInformation.BITS, 1, UnitOfInformation.GIGABYTES), + (8e12, UnitOfInformation.BITS, 1, UnitOfInformation.TERABYTES), + (8e15, UnitOfInformation.BITS, 1, UnitOfInformation.PETABYTES), + (8e18, UnitOfInformation.BITS, 1, UnitOfInformation.EXABYTES), + (8e21, UnitOfInformation.BITS, 1, UnitOfInformation.ZETTABYTES), + (8e24, UnitOfInformation.BITS, 1, UnitOfInformation.YOTTABYTES), + (8 * 2**10, UnitOfInformation.BITS, 1, UnitOfInformation.KIBIBYTES), + (8 * 2**20, UnitOfInformation.BITS, 1, UnitOfInformation.MEBIBYTES), + (8 * 2**30, UnitOfInformation.BITS, 1, UnitOfInformation.GIBIBYTES), + (8 * 2**40, UnitOfInformation.BITS, 1, UnitOfInformation.TEBIBYTES), + (8 * 2**50, UnitOfInformation.BITS, 1, UnitOfInformation.PEBIBYTES), + (8 * 2**60, UnitOfInformation.BITS, 1, UnitOfInformation.EXBIBYTES), + (8 * 2**70, UnitOfInformation.BITS, 1, UnitOfInformation.ZEBIBYTES), + (8 * 2**80, UnitOfInformation.BITS, 1, UnitOfInformation.YOBIBYTES), + ], +) +def test_information_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert InformationConverter.convert(value, from_unit, to_unit) == pytest.approx( + expected + ) + + @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ From ac3711e6abb9e70a1c1e0cda7dccb36e87880cf0 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Fri, 30 Dec 2022 13:09:36 +0000 Subject: [PATCH 028/173] Use pycarwings2 2.14 (#84792) fixes undefined --- homeassistant/components/nissan_leaf/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 87c29013544..ca932e78476 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -2,7 +2,7 @@ "domain": "nissan_leaf", "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", - "requirements": ["pycarwings2==2.13"], + "requirements": ["pycarwings2==2.14"], "codeowners": ["@filcole"], "iot_class": "cloud_polling", "loggers": ["pycarwings2"] diff --git a/requirements_all.txt b/requirements_all.txt index 82872b65b55..f9a3038cda7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1503,7 +1503,7 @@ pybotvac==0.0.23 pybravia==0.2.3 # homeassistant.components.nissan_leaf -pycarwings2==2.13 +pycarwings2==2.14 # homeassistant.components.cloudflare pycfdns==2.0.1 From ca28006d767aca861cf6c66511720866e82d1d0d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 30 Dec 2022 12:51:05 +0100 Subject: [PATCH 029/173] Add mV as a unit for voltage and enable conversions (#84805) fixes undefined --- homeassistant/components/number/__init__.py | 2 +- homeassistant/components/sensor/__init__.py | 6 ++++-- homeassistant/util/unit_conversion.py | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 5a48d00d4da..523cd50a6ed 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -296,7 +296,7 @@ class NumberDeviceClass(StrEnum): VOLTAGE = "voltage" """Voltage. - Unit of measurement: `V` + Unit of measurement: `V`, `mV` """ VOLUME = "volume" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 12f49766834..e893c0f9e79 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -88,6 +88,7 @@ from homeassistant.util.unit_conversion import ( BaseUnitConverter, DataRateConverter, DistanceConverter, + ElectricPotentialConverter, InformationConverter, MassConverter, PressureConverter, @@ -390,7 +391,7 @@ class SensorDeviceClass(StrEnum): VOLTAGE = "voltage" """Voltage. - Unit of measurement: `V` + Unit of measurement: `V`, `mV` """ VOLUME = "volume" @@ -476,6 +477,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = SensorDeviceClass.PRESSURE: PressureConverter, SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, + SensorDeviceClass.VOLTAGE: ElectricPotentialConverter, SensorDeviceClass.VOLUME: VolumeConverter, SensorDeviceClass.WATER: VolumeConverter, SensorDeviceClass.WEIGHT: MassConverter, @@ -537,7 +539,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { CONCENTRATION_MICROGRAMS_PER_CUBIC_METER }, - SensorDeviceClass.VOLTAGE: {UnitOfElectricPotential.VOLT}, + SensorDeviceClass.VOLTAGE: set(UnitOfElectricPotential), SensorDeviceClass.VOLUME: set(UnitOfVolume), SensorDeviceClass.WATER: { UnitOfVolume.CENTUM_CUBIC_FEET, diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 623b70da1a8..fc58014c143 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -4,6 +4,7 @@ from __future__ import annotations from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, UnitOfDataRate, + UnitOfElectricPotential, UnitOfEnergy, UnitOfInformation, UnitOfLength, @@ -137,6 +138,21 @@ class DistanceConverter(BaseUnitConverter): } +class ElectricPotentialConverter(BaseUnitConverter): + """Utility to convert electric potential values.""" + + UNIT_CLASS = "voltage" + NORMALIZED_UNIT = UnitOfElectricPotential.VOLT + _UNIT_CONVERSION: dict[str, float] = { + UnitOfElectricPotential.VOLT: 1, + UnitOfElectricPotential.MILLIVOLT: 1e3, + } + VALID_UNITS = { + UnitOfElectricPotential.VOLT, + UnitOfElectricPotential.MILLIVOLT, + } + + class EnergyConverter(BaseUnitConverter): """Utility to convert energy values.""" From 50d9e3efe6a79e8d1c0e1b651175203044bc35cf Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 30 Dec 2022 13:55:14 +0100 Subject: [PATCH 030/173] Add mA to SensorDeviceClass.CURRENT units (#84492) fixes undefined --- homeassistant/components/number/__init__.py | 2 +- homeassistant/components/sensor/__init__.py | 6 +++-- homeassistant/util/unit_conversion.py | 13 ++++++++++ tests/util/test_unit_conversion.py | 28 +++++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 523cd50a6ed..de2580eab75 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -91,7 +91,7 @@ class NumberDeviceClass(StrEnum): CURRENT = "current" """Current. - Unit of measurement: `A` + Unit of measurement: `A`, `mA` """ DATA_RATE = "data_rate" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index e893c0f9e79..2db8a7680c4 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -88,6 +88,7 @@ from homeassistant.util.unit_conversion import ( BaseUnitConverter, DataRateConverter, DistanceConverter, + ElectricCurrentConverter, ElectricPotentialConverter, InformationConverter, MassConverter, @@ -186,7 +187,7 @@ class SensorDeviceClass(StrEnum): CURRENT = "current" """Current. - Unit of measurement: `A` + Unit of measurement: `A`, `mA` """ DATA_RATE = "data_rate" @@ -472,6 +473,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = SensorDeviceClass.DATA_RATE: DataRateConverter, SensorDeviceClass.DATA_SIZE: InformationConverter, SensorDeviceClass.DISTANCE: DistanceConverter, + SensorDeviceClass.CURRENT: ElectricCurrentConverter, SensorDeviceClass.GAS: VolumeConverter, SensorDeviceClass.PRECIPITATION: DistanceConverter, SensorDeviceClass.PRESSURE: PressureConverter, @@ -491,7 +493,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.BATTERY: {PERCENTAGE}, SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, - SensorDeviceClass.CURRENT: {UnitOfElectricCurrent.AMPERE}, + SensorDeviceClass.CURRENT: set(UnitOfElectricCurrent), SensorDeviceClass.DATA_RATE: set(UnitOfDataRate), SensorDeviceClass.DATA_SIZE: set(UnitOfInformation), SensorDeviceClass.DISTANCE: set(UnitOfLength), diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index fc58014c143..f9f4d78899a 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -4,6 +4,7 @@ from __future__ import annotations from homeassistant.const import ( UNIT_NOT_RECOGNIZED_TEMPLATE, UnitOfDataRate, + UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, UnitOfInformation, @@ -138,6 +139,18 @@ class DistanceConverter(BaseUnitConverter): } +class ElectricCurrentConverter(BaseUnitConverter): + """Utility to convert electric current values.""" + + UNIT_CLASS = "electric_current" + NORMALIZED_UNIT = UnitOfElectricCurrent.AMPERE + _UNIT_CONVERSION: dict[str, float] = { + UnitOfElectricCurrent.AMPERE: 1, + UnitOfElectricCurrent.MILLIAMPERE: 1e3, + } + VALID_UNITS = set(UnitOfElectricCurrent) + + class ElectricPotentialConverter(BaseUnitConverter): """Utility to convert electric potential values.""" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index a0e926ed2a6..83aaf6224b5 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -3,6 +3,7 @@ import pytest from homeassistant.const import ( UnitOfDataRate, + UnitOfElectricCurrent, UnitOfEnergy, UnitOfInformation, UnitOfLength, @@ -19,6 +20,7 @@ from homeassistant.util.unit_conversion import ( BaseUnitConverter, DataRateConverter, DistanceConverter, + ElectricCurrentConverter, EnergyConverter, InformationConverter, MassConverter, @@ -44,6 +46,8 @@ INVALID_SYMBOL = "bob" (DistanceConverter, UnitOfLength.YARDS), (DistanceConverter, UnitOfLength.FEET), (DistanceConverter, UnitOfLength.INCHES), + (ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE), + (ElectricCurrentConverter, UnitOfElectricCurrent.MILLIAMPERE), (EnergyConverter, UnitOfEnergy.WATT_HOUR), (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), (EnergyConverter, UnitOfEnergy.MEGA_WATT_HOUR), @@ -93,6 +97,7 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) [ (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), (DistanceConverter, UnitOfLength.KILOMETERS), + (ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE), (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), (InformationConverter, UnitOfInformation.GIBIBYTES), (MassConverter, UnitOfMass.GRAMS), @@ -157,6 +162,12 @@ def test_convert_nonnumeric_value( 8, ), (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS, 1 / 1000), + ( + ElectricCurrentConverter, + UnitOfElectricCurrent.AMPERE, + UnitOfElectricCurrent.MILLIAMPERE, + 1 / 1000, + ), (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000), (InformationConverter, UnitOfInformation.BITS, UnitOfInformation.BYTES, 8), (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), @@ -235,6 +246,23 @@ def test_data_rate_convert( ) +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (5, UnitOfElectricCurrent.AMPERE, 5000, UnitOfElectricCurrent.MILLIAMPERE), + (5, UnitOfElectricCurrent.MILLIAMPERE, 0.005, UnitOfElectricCurrent.AMPERE), + ], +) +def test_electric_current_convert( + value: float, + from_unit: str, + expected: float, + to_unit: str, +) -> None: + """Test conversion to other units.""" + assert ElectricCurrentConverter.convert(value, from_unit, to_unit) == expected + + @pytest.mark.parametrize( "value,from_unit,expected,to_unit", [ From 0e0677b69051f239fc83fd257d0e207b2f590e2b Mon Sep 17 00:00:00 2001 From: Damian Sypniewski <16312757+dsypniewski@users.noreply.github.com> Date: Fri, 30 Dec 2022 16:48:39 +0900 Subject: [PATCH 031/173] Add option to retrieve SwitchBot Lock encryption key through config flow (#84830) Co-authored-by: J. Nick Koston --- .../components/switchbot/config_flow.py | 71 +++++- .../components/switchbot/manifest.json | 2 +- .../components/switchbot/strings.json | 18 +- .../components/switchbot/translations/en.json | 25 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switchbot/__init__.py | 2 +- .../components/switchbot/test_config_flow.py | 221 ++++++++++++++---- 8 files changed, 285 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index dfb91c4eb9a..a71e30b2f96 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -17,7 +17,12 @@ from homeassistant.components.bluetooth import ( async_discovered_service_info, ) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_ADDRESS, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.const import ( + CONF_ADDRESS, + CONF_PASSWORD, + CONF_SENSOR_TYPE, + CONF_USERNAME, +) from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult @@ -94,6 +99,8 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): "name": data["modelFriendlyName"], "address": short_address(discovery_info.address), } + if model_name == SwitchbotModel.LOCK: + return await self.async_step_lock_chose_method() if self._discovered_adv.data["isEncrypted"]: return await self.async_step_password() return await self.async_step_confirm() @@ -151,6 +158,57 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): }, ) + async def async_step_lock_auth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the SwitchBot API auth step.""" + errors = {} + assert self._discovered_adv is not None + if user_input is not None: + try: + key_details = await self.hass.async_add_executor_job( + SwitchbotLock.retrieve_encryption_key, + self._discovered_adv.address, + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + return await self.async_step_lock_key(key_details) + except RuntimeError: + errors = { + "base": "auth_failed", + } + + user_input = user_input or {} + return self.async_show_form( + step_id="lock_auth", + errors=errors, + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME) + ): str, + vol.Required(CONF_PASSWORD): str, + } + ), + description_placeholders={ + "name": name_from_discovery(self._discovered_adv), + }, + ) + + async def async_step_lock_chose_method( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the SwitchBot API chose method step.""" + assert self._discovered_adv is not None + + return self.async_show_menu( + step_id="lock_chose_method", + menu_options=["lock_auth", "lock_key"], + description_placeholders={ + "name": name_from_discovery(self._discovered_adv), + }, + ) + async def async_step_lock_key( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -160,12 +218,11 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: if not await SwitchbotLock.verify_encryption_key( self._discovered_adv.device, - user_input.get(CONF_KEY_ID), - user_input.get(CONF_ENCRYPTION_KEY), + user_input[CONF_KEY_ID], + user_input[CONF_ENCRYPTION_KEY], ): errors = { - CONF_KEY_ID: "key_id_invalid", - CONF_ENCRYPTION_KEY: "encryption_key_invalid", + "base": "encryption_key_invalid", } else: return await self._async_create_entry_from_discovery(user_input) @@ -229,7 +286,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): device_adv = self._discovered_advs[user_input[CONF_ADDRESS]] await self._async_set_device(device_adv) if device_adv.data.get("modelName") == SwitchbotModel.LOCK: - return await self.async_step_lock_key() + return await self.async_step_lock_chose_method() if device_adv.data["isEncrypted"]: return await self.async_step_password() return await self._async_create_entry_from_discovery(user_input) @@ -241,7 +298,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): device_adv = list(self._discovered_advs.values())[0] await self._async_set_device(device_adv) if device_adv.data.get("modelName") == SwitchbotModel.LOCK: - return await self.async_step_lock_key() + return await self.async_step_lock_chose_method() if device_adv.data["isEncrypted"]: return await self.async_step_password() return await self.async_step_confirm() diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b407ab73c24..b5b3d633285 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.33.0"], + "requirements": ["PySwitchbot==0.34.1"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index bb4accdbcf8..10a623a70d7 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -22,11 +22,25 @@ "key_id": "Key ID", "encryption_key": "Encryption key" } + }, + "lock_auth": { + "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "lock_chose_method": { + "description": "Choose configuration method, details can be found in the documentation.", + "menu_options": { + "lock_auth": "SwitchBot app login and password", + "lock_key": "Lock encryption key" + } } }, "error": { - "key_id_invalid": "Key ID or Encryption key is invalid", - "encryption_key_invalid": "Key ID or Encryption key is invalid" + "encryption_key_invalid": "Key ID or Encryption key is invalid", + "auth_failed": "Authentication failed" }, "abort": { "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 7e4f1af5ba0..ab2ffa8d6ac 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,11 +7,36 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, + "error": { + "auth_failed": "Authentication failed", + "encryption_key_invalid": "Key ID or Encryption key is invalid" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "Do you want to set up {name}?" }, + "lock_auth": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key." + }, + "lock_chose_method": { + "description": "Choose configuration method, details can be found in the documentation.", + "menu_options": { + "lock_auth": "SwitchBot app login and password", + "lock_key": "Lock encryption key" + } + }, + "lock_key": { + "data": { + "encryption_key": "Encryption key", + "key_id": "Key ID" + }, + "description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation." + }, "password": { "data": { "password": "Password" diff --git a/requirements_all.txt b/requirements_all.txt index f9a3038cda7..a3a1cd67e2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.33.0 +PySwitchbot==0.34.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a553d88133b..0af3d10c7ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.33.0 +PySwitchbot==0.34.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 7bc574de9b4..ce39579915f 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -180,7 +180,7 @@ WOLOCK_SERVICE_INFO = BluetoothServiceInfoBleak( advertisement=generate_advertisement_data( local_name="WoLock", manufacturer_data={2409: b"\xf1\t\x9fE\x1a]\xda\x83\x00 "}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"o\x80d"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"o\x80d"}, service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoLock"), diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 96e2e0ee172..6e1a1a14c6a 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -8,7 +8,13 @@ from homeassistant.components.switchbot.const import ( CONF_RETRY_COUNT, ) from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER -from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.const import ( + CONF_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_SENSOR_TYPE, + CONF_USERNAME, +) from homeassistant.data_entry_flow import FlowResultType from . import ( @@ -85,6 +91,66 @@ async def test_bluetooth_discovery_requires_password(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_bluetooth_discovery_lock_key(hass): + """Test discovery via bluetooth with a lock.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOLOCK_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "lock_chose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "lock_key"} + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_key" + assert result["errors"] == {} + + with patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=False, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KEY_ID: "", + CONF_ENCRYPTION_KEY: "", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_key" + assert result["errors"] == {"base": "encryption_key_invalid"} + + with patch_async_setup_entry() as mock_setup_entry, patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Lock EEFF" + assert result["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + CONF_SENSOR_TYPE: "lock", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_bluetooth_discovery_already_setup(hass): """Test discovery via bluetooth with a valid device when already setup.""" entry = MockConfigEntry( @@ -327,7 +393,7 @@ async def test_user_setup_single_bot_with_password(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_setup_wolock(hass): +async def test_user_setup_wolock_key(hass): """Test the user initiated form for a lock.""" with patch( @@ -337,14 +403,39 @@ async def test_user_setup_wolock(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "lock_chose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "lock_key"} + ) + await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} - with patch_async_setup_entry() as mock_setup_entry, patch( - "switchbot.SwitchbotLock.verify_encryption_key", return_value=True + with patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=False, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KEY_ID: "", + CONF_ENCRYPTION_KEY: "", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_key" + assert result["errors"] == {"base": "encryption_key_invalid"} + + with patch_async_setup_entry() as mock_setup_entry, patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_KEY_ID: "ff", @@ -353,9 +444,77 @@ async def test_user_setup_wolock(hass): ) await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Lock EEFF" - assert result2["data"] == { + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Lock EEFF" + assert result["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + CONF_SENSOR_TYPE: "lock", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_setup_wolock_auth(hass): + """Test the user initiated form for a lock.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOLOCK_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "lock_chose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "lock_auth"} + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_auth" + assert result["errors"] == {} + + with patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", + side_effect=RuntimeError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "", + CONF_PASSWORD: "", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_auth" + assert result["errors"] == {"base": "auth_failed"} + + with patch_async_setup_entry() as mock_setup_entry, patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, + ), patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", + return_value={ + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + }, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Lock EEFF" + assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", @@ -387,12 +546,20 @@ async def test_user_setup_wolock_or_bot(hass): USER_INPUT, ) await hass.async_block_till_done() + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "lock_chose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "lock_key"} + ) + await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} with patch_async_setup_entry() as mock_setup_entry, patch( - "switchbot.SwitchbotLock.verify_encryption_key", return_value=True + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -415,42 +582,6 @@ async def test_user_setup_wolock_or_bot(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_setup_wolock_invalid_encryption_key(hass): - """Test the user initiated form for a lock with invalid encryption key.""" - - with patch( - "homeassistant.components.switchbot.config_flow.async_discovered_service_info", - return_value=[WOLOCK_SERVICE_INFO], - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "lock_key" - assert result["errors"] == {} - - with patch_async_setup_entry() as mock_setup_entry, patch( - "switchbot.SwitchbotLock.verify_encryption_key", return_value=False - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_KEY_ID: "", - CONF_ENCRYPTION_KEY: "", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == FlowResultType.FORM - assert result2["step_id"] == "lock_key" - assert result2["errors"] == { - CONF_KEY_ID: "key_id_invalid", - CONF_ENCRYPTION_KEY: "encryption_key_invalid", - } - - assert len(mock_setup_entry.mock_calls) == 0 - - async def test_user_setup_wosensor(hass): """Test the user initiated form with password and valid mac.""" with patch( From 80ac4c0269c8129896104201adc9588f510c96b2 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Fri, 30 Dec 2022 18:35:18 +0300 Subject: [PATCH 032/173] Redesign and refactor Bravia TV config_flow (#84832) fixes undefined --- .../components/braviatv/config_flow.py | 158 +++++++------ .../components/braviatv/strings.json | 27 ++- .../components/braviatv/translations/en.json | 21 +- tests/components/braviatv/test_config_flow.py | 219 +++++++----------- 4 files changed, 194 insertions(+), 231 deletions(-) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 369aae374cf..43d2059c547 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -44,8 +44,6 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.client: BraviaTV | None = None self.device_config: dict[str, Any] = {} self.entry: ConfigEntry | None = None - self.client_id: str = "" - self.nickname: str = "" @staticmethod @callback @@ -62,8 +60,13 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) self.client = BraviaTV(host=host, session=session) - async def async_create_device(self) -> FlowResult: - """Initialize and create Bravia TV device from config.""" + async def gen_instance_ids(self) -> tuple[str, str]: + """Generate client_id and nickname.""" + uuid = await instance_id.async_get(self.hass) + return uuid, f"{NICKNAME_PREFIX} {uuid[:6]}" + + async def async_connect_device(self) -> None: + """Connect to Bravia TV device from config.""" assert self.client pin = self.device_config[CONF_PIN] @@ -72,13 +75,16 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if use_psk: await self.client.connect(psk=pin) else: - self.device_config[CONF_CLIENT_ID] = self.client_id - self.device_config[CONF_NICKNAME] = self.nickname - await self.client.connect( - pin=pin, clientid=self.client_id, nickname=self.nickname - ) + client_id = self.device_config[CONF_CLIENT_ID] + nickname = self.device_config[CONF_NICKNAME] + await self.client.connect(pin=pin, clientid=client_id, nickname=nickname) await self.client.set_wol_mode(True) + async def async_create_device(self) -> FlowResult: + """Create Bravia TV device from config.""" + assert self.client + await self.async_connect_device() + system_info = await self.client.get_system_info() cid = system_info[ATTR_CID].lower() title = system_info[ATTR_MODEL] @@ -90,6 +96,16 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=self.device_config) + async def async_reauth_device(self) -> FlowResult: + """Reauthorize Bravia TV device from config.""" + assert self.entry + assert self.client + await self.async_connect_device() + + self.hass.config_entries.async_update_entry(self.entry, data=self.device_config) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -100,28 +116,51 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host = user_input[CONF_HOST] if is_host_valid(host): self.device_config[CONF_HOST] = host - self.create_client() return await self.async_step_authorize() errors[CONF_HOST] = "invalid_host" return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required(CONF_HOST, default=""): str}), + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), errors=errors, ) async def async_step_authorize( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Authorize Bravia TV device.""" + """Handle authorize step.""" + self.create_client() + + if user_input is not None: + self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK] + if user_input[CONF_USE_PSK]: + return await self.async_step_psk() + return await self.async_step_pin() + + return self.async_show_form( + step_id="authorize", + data_schema=vol.Schema( + { + vol.Required(CONF_USE_PSK, default=False): bool, + } + ), + ) + + async def async_step_pin( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle PIN authorize step.""" errors: dict[str, str] = {} - self.client_id, self.nickname = await self.gen_instance_ids() + client_id, nickname = await self.gen_instance_ids() if user_input is not None: self.device_config[CONF_PIN] = user_input[CONF_PIN] - self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK] + self.device_config[CONF_CLIENT_ID] = client_id + self.device_config[CONF_NICKNAME] = nickname try: + if self.entry: + return await self.async_reauth_device() return await self.async_create_device() except BraviaTVAuthError: errors["base"] = "invalid_auth" @@ -133,16 +172,44 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert self.client try: - await self.client.pair(self.client_id, self.nickname) + await self.client.pair(client_id, nickname) except BraviaTVError: return self.async_abort(reason="no_ip_control") return self.async_show_form( - step_id="authorize", + step_id="pin", data_schema=vol.Schema( { - vol.Required(CONF_PIN, default=""): str, - vol.Required(CONF_USE_PSK, default=False): bool, + vol.Required(CONF_PIN): str, + } + ), + errors=errors, + ) + + async def async_step_psk( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle PSK authorize step.""" + errors: dict[str, str] = {} + + if user_input is not None: + self.device_config[CONF_PIN] = user_input[CONF_PIN] + try: + if self.entry: + return await self.async_reauth_device() + return await self.async_create_device() + except BraviaTVAuthError: + errors["base"] = "invalid_auth" + except BraviaTVNotSupported: + errors["base"] = "unsupported_model" + except BraviaTVError: + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="psk", + data_schema=vol.Schema( + { + vol.Required(CONF_PIN): str, } ), errors=errors, @@ -181,7 +248,6 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Allow the user to confirm adding the device.""" if user_input is not None: - self.create_client() return await self.async_step_authorize() return self.async_show_form(step_id="confirm") @@ -190,59 +256,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle configuration by re-auth.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self.device_config = {**entry_data} - 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.""" - self.create_client() - client_id, nickname = await self.gen_instance_ids() - - assert self.client is not None - assert self.entry is not None - - if user_input is not None: - pin = user_input[CONF_PIN] - use_psk = user_input[CONF_USE_PSK] - try: - if use_psk: - await self.client.connect(psk=pin) - else: - self.device_config[CONF_CLIENT_ID] = client_id - self.device_config[CONF_NICKNAME] = nickname - await self.client.connect( - pin=pin, clientid=client_id, nickname=nickname - ) - await self.client.set_wol_mode(True) - except BraviaTVError: - return self.async_abort(reason="reauth_unsuccessful") - else: - self.hass.config_entries.async_update_entry( - self.entry, data={**self.device_config, **user_input} - ) - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_abort(reason="reauth_successful") - - try: - await self.client.pair(client_id, nickname) - except BraviaTVError: - return self.async_abort(reason="reauth_unsuccessful") - - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema( - { - vol.Required(CONF_PIN, default=""): str, - vol.Required(CONF_USE_PSK, default=False): bool, - } - ), - ) - - async def gen_instance_ids(self) -> tuple[str, str]: - """Generate client_id and nickname.""" - uuid = await instance_id.async_get(self.hass) - return uuid, f"{NICKNAME_PREFIX} {uuid[:6]}" + return await self.async_step_authorize() class BraviaTVOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index ac651955166..f40494f2251 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -9,21 +9,27 @@ }, "authorize": { "title": "Authorize Sony Bravia TV", - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check «Use PSK authentication» box and enter your PSK instead of PIN.", + "description": "Make sure that «Control remotely» is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.", "data": { - "pin": "[%key:common::config_flow::data::pin%]", "use_psk": "Use PSK authentication" } }, + "pin": { + "title": "Authorize Sony Bravia TV", + "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device.", + "data": { + "pin": "[%key:common::config_flow::data::pin%]" + } + }, + "psk": { + "title": "Authorize Sony Bravia TV", + "description": "To set up PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Set «Authentication» to «Normal and Pre-Shared Key» or «Pre-Shared Key» and define your Pre-Shared-Key string (e.g. sony). \n\nThen enter your PSK here.", + "data": { + "pin": "PSK" + } + }, "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" - }, - "reauth_confirm": { - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check «Use PSK authentication» box and enter your PSK instead of PIN.", - "data": { - "pin": "[%key:common::config_flow::data::pin%]", - "use_psk": "Use PSK authentication" - } } }, "error": { @@ -36,8 +42,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", "not_bravia_device": "The device is not a Bravia TV.", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index 33ace3aa04a..6cfa94de1bd 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -4,8 +4,7 @@ "already_configured": "Device is already configured", "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", "not_bravia_device": "The device is not a Bravia TV.", - "reauth_successful": "Re-authentication was successful", - "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "PIN Code", "use_psk": "Use PSK authentication" }, - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check \u00abUse PSK authentication\u00bb box and enter your PSK instead of PIN.", + "description": "Make sure that \u00abControl remotely\u00bb is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.", "title": "Authorize Sony Bravia TV" }, "confirm": { "description": "Do you want to start setup?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "PIN Code", - "use_psk": "Use PSK authentication" + "pin": "PIN Code" }, - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check \u00abUse PSK authentication\u00bb box and enter your PSK instead of PIN." + "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device.", + "title": "Authorize Sony Bravia TV" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "To set up PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Set \u00abAuthentication\u00bb to \u00abNormal and Pre-Shared Key\u00bb or \u00abPre-Shared Key\u00bb and define your Pre-Shared-Key string (e.g. sony). \n\nThen enter your PSK here.", + "title": "Authorize Sony Bravia TV" }, "user": { "data": { diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 18576207a30..40b1b7499a9 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -124,7 +124,14 @@ async def test_ssdp_discovery(hass): assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PIN: "1234", CONF_USE_PSK: False} + result["flow_id"], user_input={CONF_USE_PSK: False} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "pin" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: "1234"} ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -185,68 +192,76 @@ async def test_user_invalid_host(hass): assert result["errors"] == {CONF_HOST: "invalid_host"} -async def test_authorize_invalid_auth(hass): - """Test that authorization errors shown on the authorization step.""" +@pytest.mark.parametrize( + "side_effect, error_message", + [ + (BraviaTVAuthError, "invalid_auth"), + (BraviaTVNotSupported, "unsupported_model"), + (BraviaTVConnectionError, "cannot_connect"), + ], +) +async def test_pin_form_error(hass, side_effect, error_message): + """Test that PIN form errors are correct.""" with patch( "pybravia.BraviaTV.connect", - side_effect=BraviaTVAuthError, + side_effect=side_effect, ), patch("pybravia.BraviaTV.pair"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_USE_PSK: False} + ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {"base": error_message} -async def test_authorize_cannot_connect(hass): - """Test that errors are shown when cannot connect to host at the authorize step.""" +@pytest.mark.parametrize( + "side_effect, error_message", + [ + (BraviaTVAuthError, "invalid_auth"), + (BraviaTVNotSupported, "unsupported_model"), + (BraviaTVConnectionError, "cannot_connect"), + ], +) +async def test_psk_form_error(hass, side_effect, error_message): + """Test that PSK form errors are correct.""" with patch( "pybravia.BraviaTV.connect", - side_effect=BraviaTVConnectionError, - ), patch("pybravia.BraviaTV.pair"): + side_effect=side_effect, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PIN: "1234"} + result["flow_id"], user_input={CONF_USE_PSK: True} ) - - assert result["errors"] == {"base": "cannot_connect"} - - -async def test_authorize_model_unsupported(hass): - """Test that errors are shown when the TV is not supported at the authorize step.""" - with patch( - "pybravia.BraviaTV.connect", - side_effect=BraviaTVNotSupported, - ), patch("pybravia.BraviaTV.pair"): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "10.10.10.12"} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PIN: "1234"} + result["flow_id"], user_input={CONF_PIN: "mypsk"} ) - assert result["errors"] == {"base": "unsupported_model"} + assert result["errors"] == {"base": error_message} -async def test_authorize_no_ip_control(hass): - """Test that errors are shown when IP Control is disabled on the TV.""" +async def test_no_ip_control(hass): + """Test that error are shown when IP Control is disabled on the TV.""" with patch("pybravia.BraviaTV.pair", side_effect=BraviaTVError): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_USE_PSK: False} + ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_ip_control" async def test_duplicate_error(hass): - """Test that errors are shown when duplicates are added.""" + """Test that error are shown when duplicates are added.""" config_entry = MockConfigEntry( domain=DOMAIN, unique_id="very_unique_string", @@ -268,6 +283,9 @@ async def test_duplicate_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_USE_PSK: False} + ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) @@ -277,7 +295,7 @@ async def test_duplicate_error(hass): async def test_create_entry(hass): - """Test that the user step works.""" + """Test that entry is added correctly with PIN auth.""" uuid = await instance_id.async_get(hass) with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( @@ -296,7 +314,14 @@ async def test_create_entry(hass): assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PIN: "1234", CONF_USE_PSK: False} + result["flow_id"], user_input={CONF_USE_PSK: False} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "pin" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: "1234"} ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -312,47 +337,9 @@ async def test_create_entry(hass): } -async def test_create_entry_with_ipv6_address(hass): - """Test that the user step works with device IPv6 address.""" - uuid = await instance_id.async_get(hass) - - with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( - "pybravia.BraviaTV.set_wol_mode" - ), patch( - "pybravia.BraviaTV.get_system_info", - return_value=BRAVIA_SYSTEM_INFO, - ), patch( - "homeassistant.components.braviatv.async_setup_entry", return_value=True - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: "2001:db8::1428:57ab"}, - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "authorize" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PIN: "1234", CONF_USE_PSK: False} - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["result"].unique_id == "very_unique_string" - assert result["title"] == "TV-Model" - assert result["data"] == { - CONF_HOST: "2001:db8::1428:57ab", - CONF_PIN: "1234", - CONF_USE_PSK: False, - CONF_MAC: "AA:BB:CC:DD:EE:FF", - CONF_CLIENT_ID: uuid, - CONF_NICKNAME: f"{NICKNAME_PREFIX} {uuid[:6]}", - } - - async def test_create_entry_psk(hass): - """Test that the user step works with PSK auth.""" - with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( + """Test that entry is added correctly with PSK auth.""" + with patch("pybravia.BraviaTV.connect"), patch( "pybravia.BraviaTV.set_wol_mode" ), patch( "pybravia.BraviaTV.get_system_info", @@ -368,7 +355,14 @@ async def test_create_entry_psk(hass): assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PIN: "mypsk", CONF_USE_PSK: True} + result["flow_id"], user_input={CONF_USE_PSK: True} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "psk" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: "mypsk"} ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -474,11 +468,14 @@ async def test_options_flow_error(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - "user_input", - [{CONF_PIN: "mypsk", CONF_USE_PSK: True}, {CONF_PIN: "1234", CONF_USE_PSK: False}], + "use_psk, new_pin", + [ + (True, "7777"), + (False, "newpsk"), + ], ) -async def test_reauth_successful(hass, user_input): - """Test starting a reauthentication flow.""" +async def test_reauth_successful(hass, use_psk, new_pin): + """Test that the reauthorization is successful.""" config_entry = MockConfigEntry( domain=DOMAIN, unique_id="very_unique_string", @@ -508,73 +505,15 @@ async def test_reauth_successful(hass, user_input): ) assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" + assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=user_input, + result["flow_id"], user_input={CONF_USE_PSK: use_psk} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PIN: new_pin} ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" - - -async def test_reauth_unsuccessful(hass): - """Test reauthentication flow failed.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="very_unique_string", - data={ - CONF_HOST: "bravia-host", - CONF_PIN: "1234", - CONF_MAC: "AA:BB:CC:DD:EE:FF", - }, - title="TV-Model", - ) - config_entry.add_to_hass(hass) - - with patch( - "pybravia.BraviaTV.connect", - side_effect=BraviaTVAuthError, - ), patch("pybravia.BraviaTV.pair"): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, - data=config_entry.data, - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PIN: "mypsk", CONF_USE_PSK: True}, - ) - - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "reauth_unsuccessful" - - -async def test_reauth_unsuccessful_during_pairing(hass): - """Test reauthentication flow failed because of pairing error.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="very_unique_string", - data={ - CONF_HOST: "bravia-host", - CONF_PIN: "1234", - CONF_MAC: "AA:BB:CC:DD:EE:FF", - }, - title="TV-Model", - ) - config_entry.add_to_hass(hass) - - with patch("pybravia.BraviaTV.pair", side_effect=BraviaTVError): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, - data=config_entry.data, - ) - - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "reauth_unsuccessful" + assert config_entry.data[CONF_PIN] == new_pin From fb41b024c0f20e2610b348bc4e87fb1fa3c1272c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 Dec 2022 14:55:50 +0100 Subject: [PATCH 033/173] Update frontend to 20221230.0 (#84842) --- 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 aa9dfb47f92..138d2a4aa46 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221228.0"], + "requirements": ["home-assistant-frontend==20221230.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 22288959953..66768bc3b2d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ dbus-fast==1.82.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.9.0 -home-assistant-frontend==20221228.0 +home-assistant-frontend==20221230.0 httpx==0.23.1 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index a3a1cd67e2a..5c42dd28119 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -888,7 +888,7 @@ hole==0.8.0 holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20221228.0 +home-assistant-frontend==20221230.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0af3d10c7ba..a5560bbe112 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -668,7 +668,7 @@ hole==0.8.0 holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20221228.0 +home-assistant-frontend==20221230.0 # homeassistant.components.home_connect homeconnect==0.7.2 From a50622cbfda79fce72703c11b63438c2d81ac14d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Dec 2022 16:21:41 +0100 Subject: [PATCH 034/173] Add availability property to DSMR sensors (#84848) --- homeassistant/components/dsmr/sensor.py | 5 +++++ tests/components/dsmr/test_sensor.py | 15 +++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 75a21f6d677..7147655549b 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -568,6 +568,11 @@ class DSMREntity(SensorEntity): attr: str | None = getattr(dsmr_object, attribute) return attr + @property + def available(self) -> bool: + """Entity is only available if there is a telegram.""" + return bool(self.telegram) + @property def native_value(self) -> StateType: """Return the state of sensor, if available, translate if needed.""" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 81ec3871b64..31d3496e3a7 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -23,8 +23,9 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, - STATE_UNKNOWN, + STATE_UNAVAILABLE, VOLUME_CUBIC_METERS, + UnitOfPower, ) from homeassistant.helpers import entity_registry as er @@ -56,13 +57,13 @@ async def test_default_setup(hass, dsmr_connection_fixture): telegram = { CURRENT_ELECTRICITY_USAGE: CosemObject( - [{"value": Decimal("0.0"), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal("0.0"), "unit": UnitOfPower.WATT}] ), ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": "m3"}, + {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, ] ), } @@ -88,9 +89,9 @@ async def test_default_setup(hass, dsmr_connection_fixture): telegram_callback = connection_factory.call_args_list[0][0][2] - # make sure entities have been created and return 'unknown' state + # make sure entities have been created and return 'unavailable' state power_consumption = hass.states.get("sensor.electricity_meter_power_consumption") - assert power_consumption.state == STATE_UNKNOWN + assert power_consumption.state == STATE_UNAVAILABLE assert ( power_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER ) @@ -110,9 +111,7 @@ async def test_default_setup(hass, dsmr_connection_fixture): # ensure entities have new state value after incoming telegram power_consumption = hass.states.get("sensor.electricity_meter_power_consumption") assert power_consumption.state == "0.0" - assert ( - power_consumption.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR - ) + assert power_consumption.attributes.get("unit_of_measurement") == UnitOfPower.WATT # tariff should be translated in human readable and have no unit active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") From 7be60d4569ecabf60e0933dda2008333e80be00c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Dec 2022 16:50:35 +0100 Subject: [PATCH 035/173] Bumped version to 2023.1.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 db4132465c6..e36d25046f4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 295bfed8057..a49385b334d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.0b2" +version = "2023.1.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 565d4f85c1091b46c1d97055c27d1c9942305ba2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 30 Dec 2022 14:47:41 -0700 Subject: [PATCH 036/173] Ensure AirVisual Pro migration includes device and entity customizations (#84798) * Ensure AirVisual Pro migration includes device and entity customizations * Update homeassistant/components/airvisual/__init__.py Co-authored-by: Martin Hjelmare * Code review * Fix tests * Fix tests FOR REAL Co-authored-by: Martin Hjelmare --- .../components/airvisual/__init__.py | 102 +++++++++++------- tests/components/airvisual/conftest.py | 24 +++-- .../components/airvisual/test_config_flow.py | 59 +++++----- 3 files changed, 108 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index f8cb3250452..16579be9a72 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -32,6 +32,7 @@ from homeassistant.helpers import ( aiohttp_client, config_validation as cv, device_registry as dr, + entity_registry as er, ) from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -117,36 +118,6 @@ def async_get_geography_id(geography_dict: Mapping[str, Any]) -> str: ) -@callback -def async_get_pro_config_entry_by_ip_address( - hass: HomeAssistant, ip_address: str -) -> ConfigEntry: - """Get the Pro config entry related to an IP address.""" - [config_entry] = [ - entry - for entry in hass.config_entries.async_entries(DOMAIN_AIRVISUAL_PRO) - if entry.data[CONF_IP_ADDRESS] == ip_address - ] - return config_entry - - -@callback -def async_get_pro_device_by_config_entry( - hass: HomeAssistant, config_entry: ConfigEntry -) -> dr.DeviceEntry: - """Get the Pro device entry related to a config entry. - - Note that a Pro config entry can only contain a single device. - """ - device_registry = dr.async_get(hass) - [device_entry] = [ - device_entry - for device_entry in device_registry.devices.values() - if config_entry.entry_id in device_entry.config_entries - ] - return device_entry - - @callback def async_sync_geo_coordinator_update_intervals( hass: HomeAssistant, api_key: str @@ -306,14 +277,31 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: version = 3 if entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_NODE_PRO: + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) ip_address = entry.data[CONF_IP_ADDRESS] - # Get the existing Pro device entry before it is removed by the migration: - old_device_entry = async_get_pro_device_by_config_entry(hass, entry) + # Store the existing Pro device before the migration removes it: + old_device_entry = next( + entry + for entry in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ) + ) + # Store the existing Pro entity entries (mapped by unique ID) before the + # migration removes it: + old_entity_entries: dict[str, er.RegistryEntry] = { + entry.unique_id: entry + for entry in er.async_entries_for_device( + entity_registry, old_device_entry.id, include_disabled_entities=True + ) + } + + # Remove this config entry and create a new one under the `airvisual_pro` + # domain: new_entry_data = {**entry.data} new_entry_data.pop(CONF_INTEGRATION_TYPE) - tasks = [ hass.config_entries.async_remove(entry.entry_id), hass.config_entries.flow.async_init( @@ -324,18 +312,52 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] await asyncio.gather(*tasks) + # After the migration has occurred, grab the new config and device entries + # (now under the `airvisual_pro` domain): + new_config_entry = next( + entry + for entry in hass.config_entries.async_entries(DOMAIN_AIRVISUAL_PRO) + if entry.data[CONF_IP_ADDRESS] == ip_address + ) + new_device_entry = next( + entry + for entry in dr.async_entries_for_config_entry( + device_registry, new_config_entry.entry_id + ) + ) + + # Update the new device entry with any customizations from the old one: + device_registry.async_update_device( + new_device_entry.id, + area_id=old_device_entry.area_id, + disabled_by=old_device_entry.disabled_by, + name_by_user=old_device_entry.name_by_user, + ) + + # Update the new entity entries with any customizations from the old ones: + for new_entity_entry in er.async_entries_for_device( + entity_registry, new_device_entry.id, include_disabled_entities=True + ): + if old_entity_entry := old_entity_entries.get( + new_entity_entry.unique_id + ): + entity_registry.async_update_entity( + new_entity_entry.entity_id, + area_id=old_entity_entry.area_id, + device_class=old_entity_entry.device_class, + disabled_by=old_entity_entry.disabled_by, + hidden_by=old_entity_entry.hidden_by, + icon=old_entity_entry.icon, + name=old_entity_entry.name, + new_entity_id=old_entity_entry.entity_id, + unit_of_measurement=old_entity_entry.unit_of_measurement, + ) + # If any automations are using the old device ID, create a Repairs issues # with instructions on how to update it: if device_automations := automation.automations_with_device( hass, old_device_entry.id ): - new_config_entry = async_get_pro_config_entry_by_ip_address( - hass, ip_address - ) - new_device_entry = async_get_pro_device_by_config_entry( - hass, new_config_entry - ) - async_create_issue( hass, DOMAIN, diff --git a/tests/components/airvisual/conftest.py b/tests/components/airvisual/conftest.py index 3e83b41a5af..8ef060c3116 100644 --- a/tests/components/airvisual/conftest.py +++ b/tests/components/airvisual/conftest.py @@ -1,6 +1,6 @@ """Define test fixtures for AirVisual.""" import json -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -56,17 +56,27 @@ def data_fixture(): return json.loads(load_fixture("data.json", "airvisual")) +@pytest.fixture(name="pro_data", scope="session") +def pro_data_fixture(): + """Define an update coordinator data example for the Pro.""" + return json.loads(load_fixture("data.json", "airvisual_pro")) + + +@pytest.fixture(name="pro") +def pro_fixture(pro_data): + """Define a mocked NodeSamba object.""" + return Mock( + async_connect=AsyncMock(), + async_disconnect=AsyncMock(), + async_get_latest_measurements=AsyncMock(return_value=pro_data), + ) + + @pytest.fixture(name="setup_airvisual") async def setup_airvisual_fixture(hass, config, data): """Define a fixture to set up AirVisual.""" with patch("pyairvisual.air_quality.AirQuality.city"), patch( "pyairvisual.air_quality.AirQuality.nearest_city", return_value=data - ), patch("pyairvisual.node.NodeSamba.async_connect"), patch( - "pyairvisual.node.NodeSamba.async_get_latest_measurements" - ), patch( - "pyairvisual.node.NodeSamba.async_disconnect" - ), patch( - "homeassistant.components.airvisual.PLATFORMS", [] ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index d322726340a..7bad9af1002 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the AirVisual config flow.""" -from unittest.mock import Mock, patch +from unittest.mock import patch from pyairvisual.cloud_api import ( InvalidKeyError, @@ -21,6 +21,7 @@ from homeassistant.components.airvisual import ( INTEGRATION_TYPE_GEOGRAPHY_NAME, INTEGRATION_TYPE_NODE_PRO, ) +from homeassistant.components.airvisual_pro import DOMAIN as AIRVISUAL_PRO_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, @@ -31,8 +32,7 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, CONF_STATE, ) -from homeassistant.helpers import issue_registry as ir -from homeassistant.setup import async_setup_component +from homeassistant.helpers import device_registry as dr, issue_registry as ir from tests.common import MockConfigEntry @@ -169,42 +169,41 @@ async def test_migration_1_2(hass, config, config_entry, setup_airvisual, unique } -@pytest.mark.parametrize( - "config,config_entry_version,unique_id", - [ - ( - { - CONF_IP_ADDRESS: "192.168.1.100", - CONF_PASSWORD: "abcde12345", - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, - }, - 2, - "192.16.1.100", - ) - ], -) -async def test_migration_2_3(hass, config, config_entry, unique_id): +async def test_migration_2_3(hass, pro): """Test migrating from version 2 to 3.""" + old_pro_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="192.168.1.100", + data={ + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "abcde12345", + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, + }, + version=2, + ) + old_pro_entry.add_to_hass(hass) + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + name="192.168.1.100", + config_entry_id=old_pro_entry.entry_id, + identifiers={(DOMAIN, "ABCDE12345")}, + ) + with patch( "homeassistant.components.airvisual.automation.automations_with_device", return_value=["automation.test_automation"], ), patch( - "homeassistant.components.airvisual.async_get_pro_config_entry_by_ip_address", - return_value=MockConfigEntry( - domain="airvisual_pro", - unique_id="192.168.1.100", - data={CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "abcde12345"}, - version=3, - ), + "homeassistant.components.airvisual_pro.NodeSamba", return_value=pro ), patch( - "homeassistant.components.airvisual.async_get_pro_device_by_config_entry", - return_value=Mock(id="abcde12345"), + "homeassistant.components.airvisual_pro.config_flow.NodeSamba", return_value=pro ): - assert await async_setup_component(hass, DOMAIN, config) + await hass.config_entries.async_setup(old_pro_entry.entry_id) await hass.async_block_till_done() - airvisual_config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(airvisual_config_entries) == 0 + for domain, entry_count in ((DOMAIN, 0), (AIRVISUAL_PRO_DOMAIN, 1)): + entries = hass.config_entries.async_entries(domain) + assert len(entries) == entry_count issue_registry = ir.async_get(hass) assert len(issue_registry.issues) == 1 From 9932c0cb91fbbaaef43c1c48e4b976047ed0fe4c Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Fri, 30 Dec 2022 11:16:49 +0300 Subject: [PATCH 037/173] Bump pybravia to 0.2.5 (#84835) --- homeassistant/components/braviatv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index fa009bf05ef..83fe34fed28 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.2.3"], + "requirements": ["pybravia==0.2.5"], "codeowners": ["@bieniu", "@Drafteed"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 5c42dd28119..985e4863ca5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1500,7 +1500,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.3 +pybravia==0.2.5 # homeassistant.components.nissan_leaf pycarwings2==2.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5560bbe112..0a77df5b33c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1079,7 +1079,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.3 +pybravia==0.2.5 # homeassistant.components.cloudflare pycfdns==2.0.1 From 7ab27cd9bf98f6c8266bdbb3e2d938a0199c8e51 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Dec 2022 19:07:49 +0100 Subject: [PATCH 038/173] Do not validate device classes when entity state is unknown (#84860) --- homeassistant/components/sensor/__init__.py | 1 + tests/components/sensor/test_init.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2db8a7680c4..7beac83f059 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -968,6 +968,7 @@ class SensorEntity(Entity): # Validate unit of measurement used for sensors with a device class if ( not self._invalid_unit_of_measurement_reported + and value is not None and device_class and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None and native_unit_of_measurement not in units diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 33c1d9e8889..d0027a6a07c 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1144,7 +1144,7 @@ async def test_device_classes_with_invalid_unit_of_measurement( platform.init(empty=True) platform.ENTITIES["0"] = platform.MockSensor( name="Test", - native_value=None, + native_value="1.0", device_class=device_class, native_unit_of_measurement="INVALID!", ) From c5f7d7ae857d45f1e71fc97557a7a6e5225bf94c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 30 Dec 2022 19:08:11 +0100 Subject: [PATCH 039/173] Only reflect unavailable state in DSMR when disconnected (#84862) * Only reflect unavailable state in DSMR when disonnected * Addressreview comment --- homeassistant/components/dsmr/sensor.py | 34 +++++++++++++++++++------ tests/components/dsmr/test_sensor.py | 5 ++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 7147655549b..36f734273f2 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -401,7 +401,7 @@ async def async_setup_entry( ) @Throttle(min_time_between_updates) - def update_entities_telegram(telegram: dict[str, DSMRObject]) -> None: + def update_entities_telegram(telegram: dict[str, DSMRObject] | None) -> None: """Update entities with latest telegram and trigger state update.""" # Make all device entities aware of new telegram for entity in entities: @@ -445,6 +445,11 @@ async def async_setup_entry( while hass.state == CoreState.not_running or hass.is_running: # Start DSMR asyncio.Protocol reader + + # Reflect connected state in devices state by setting an + # empty telegram resulting in `unknown` states + update_entities_telegram({}) + try: transport, protocol = await hass.loop.create_task(reader_factory()) @@ -472,8 +477,8 @@ async def async_setup_entry( protocol = None # Reflect disconnect state in devices state by setting an - # empty telegram resulting in `unknown` states - update_entities_telegram({}) + # None telegram resulting in `unavailable` states + update_entities_telegram(None) # throttle reconnect attempts await asyncio.sleep( @@ -487,11 +492,19 @@ async def async_setup_entry( transport = None protocol = None + # Reflect disconnect state in devices state by setting an + # None telegram resulting in `unavailable` states + update_entities_telegram(None) + # throttle reconnect attempts await asyncio.sleep( entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL) ) except CancelledError: + # Reflect disconnect state in devices state by setting an + # None telegram resulting in `unavailable` states + update_entities_telegram(None) + if stop_listener and ( hass.state == CoreState.not_running or hass.is_running ): @@ -534,7 +547,7 @@ class DSMREntity(SensorEntity): """Initialize entity.""" self.entity_description = entity_description self._entry = entry - self.telegram: dict[str, DSMRObject] = {} + self.telegram: dict[str, DSMRObject] | None = {} device_serial = entry.data[CONF_SERIAL_ID] device_name = DEVICE_NAME_ELECTRICITY @@ -551,16 +564,21 @@ class DSMREntity(SensorEntity): self._attr_unique_id = f"{device_serial}_{entity_description.key}" @callback - def update_data(self, telegram: dict[str, DSMRObject]) -> None: + def update_data(self, telegram: dict[str, DSMRObject] | None) -> None: """Update data.""" self.telegram = telegram - if self.hass and self.entity_description.obis_reference in self.telegram: + if self.hass and ( + telegram is None or self.entity_description.obis_reference in telegram + ): self.async_write_ha_state() def get_dsmr_object_attr(self, attribute: str) -> str | None: """Read attribute from last received telegram for this DSMR object.""" # Make sure telegram contains an object for this entities obis - if self.entity_description.obis_reference not in self.telegram: + if ( + self.telegram is None + or self.entity_description.obis_reference not in self.telegram + ): return None # Get the attribute value if the object has it @@ -571,7 +589,7 @@ class DSMREntity(SensorEntity): @property def available(self) -> bool: """Entity is only available if there is a telegram.""" - return bool(self.telegram) + return self.telegram is not None @property def native_value(self) -> StateType: diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 31d3496e3a7..ee0ffa5db5f 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, STATE_UNAVAILABLE, + STATE_UNKNOWN, VOLUME_CUBIC_METERS, UnitOfPower, ) @@ -783,6 +784,10 @@ async def test_reconnect(hass, dsmr_connection_fixture): assert connection_factory.call_count == 1 + state = hass.states.get("sensor.electricity_meter_power_consumption") + assert state + assert state.state == STATE_UNKNOWN + # indicate disconnect, release wait lock and allow reconnect to happen closed.set() # wait for lock set to resolve From 64088905437f32698c4c374963f1ae5bc0a10e2e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 30 Dec 2022 21:17:47 +0100 Subject: [PATCH 040/173] Bump aiounifi to v43 (#84864) --- 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 c9e9464a317..34bba257a84 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==42"], + "requirements": ["aiounifi==43"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 985e4863ca5..c84f3fbeb85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,7 +288,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==42 +aiounifi==43 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a77df5b33c..c8bc757adcd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -263,7 +263,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==42 +aiounifi==43 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 043d58d697c14318d1489adebc0d294ad139dabc Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Fri, 30 Dec 2022 18:31:16 +0000 Subject: [PATCH 041/173] Bump pyroon library to 0.1.2. (#84865) --- homeassistant/components/roon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 297bf5e9d7a..272c353601e 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,7 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": ["roonapi==0.1.1"], + "requirements": ["roonapi==0.1.2"], "codeowners": ["@pavoni"], "iot_class": "local_push", "loggers": ["roonapi"] diff --git a/requirements_all.txt b/requirements_all.txt index c84f3fbeb85..b6b2b0ef71e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2220,7 +2220,7 @@ rokuecp==0.17.0 roombapy==1.6.5 # homeassistant.components.roon -roonapi==0.1.1 +roonapi==0.1.2 # homeassistant.components.rova rova==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8bc757adcd..a5a5ea6e323 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1547,7 +1547,7 @@ rokuecp==0.17.0 roombapy==1.6.5 # homeassistant.components.roon -roonapi==0.1.1 +roonapi==0.1.2 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From a7ba242f1f8871a1ede670eb5a3b46243b52ed20 Mon Sep 17 00:00:00 2001 From: William Scanlon <6432770+w1ll1am23@users.noreply.github.com> Date: Fri, 30 Dec 2022 16:12:01 -0500 Subject: [PATCH 042/173] Bump pyeconet to 0.1.17 (#84868) --- homeassistant/components/econet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index f8df1a4134e..19455d8dffb 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -3,7 +3,7 @@ "name": "Rheem EcoNet Products", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/econet", - "requirements": ["pyeconet==0.1.15"], + "requirements": ["pyeconet==0.1.17"], "codeowners": ["@vangorra", "@w1ll1am23"], "iot_class": "cloud_push", "loggers": ["paho_mqtt", "pyeconet"] diff --git a/requirements_all.txt b/requirements_all.txt index b6b2b0ef71e..041298aa9ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1560,7 +1560,7 @@ pydroid-ipcam==2.0.0 pyebox==1.1.4 # homeassistant.components.econet -pyeconet==0.1.15 +pyeconet==0.1.17 # homeassistant.components.edimax pyedimax==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5a5ea6e323..6c6cb205c53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1106,7 +1106,7 @@ pydexcom==0.2.3 pydroid-ipcam==2.0.0 # homeassistant.components.econet -pyeconet==0.1.15 +pyeconet==0.1.17 # homeassistant.components.efergy pyefergy==22.1.1 From c77b78928e0cf533f6bfc7350e9ff9ad12239256 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Dec 2022 23:57:35 +0100 Subject: [PATCH 043/173] Bump motionblinds to 0.6.14 (#84873) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 6d80d31a69d..68b4ffc8477 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.13"], + "requirements": ["motionblinds==0.6.14"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index 041298aa9ef..43cc42d9512 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1119,7 +1119,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.13 +motionblinds==0.6.14 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c6cb205c53..4a9d3801e83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.13 +motionblinds==0.6.14 # homeassistant.components.motioneye motioneye-client==0.3.12 From 32736b33365152eb9e04af8e0a21e426258469c9 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 1 Jan 2023 23:32:17 +0100 Subject: [PATCH 044/173] Process late feedback for Reolink (#84884) Co-authored-by: Martin Hjelmare --- homeassistant/components/reolink/__init__.py | 43 ++++++++------ homeassistant/components/reolink/camera.py | 25 +++++--- .../components/reolink/config_flow.py | 59 ++++++++++--------- homeassistant/components/reolink/const.py | 4 -- homeassistant/components/reolink/entity.py | 44 +++++++------- homeassistant/components/reolink/host.py | 27 ++++----- .../components/reolink/manifest.json | 2 - tests/components/reolink/test_config_flow.py | 17 +++++- 8 files changed, 120 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 2b565a6d4b8..db61e4aa627 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -12,16 +12,19 @@ import async_timeout from reolink_ip.exceptions import ApiError, InvalidContentTypeError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DEVICE_UPDATE_INTERVAL, DOMAIN, PLATFORMS +from .const import DOMAIN from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) +PLATFORMS = [Platform.CAMERA] +DEVICE_UPDATE_INTERVAL = 60 + @dataclass class ReolinkData: @@ -31,14 +34,15 @@ class ReolinkData: device_coordinator: DataUpdateCoordinator -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Reolink from a config entry.""" - host = ReolinkHost(hass, dict(entry.data), dict(entry.options)) + host = ReolinkHost(hass, config_entry.data, config_entry.options) try: if not await host.async_init(): raise ConfigEntryNotReady( - f"Error while trying to setup {host.api.host}:{host.api.port}: failed to obtain data from device." + f"Error while trying to setup {host.api.host}:{host.api.port}: " + "failed to obtain data from device." ) except ( ClientConnectorError, @@ -50,14 +54,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f'Error while trying to setup {host.api.host}:{host.api.port}: "{str(err)}".' ) from err - entry.async_on_unload( + config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop) ) async def async_device_config_update(): - """Perform the update of the host config-state cache, and renew the ONVIF-subscription.""" + """Update the host state cache and renew the ONVIF-subscription.""" async with async_timeout.timeout(host.api.timeout): - await host.update_states() # Login session is implicitly updated here, so no need to explicitly do it in a timer + # Login session is implicitly updated here + await host.update_states() coordinator_device_config_update = DataUpdateCoordinator( hass, @@ -69,30 +74,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator_device_config_update.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ReolinkData( + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData( host=host, device_coordinator=coordinator_device_config_update, ) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(entry_update_listener)) + config_entry.async_on_unload( + config_entry.add_update_listener(entry_update_listener) + ) return True -async def entry_update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry): """Update the configuration of the host entity.""" - await hass.config_entries.async_reload(entry.entry_id) + await hass.config_entries.async_reload(config_entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - host: ReolinkHost = hass.data[DOMAIN][entry.entry_id].host + host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host await host.stop() - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) + if unload_ok := await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ): + hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/reolink/camera.py b/homeassistant/components/reolink/camera.py index 97369edcd64..aafc9686eea 100644 --- a/homeassistant/components/reolink/camera.py +++ b/homeassistant/components/reolink/camera.py @@ -8,9 +8,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import ReolinkData from .const import DOMAIN from .entity import ReolinkCoordinatorEntity -from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -18,10 +18,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Reolink IP Camera.""" - host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host + reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] + host = reolink_data.host cameras = [] for channel in host.api.channels: @@ -30,25 +31,31 @@ async def async_setup_entry( streams.append("ext") for stream in streams: - cameras.append(ReolinkCamera(hass, config_entry, channel, stream)) + cameras.append(ReolinkCamera(reolink_data, config_entry, channel, stream)) - async_add_devices(cameras, update_before_add=True) + async_add_entities(cameras, update_before_add=True) class ReolinkCamera(ReolinkCoordinatorEntity, Camera): """An implementation of a Reolink IP camera.""" _attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM + _attr_has_entity_name = True - def __init__(self, hass, config, channel, stream): + def __init__( + self, + reolink_data: ReolinkData, + config_entry: ConfigEntry, + channel: int, + stream: str, + ) -> None: """Initialize Reolink camera stream.""" - ReolinkCoordinatorEntity.__init__(self, hass, config) + ReolinkCoordinatorEntity.__init__(self, reolink_data, config_entry, channel) Camera.__init__(self) - self._channel = channel self._stream = stream - self._attr_name = f"{self._host.api.camera_name(self._channel)} {self._stream}" + self._attr_name = self._stream self._attr_unique_id = f"{self._host.unique_id}_{self._channel}_{self._stream}" self._attr_entity_registry_enabled_default = stream == "sub" diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 169e2624d46..c351a125056 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import cast +from typing import Any from reolink_ip.exceptions import ApiError, CredentialsInvalidError import voluptuous as vol @@ -18,6 +18,8 @@ from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) +DEFAULT_OPTIONS = {CONF_PROTOCOL: DEFAULT_PROTOCOL} + class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): """Handle Reolink options.""" @@ -26,10 +28,12 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): """Initialize ReolinkOptionsFlowHandler.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Reolink options.""" if user_input is not None: - 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", @@ -37,9 +41,7 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): { vol.Required( CONF_PROTOCOL, - default=self.config_entry.options.get( - CONF_PROTOCOL, DEFAULT_PROTOCOL - ), + default=self.config_entry.options[CONF_PROTOCOL], ): vol.In(["rtsp", "rtmp"]), } ), @@ -51,8 +53,6 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - host: ReolinkHost | None = None - @staticmethod @callback def async_get_options_flow( @@ -61,14 +61,16 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Options callback for Reolink.""" return ReolinkOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} placeholders = {} if user_input is not None: try: - await self.async_obtain_host_settings(self.hass, user_input) + host = await async_obtain_host_settings(self.hass, user_input) except CannotConnect: errors[CONF_HOST] = "cannot_connect" except CredentialsInvalidError: @@ -81,19 +83,17 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): placeholders["error"] = str(err) errors[CONF_HOST] = "unknown" - self.host = cast(ReolinkHost, self.host) - if not errors: - user_input[CONF_PORT] = self.host.api.port - user_input[CONF_USE_HTTPS] = self.host.api.use_https + user_input[CONF_PORT] = host.api.port + user_input[CONF_USE_HTTPS] = host.api.use_https - await self.async_set_unique_id( - self.host.unique_id, raise_on_progress=False - ) + await self.async_set_unique_id(host.unique_id, raise_on_progress=False) self._abort_if_unique_id_configured(updates=user_input) return self.async_create_entry( - title=str(self.host.api.nvr_name), data=user_input + title=str(host.api.nvr_name), + data=user_input, + options=DEFAULT_OPTIONS, ) data_schema = vol.Schema( @@ -118,19 +118,20 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=placeholders, ) - async def async_obtain_host_settings( - self, hass: core.HomeAssistant, user_input: dict - ): - """Initialize the Reolink host and get the host information.""" - host = ReolinkHost(hass, user_input, {}) - try: - if not await host.async_init(): - raise CannotConnect - finally: - await host.stop() +async def async_obtain_host_settings( + hass: core.HomeAssistant, user_input: dict +) -> ReolinkHost: + """Initialize the Reolink host and get the host information.""" + host = ReolinkHost(hass, user_input, DEFAULT_OPTIONS) - self.host = host + try: + if not await host.async_init(): + raise CannotConnect + finally: + await host.stop() + + return host class CannotConnect(exceptions.HomeAssistantError): diff --git a/homeassistant/components/reolink/const.py b/homeassistant/components/reolink/const.py index 95bd5da3c96..180c3ccae11 100644 --- a/homeassistant/components/reolink/const.py +++ b/homeassistant/components/reolink/const.py @@ -1,13 +1,9 @@ """Constants for the Reolink Camera integration.""" DOMAIN = "reolink" -PLATFORMS = ["camera"] CONF_USE_HTTPS = "use_https" CONF_PROTOCOL = "protocol" DEFAULT_PROTOCOL = "rtsp" DEFAULT_TIMEOUT = 60 - -HOST = "host" -DEVICE_UPDATE_INTERVAL = 60 diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 7e210114556..403ea278889 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -1,5 +1,7 @@ """Reolink parent entity class.""" +from __future__ import annotations +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -11,24 +13,20 @@ from .const import DOMAIN class ReolinkCoordinatorEntity(CoordinatorEntity): """Parent class for Reolink Entities.""" - def __init__(self, hass, config): + def __init__( + self, reolink_data: ReolinkData, config_entry: ConfigEntry, channel: int | None + ) -> None: """Initialize ReolinkCoordinatorEntity.""" - self._hass = hass - entry_data: ReolinkData = self._hass.data[DOMAIN][config.entry_id] - coordinator = entry_data.device_coordinator + coordinator = reolink_data.device_coordinator super().__init__(coordinator) - self._host = entry_data.host - self._channel = None + self._host = reolink_data.host + self._channel = channel - @property - def device_info(self): - """Information about this entity/device.""" http_s = "https" if self._host.api.use_https else "http" conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}" - if self._host.api.is_nvr and self._channel is not None: - return DeviceInfo( + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{self._host.unique_id}_ch{self._channel}")}, via_device=(DOMAIN, self._host.unique_id), name=self._host.api.camera_name(self._channel), @@ -36,19 +34,19 @@ class ReolinkCoordinatorEntity(CoordinatorEntity): manufacturer=self._host.api.manufacturer, configuration_url=conf_url, ) - - return DeviceInfo( - identifiers={(DOMAIN, self._host.unique_id)}, - connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)}, - name=self._host.api.nvr_name, - model=self._host.api.model, - manufacturer=self._host.api.manufacturer, - hw_version=self._host.api.hardware_version, - sw_version=self._host.api.sw_version, - configuration_url=conf_url, - ) + else: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._host.unique_id)}, + connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)}, + name=self._host.api.nvr_name, + model=self._host.api.model, + manufacturer=self._host.api.manufacturer, + hw_version=self._host.api.hardware_version, + sw_version=self._host.api.sw_version, + configuration_url=conf_url, + ) @property def available(self) -> bool: """Return True if entity is available.""" - return self._host.api.session_active + return self._host.api.session_active and super().available diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index fbd88c94ccc..0a5e378a78d 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -2,7 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import logging +from typing import Any import aiohttp from reolink_ip.api import Host @@ -16,7 +18,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNA from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac -from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_PROTOCOL, DEFAULT_TIMEOUT +from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -27,18 +29,14 @@ class ReolinkHost: def __init__( self, hass: HomeAssistant, - config: dict, - options: dict, + config: Mapping[str, Any], + options: Mapping[str, Any], ) -> None: """Initialize Reolink Host. Could be either NVR, or Camera.""" self._hass: HomeAssistant = hass self._clientsession: aiohttp.ClientSession | None = None - self._unique_id: str | None = None - - cur_protocol = ( - DEFAULT_PROTOCOL if CONF_PROTOCOL not in options else options[CONF_PROTOCOL] - ) + self._unique_id: str = "" self._api = Host( config[CONF_HOST], @@ -46,12 +44,12 @@ class ReolinkHost: config[CONF_PASSWORD], port=config.get(CONF_PORT), use_https=config.get(CONF_USE_HTTPS), - protocol=cur_protocol, + protocol=options[CONF_PROTOCOL], timeout=DEFAULT_TIMEOUT, ) @property - def unique_id(self): + def unique_id(self) -> str: """Create the unique ID, base for all entities.""" return self._unique_id @@ -99,23 +97,22 @@ class ReolinkHost: ): if enable_onvif: _LOGGER.error( - "Unable to switch on ONVIF on %s. You need it to be ON to receive notifications", + "Failed to enable ONVIF on %s. Set it to ON to receive notifications", self._api.nvr_name, ) if enable_rtmp: _LOGGER.error( - "Unable to switch on RTMP on %s. You need it to be ON", + "Failed to enable RTMP on %s. Set it to ON", self._api.nvr_name, ) elif enable_rtsp: _LOGGER.error( - "Unable to switch on RTSP on %s. You need it to be ON", + "Failed to enable RTSP on %s. Set it to ON", self._api.nvr_name, ) - if self._unique_id is None: - self._unique_id = format_mac(self._api.mac_address) + self._unique_id = format_mac(self._api.mac_address) return True diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 4db59caa42f..b5483be23ab 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -4,8 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", "requirements": ["reolink-ip==0.0.40"], - "dependencies": ["webhook"], - "after_dependencies": ["http"], "codeowners": ["@starkillerOG", "@JimStar"], "iot_class": "local_polling", "loggers": ["reolink-ip"] diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index fcf9280eb9a..ad017186075 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -82,6 +82,9 @@ async def test_config_flow_manual_success(hass): CONF_PORT: TEST_PORT, const.CONF_USE_HTTPS: TEST_USE_HTTPS, } + assert result["options"] == { + const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + } async def test_config_flow_errors(hass): @@ -174,6 +177,9 @@ async def test_config_flow_errors(hass): CONF_PORT: TEST_PORT, const.CONF_USE_HTTPS: TEST_USE_HTTPS, } + assert result["options"] == { + const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + } async def test_options_flow(hass): @@ -188,6 +194,9 @@ async def test_options_flow(hass): CONF_PORT: TEST_PORT, const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, + options={ + const.CONF_PROTOCOL: "rtsp", + }, title=TEST_NVR_NAME, ) config_entry.add_to_hass(hass) @@ -202,12 +211,12 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={const.CONF_PROTOCOL: "rtsp"}, + user_input={const.CONF_PROTOCOL: "rtmp"}, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { - const.CONF_PROTOCOL: "rtsp", + const.CONF_PROTOCOL: "rtmp", } @@ -223,6 +232,9 @@ async def test_change_connection_settings(hass): CONF_PORT: TEST_PORT, const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, + options={ + const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + }, title=TEST_NVR_NAME, ) config_entry.add_to_hass(hass) @@ -245,6 +257,7 @@ async def test_change_connection_settings(hass): ) assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == TEST_HOST2 assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 From 9655619667784c64683fef1e6e3bc10cb796ef48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Dec 2022 18:05:25 -1000 Subject: [PATCH 045/173] Small fixes for SwitchBot Locks (#84888) Co-authored-by: Aaron Bach --- .../components/switchbot/__init__.py | 6 ++- .../components/switchbot/config_flow.py | 21 ++++---- .../components/switchbot/manifest.json | 2 +- .../components/switchbot/strings.json | 8 +-- .../components/switchbot/translations/en.json | 8 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/switchbot/test_config_flow.py | 49 +++++++++++++++++-- 8 files changed, 72 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index b79e42ba5b9..5d4f29b9dfe 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -45,7 +45,11 @@ PLATFORMS_BY_TYPE = { SupportedModels.CONTACT.value: [Platform.BINARY_SENSOR, Platform.SENSOR], SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR], SupportedModels.HUMIDIFIER.value: [Platform.HUMIDIFIER, Platform.SENSOR], - SupportedModels.LOCK.value: [Platform.BINARY_SENSOR, Platform.LOCK], + SupportedModels.LOCK.value: [ + Platform.BINARY_SENSOR, + Platform.LOCK, + Platform.SENSOR, + ], } CLASS_BY_DEVICE = { SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index a71e30b2f96..6ba0e463718 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -5,7 +5,9 @@ import logging from typing import Any from switchbot import ( + SwitchbotAccountConnectionError, SwitchBotAdvertisement, + SwitchbotAuthenticationError, SwitchbotLock, SwitchbotModel, parse_advertisement_data, @@ -100,7 +102,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): "address": short_address(discovery_info.address), } if model_name == SwitchbotModel.LOCK: - return await self.async_step_lock_chose_method() + return await self.async_step_lock_choose_method() if self._discovered_adv.data["isEncrypted"]: return await self.async_step_password() return await self.async_step_confirm() @@ -172,11 +174,12 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): user_input[CONF_USERNAME], user_input[CONF_PASSWORD], ) + except SwitchbotAccountConnectionError as ex: + raise AbortFlow("cannot_connect") from ex + except SwitchbotAuthenticationError: + errors = {"base": "auth_failed"} + else: return await self.async_step_lock_key(key_details) - except RuntimeError: - errors = { - "base": "auth_failed", - } user_input = user_input or {} return self.async_show_form( @@ -195,14 +198,14 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_lock_chose_method( + async def async_step_lock_choose_method( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the SwitchBot API chose method step.""" assert self._discovered_adv is not None return self.async_show_menu( - step_id="lock_chose_method", + step_id="lock_choose_method", menu_options=["lock_auth", "lock_key"], description_placeholders={ "name": name_from_discovery(self._discovered_adv), @@ -286,7 +289,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): device_adv = self._discovered_advs[user_input[CONF_ADDRESS]] await self._async_set_device(device_adv) if device_adv.data.get("modelName") == SwitchbotModel.LOCK: - return await self.async_step_lock_chose_method() + return await self.async_step_lock_choose_method() if device_adv.data["isEncrypted"]: return await self.async_step_password() return await self._async_create_entry_from_discovery(user_input) @@ -298,7 +301,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): device_adv = list(self._discovered_advs.values())[0] await self._async_set_device(device_adv) if device_adv.data.get("modelName") == SwitchbotModel.LOCK: - return await self.async_step_lock_chose_method() + return await self.async_step_lock_choose_method() if device_adv.data["isEncrypted"]: return await self.async_step_password() return await self.async_step_confirm() diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b5b3d633285..f18a80b1b89 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.34.1"], + "requirements": ["PySwitchbot==0.36.0"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 10a623a70d7..08fd960334a 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -30,11 +30,11 @@ "password": "[%key:common::config_flow::data::password%]" } }, - "lock_chose_method": { - "description": "Choose configuration method, details can be found in the documentation.", + "lock_choose_method": { + "description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.", "menu_options": { - "lock_auth": "SwitchBot app login and password", - "lock_key": "Lock encryption key" + "lock_auth": "SwitchBot account (recommended)", + "lock_key": "Enter lock encryption key manually" } } }, diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index ab2ffa8d6ac..b5658e58d6b 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -23,11 +23,11 @@ }, "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key." }, - "lock_chose_method": { - "description": "Choose configuration method, details can be found in the documentation.", + "lock_choose_method": { + "description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.", "menu_options": { - "lock_auth": "SwitchBot app login and password", - "lock_key": "Lock encryption key" + "lock_auth": "SwitchBot account (recommended)", + "lock_key": "Enter lock encryption key manually" } }, "lock_key": { diff --git a/requirements_all.txt b/requirements_all.txt index 43cc42d9512..756240346cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.34.1 +PySwitchbot==0.36.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a9d3801e83..2e1dafe0009 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.34.1 +PySwitchbot==0.36.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 6e1a1a14c6a..1a3db48f192 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -2,6 +2,8 @@ from unittest.mock import patch +from switchbot import SwitchbotAccountConnectionError, SwitchbotAuthenticationError + from homeassistant.components.switchbot.const import ( CONF_ENCRYPTION_KEY, CONF_KEY_ID, @@ -99,7 +101,7 @@ async def test_bluetooth_discovery_lock_key(hass): data=WOLOCK_SERVICE_INFO, ) assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "lock_chose_method" + assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} @@ -404,7 +406,7 @@ async def test_user_setup_wolock_key(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "lock_chose_method" + assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} @@ -467,7 +469,7 @@ async def test_user_setup_wolock_auth(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "lock_chose_method" + assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_auth"} @@ -479,7 +481,7 @@ async def test_user_setup_wolock_auth(hass): with patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", - side_effect=RuntimeError, + side_effect=SwitchbotAuthenticationError, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -524,6 +526,43 @@ async def test_user_setup_wolock_auth(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_user_setup_wolock_auth_switchbot_api_down(hass): + """Test the user initiated form for a lock when the switchbot api is down.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOLOCK_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "lock_choose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "lock_auth"} + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "lock_auth" + assert result["errors"] == {} + + with patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", + side_effect=SwitchbotAccountConnectionError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "", + CONF_PASSWORD: "", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + async def test_user_setup_wolock_or_bot(hass): """Test the user initiated form for a lock.""" @@ -547,7 +586,7 @@ async def test_user_setup_wolock_or_bot(hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.MENU - assert result["step_id"] == "lock_chose_method" + assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} From 93488cfa0f84cbe5c74f1b2839f623ae519c3364 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 31 Dec 2022 03:16:09 -0700 Subject: [PATCH 046/173] Don't include distance in PurpleAir sensor selector (#84893) --- homeassistant/components/purpleair/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index 6b03480fb4d..3b5720434b4 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -74,8 +74,7 @@ def async_get_nearby_sensors_options( """Return a set of nearby sensors as SelectOptionDict objects.""" return [ SelectOptionDict( - value=str(result.sensor.sensor_index), - label=f"{result.sensor.name} ({round(result.distance, 1)} km away)", + value=str(result.sensor.sensor_index), label=cast(str, result.sensor.name) ) for result in nearby_sensor_results ] From ff76567061dc7f5272cfdd953b03b4f55829c1c2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 1 Jan 2023 11:55:05 -0700 Subject: [PATCH 047/173] Fix issues with PurpleAir sensor device class and unit (#84896) --- homeassistant/components/purpleair/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/purpleair/sensor.py b/homeassistant/components/purpleair/sensor.py index 9037ece5470..44fa63b2fbc 100644 --- a/homeassistant/components/purpleair/sensor.py +++ b/homeassistant/components/purpleair/sensor.py @@ -166,7 +166,7 @@ SENSOR_DESCRIPTIONS = [ name="Uptime", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - icon="mdi:timer", + device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda sensor: sensor.uptime, @@ -174,8 +174,7 @@ SENSOR_DESCRIPTIONS = [ PurpleAirSensorEntityDescription( key="voc", name="VOC", - device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - native_unit_of_measurement=CONCENTRATION_IAQ, + device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda sensor: sensor.voc, ), From 60f067b68fb32bb48109b92eebfd28c1525322aa Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 1 Jan 2023 06:17:34 -0700 Subject: [PATCH 048/173] Use serial number for AirVisal Pro config entry unique ID (#84902) * Use serial number for AirVisal Pro config entry unique ID * Code review --- .../components/airvisual_pro/config_flow.py | 46 ++++++++++++++----- tests/components/airvisual_pro/conftest.py | 10 +--- .../airvisual_pro/test_config_flow.py | 10 +++- .../airvisual_pro/test_diagnostics.py | 2 +- 4 files changed, 45 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/airvisual_pro/config_flow.py b/homeassistant/components/airvisual_pro/config_flow.py index 7cf03009932..23da39150c5 100644 --- a/homeassistant/components/airvisual_pro/config_flow.py +++ b/homeassistant/components/airvisual_pro/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from dataclasses import dataclass, field from typing import Any from pyairvisual.node import ( @@ -33,13 +34,24 @@ STEP_USER_SCHEMA = vol.Schema( ) -async def async_validate_credentials(ip_address: str, password: str) -> dict[str, Any]: - """Validate an IP address/password combo (and return any errors as appropriate).""" +@dataclass +class ValidationResult: + """Define a validation result.""" + + serial_number: str | None = None + errors: dict[str, Any] = field(default_factory=dict) + + +async def async_validate_credentials( + ip_address: str, password: str +) -> ValidationResult: + """Validate an IP address/password combo.""" node = NodeSamba(ip_address, password) errors = {} try: await node.async_connect() + measurements = await node.async_get_latest_measurements() except InvalidAuthenticationError as err: LOGGER.error("Invalid password for Pro at IP address %s: %s", ip_address, err) errors["base"] = "invalid_auth" @@ -52,10 +64,12 @@ async def async_validate_credentials(ip_address: str, password: str) -> dict[str except Exception as err: # pylint: disable=broad-except LOGGER.exception("Unknown error while connecting to %s: %s", ip_address, err) errors["base"] = "unknown" + else: + return ValidationResult(serial_number=measurements["serial_number"]) finally: await node.async_disconnect() - return errors + return ValidationResult(errors=errors) class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -89,11 +103,15 @@ class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): assert self._reauth_entry - if errors := await async_validate_credentials( + validation_result = await async_validate_credentials( self._reauth_entry.data[CONF_IP_ADDRESS], user_input[CONF_PASSWORD] - ): + ) + + if validation_result.errors: return self.async_show_form( - step_id="reauth_confirm", data_schema=STEP_REAUTH_SCHEMA, errors=errors + step_id="reauth_confirm", + data_schema=STEP_REAUTH_SCHEMA, + errors=validation_result.errors, ) self.hass.config_entries.async_update_entry( @@ -113,14 +131,18 @@ class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ip_address = user_input[CONF_IP_ADDRESS] - await self.async_set_unique_id(ip_address) - self._abort_if_unique_id_configured() - - if errors := await async_validate_credentials( + validation_result = await async_validate_credentials( ip_address, user_input[CONF_PASSWORD] - ): + ) + + if validation_result.errors: return self.async_show_form( - step_id="user", data_schema=STEP_USER_SCHEMA, errors=errors + step_id="user", + data_schema=STEP_USER_SCHEMA, + errors=validation_result.errors, ) + await self.async_set_unique_id(validation_result.serial_number) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=ip_address, data=user_input) diff --git a/tests/components/airvisual_pro/conftest.py b/tests/components/airvisual_pro/conftest.py index c5851e940de..5846a988688 100644 --- a/tests/components/airvisual_pro/conftest.py +++ b/tests/components/airvisual_pro/conftest.py @@ -12,9 +12,9 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, unique_id): +def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) + entry = MockConfigEntry(domain=DOMAIN, unique_id="XXXXXXX", data=config) entry.add_to_hass(hass) return entry @@ -69,9 +69,3 @@ async def setup_airvisual_pro_fixture(hass, config, pro): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield - - -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "192.168.1.101" diff --git a/tests/components/airvisual_pro/test_config_flow.py b/tests/components/airvisual_pro/test_config_flow.py index 32c4e14ba76..61cd43f058d 100644 --- a/tests/components/airvisual_pro/test_config_flow.py +++ b/tests/components/airvisual_pro/test_config_flow.py @@ -52,10 +52,16 @@ async def test_create_entry( } -async def test_duplicate_error(hass, config, config_entry): +async def test_duplicate_error(hass, config, config_entry, setup_airvisual_pro): """Test that errors are shown when duplicates are added.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/airvisual_pro/test_diagnostics.py b/tests/components/airvisual_pro/test_diagnostics.py index ab780b90704..5847dd7ed88 100644 --- a/tests/components/airvisual_pro/test_diagnostics.py +++ b/tests/components/airvisual_pro/test_diagnostics.py @@ -17,7 +17,7 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_airvisua "pref_disable_new_entities": False, "pref_disable_polling": False, "source": "user", - "unique_id": "192.168.1.101", + "unique_id": "XXXXXXX", "disabled_by": None, }, "data": { From 516c2b0cdb590f551dbd24d8a1990ced29520c57 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 31 Dec 2022 12:07:31 -0800 Subject: [PATCH 049/173] Google Assistant SDK: Log command and response (#84904) Log command and response --- homeassistant/components/google_assistant_sdk/helpers.py | 7 ++++++- .../components/google_assistant_sdk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google_assistant_sdk/test_notify.py | 9 ++++++--- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index f91f8f4241f..15e325f10c1 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -1,6 +1,8 @@ """Helper classes for Google Assistant SDK integration.""" from __future__ import annotations +import logging + import aiohttp from gassist_text import TextAssistant from google.oauth2.credentials import Credentials @@ -12,6 +14,8 @@ from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from .const import CONF_LANGUAGE_CODE, DOMAIN, SUPPORTED_LANGUAGE_CODES +_LOGGER = logging.getLogger(__name__) + DEFAULT_LANGUAGE_CODES = { "de": "de-DE", "en": "en-US", @@ -39,7 +43,8 @@ async def async_send_text_commands(commands: list[str], hass: HomeAssistant) -> language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass)) with TextAssistant(credentials, language_code) as assistant: for command in commands: - assistant.assist(command) + text_response = assistant.assist(command)[0] + _LOGGER.debug("command: %s\nresponse: %s", command, text_response) def default_language_code(hass: HomeAssistant): diff --git a/homeassistant/components/google_assistant_sdk/manifest.json b/homeassistant/components/google_assistant_sdk/manifest.json index 2f16d0deef3..e1b390f9496 100644 --- a/homeassistant/components/google_assistant_sdk/manifest.json +++ b/homeassistant/components/google_assistant_sdk/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/google_assistant_sdk/", - "requirements": ["gassist-text==0.0.5"], + "requirements": ["gassist-text==0.0.7"], "codeowners": ["@tronikos"], "iot_class": "cloud_polling", "integration_type": "service" diff --git a/requirements_all.txt b/requirements_all.txt index 756240346cc..f4810489bc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ fritzconnection==1.10.3 gTTS==2.2.4 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.5 +gassist-text==0.0.7 # homeassistant.components.google gcal-sync==4.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e1dafe0009..606a9546f3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -557,7 +557,7 @@ fritzconnection==1.10.3 gTTS==2.2.4 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.5 +gassist-text==0.0.7 # homeassistant.components.google gcal-sync==4.1.0 diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index 5dbaa3aa79b..5a2d11b861b 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -41,7 +41,8 @@ async def test_broadcast_one_target( target = "basement" expected_command = "broadcast to basement time for dinner" with patch( - "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist" + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + return_value=["text_response", None], ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, @@ -64,7 +65,8 @@ async def test_broadcast_two_targets( expected_command1 = "broadcast to basement time for dinner" expected_command2 = "broadcast to master bedroom time for dinner" with patch( - "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist" + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + return_value=["text_response", None], ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, @@ -84,7 +86,8 @@ async def test_broadcast_empty_message( await setup_integration() with patch( - "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist" + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + return_value=["text_response", None], ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, From 67e1872ab6a5d1844d4c0006cc6719b235766b2b Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 31 Dec 2022 05:01:05 -0800 Subject: [PATCH 050/173] Fix free/busy google calendars (#84907) fixes undefined --- homeassistant/components/google/calendar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index a96eb6b2ca6..702146ee052 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -221,8 +221,7 @@ async def async_setup_entry( ) if ( search := data.get(CONF_SEARCH) - or calendar_item.access_role == AccessRole.FREE_BUSY_READER - ): + ) or calendar_item.access_role == AccessRole.FREE_BUSY_READER: coordinator = CalendarQueryUpdateCoordinator( hass, calendar_service, From bc1d22f4ecae76c408209a63029f7c142bd2c134 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 31 Dec 2022 13:59:39 +0100 Subject: [PATCH 051/173] Bump pydeconz to v106 (#84914) fixes undefined --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 5de15b16177..2bf17cabbbf 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==105"], + "requirements": ["pydeconz==106"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index f4810489bc5..f4f65f14928 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1542,7 +1542,7 @@ pydaikin==2.8.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==105 +pydeconz==106 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 606a9546f3e..751ece0a7e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1097,7 +1097,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.8.0 # homeassistant.components.deconz -pydeconz==105 +pydeconz==106 # homeassistant.components.dexcom pydexcom==0.2.3 From 95ae37cd8780ddaa0a830bbc87ca6a02e5b0907f Mon Sep 17 00:00:00 2001 From: ChopperRob <47484504+ChopperRob@users.noreply.github.com> Date: Sun, 1 Jan 2023 02:48:55 +0100 Subject: [PATCH 052/173] Fix haveibeenpwned user-agent string (#84919) * Fixed user-agent string not being accepted as an valid header * Update homeassistant/components/haveibeenpwned/sensor.py Co-authored-by: Martin Hjelmare * Removed the aiohttp import Co-authored-by: Martin Hjelmare --- homeassistant/components/haveibeenpwned/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 199035b2713..7caf9690bd8 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -5,7 +5,6 @@ from datetime import timedelta from http import HTTPStatus import logging -from aiohttp.hdrs import USER_AGENT import requests import voluptuous as vol @@ -160,7 +159,7 @@ class HaveIBeenPwnedData: """Get the latest data for current email from REST service.""" try: url = f"{URL}{self._email}?truncateResponse=false" - header = {USER_AGENT: HA_USER_AGENT, "hibp-api-key": self._api_key} + header = {"User-Agent": HA_USER_AGENT, "hibp-api-key": self._api_key} _LOGGER.debug("Checking for breaches for email: %s", self._email) req = requests.get(url, headers=header, allow_redirects=True, timeout=5) From 76fa24aba16cfda3f5e4464d52af9525b085d3e6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 1 Jan 2023 17:16:18 -0800 Subject: [PATCH 053/173] Fix Climate device HVAC mode trigger UI (#84930) * Fix Climate device HVAC mode trigger UI * Use updated order of test case results --- .../components/climate/device_trigger.py | 5 ++++- .../components/climate/test_device_trigger.py | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index 16f7aca3c28..0b0bedb49bb 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -174,7 +174,10 @@ async def async_get_trigger_capabilities( if trigger_type == "hvac_mode_changed": return { "extra_fields": vol.Schema( - {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + { + vol.Required(state_trigger.CONF_TO): vol.In(const.HVAC_MODES), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } ) } diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 25e1a9d920d..00099538a6f 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -270,7 +270,23 @@ async def test_get_trigger_capabilities_hvac_mode(hass): assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "for", "optional": True, "type": "positive_time_period_dict"}] + ) == [ + { + "name": "to", + "options": [ + ("off", "off"), + ("heat", "heat"), + ("cool", "cool"), + ("heat_cool", "heat_cool"), + ("auto", "auto"), + ("dry", "dry"), + ("fan_only", "fan_only"), + ], + "required": True, + "type": "select", + }, + {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + ] @pytest.mark.parametrize( From 0d385d3b67294686b825dbfe9f04809db058a027 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 31 Dec 2022 21:38:34 -1000 Subject: [PATCH 054/173] Fix failing HomeKit Controller diagnostics tests (#84936) --- .../homekit_controller/test_diagnostics.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index eecb7ff51f8..218347c4ad6 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -1,4 +1,6 @@ """Test homekit_controller diagnostics.""" +from unittest.mock import ANY + from aiohttp import ClientSession from homeassistant.components.homekit_controller.const import KNOWN_DEVICES @@ -247,8 +249,8 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "friendly_name": "Koogeek-LS1-20833F Identify" }, "entity_id": "button.koogeek_ls1_20833f_identify", - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", + "last_changed": ANY, + "last_updated": ANY, "state": "unknown", }, "unit_of_measurement": None, @@ -269,8 +271,8 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "supported_features": 0, }, "entity_id": "light.koogeek_ls1_20833f_light_strip", - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", + "last_changed": ANY, + "last_updated": ANY, "state": "off", }, "unit_of_measurement": None, @@ -518,8 +520,8 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "friendly_name": "Koogeek-LS1-20833F " "Identify" }, "entity_id": "button.koogeek_ls1_20833f_identify", - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", + "last_changed": ANY, + "last_updated": ANY, "state": "unknown", }, "unit_of_measurement": None, @@ -540,8 +542,8 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "supported_features": 0, }, "entity_id": "light.koogeek_ls1_20833f_light_strip", - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", + "last_changed": ANY, + "last_updated": ANY, "state": "off", }, "unit_of_measurement": None, From bc9202cf02251042d64166fbfc17fa95718a632d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Jan 2023 03:35:01 -1000 Subject: [PATCH 055/173] Bump pySwitchbot to 0.36.1 (#84937) changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.36.0...0.36.1 small fix for the battery not updating with passive scanning after lock operation --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index f18a80b1b89..c7c50e5cf6e 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.36.0"], + "requirements": ["PySwitchbot==0.36.1"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index f4f65f14928..556e4e92bb0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.0 +PySwitchbot==0.36.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 751ece0a7e1..e4e153a16c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.0 +PySwitchbot==0.36.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From ad782166c710bcf9c84db2bccb18d41a1947be0e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 1 Jan 2023 17:11:34 -0800 Subject: [PATCH 056/173] Fix caldav calendars with custom timezones (#84955) * Fix caldav calendars with custom timezones * Revert whitespace change --- homeassistant/components/caldav/calendar.py | 28 ++++++++++++------- tests/components/caldav/test_calendar.py | 31 ++++++++++++++++++++- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 2d5c7217043..3fe5aab59c8 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -1,7 +1,7 @@ """Support for WebDav Calendar.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta import logging import re @@ -185,8 +185,8 @@ class WebDavCalendarData: event_list.append( CalendarEvent( summary=self.get_attr_value(vevent, "summary") or "", - start=vevent.dtstart.value, - end=self.get_end_date(vevent), + start=self.to_local(vevent.dtstart.value), + end=self.to_local(self.get_end_date(vevent)), location=self.get_attr_value(vevent, "location"), description=self.get_attr_value(vevent, "description"), ) @@ -269,8 +269,8 @@ class WebDavCalendarData: ) self.event = CalendarEvent( summary=summary, - start=vevent.dtstart.value, - end=self.get_end_date(vevent), + start=self.to_local(vevent.dtstart.value), + end=self.to_local(self.get_end_date(vevent)), location=self.get_attr_value(vevent, "location"), description=self.get_attr_value(vevent, "description"), ) @@ -308,15 +308,23 @@ class WebDavCalendarData: def to_datetime(obj): """Return a datetime.""" if isinstance(obj, datetime): - if obj.tzinfo is None: - # floating value, not bound to any time zone in particular - # represent same time regardless of which time zone is currently being observed - return obj.replace(tzinfo=dt.DEFAULT_TIME_ZONE) - return obj + return WebDavCalendarData.to_local(obj) return dt.dt.datetime.combine(obj, dt.dt.time.min).replace( tzinfo=dt.DEFAULT_TIME_ZONE ) + @staticmethod + def to_local(obj: datetime | date) -> datetime | date: + """Return a datetime as a local datetime, leaving dates unchanged. + + This handles giving floating times a timezone for comparison + with all day events and dropping the custom timezone object + used by the caldav client and dateutil so the datetime can be copied. + """ + if isinstance(obj, datetime): + return dt.as_local(obj) + return obj + @staticmethod def get_attr_value(obj, attribute): """Return the value of the attribute if defined.""" diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index e9c58034cbe..c7031fa9c04 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -226,6 +226,35 @@ DTEND:20151127T003000Z RRULE:FREQ=HOURLY;INTERVAL=1;COUNT=12 END:VEVENT END:VCALENDAR +""", + """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Global Corp.//CalDAV Client//EN +BEGIN:VTIMEZONE +TZID:Europe/London +BEGIN:STANDARD +DTSTART:19961027T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +TZNAME:GMT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0000 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19810329T010000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +TZNAME:BST +TZOFFSETFROM:+0000 +TZOFFSETTO:+0100 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +UID:15 +DTSTAMP:20221125T000000Z +DTSTART;TZID=Europe/London:20221127T000000 +DTEND;TZID=Europe/London:20221127T003000 +SUMMARY:Event with a provided Timezone +END:VEVENT +END:VCALENDAR """, ] @@ -929,7 +958,7 @@ async def test_get_events(hass, calendar, get_api_events): await hass.async_block_till_done() events = await get_api_events("calendar.private") - assert len(events) == 15 + assert len(events) == 16 assert calendar.call From 2d120cb6bafb6e4af7501d64b042d3bc51981cdb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Jan 2023 20:25:29 -0500 Subject: [PATCH 057/173] Bumped version to 2023.1.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 e36d25046f4..e5e8f40806b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index a49385b334d..bcb8021f697 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.0b3" +version = "2023.1.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f3fab5c1f5b9a6ae489b0dec9d9fd9338cf29e59 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 3 Jan 2023 02:28:21 +0100 Subject: [PATCH 058/173] Handle not available add-on in hassio add-on manager (#84943) * Handle not available add-on in hassio add-on manager * Fix zwave_js tests * Fix sky connect tests * Fix matter tests * Fix yellow tests * Update hardware tests --- .../components/hassio/addon_manager.py | 11 +++++ tests/components/hassio/test_addon_manager.py | 44 ++++++++++++++++++- .../homeassistant_hardware/conftest.py | 2 + .../homeassistant_sky_connect/conftest.py | 2 + .../homeassistant_yellow/conftest.py | 2 + tests/components/matter/conftest.py | 9 ++++ tests/components/matter/test_init.py | 2 +- tests/components/zwave_js/conftest.py | 11 +++++ tests/components/zwave_js/test_config_flow.py | 1 + 9 files changed, 81 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py index f240937c7f5..46eca080b1b 100644 --- a/homeassistant/components/hassio/addon_manager.py +++ b/homeassistant/components/hassio/addon_manager.py @@ -70,6 +70,7 @@ def api_error( class AddonInfo: """Represent the current add-on info state.""" + available: bool hostname: str | None options: dict[str, Any] state: AddonState @@ -144,6 +145,7 @@ class AddonManager: self._logger.debug("Add-on store info: %s", addon_store_info) if not addon_store_info["installed"]: return AddonInfo( + available=addon_store_info["available"], hostname=None, options={}, state=AddonState.NOT_INSTALLED, @@ -154,6 +156,7 @@ class AddonManager: addon_info = await async_get_addon_info(self._hass, self.addon_slug) addon_state = self.async_get_addon_state(addon_info) return AddonInfo( + available=addon_info["available"], hostname=addon_info["hostname"], options=addon_info["options"], state=addon_state, @@ -184,6 +187,11 @@ class AddonManager: @api_error("Failed to install the {addon_name} add-on") async def async_install_addon(self) -> None: """Install the managed add-on.""" + addon_info = await self.async_get_addon_info() + + if not addon_info.available: + raise AddonError(f"{self.addon_name} add-on is not available anymore") + await async_install_addon(self._hass, self.addon_slug) @api_error("Failed to uninstall the {addon_name} add-on") @@ -196,6 +204,9 @@ class AddonManager: """Update the managed add-on if needed.""" addon_info = await self.async_get_addon_info() + if not addon_info.available: + raise AddonError(f"{self.addon_name} add-on is not available anymore") + if addon_info.state is AddonState.NOT_INSTALLED: raise AddonError(f"{self.addon_name} add-on is not installed") diff --git a/tests/components/hassio/test_addon_manager.py b/tests/components/hassio/test_addon_manager.py index 7135f1ea646..5ee7856b9f7 100644 --- a/tests/components/hassio/test_addon_manager.py +++ b/tests/components/hassio/test_addon_manager.py @@ -32,6 +32,7 @@ def addon_not_installed_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> AsyncMock: """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -41,10 +42,12 @@ def mock_addon_installed( ) -> AsyncMock: """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-test-addon" addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -67,6 +70,7 @@ def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +85,7 @@ def addon_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -180,6 +185,26 @@ async def test_not_installed_raises_exception( assert str(err.value) == "Test add-on is not installed" +async def test_not_available_raises_exception( + addon_manager: AddonManager, + addon_store_info: AsyncMock, + addon_info: AsyncMock, +) -> None: + """Test addon not available raises exception.""" + addon_store_info.return_value["available"] = False + addon_info.return_value["available"] = False + + with pytest.raises(AddonError) as err: + await addon_manager.async_install_addon() + + assert str(err.value) == "Test add-on is not available anymore" + + with pytest.raises(AddonError) as err: + await addon_manager.async_update_addon() + + assert str(err.value) == "Test add-on is not available anymore" + + async def test_get_addon_discovery_info( addon_manager: AddonManager, get_addon_discovery_info: AsyncMock ) -> None: @@ -222,6 +247,7 @@ async def test_get_addon_info_not_installed( ) -> None: """Test get addon info when addon is not installed..""" assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname=None, options={}, state=AddonState.NOT_INSTALLED, @@ -243,6 +269,7 @@ async def test_get_addon_info( """Test get addon info when addon is installed.""" addon_installed.return_value["state"] = addon_info_state assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=addon_state, @@ -308,18 +335,29 @@ async def test_set_addon_options_error( async def test_install_addon( - addon_manager: AddonManager, install_addon: AsyncMock + addon_manager: AddonManager, + install_addon: AsyncMock, + addon_store_info: AsyncMock, + addon_info: AsyncMock, ) -> None: """Test install addon.""" + addon_store_info.return_value["available"] = True + addon_info.return_value["available"] = True + await addon_manager.async_install_addon() assert install_addon.call_count == 1 async def test_install_addon_error( - addon_manager: AddonManager, install_addon: AsyncMock + addon_manager: AddonManager, + install_addon: AsyncMock, + addon_store_info: AsyncMock, + addon_info: AsyncMock, ) -> None: """Test install addon raises error.""" + addon_store_info.return_value["available"] = True + addon_info.return_value["available"] = True install_addon.side_effect = HassioAPIError("Boom") with pytest.raises(AddonError) as err: @@ -341,6 +379,7 @@ async def test_schedule_install_addon( assert addon_manager.task_in_progress() is True assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=AddonState.INSTALLING, @@ -676,6 +715,7 @@ async def test_schedule_update_addon( assert addon_manager.task_in_progress() is True assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=AddonState.UPDATING, diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py index fd0ce2e761b..4add48781a9 100644 --- a/tests/components/homeassistant_hardware/conftest.py +++ b/tests/components/homeassistant_hardware/conftest.py @@ -67,6 +67,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +82,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index 2d333c62b2d..f7f0bb8d128 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -69,6 +69,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -83,6 +84,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index 62595c11fe1..bc48c6b01fd 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -67,6 +67,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +82,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 03c8bc35687..b541e7c439e 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -77,6 +77,7 @@ def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -91,6 +92,7 @@ def addon_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -105,6 +107,7 @@ def addon_not_installed_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> AsyncMock: """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -114,10 +117,12 @@ def addon_installed_fixture( ) -> AsyncMock: """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-matter-server" addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -130,10 +135,12 @@ def addon_running_fixture( ) -> AsyncMock: """Mock add-on already running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-matter-server" addon_info.return_value["state"] = "started" addon_info.return_value["version"] = "1.0.0" @@ -149,10 +156,12 @@ def install_addon_fixture( async def install_addon_side_effect(hass: HomeAssistant, slug: str) -> None: """Mock install add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index f34e428ecc0..c40d35ff74a 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -116,7 +116,7 @@ async def test_install_addon( await hass.async_block_till_done() assert entry.state is ConfigEntryState.SETUP_RETRY - assert addon_store_info.call_count == 2 + assert addon_store_info.call_count == 3 assert install_addon.call_count == 1 assert install_addon.call_args == call(hass, "core_matter_server") assert start_addon.call_count == 1 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index ba97cfe4c36..0b0503a3e29 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -30,6 +30,7 @@ def mock_addon_info(addon_info_side_effect): side_effect=addon_info_side_effect, ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -53,6 +54,7 @@ def mock_addon_store_info(addon_store_info_side_effect): side_effect=addon_store_info_side_effect, ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -64,10 +66,12 @@ def mock_addon_store_info(addon_store_info_side_effect): def mock_addon_running(addon_store_info, addon_info): """Mock add-on already running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "started" addon_info.return_value["version"] = "1.0.0" return addon_info @@ -77,10 +81,12 @@ def mock_addon_running(addon_store_info, addon_info): def mock_addon_installed(addon_store_info, addon_info): """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" return addon_info @@ -89,6 +95,7 @@ def mock_addon_installed(addon_store_info, addon_info): @pytest.fixture(name="addon_not_installed") def mock_addon_not_installed(addon_store_info, addon_info): """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -126,10 +133,12 @@ def install_addon_side_effect_fixture(addon_store_info, addon_info): async def install_addon(hass, slug): """Mock install add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -162,10 +171,12 @@ def start_addon_side_effect_fixture(addon_store_info, addon_info): async def start_addon(hass, slug): """Mock start add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "started" return start_addon diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index eacf4b61cc8..2bff9c2cccb 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -536,6 +536,7 @@ async def test_abort_hassio_discovery_for_other_addon( async def test_usb_discovery( hass, supervisor, + addon_not_installed, install_addon, addon_options, get_addon_discovery_info, From 3ba59fbebe4830839da312bead2583f9de1f9caa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jan 2023 20:30:09 -0500 Subject: [PATCH 059/173] Bumped version to 2022.12.9 --- 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 16015beec72..a2d2a23bf6e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "8" +PATCH_VERSION: Final = "9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 8cb74b24793..05fda7fa5a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.12.8" +version = "2022.12.9" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2ad1a53038b3396b53078c095b4feb371da7adc6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 2 Jan 2023 14:12:36 +0100 Subject: [PATCH 060/173] Consider 95% as closed for Motion blinds venetian blinds (#84872) --- homeassistant/components/motion_blinds/cover.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index e0d02750d6d..53ee4f5b561 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -352,6 +352,13 @@ class MotionTiltDevice(MotionPositionDevice): return None return self._blind.angle * 100 / 180 + @property + def is_closed(self) -> bool | None: + """Return if the cover is closed or not.""" + if self._blind.position is None: + return None + return self._blind.position >= 95 + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" async with self._api_lock: From 93ac908776d51832c44c79d20165d392754c8299 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 3 Jan 2023 02:28:21 +0100 Subject: [PATCH 061/173] Handle not available add-on in hassio add-on manager (#84943) * Handle not available add-on in hassio add-on manager * Fix zwave_js tests * Fix sky connect tests * Fix matter tests * Fix yellow tests * Update hardware tests --- .../components/hassio/addon_manager.py | 11 +++++ tests/components/hassio/test_addon_manager.py | 44 ++++++++++++++++++- .../homeassistant_hardware/conftest.py | 2 + .../homeassistant_sky_connect/conftest.py | 2 + .../homeassistant_yellow/conftest.py | 2 + tests/components/matter/conftest.py | 9 ++++ tests/components/matter/test_init.py | 2 +- tests/components/zwave_js/conftest.py | 11 +++++ tests/components/zwave_js/test_config_flow.py | 1 + 9 files changed, 81 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py index f240937c7f5..46eca080b1b 100644 --- a/homeassistant/components/hassio/addon_manager.py +++ b/homeassistant/components/hassio/addon_manager.py @@ -70,6 +70,7 @@ def api_error( class AddonInfo: """Represent the current add-on info state.""" + available: bool hostname: str | None options: dict[str, Any] state: AddonState @@ -144,6 +145,7 @@ class AddonManager: self._logger.debug("Add-on store info: %s", addon_store_info) if not addon_store_info["installed"]: return AddonInfo( + available=addon_store_info["available"], hostname=None, options={}, state=AddonState.NOT_INSTALLED, @@ -154,6 +156,7 @@ class AddonManager: addon_info = await async_get_addon_info(self._hass, self.addon_slug) addon_state = self.async_get_addon_state(addon_info) return AddonInfo( + available=addon_info["available"], hostname=addon_info["hostname"], options=addon_info["options"], state=addon_state, @@ -184,6 +187,11 @@ class AddonManager: @api_error("Failed to install the {addon_name} add-on") async def async_install_addon(self) -> None: """Install the managed add-on.""" + addon_info = await self.async_get_addon_info() + + if not addon_info.available: + raise AddonError(f"{self.addon_name} add-on is not available anymore") + await async_install_addon(self._hass, self.addon_slug) @api_error("Failed to uninstall the {addon_name} add-on") @@ -196,6 +204,9 @@ class AddonManager: """Update the managed add-on if needed.""" addon_info = await self.async_get_addon_info() + if not addon_info.available: + raise AddonError(f"{self.addon_name} add-on is not available anymore") + if addon_info.state is AddonState.NOT_INSTALLED: raise AddonError(f"{self.addon_name} add-on is not installed") diff --git a/tests/components/hassio/test_addon_manager.py b/tests/components/hassio/test_addon_manager.py index 7135f1ea646..5ee7856b9f7 100644 --- a/tests/components/hassio/test_addon_manager.py +++ b/tests/components/hassio/test_addon_manager.py @@ -32,6 +32,7 @@ def addon_not_installed_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> AsyncMock: """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -41,10 +42,12 @@ def mock_addon_installed( ) -> AsyncMock: """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-test-addon" addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -67,6 +70,7 @@ def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +85,7 @@ def addon_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -180,6 +185,26 @@ async def test_not_installed_raises_exception( assert str(err.value) == "Test add-on is not installed" +async def test_not_available_raises_exception( + addon_manager: AddonManager, + addon_store_info: AsyncMock, + addon_info: AsyncMock, +) -> None: + """Test addon not available raises exception.""" + addon_store_info.return_value["available"] = False + addon_info.return_value["available"] = False + + with pytest.raises(AddonError) as err: + await addon_manager.async_install_addon() + + assert str(err.value) == "Test add-on is not available anymore" + + with pytest.raises(AddonError) as err: + await addon_manager.async_update_addon() + + assert str(err.value) == "Test add-on is not available anymore" + + async def test_get_addon_discovery_info( addon_manager: AddonManager, get_addon_discovery_info: AsyncMock ) -> None: @@ -222,6 +247,7 @@ async def test_get_addon_info_not_installed( ) -> None: """Test get addon info when addon is not installed..""" assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname=None, options={}, state=AddonState.NOT_INSTALLED, @@ -243,6 +269,7 @@ async def test_get_addon_info( """Test get addon info when addon is installed.""" addon_installed.return_value["state"] = addon_info_state assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=addon_state, @@ -308,18 +335,29 @@ async def test_set_addon_options_error( async def test_install_addon( - addon_manager: AddonManager, install_addon: AsyncMock + addon_manager: AddonManager, + install_addon: AsyncMock, + addon_store_info: AsyncMock, + addon_info: AsyncMock, ) -> None: """Test install addon.""" + addon_store_info.return_value["available"] = True + addon_info.return_value["available"] = True + await addon_manager.async_install_addon() assert install_addon.call_count == 1 async def test_install_addon_error( - addon_manager: AddonManager, install_addon: AsyncMock + addon_manager: AddonManager, + install_addon: AsyncMock, + addon_store_info: AsyncMock, + addon_info: AsyncMock, ) -> None: """Test install addon raises error.""" + addon_store_info.return_value["available"] = True + addon_info.return_value["available"] = True install_addon.side_effect = HassioAPIError("Boom") with pytest.raises(AddonError) as err: @@ -341,6 +379,7 @@ async def test_schedule_install_addon( assert addon_manager.task_in_progress() is True assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=AddonState.INSTALLING, @@ -676,6 +715,7 @@ async def test_schedule_update_addon( assert addon_manager.task_in_progress() is True assert await addon_manager.async_get_addon_info() == AddonInfo( + available=True, hostname="core-test-addon", options={}, state=AddonState.UPDATING, diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py index fd0ce2e761b..4add48781a9 100644 --- a/tests/components/homeassistant_hardware/conftest.py +++ b/tests/components/homeassistant_hardware/conftest.py @@ -67,6 +67,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +82,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index 2d333c62b2d..f7f0bb8d128 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -69,6 +69,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -83,6 +84,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index 62595c11fe1..bc48c6b01fd 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -67,6 +67,7 @@ def addon_store_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": True, "installed": None, "state": None, "version": "1.0.0", @@ -81,6 +82,7 @@ def addon_info_fixture(): "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": True, "hostname": None, "options": {}, "state": None, diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 8310d725c8e..486e2fd26ac 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -80,6 +80,7 @@ def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -94,6 +95,7 @@ def addon_info_fixture() -> Generator[AsyncMock, None, None]: "homeassistant.components.hassio.addon_manager.async_get_addon_info", ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -108,6 +110,7 @@ def addon_not_installed_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> AsyncMock: """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -117,10 +120,12 @@ def addon_installed_fixture( ) -> AsyncMock: """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-matter-server" addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -133,10 +138,12 @@ def addon_running_fixture( ) -> AsyncMock: """Mock add-on already running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["hostname"] = "core-matter-server" addon_info.return_value["state"] = "started" addon_info.return_value["version"] = "1.0.0" @@ -152,10 +159,12 @@ def install_addon_fixture( async def install_addon_side_effect(hass: HomeAssistant, slug: str) -> None: """Mock install add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index fbc538016dc..a3febe799a5 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -307,7 +307,7 @@ async def test_install_addon( await hass.async_block_till_done() assert entry.state is ConfigEntryState.SETUP_RETRY - assert addon_store_info.call_count == 2 + assert addon_store_info.call_count == 3 assert install_addon.call_count == 1 assert install_addon.call_args == call(hass, "core_matter_server") assert start_addon.call_count == 1 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index ba97cfe4c36..0b0503a3e29 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -30,6 +30,7 @@ def mock_addon_info(addon_info_side_effect): side_effect=addon_info_side_effect, ) as addon_info: addon_info.return_value = { + "available": False, "hostname": None, "options": {}, "state": None, @@ -53,6 +54,7 @@ def mock_addon_store_info(addon_store_info_side_effect): side_effect=addon_store_info_side_effect, ) as addon_store_info: addon_store_info.return_value = { + "available": False, "installed": None, "state": None, "version": "1.0.0", @@ -64,10 +66,12 @@ def mock_addon_store_info(addon_store_info_side_effect): def mock_addon_running(addon_store_info, addon_info): """Mock add-on already running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "started" addon_info.return_value["version"] = "1.0.0" return addon_info @@ -77,10 +81,12 @@ def mock_addon_running(addon_store_info, addon_info): def mock_addon_installed(addon_store_info, addon_info): """Mock add-on already installed but not running.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" return addon_info @@ -89,6 +95,7 @@ def mock_addon_installed(addon_store_info, addon_info): @pytest.fixture(name="addon_not_installed") def mock_addon_not_installed(addon_store_info, addon_info): """Mock add-on not installed.""" + addon_store_info.return_value["available"] = True return addon_info @@ -126,10 +133,12 @@ def install_addon_side_effect_fixture(addon_store_info, addon_info): async def install_addon(hass, slug): """Mock install add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "stopped", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "stopped" addon_info.return_value["version"] = "1.0.0" @@ -162,10 +171,12 @@ def start_addon_side_effect_fixture(addon_store_info, addon_info): async def start_addon(hass, slug): """Mock start add-on.""" addon_store_info.return_value = { + "available": True, "installed": "1.0.0", "state": "started", "version": "1.0.0", } + addon_info.return_value["available"] = True addon_info.return_value["state"] = "started" return start_addon diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index eacf4b61cc8..2bff9c2cccb 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -536,6 +536,7 @@ async def test_abort_hassio_discovery_for_other_addon( async def test_usb_discovery( hass, supervisor, + addon_not_installed, install_addon, addon_options, get_addon_discovery_info, From 4013d4c48d1b749be2f6d3d0e1a2f8dbefaaab2c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 Jan 2023 12:30:25 +0100 Subject: [PATCH 062/173] Revert "Add aliases to device registry items" (#84976) --- .../components/config/device_registry.py | 6 --- homeassistant/helpers/device_registry.py | 10 +---- .../components/config/test_device_registry.py | 40 ------------------- tests/helpers/test_device_registry.py | 17 ++------ 4 files changed, 5 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 348fc475cb8..42d2386977f 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -75,7 +75,6 @@ async def async_setup(hass): @websocket_api.websocket_command( { vol.Required("type"): "config/device_registry/update", - vol.Optional("aliases"): list, vol.Optional("area_id"): vol.Any(str, None), vol.Required("device_id"): str, # We only allow setting disabled_by user via API. @@ -96,10 +95,6 @@ def websocket_update_device( msg.pop("type") msg_id = msg.pop("id") - if "aliases" in msg: - # Convert aliases to a set - msg["aliases"] = set(msg["aliases"]) - if msg.get("disabled_by") is not None: msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"]) @@ -165,7 +160,6 @@ async def websocket_remove_config_entry_from_device( def _entry_dict(entry): """Convert entry to API format.""" return { - "aliases": entry.aliases, "area_id": entry.area_id, "configuration_url": entry.configuration_url, "config_entries": list(entry.config_entries), diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 294c2ab4832..a7c1ebdb434 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -32,7 +32,7 @@ DATA_REGISTRY = "device_registry" EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 4 +STORAGE_VERSION_MINOR = 3 SAVE_DELAY = 10 CLEANUP_DELAY = 10 @@ -70,7 +70,6 @@ class DeviceEntryType(StrEnum): class DeviceEntry: """Device Registry Entry.""" - aliases: set[str] = attr.ib(factory=set) 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) @@ -175,9 +174,6 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): # Version 1.3 adds hw_version for device in old_data["devices"]: device["hw_version"] = None - if old_minor_version < 4: - for device in old_data["devices"]: - device["aliases"] = [] if old_major_version > 1: raise NotImplementedError @@ -382,7 +378,6 @@ class DeviceRegistry: device_id: str, *, add_config_entry_id: str | UndefinedType = UNDEFINED, - aliases: set[str] | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, configuration_url: str | None | UndefinedType = UNDEFINED, disabled_by: DeviceEntryDisabler | None | UndefinedType = UNDEFINED, @@ -473,7 +468,6 @@ class DeviceRegistry: old_values["identifiers"] = old.identifiers for attr_name, value in ( - ("aliases", aliases), ("area_id", area_id), ("configuration_url", configuration_url), ("disabled_by", disabled_by), @@ -552,7 +546,6 @@ class DeviceRegistry: if data is not None: for device in data["devices"]: devices[device["id"]] = DeviceEntry( - aliases=set(device["aliases"]), area_id=device["area_id"], config_entries=set(device["config_entries"]), configuration_url=device["configuration_url"], @@ -600,7 +593,6 @@ class DeviceRegistry: data["devices"] = [ { - "aliases": list(entry.aliases), "area_id": entry.area_id, "config_entries": list(entry.config_entries), "configuration_url": entry.configuration_url, diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 3582601f7e3..4f47e463751 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -1,6 +1,5 @@ """Test device_registry API.""" import pytest -from pytest_unordered import unordered from homeassistant.components.config import device_registry from homeassistant.helpers import device_registry as helpers_dr @@ -53,7 +52,6 @@ async def test_list_devices(hass, client, registry): assert msg["result"] == [ { - "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -70,7 +68,6 @@ async def test_list_devices(hass, client, registry): "via_device_id": None, }, { - "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -96,7 +93,6 @@ async def test_list_devices(hass, client, registry): assert msg["result"] == [ { - "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -164,42 +160,6 @@ async def test_update_device(hass, client, registry, payload_key, payload_value) assert isinstance(device.disabled_by, (helpers_dr.DeviceEntryDisabler, type(None))) -@pytest.mark.parametrize("aliases", (["alias_1", "alias_2"], ["alias_1", "alias_1"])) -async def test_update_aliases(hass, client, registry, aliases): - """Test update entry.""" - device = registry.async_get_or_create( - config_entry_id="1234", - connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, - identifiers={("bridgeid", "0123")}, - manufacturer="manufacturer", - model="model", - ) - - assert not device.aliases == {} - - await client.send_json( - { - "id": 1, - "type": "config/device_registry/update", - "device_id": device.id, - "aliases": aliases, - } - ) - - msg = await client.receive_json() - await hass.async_block_till_done() - assert len(registry.devices) == 1 - - device = registry.async_get_device( - identifiers={("bridgeid", "0123")}, - connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, - ) - - # Test that the aliases list is stored by the registry as a set - assert msg["result"]["aliases"] == unordered(list(set(aliases))) - assert device.aliases == set(aliases) - - async def test_remove_config_entry_from_device(hass, hass_ws_client): """Test removing config entry from device.""" assert await async_setup_component(hass, "config", {}) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 7888054aaf7..2c9a7956874 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -176,7 +176,6 @@ async def test_loading_from_storage(hass, hass_storage): "data": { "devices": [ { - "aliases": ["alias_1", "alias_2"], "area_id": "12345A", "config_entries": ["1234"], "configuration_url": "configuration_url", @@ -219,7 +218,6 @@ async def test_loading_from_storage(hass, hass_storage): model="model", ) assert entry == device_registry.DeviceEntry( - aliases={"alias_1", "alias_2"}, area_id="12345A", config_entries={"1234"}, configuration_url="configuration_url", @@ -263,8 +261,8 @@ async def test_loading_from_storage(hass, hass_storage): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_from_1_1(hass, hass_storage): - """Test migration from version 1.1.""" +async def test_migration_1_1_to_1_3(hass, hass_storage): + """Test migration from version 1.1 to 1.3.""" hass_storage[device_registry.STORAGE_KEY] = { "version": 1, "minor_version": 1, @@ -339,7 +337,6 @@ async def test_migration_from_1_1(hass, hass_storage): "data": { "devices": [ { - "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -357,7 +354,6 @@ async def test_migration_from_1_1(hass, hass_storage): "via_device_id": None, }, { - "aliases": [], "area_id": None, "config_entries": [None], "configuration_url": None, @@ -389,8 +385,8 @@ async def test_migration_from_1_1(hass, hass_storage): @pytest.mark.parametrize("load_registries", [False]) -async def test_migration_from_1_2(hass, hass_storage): - """Test migration from version 1.2.""" +async def test_migration_1_2_to_1_3(hass, hass_storage): + """Test migration from version 1.2 to 1.3.""" hass_storage[device_registry.STORAGE_KEY] = { "version": 1, "minor_version": 2, @@ -464,7 +460,6 @@ async def test_migration_from_1_2(hass, hass_storage): "data": { "devices": [ { - "aliases": [], "area_id": None, "config_entries": ["1234"], "configuration_url": None, @@ -482,7 +477,6 @@ async def test_migration_from_1_2(hass, hass_storage): "via_device_id": None, }, { - "aliases": [], "area_id": None, "config_entries": [None], "configuration_url": None, @@ -914,7 +908,6 @@ async def test_update(hass, registry, update_events): with patch.object(registry, "async_schedule_save") as mock_save: updated_entry = registry.async_update_device( entry.id, - aliases={"alias_1", "alias_2"}, area_id="12345A", configuration_url="configuration_url", disabled_by=device_registry.DeviceEntryDisabler.USER, @@ -933,7 +926,6 @@ async def test_update(hass, registry, update_events): assert mock_save.call_count == 1 assert updated_entry != entry assert updated_entry == device_registry.DeviceEntry( - aliases={"alias_1", "alias_2"}, area_id="12345A", config_entries={"1234"}, configuration_url="configuration_url", @@ -976,7 +968,6 @@ async def test_update(hass, registry, update_events): assert update_events[1]["action"] == "update" assert update_events[1]["device_id"] == entry.id assert update_events[1]["changes"] == { - "aliases": set(), "area_id": None, "configuration_url": None, "disabled_by": None, From f3ec82543e2968134306fdbd79bd9d787bf96fd1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 2 Jan 2023 22:17:20 +0100 Subject: [PATCH 063/173] Bump motionblinds to 0.6.15 (#84994) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 68b4ffc8477..2ac02df992e 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.14"], + "requirements": ["motionblinds==0.6.15"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index 556e4e92bb0..4da81593f3b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1119,7 +1119,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.14 +motionblinds==0.6.15 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4e153a16c1..0b26b8dabc4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.14 +motionblinds==0.6.15 # homeassistant.components.motioneye motioneye-client==0.3.12 From d4e55ee03094411ddeb7101580b254cc98b9d933 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 2 Jan 2023 15:46:50 -0500 Subject: [PATCH 064/173] Bump ZHA quirks (#85004) --- 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 c2a296e398a..7f8568f1ab3 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.34.5", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.89", + "zha-quirks==0.0.90", "zigpy-deconz==0.19.2", "zigpy==0.52.3", "zigpy-xbee==0.16.2", diff --git a/requirements_all.txt b/requirements_all.txt index 4da81593f3b..f4db3ef74b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2647,7 +2647,7 @@ zengge==0.2 zeroconf==0.47.1 # homeassistant.components.zha -zha-quirks==0.0.89 +zha-quirks==0.0.90 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b26b8dabc4..e497e2e9eee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1854,7 +1854,7 @@ zamg==0.2.2 zeroconf==0.47.1 # homeassistant.components.zha -zha-quirks==0.0.89 +zha-quirks==0.0.90 # homeassistant.components.zha zigpy-deconz==0.19.2 From 7c199b36f82713f334128a5a53d9e202e6a72a51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Jan 2023 11:33:01 -1000 Subject: [PATCH 065/173] Bump home-assistant-bluetooth to 1.9.1 (#85005) fixes #83722 --- 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 66768bc3b2d..5bf45262241 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ cryptography==38.0.3 dbus-fast==1.82.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 -home-assistant-bluetooth==1.9.0 +home-assistant-bluetooth==1.9.1 home-assistant-frontend==20221230.0 httpx==0.23.1 ifaddr==0.1.7 diff --git a/pyproject.toml b/pyproject.toml index bcb8021f697..50bc0a06938 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.23.1", - "home-assistant-bluetooth==1.9.0", + "home-assistant-bluetooth==1.9.1", "ifaddr==0.1.7", "jinja2==3.1.2", "lru-dict==1.1.8", diff --git a/requirements.txt b/requirements.txt index 077120c4ae1..ea7119511e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.3.0 httpx==0.23.1 -home-assistant-bluetooth==1.9.0 +home-assistant-bluetooth==1.9.1 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 From b23ab3c65afb0e22f4505d8a81de1b37354a4763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 2 Jan 2023 22:16:38 +0100 Subject: [PATCH 066/173] Update Tibber lib to 0.26.7. Improve error handling of realtime data (#85008) --- 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 cc296d06ea8..2082d6ddf30 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.26.6"], + "requirements": ["pyTibber==0.26.7"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index f4db3ef74b0..3bf7f1e4327 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1439,7 +1439,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.6 +pyTibber==0.26.7 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e497e2e9eee..ddb0ef31024 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1039,7 +1039,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.6 +pyTibber==0.26.7 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From 5f4d28655679a62b7ee0c5336f22977e216ae9c0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 2 Jan 2023 23:31:42 +0100 Subject: [PATCH 067/173] Update frontend to 20230102.0 (#85010) Co-authored-by: Franck Nijhof --- 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 138d2a4aa46..e8833517043 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221230.0"], + "requirements": ["home-assistant-frontend==20230102.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5bf45262241..dee58b6e51e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ dbus-fast==1.82.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.9.1 -home-assistant-frontend==20221230.0 +home-assistant-frontend==20230102.0 httpx==0.23.1 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3bf7f1e4327..6ba0e543405 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -888,7 +888,7 @@ hole==0.8.0 holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20221230.0 +home-assistant-frontend==20230102.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ddb0ef31024..c7d0cbe8697 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -668,7 +668,7 @@ hole==0.8.0 holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20221230.0 +home-assistant-frontend==20230102.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 0b47bf1f0b9bf93c9c576f0868da112e1ed184aa Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 3 Jan 2023 02:49:55 +0100 Subject: [PATCH 068/173] Switch to reolink-aio (#85014) * switch to reolink-aio * fix imports --- CODEOWNERS | 4 ++-- homeassistant/components/reolink/__init__.py | 2 +- homeassistant/components/reolink/config_flow.py | 2 +- homeassistant/components/reolink/host.py | 4 ++-- homeassistant/components/reolink/manifest.json | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/reolink/test_config_flow.py | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a62841ae884..464bb252a2e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -947,8 +947,8 @@ build.json @home-assistant/supervisor /tests/components/remote/ @home-assistant/core /homeassistant/components/renault/ @epenet /tests/components/renault/ @epenet -/homeassistant/components/reolink/ @starkillerOG @JimStar -/tests/components/reolink/ @starkillerOG @JimStar +/homeassistant/components/reolink/ @starkillerOG +/tests/components/reolink/ @starkillerOG /homeassistant/components/repairs/ @home-assistant/core /tests/components/repairs/ @home-assistant/core /homeassistant/components/repetier/ @MTrab @ShadowBr0ther diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index db61e4aa627..a4daba45ba7 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -9,7 +9,7 @@ import logging from aiohttp import ClientConnectorError import async_timeout -from reolink_ip.exceptions import ApiError, InvalidContentTypeError +from reolink_aio.exceptions import ApiError, InvalidContentTypeError from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index c351a125056..31f1a10dc1e 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from reolink_ip.exceptions import ApiError, CredentialsInvalidError +from reolink_aio.exceptions import ApiError, CredentialsInvalidError import voluptuous as vol from homeassistant import config_entries, core, exceptions diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 0a5e378a78d..fc5e4947afa 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -7,8 +7,8 @@ import logging from typing import Any import aiohttp -from reolink_ip.api import Host -from reolink_ip.exceptions import ( +from reolink_aio.api import Host +from reolink_aio.exceptions import ( ApiError, CredentialsInvalidError, InvalidContentTypeError, diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index b5483be23ab..2c0deafca45 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,8 +3,8 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-ip==0.0.40"], - "codeowners": ["@starkillerOG", "@JimStar"], + "requirements": ["reolink-aio==0.1.1"], + "codeowners": ["@starkillerOG"], "iot_class": "local_polling", - "loggers": ["reolink-ip"] + "loggers": ["reolink-aio"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6ba0e543405..57d98a60296 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2190,7 +2190,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-ip==0.0.40 +reolink-aio==0.1.1 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7d0cbe8697..8cfec7c52ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1529,7 +1529,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-ip==0.0.40 +reolink-aio==0.1.1 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index ad017186075..b69fab9797f 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -3,7 +3,7 @@ import json from unittest.mock import AsyncMock, Mock, patch import pytest -from reolink_ip.exceptions import ApiError, CredentialsInvalidError +from reolink_aio.exceptions import ApiError, CredentialsInvalidError from homeassistant import config_entries, data_entry_flow from homeassistant.components.reolink import const From a10b9572c738fb7e3b5edea39d7432581e41d7bb Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 3 Jan 2023 00:52:47 +0100 Subject: [PATCH 069/173] Bump pyatmo to v7.5.0 (#85016) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index ac478282614..e9f3a99a8a5 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.4.0"], + "requirements": ["pyatmo==7.5.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index 57d98a60296..c488a0460e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1473,7 +1473,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.4.0 +pyatmo==7.5.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8cfec7c52ed..84ec03e40e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1061,7 +1061,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.4.0 +pyatmo==7.5.0 # homeassistant.components.apple_tv pyatv==0.10.3 From 4a7db6ee71b7f17a5cbedc97345c31ceec374db2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Jan 2023 15:45:52 -1000 Subject: [PATCH 070/173] Bump httpx to 0.23.2 (#85023) changelogs: https://github.com/encode/httpcore/compare/0.16.2...0.16.3 https://github.com/encode/httpx/compare/0.23.1...0.23.2 --- homeassistant/package_constraints.txt | 4 ++-- pyproject.toml | 2 +- requirements.txt | 2 +- script/gen_requirements_all.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dee58b6e51e..05f5134f67f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.9.1 home-assistant-frontend==20230102.0 -httpx==0.23.1 +httpx==0.23.2 ifaddr==0.1.7 janus==1.0.0 jinja2==3.1.2 @@ -90,7 +90,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==3.6.2 h11==0.14.0 -httpcore==0.16.2 +httpcore==0.16.3 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation diff --git a/pyproject.toml b/pyproject.toml index 50bc0a06938..a8167f20e07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "ciso8601==2.3.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all - "httpx==0.23.1", + "httpx==0.23.2", "home-assistant-bluetooth==1.9.1", "ifaddr==0.1.7", "jinja2==3.1.2", diff --git a/requirements.txt b/requirements.txt index ea7119511e8..3e2e15d4883 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ awesomeversion==22.9.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.3.0 -httpx==0.23.1 +httpx==0.23.2 home-assistant-bluetooth==1.9.1 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d575b61375e..513960c2030 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -101,7 +101,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==3.6.2 h11==0.14.0 -httpcore==0.16.2 +httpcore==0.16.3 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation From 368ea0586d83d1b350305bd7a23fe6edd23ec16b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jan 2023 21:10:23 -0500 Subject: [PATCH 071/173] Bump slixmpp to 1.8.3 (#85031) --- homeassistant/components/xmpp/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 5fc8d6e50a2..4f11b83a55d 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,7 +2,7 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.8.2"], + "requirements": ["slixmpp==1.8.3"], "codeowners": ["@fabaff", "@flowolf"], "iot_class": "cloud_push", "loggers": ["pyasn1", "slixmpp"] diff --git a/requirements_all.txt b/requirements_all.txt index c488a0460e2..3ffe06f045b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2308,7 +2308,7 @@ sisyphus-control==3.1.2 slackclient==2.5.0 # homeassistant.components.xmpp -slixmpp==1.8.2 +slixmpp==1.8.3 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 From 9a1669103b7b68650bb3798b74abb84298f6bd1e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Jan 2023 16:10:39 -1000 Subject: [PATCH 072/173] Fix bluetooth not being loaded with esphome proxies when removed from default_config (#85032) * Fix bluetooth not being loaded with esphome proxies when removed from default_config fixes #84960 * actually commit the conftest change --- homeassistant/components/esphome/manifest.json | 3 ++- tests/components/esphome/conftest.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 83ebc60edd6..ce3dc116715 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -7,7 +7,8 @@ "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], - "after_dependencies": ["bluetooth", "zeroconf", "tag"], + "dependencies": ["bluetooth"], + "after_dependencies": ["zeroconf", "tag"], "iot_class": "local_push", "integration_type": "device", "loggers": ["aioesphomeapi", "noiseprotocol"] diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index cc4c8af9d73..3382e978a19 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -14,6 +14,11 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + @pytest.fixture(autouse=True) def esphome_mock_async_zeroconf(mock_async_zeroconf): """Auto mock zeroconf.""" From 9ecee11af65447e7a14d44c3966f6d529e0dbb50 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jan 2023 21:30:16 -0500 Subject: [PATCH 073/173] Bumped version to 2023.1.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 e5e8f40806b..6afffd28d8d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index a8167f20e07..e8d9c78ec19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.0b4" +version = "2023.1.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6ebf2ec9ec814fceb22449331d3d392123b1fd2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 31 Dec 2022 21:38:34 -1000 Subject: [PATCH 074/173] Fix failing HomeKit Controller diagnostics tests (#84936) --- .../homekit_controller/test_diagnostics.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index eecb7ff51f8..218347c4ad6 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -1,4 +1,6 @@ """Test homekit_controller diagnostics.""" +from unittest.mock import ANY + from aiohttp import ClientSession from homeassistant.components.homekit_controller.const import KNOWN_DEVICES @@ -247,8 +249,8 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "friendly_name": "Koogeek-LS1-20833F Identify" }, "entity_id": "button.koogeek_ls1_20833f_identify", - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", + "last_changed": ANY, + "last_updated": ANY, "state": "unknown", }, "unit_of_measurement": None, @@ -269,8 +271,8 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "supported_features": 0, }, "entity_id": "light.koogeek_ls1_20833f_light_strip", - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", + "last_changed": ANY, + "last_updated": ANY, "state": "off", }, "unit_of_measurement": None, @@ -518,8 +520,8 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "friendly_name": "Koogeek-LS1-20833F " "Identify" }, "entity_id": "button.koogeek_ls1_20833f_identify", - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", + "last_changed": ANY, + "last_updated": ANY, "state": "unknown", }, "unit_of_measurement": None, @@ -540,8 +542,8 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "supported_features": 0, }, "entity_id": "light.koogeek_ls1_20833f_light_strip", - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", + "last_changed": ANY, + "last_updated": ANY, "state": "off", }, "unit_of_measurement": None, From 6e9f0eca03f5107a4e27b336e2c002b51e92b2fa Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Tue, 3 Jan 2023 20:23:52 +0100 Subject: [PATCH 075/173] Fix integer only LCN variable values (#85035) --- homeassistant/components/lcn/manifest.json | 2 +- homeassistant/components/lcn/services.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index eea72a0e508..8a962db3514 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,7 +3,7 @@ "name": "LCN", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.15"], + "requirements": ["pypck==0.7.16"], "codeowners": ["@alengwenus"], "iot_class": "local_push", "loggers": ["pypck"] diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index 08dd5d711a8..ca07dbe0ef6 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -194,7 +194,7 @@ class VarAbs(LcnServiceCall): vol.Required(CONF_VARIABLE): vol.All( vol.Upper, vol.In(VARIABLES + SETPOINTS) ), - vol.Optional(CONF_VALUE, default=0): cv.positive_int, + vol.Optional(CONF_VALUE, default=0): vol.Coerce(float), vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All( vol.Upper, vol.In(VAR_UNITS) ), @@ -234,7 +234,7 @@ class VarRel(LcnServiceCall): vol.Required(CONF_VARIABLE): vol.All( vol.Upper, vol.In(VARIABLES + SETPOINTS + THRESHOLDS) ), - vol.Optional(CONF_VALUE, default=0): int, + vol.Optional(CONF_VALUE, default=0): vol.Coerce(float), vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All( vol.Upper, vol.In(VAR_UNITS) ), diff --git a/requirements_all.txt b/requirements_all.txt index 3ffe06f045b..b37e68ac87c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1832,7 +1832,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.15 +pypck==0.7.16 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84ec03e40e5..155924289cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1306,7 +1306,7 @@ pyowm==3.2.0 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.15 +pypck==0.7.16 # homeassistant.components.plaato pyplaato==0.0.18 From b80467dc5857a9907a32c0291a09496de2533d63 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Jan 2023 15:28:39 +0100 Subject: [PATCH 076/173] Update adguard to 0.6.1 (#85052) * Update adguard to 0.6.0 * Update adguard to 0.6.1 --- homeassistant/components/adguard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 91e1393c734..32d801fa6a6 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -3,7 +3,7 @@ "name": "AdGuard Home", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adguard", - "requirements": ["adguardhome==0.5.1"], + "requirements": ["adguardhome==0.6.1"], "codeowners": ["@frenck"], "iot_class": "local_polling", "integration_type": "service", diff --git a/requirements_all.txt b/requirements_all.txt index b37e68ac87c..f61885b58c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -86,7 +86,7 @@ adb-shell[async]==0.4.3 adext==0.4.2 # homeassistant.components.adguard -adguardhome==0.5.1 +adguardhome==0.6.1 # homeassistant.components.advantage_air advantage_air==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 155924289cd..975a7433c20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -76,7 +76,7 @@ adb-shell[async]==0.4.3 adext==0.4.2 # homeassistant.components.adguard -adguardhome==0.5.1 +adguardhome==0.6.1 # homeassistant.components.advantage_air advantage_air==0.4.1 From c8c68f05ecd21308addd0247412a57720ce042ff Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 3 Jan 2023 19:24:24 -0700 Subject: [PATCH 077/173] Remove workaround for reloading PurpleAir upon device removal (#85086) --- .../components/purpleair/__init__.py | 25 +---- .../components/purpleair/config_flow.py | 100 +++++++++--------- homeassistant/components/purpleair/const.py | 1 - .../components/purpleair/test_config_flow.py | 2 - 4 files changed, 52 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/purpleair/__init__.py b/homeassistant/components/purpleair/__init__.py index 7acdbf1fabd..c90f4c9031c 100644 --- a/homeassistant/components/purpleair/__init__.py +++ b/homeassistant/components/purpleair/__init__.py @@ -6,12 +6,10 @@ 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 -import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .config_flow import async_remove_sensor_by_device_id -from .const import CONF_LAST_UPDATE_SENSOR_ADD, DOMAIN +from .const import DOMAIN from .coordinator import PurpleAirDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] @@ -32,26 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_handle_entry_update(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle an options update.""" - if entry.options.get(CONF_LAST_UPDATE_SENSOR_ADD) is True: - # If the last options update was to add a sensor, we reload the config entry: - await hass.config_entries.async_reload(entry.entry_id) - - -async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry -) -> bool: - """Remove a config entry from a device.""" - new_entry_options = async_remove_sensor_by_device_id( - hass, - config_entry, - device_entry.id, - # remove_device is set to False because in this instance, the device has - # already been removed: - remove_device=False, - ) - return hass.config_entries.async_update_entry( - config_entry, options=new_entry_options - ) + await hass.config_entries.async_reload(entry.entry_id) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index 3b5720434b4..0b1be019350 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -1,6 +1,7 @@ """Config flow for PurpleAir integration.""" from __future__ import annotations +import asyncio from collections.abc import Mapping from copy import deepcopy from dataclasses import dataclass, field @@ -14,13 +15,15 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import ( aiohttp_client, config_validation as cv, device_registry as dr, + entity_registry as er, ) +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -28,7 +31,7 @@ from homeassistant.helpers.selector import ( SelectSelectorMode, ) -from .const import CONF_LAST_UPDATE_SENSOR_ADD, CONF_SENSOR_INDICES, DOMAIN, LOGGER +from .const import CONF_SENSOR_INDICES, DOMAIN, LOGGER CONF_DISTANCE = "distance" CONF_NEARBY_SENSOR_OPTIONS = "nearby_sensor_options" @@ -117,50 +120,6 @@ def async_get_remove_sensor_schema(sensors: list[SelectOptionDict]) -> vol.Schem ) -@callback -def async_get_sensor_index( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry -) -> int: - """Get the sensor index related to a config and device entry. - - Note that this method expects that there will always be a single sensor index per - DeviceEntry. - """ - [sensor_index] = [ - sensor_index - for sensor_index in config_entry.options[CONF_SENSOR_INDICES] - if (DOMAIN, str(sensor_index)) in device_entry.identifiers - ] - - return cast(int, sensor_index) - - -@callback -def async_remove_sensor_by_device_id( - hass: HomeAssistant, - config_entry: ConfigEntry, - device_id: str, - *, - remove_device: bool = True, -) -> dict[str, Any]: - """Remove a sensor and return update config entry options.""" - device_registry = dr.async_get(hass) - device_entry = device_registry.async_get(device_id) - assert device_entry - - removed_sensor_index = async_get_sensor_index(hass, config_entry, device_entry) - options = deepcopy({**config_entry.options}) - options[CONF_LAST_UPDATE_SENSOR_ADD] = False - options[CONF_SENSOR_INDICES].remove(removed_sensor_index) - - if remove_device: - device_registry.async_update_device( - device_entry.id, remove_config_entry_id=config_entry.entry_id - ) - - return options - - @dataclass class ValidationResult: """Define a validation result.""" @@ -407,7 +366,6 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): return self.async_abort(reason="already_configured") options = deepcopy({**self.config_entry.options}) - options[CONF_LAST_UPDATE_SENSOR_ADD] = True options[CONF_SENSOR_INDICES].append(sensor_index) return self.async_create_entry(title="", data=options) @@ -432,8 +390,50 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): ), ) - new_entry_options = async_remove_sensor_by_device_id( - self.hass, self.config_entry, user_input[CONF_SENSOR_DEVICE_ID] + device_registry = dr.async_get(self.hass) + entity_registry = er.async_get(self.hass) + + device_id = user_input[CONF_SENSOR_DEVICE_ID] + device_entry = cast(dr.DeviceEntry, device_registry.async_get(device_id)) + + # Determine the entity entries that belong to this device. + entity_entries = er.async_entries_for_device( + entity_registry, device_id, include_disabled_entities=True ) - return self.async_create_entry(title="", data=new_entry_options) + device_entities_removed_event = asyncio.Event() + + @callback + def async_device_entity_state_changed(_: Event) -> None: + """Listen and respond when all device entities are removed.""" + if all( + self.hass.states.get(entity_entry.entity_id) is None + for entity_entry in entity_entries + ): + device_entities_removed_event.set() + + # Track state changes for this device's entities and when they're removed, + # finish the flow: + cancel_state_track = async_track_state_change_event( + self.hass, + [entity_entry.entity_id for entity_entry in entity_entries], + async_device_entity_state_changed, + ) + device_registry.async_update_device( + device_id, remove_config_entry_id=self.config_entry.entry_id + ) + await device_entities_removed_event.wait() + + # Once we're done, we can cancel the state change tracker callback: + cancel_state_track() + + # Build new config entry options: + removed_sensor_index = next( + sensor_index + for sensor_index in self.config_entry.options[CONF_SENSOR_INDICES] + if (DOMAIN, str(sensor_index)) in device_entry.identifiers + ) + options = deepcopy({**self.config_entry.options}) + options[CONF_SENSOR_INDICES].remove(removed_sensor_index) + + return self.async_create_entry(title="", data=options) diff --git a/homeassistant/components/purpleair/const.py b/homeassistant/components/purpleair/const.py index 1de915e3545..60f51a9e7dd 100644 --- a/homeassistant/components/purpleair/const.py +++ b/homeassistant/components/purpleair/const.py @@ -5,6 +5,5 @@ DOMAIN = "purpleair" LOGGER = logging.getLogger(__package__) -CONF_LAST_UPDATE_SENSOR_ADD = "last_update_sensor_add" CONF_READ_KEY = "read_key" CONF_SENSOR_INDICES = "sensor_indices" diff --git a/tests/components/purpleair/test_config_flow.py b/tests/components/purpleair/test_config_flow.py index 4acb01aa305..2f4af57a3c5 100644 --- a/tests/components/purpleair/test_config_flow.py +++ b/tests/components/purpleair/test_config_flow.py @@ -205,7 +205,6 @@ async def test_options_add_sensor( ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { - "last_update_sensor_add": True, "sensor_indices": [123456, 567890], } @@ -265,7 +264,6 @@ async def test_options_remove_sensor(hass, config_entry, setup_purpleair): ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { - "last_update_sensor_add": False, "sensor_indices": [], } From 6108e581b186bf20499399520df4b80a2e3d250e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 4 Jan 2023 10:29:53 +0100 Subject: [PATCH 078/173] Do not reset current selection on reconfig or MQTT select (#85099) * Do not reset current selection on reconfig * Add a test --- homeassistant/components/mqtt/select.py | 2 +- tests/components/mqtt/test_common.py | 13 ++++++++++++ tests/components/mqtt/test_select.py | 27 ++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index d574cf081ba..ea8a20d1db5 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -115,6 +115,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): discovery_data: DiscoveryInfoType | None, ) -> None: """Initialize the MQTT select.""" + self._attr_current_option = None SelectEntity.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -125,7 +126,6 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" - self._attr_current_option = None self._optimistic = config[CONF_OPTIMISTIC] self._attr_options = config[CONF_OPTIONS] diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index e5880b981a2..9a5a01ec3fd 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -18,6 +18,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, ) +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.dispatcher import async_dispatcher_send @@ -1820,3 +1821,15 @@ async def help_test_unload_config_entry_with_platform( discovery_setup_entity = hass.states.get(f"{domain}.discovery_setup") assert discovery_setup_entity is None + + +async def help_test_discovery_setup( + hass: HomeAssistant, domain: str, discovery_data_payload: str, name: str +) -> None: + """Test setting up an MQTT entity using discovery.""" + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/{name}/config", discovery_data_payload + ) + await hass.async_block_till_done() + state = hass.states.get(f"{domain}.{name}") + assert state.state is not None diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index e82bd20aa2b..ddfd0074694 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -29,6 +29,7 @@ from .test_common import ( 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, @@ -455,7 +456,7 @@ async def test_discovery_update_select(hass, mqtt_mock_entry_no_yaml_config, cap "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", - "options": ["milk", "beer"], + "options": ["milk"], } await help_test_discovery_update( @@ -701,3 +702,27 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config ) + + +async def test_persistent_state_after_reconfig( + hass: ha.HomeAssistant, mqtt_mock_entry_no_yaml_config +) -> None: + """Test of the state is persistent after reconfiguring the select options.""" + await mqtt_mock_entry_no_yaml_config() + discovery_data = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' + await help_test_discovery_setup(hass, SELECT_DOMAIN, discovery_data, "milk") + + # assign an initial state + async_fire_mqtt_message(hass, "test-topic", "beer") + state = hass.states.get("select.milk") + assert state.state == "beer" + assert state.attributes["options"] == ["milk", "beer"] + + # remove "milk" option + discovery_data = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["beer"]}' + await help_test_discovery_setup(hass, SELECT_DOMAIN, discovery_data, "milk") + + # assert the state persistent + state = hass.states.get("select.milk") + assert state.state == "beer" + assert state.attributes["options"] == ["beer"] From 1b43323f5e92750c2ebc6635aa04c09481b5f486 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 4 Jan 2023 15:21:07 +0100 Subject: [PATCH 079/173] Allow MQTT device_class or state_class to be set as `None` (#85106) * Allow MQTT device_class to be set as `None` * Add test * Also allow sensor state_class to be `None` --- homeassistant/components/mqtt/binary_sensor.py | 2 +- homeassistant/components/mqtt/button.py | 4 ++-- homeassistant/components/mqtt/cover.py | 2 +- homeassistant/components/mqtt/number.py | 2 +- homeassistant/components/mqtt/sensor.py | 4 ++-- homeassistant/components/mqtt/switch.py | 2 +- homeassistant/components/mqtt/update.py | 2 +- tests/components/mqtt/test_sensor.py | 14 ++++++++++++++ 8 files changed, 23 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index f604bbbdd64..5ed9fdfb76f 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -59,7 +59,7 @@ CONF_EXPIRE_AFTER = "expire_after" PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 3188aa2ee0f..d50a06a46d8 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -6,7 +6,7 @@ import functools import voluptuous as vol from homeassistant.components import button -from homeassistant.components.button import ButtonEntity +from homeassistant.components.button import DEVICE_CLASSES_SCHEMA, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME from homeassistant.core import HomeAssistant @@ -39,7 +39,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_DEVICE_CLASS): button.DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 9d5f58eaa6d..e13b704436f 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -161,7 +161,7 @@ def validate_options(config: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_GET_POSITION_TOPIC): valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 8f33eea2c64..6ec26e9a904 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -87,7 +87,7 @@ def validate_config(config: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(NumberMode), diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index f05082849fc..dbb414921b5 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -98,13 +98,13 @@ def validate_options(conf: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( { - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_LAST_RESET_TOPIC): valid_subscribe_topic, vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + vol.Optional(CONF_STATE_CLASS): vol.Any(STATE_CLASSES_SCHEMA, None), vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 09e72955e63..7c06a587c1d 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -62,7 +62,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_STATE_OFF): cv.string, vol.Optional(CONF_STATE_ON): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 12d7723da3b..9ee0690b120 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -54,7 +54,7 @@ CONF_TITLE = "title" PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_ENTITY_PICTURE): cv.string, vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template, vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic, diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 750a6d79edd..3b586380fa0 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -690,6 +690,11 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): "device_class": "temperature", }, {"name": "Test 2", "state_topic": "test-topic"}, + { + "name": "Test 3", + "state_topic": "test-topic", + "device_class": None, + }, ] } }, @@ -701,6 +706,8 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes["device_class"] == "temperature" state = hass.states.get("sensor.test_2") assert "device_class" not in state.attributes + state = hass.states.get("sensor.test_3") + assert "device_class" not in state.attributes async def test_invalid_state_class(hass, mqtt_mock_entry_no_yaml_config): @@ -739,6 +746,11 @@ async def test_valid_state_class(hass, mqtt_mock_entry_with_yaml_config): "state_class": "measurement", }, {"name": "Test 2", "state_topic": "test-topic"}, + { + "name": "Test 3", + "state_topic": "test-topic", + "state_class": None, + }, ] } }, @@ -750,6 +762,8 @@ async def test_valid_state_class(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes["state_class"] == "measurement" state = hass.states.get("sensor.test_2") assert "state_class" not in state.attributes + state = hass.states.get("sensor.test_3") + assert "state_class" not in state.attributes async def test_setting_attribute_via_mqtt_json_message( From 0fd113db59499980946e032c475cedc66cc63575 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Jan 2023 14:38:17 +0100 Subject: [PATCH 080/173] Update frontend to 20230104.0 (#85107) --- 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 e8833517043..0091d5dcf98 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20230102.0"], + "requirements": ["home-assistant-frontend==20230104.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 05f5134f67f..75889538895 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ dbus-fast==1.82.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.9.1 -home-assistant-frontend==20230102.0 +home-assistant-frontend==20230104.0 httpx==0.23.2 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index f61885b58c7..f899d8373b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -888,7 +888,7 @@ hole==0.8.0 holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20230102.0 +home-assistant-frontend==20230104.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 975a7433c20..98494fc32e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -668,7 +668,7 @@ hole==0.8.0 holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20230102.0 +home-assistant-frontend==20230104.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 90ac0c870f5e77ef349a3982a621636448bc0eeb Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 4 Jan 2023 14:38:42 +0100 Subject: [PATCH 081/173] Remove illuminance device class for sensors in devolo Home Control (#85108) --- homeassistant/components/devolo_home_control/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 5009dbb8563..51a804df7ad 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -21,7 +21,6 @@ from .devolo_device import DevoloDeviceEntity DEVICE_CLASS_MAPPING = { "battery": SensorDeviceClass.BATTERY, "temperature": SensorDeviceClass.TEMPERATURE, - "light": SensorDeviceClass.ILLUMINANCE, "humidity": SensorDeviceClass.HUMIDITY, "current": SensorDeviceClass.POWER, "total": SensorDeviceClass.ENERGY, From ab2f05d3e9abc414450d5703ccbc1ed98ebbd5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 4 Jan 2023 14:36:41 +0100 Subject: [PATCH 082/173] Handle zone exception when setting up Cloudflare (#85110) --- homeassistant/components/cloudflare/__init__.py | 3 ++- tests/components/cloudflare/test_init.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 3a8f6b39ae7..9608347c8e7 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -10,6 +10,7 @@ from pycfdns.exceptions import ( CloudflareAuthenticationException, CloudflareConnectionException, CloudflareException, + CloudflareZoneException, ) from homeassistant.config_entries import ConfigEntry @@ -47,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: zone_id = await cfupdate.get_zone_id() except CloudflareAuthenticationException as error: raise ConfigEntryAuthFailed from error - except CloudflareConnectionException as error: + except (CloudflareConnectionException, CloudflareZoneException) as error: raise ConfigEntryNotReady from error async def update_records(now): diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index 6e7f450d711..c476b3ef376 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -4,6 +4,7 @@ from unittest.mock import patch from pycfdns.exceptions import ( CloudflareAuthenticationException, CloudflareConnectionException, + CloudflareZoneException, ) import pytest @@ -31,14 +32,21 @@ async def test_unload_entry(hass, cfupdate): assert not hass.data.get(DOMAIN) -async def test_async_setup_raises_entry_not_ready(hass, cfupdate): +@pytest.mark.parametrize( + "side_effect", + ( + CloudflareConnectionException(), + CloudflareZoneException(), + ), +) +async def test_async_setup_raises_entry_not_ready(hass, cfupdate, side_effect): """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" instance = cfupdate.return_value entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) entry.add_to_hass(hass) - instance.get_zone_id.side_effect = CloudflareConnectionException() + instance.get_zone_id.side_effect = side_effect await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY From 09b3611a6301dc4c3b8e783a556e0a4ae8f8a8ab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Jan 2023 17:05:21 +0100 Subject: [PATCH 083/173] Bumped version to 2023.1.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 6afffd28d8d..a606656f579 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "0b5" +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, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index e8d9c78ec19..d116e960cbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.0b5" +version = "2023.1.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6ba6991ecdcb983da3e818df1ad68432e8d1cb8e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Jan 2023 07:29:26 -1000 Subject: [PATCH 084/173] Bump home-assistant-bluetooth to 1.9.2 (#85123) --- 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 75889538895..c585c3c69e4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ cryptography==38.0.3 dbus-fast==1.82.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 -home-assistant-bluetooth==1.9.1 +home-assistant-bluetooth==1.9.2 home-assistant-frontend==20230104.0 httpx==0.23.2 ifaddr==0.1.7 diff --git a/pyproject.toml b/pyproject.toml index d116e960cbe..7d577d96705 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.23.2", - "home-assistant-bluetooth==1.9.1", + "home-assistant-bluetooth==1.9.2", "ifaddr==0.1.7", "jinja2==3.1.2", "lru-dict==1.1.8", diff --git a/requirements.txt b/requirements.txt index 3e2e15d4883..8438cedd87d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.3.0 httpx==0.23.2 -home-assistant-bluetooth==1.9.1 +home-assistant-bluetooth==1.9.2 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 From cc3c5772c5ef4512dc8e2982bacb917211bb9600 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 4 Jan 2023 19:47:10 +0100 Subject: [PATCH 085/173] Fix Z-Wave JS sensor units and device classes (#85129) fixes undefined --- .../zwave_js/discovery_data_template.py | 31 +-- .../components/zwave_js/manifest.json | 2 +- homeassistant/components/zwave_js/sensor.py | 178 +++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_sensor.py | 59 ++++-- 6 files changed, 214 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 5f43d802e30..7d9f20b111e 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -34,6 +34,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import ( PRESSURE_SENSORS, SIGNAL_STRENGTH_SENSORS, TEMPERATURE_SENSORS, + UNIT_A_WEIGHTED_DECIBELS, UNIT_AMPERE as SENSOR_UNIT_AMPERE, UNIT_BTU_H, UNIT_CELSIUS, @@ -52,6 +53,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import ( UNIT_INCHES_PER_HOUR, UNIT_KILOGRAM, UNIT_KILOHERTZ, + UNIT_KILOPASCAL, UNIT_LITER, UNIT_LUX, UNIT_M_S, @@ -69,6 +71,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import ( UNIT_RSSI, UNIT_SECOND, UNIT_SYSTOLIC, + UNIT_UV_INDEX, UNIT_VOLT as SENSOR_UNIT_VOLT, UNIT_WATT as SENSOR_UNIT_WATT, UNIT_WATT_PER_SQUARE_METER, @@ -94,8 +97,8 @@ from homeassistant.const import ( DEGREE, LIGHT_LUX, PERCENTAGE, - SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UV_INDEX, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, @@ -105,6 +108,7 @@ from homeassistant.const import ( UnitOfMass, UnitOfPower, UnitOfPressure, + UnitOfSoundPressure, UnitOfSpeed, UnitOfTemperature, UnitOfTime, @@ -134,7 +138,7 @@ from .const import ( ) from .helpers import ZwaveValueID -METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = { +METER_DEVICE_CLASS_MAP: dict[str, list[MeterScaleType]] = { ENTITY_DESC_KEY_CURRENT: CURRENT_METER_TYPES, ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_METER_TYPES, ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: ENERGY_TOTAL_INCREASING_METER_TYPES, @@ -142,7 +146,7 @@ METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = { ENTITY_DESC_KEY_POWER_FACTOR: POWER_FACTOR_METER_TYPES, } -MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = { +MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, list[MultilevelSensorType]] = { ENTITY_DESC_KEY_CO: CO_SENSORS, ENTITY_DESC_KEY_CO2: CO2_SENSORS, ENTITY_DESC_KEY_CURRENT: CURRENT_SENSORS, @@ -156,7 +160,7 @@ MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = { ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_SENSORS, } -METER_UNIT_MAP: dict[str, set[MeterScaleType]] = { +METER_UNIT_MAP: dict[str, list[MeterScaleType]] = { UnitOfElectricCurrent.AMPERE: METER_UNIT_AMPERE, UnitOfVolume.CUBIC_FEET: UNIT_CUBIC_FEET, UnitOfVolume.CUBIC_METERS: METER_UNIT_CUBIC_METER, @@ -166,7 +170,7 @@ METER_UNIT_MAP: dict[str, set[MeterScaleType]] = { UnitOfPower.WATT: METER_UNIT_WATT, } -MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { +MULTILEVEL_SENSOR_UNIT_MAP: dict[str, list[MultilevelSensorScaleType]] = { UnitOfElectricCurrent.AMPERE: SENSOR_UNIT_AMPERE, UnitOfPower.BTU_PER_HOUR: UNIT_BTU_H, UnitOfTemperature.CELSIUS: UNIT_CELSIUS, @@ -174,17 +178,19 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE: UNIT_CUBIC_FEET_PER_MINUTE, UnitOfVolume.CUBIC_METERS: SENSOR_UNIT_CUBIC_METER, UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: UNIT_CUBIC_METER_PER_HOUR, - SIGNAL_STRENGTH_DECIBELS: UNIT_DECIBEL, + UnitOfSoundPressure.DECIBEL: UNIT_DECIBEL, + UnitOfSoundPressure.WEIGHTED_DECIBEL_A: UNIT_A_WEIGHTED_DECIBELS, DEGREE: UNIT_DEGREES, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: { + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: [ *UNIT_DENSITY, *UNIT_MICROGRAM_PER_CUBIC_METER, - }, + ], UnitOfTemperature.FAHRENHEIT: UNIT_FAHRENHEIT, UnitOfLength.FEET: UNIT_FEET, UnitOfVolume.GALLONS: UNIT_GALLONS, UnitOfFrequency.HERTZ: UNIT_HERTZ, UnitOfPressure.INHG: UNIT_INCHES_OF_MERCURY, + UnitOfPressure.KPA: UNIT_KILOPASCAL, UnitOfVolumetricFlux.INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR, UnitOfMass.KILOGRAMS: UNIT_KILOGRAM, UnitOfFrequency.KILOHERTZ: UNIT_KILOHERTZ, @@ -197,7 +203,7 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { UnitOfSpeed.MILES_PER_HOUR: UNIT_MPH, UnitOfSpeed.METERS_PER_SECOND: UNIT_M_S, CONCENTRATION_PARTS_PER_MILLION: UNIT_PARTS_MILLION, - PERCENTAGE: {*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI}, + PERCENTAGE: [*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI], UnitOfMass.POUNDS: UNIT_POUNDS, UnitOfPressure.PSI: UNIT_POUND_PER_SQUARE_INCH, SIGNAL_STRENGTH_DECIBELS_MILLIWATT: UNIT_POWER_LEVEL, @@ -206,6 +212,7 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { UnitOfElectricPotential.VOLT: SENSOR_UNIT_VOLT, UnitOfPower.WATT: SENSOR_UNIT_WATT, UnitOfIrradiance.WATTS_PER_SQUARE_METER: UNIT_WATT_PER_SQUARE_METER, + UV_INDEX: UNIT_UV_INDEX, } _LOGGER = logging.getLogger(__name__) @@ -319,9 +326,9 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): enum_value: MultilevelSensorType | MultilevelSensorScaleType | MeterScaleType, set_map: Mapping[ str, - set[MultilevelSensorType] - | set[MultilevelSensorScaleType] - | set[MeterScaleType], + list[MultilevelSensorType] + | list[MultilevelSensorScaleType] + | list[MeterScaleType], ], ) -> str | None: """Find a key in a set map that matches a given enum value.""" diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 4dc86fed92c..9b313582eb5 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.43.1"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.44.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index c3414836ad8..0177d694ed4 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -24,6 +24,18 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfPressure, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform @@ -76,98 +88,207 @@ STATUS_ICON: dict[NodeStatus, str] = { } -ENTITY_DESCRIPTION_KEY_MAP: dict[str, SensorEntityDescription] = { - ENTITY_DESC_KEY_BATTERY: SensorEntityDescription( +# These descriptions should include device class. +ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[ + tuple[str, str], SensorEntityDescription +] = { + (ENTITY_DESC_KEY_BATTERY, PERCENTAGE): SensorEntityDescription( ENTITY_DESC_KEY_BATTERY, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, ), - ENTITY_DESC_KEY_CURRENT: SensorEntityDescription( + (ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription( ENTITY_DESC_KEY_CURRENT, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, ), - ENTITY_DESC_KEY_VOLTAGE: SensorEntityDescription( + (ENTITY_DESC_KEY_VOLTAGE, UnitOfElectricPotential.VOLT): SensorEntityDescription( ENTITY_DESC_KEY_VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, ), - ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription( - ENTITY_DESC_KEY_ENERGY_MEASUREMENT, - device_class=SensorDeviceClass.ENERGY, + ( + ENTITY_DESC_KEY_VOLTAGE, + UnitOfElectricPotential.MILLIVOLT, + ): SensorEntityDescription( + ENTITY_DESC_KEY_VOLTAGE, + device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, ), - ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: SensorEntityDescription( + ( + ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, + UnitOfEnergy.KILO_WATT_HOUR, + ): SensorEntityDescription( ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), - ENTITY_DESC_KEY_POWER: SensorEntityDescription( + (ENTITY_DESC_KEY_POWER, UnitOfPower.WATT): SensorEntityDescription( ENTITY_DESC_KEY_POWER, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, ), - ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription( + (ENTITY_DESC_KEY_POWER_FACTOR, PERCENTAGE): SensorEntityDescription( ENTITY_DESC_KEY_POWER_FACTOR, device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, ), - ENTITY_DESC_KEY_CO: SensorEntityDescription( + (ENTITY_DESC_KEY_CO, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription( ENTITY_DESC_KEY_CO, device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, ), - ENTITY_DESC_KEY_CO2: SensorEntityDescription( + (ENTITY_DESC_KEY_CO2, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription( ENTITY_DESC_KEY_CO2, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, ), - ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription( + (ENTITY_DESC_KEY_HUMIDITY, PERCENTAGE): SensorEntityDescription( ENTITY_DESC_KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, ), - ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription( + (ENTITY_DESC_KEY_ILLUMINANCE, LIGHT_LUX): SensorEntityDescription( ENTITY_DESC_KEY_ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, ), - ENTITY_DESC_KEY_PRESSURE: SensorEntityDescription( + (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.KPA): SensorEntityDescription( ENTITY_DESC_KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.KPA, ), - ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription( + (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.PSI): SensorEntityDescription( + ENTITY_DESC_KEY_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.PSI, + ), + (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.INHG): SensorEntityDescription( + ENTITY_DESC_KEY_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.INHG, + ), + (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.MMHG): SensorEntityDescription( + ENTITY_DESC_KEY_PRESSURE, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.MMHG, + ), + ( + ENTITY_DESC_KEY_SIGNAL_STRENGTH, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( ENTITY_DESC_KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), - ENTITY_DESC_KEY_TEMPERATURE: SensorEntityDescription( + (ENTITY_DESC_KEY_TEMPERATURE, UnitOfTemperature.CELSIUS): SensorEntityDescription( ENTITY_DESC_KEY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), - ENTITY_DESC_KEY_TARGET_TEMPERATURE: SensorEntityDescription( + ( + ENTITY_DESC_KEY_TEMPERATURE, + UnitOfTemperature.FAHRENHEIT, + ): SensorEntityDescription( + ENTITY_DESC_KEY_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + ), + ( + ENTITY_DESC_KEY_TARGET_TEMPERATURE, + UnitOfTemperature.CELSIUS, + ): SensorEntityDescription( ENTITY_DESC_KEY_TARGET_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, - state_class=None, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + ( + ENTITY_DESC_KEY_TARGET_TEMPERATURE, + UnitOfTemperature.FAHRENHEIT, + ): SensorEntityDescription( + ENTITY_DESC_KEY_TARGET_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + ), +} + +# These descriptions are without device class. +ENTITY_DESCRIPTION_KEY_MAP = { + ENTITY_DESC_KEY_CO: SensorEntityDescription( + ENTITY_DESC_KEY_CO, + state_class=SensorStateClass.MEASUREMENT, + ), + ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription( + ENTITY_DESC_KEY_ENERGY_MEASUREMENT, + state_class=SensorStateClass.MEASUREMENT, + ), + ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription( + ENTITY_DESC_KEY_HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription( + ENTITY_DESC_KEY_ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription( + ENTITY_DESC_KEY_POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), + ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription( + ENTITY_DESC_KEY_SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_MEASUREMENT: SensorEntityDescription( ENTITY_DESC_KEY_MEASUREMENT, - device_class=None, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_TOTAL_INCREASING: SensorEntityDescription( ENTITY_DESC_KEY_TOTAL_INCREASING, - device_class=None, state_class=SensorStateClass.TOTAL_INCREASING, ), } +def get_entity_description( + data: NumericSensorDataTemplateData, +) -> SensorEntityDescription: + """Return the entity description for the given data.""" + data_description_key = data.entity_description_key or "" + data_unit = data.unit_of_measurement or "" + return ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP.get( + (data_description_key, data_unit), + ENTITY_DESCRIPTION_KEY_MAP.get( + data_description_key, + SensorEntityDescription( + "base_sensor", native_unit_of_measurement=data.unit_of_measurement + ), + ), + ) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -187,9 +308,8 @@ async def async_setup_entry( data: NumericSensorDataTemplateData = info.platform_data else: data = NumericSensorDataTemplateData() - entity_description = ENTITY_DESCRIPTION_KEY_MAP.get( - data.entity_description_key or "", SensorEntityDescription("base_sensor") - ) + + entity_description = get_entity_description(data) if info.platform_hint == "string_sensor": entities.append( @@ -308,11 +428,9 @@ class ZWaveNumericSensor(ZwaveSensorBase): @callback def on_value_update(self) -> None: """Handle scale changes for this value on value updated event.""" - self._attr_native_unit_of_measurement = ( - NumericSensorDataTemplate() - .resolve_data(self.info.primary_value) - .unit_of_measurement - ) + data = NumericSensorDataTemplate().resolve_data(self.info.primary_value) + self.entity_description = get_entity_description(data) + self._attr_native_unit_of_measurement = data.unit_of_measurement @property def native_value(self) -> float: @@ -324,6 +442,8 @@ class ZWaveNumericSensor(ZwaveSensorBase): @property def native_unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" + if self.entity_description.native_unit_of_measurement is not None: + return self.entity_description.native_unit_of_measurement if self._attr_native_unit_of_measurement is not None: return self._attr_native_unit_of_measurement if self.info.primary_value.metadata.unit is None: diff --git a/requirements_all.txt b/requirements_all.txt index f899d8373b5..2777f4c5226 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2674,7 +2674,7 @@ zigpy==0.52.3 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.43.1 +zwave-js-server-python==0.44.0 # homeassistant.components.zwave_me zwave_me_ws==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98494fc32e6..75dd5bbf45d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1872,7 +1872,7 @@ zigpy-znp==0.9.2 zigpy==0.52.3 # homeassistant.components.zwave_js -zwave-js-server-python==0.43.1 +zwave-js-server-python==0.44.0 # homeassistant.components.zwave_me zwave_me_ws==0.3.0 diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index a32537b1d0d..59ca814a197 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -24,12 +24,13 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, + PERCENTAGE, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, ) from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -49,21 +50,25 @@ from .common import ( ) -async def test_numeric_sensor(hass, multisensor_6, integration): +async def test_numeric_sensor( + hass, multisensor_6, express_controls_ezmultipli, integration +): """Test the numeric sensor.""" state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert state assert state.state == "9.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT state = hass.states.get(BATTERY_SENSOR) assert state assert state.state == "100.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT ent_reg = er.async_get(hass) entity_entry = ent_reg.async_get(BATTERY_SENSOR) @@ -74,8 +79,27 @@ async def test_numeric_sensor(hass, multisensor_6, integration): assert state assert state.state == "65.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.HUMIDITY + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.multisensor_6_ultraviolet") + + assert state + assert state.state == "0.0" + # TODO: Add UV_INDEX unit of measurement to this sensor + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + # TODO: Add measurement state class to this sensor + assert ATTR_STATE_CLASS not in state.attributes + + state = hass.states.get("sensor.hsm200_illuminance") + + assert state + assert state.state == "61.0" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT async def test_energy_sensors(hass, hank_binary_switch, integration): @@ -84,7 +108,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state assert state.state == "0.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT @@ -92,7 +116,7 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state assert state.state == "0.16" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING @@ -100,14 +124,14 @@ async def test_energy_sensors(hass, hank_binary_switch, integration): assert state assert state.state == "122.96" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ELECTRIC_POTENTIAL_VOLT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfElectricPotential.VOLT assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.VOLTAGE state = hass.states.get(CURRENT_SENSOR) assert state assert state.state == "0.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ELECTRIC_CURRENT_AMPERE + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfElectricCurrent.AMPERE assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.CURRENT @@ -401,7 +425,8 @@ async def test_unit_change(hass, zp3111, client, integration): state = hass.states.get(entity_id) assert state assert state.state == "21.98" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE event = Event( "metadata updated", { @@ -431,7 +456,8 @@ async def test_unit_change(hass, zp3111, client, integration): state = hass.states.get(entity_id) assert state assert state.state == "21.98" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE event = Event( "value updated", { @@ -454,4 +480,5 @@ async def test_unit_change(hass, zp3111, client, integration): state = hass.states.get(entity_id) assert state assert state.state == "100.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE From 9c689d757c5a9ea2d45cb0ed54e2ff8db93ea19c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 5 Jan 2023 21:38:24 +0100 Subject: [PATCH 086/173] Limit calls in UniFi to write state (#85248) Limit calls to write state to when relevant --- homeassistant/components/unifi/entity.py | 10 ++++++++-- homeassistant/components/unifi/sensor.py | 12 ++++++++++-- homeassistant/components/unifi/switch.py | 15 ++++++++++++--- homeassistant/components/unifi/update.py | 8 +++++++- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index 79a80fad73c..0809e81fe54 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -65,6 +65,7 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): self.entity_description = description self._removed = False + self._write_state = False self._attr_available = description.available_fn(controller, obj_id) self._attr_device_info = description.device_info_fn(controller.api, obj_id) @@ -117,9 +118,14 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): self.hass.async_create_task(self.remove_item({self._obj_id})) return - self._attr_available = description.available_fn(self.controller, self._obj_id) + if ( + available := description.available_fn(self.controller, self._obj_id) + ) != self.available: + self._attr_available = available + self._write_state = True self.async_update_state(event, obj_id) - self.async_write_ha_state() + if self._write_state: + self.async_write_ha_state() @callback def async_signal_reachable_callback(self) -> None: diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index e320d1a0d4e..585221d05bb 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -253,11 +253,19 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]): self.hass.async_create_task(self.remove_item({self._obj_id})) return + update_state = False + obj = description.object_fn(self.controller.api, self._obj_id) if (value := description.value_fn(self.controller, obj)) != self.native_value: self._attr_native_value = value - self._attr_available = description.available_fn(self.controller, self._obj_id) - self.async_write_ha_state() + update_state = True + if ( + available := description.available_fn(self.controller, self._obj_id) + ) != self.available: + self._attr_available = available + update_state = True + if update_state: + self.async_write_ha_state() @callback def async_signal_reachable_callback(self) -> None: diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 44007e4c1a8..1d83effba36 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -410,11 +410,20 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]): self.hass.async_create_task(self.remove_item({self._obj_id})) return + update_state = False + if not description.only_event_for_state_change: obj = description.object_fn(self.controller.api, self._obj_id) - self._attr_is_on = description.is_on_fn(self.controller.api, obj) - self._attr_available = description.available_fn(self.controller, self._obj_id) - self.async_write_ha_state() + if (is_on := description.is_on_fn(self.controller.api, obj)) != self.is_on: + self._attr_is_on = is_on + update_state = True + if ( + available := description.available_fn(self.controller, self._obj_id) + ) != self.available: + self._attr_available = available + update_state = True + if update_state: + self.async_write_ha_state() @callback def async_signal_reachable_callback(self) -> None: diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index 6cff6b7932d..0810cbb780c 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -163,6 +163,12 @@ class UnifiDeviceUpdateEntity(UnifiEntity[HandlerT, DataT], UpdateEntity): description = self.entity_description obj = description.object_fn(self.controller.api, self._obj_id) - self._attr_in_progress = description.state_fn(self.controller.api, obj) + if ( + in_progress := description.state_fn(self.controller.api, obj) + ) != self.in_progress: + self._attr_in_progress = in_progress + self._write_state = True self._attr_installed_version = obj.version self._attr_latest_version = obj.upgrade_to_firmware or obj.version + if self.installed_version != self.latest_version: + self._write_state = True From 8bf2299407a948c7e3becf4650a0ce28920ec221 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 6 Jan 2023 03:49:22 +0100 Subject: [PATCH 087/173] Only subscribe to relevant IDs for state updates (#85252) Make sure to only subscribe to the relevant ID --- homeassistant/components/unifi/sensor.py | 1 + homeassistant/components/unifi/switch.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 585221d05bb..e017ff4ed0e 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -217,6 +217,7 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]): self.async_on_remove( handler.subscribe( self.async_signalling_callback, + id_filter=self._obj_id, ) ) self.async_on_remove( diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 1d83effba36..b0f042c78cd 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -361,6 +361,7 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]): self.async_on_remove( handler.subscribe( self.async_signalling_callback, + id_filter=self._obj_id, ) ) self.async_on_remove( From dcd07d313598b1c781da381372e6f1551a87c22b Mon Sep 17 00:00:00 2001 From: William Scanlon <6432770+w1ll1am23@users.noreply.github.com> Date: Wed, 4 Jan 2023 21:09:54 -0500 Subject: [PATCH 088/173] Bump pyeconet to 0.1.18 to fix energy usage (#85094) --- homeassistant/components/econet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 19455d8dffb..8aed197cf4c 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -3,7 +3,7 @@ "name": "Rheem EcoNet Products", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/econet", - "requirements": ["pyeconet==0.1.17"], + "requirements": ["pyeconet==0.1.18"], "codeowners": ["@vangorra", "@w1ll1am23"], "iot_class": "cloud_push", "loggers": ["paho_mqtt", "pyeconet"] diff --git a/requirements_all.txt b/requirements_all.txt index 2777f4c5226..cf138171729 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1560,7 +1560,7 @@ pydroid-ipcam==2.0.0 pyebox==1.1.4 # homeassistant.components.econet -pyeconet==0.1.17 +pyeconet==0.1.18 # homeassistant.components.edimax pyedimax==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75dd5bbf45d..941538838b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1106,7 +1106,7 @@ pydexcom==0.2.3 pydroid-ipcam==2.0.0 # homeassistant.components.econet -pyeconet==0.1.17 +pyeconet==0.1.18 # homeassistant.components.efergy pyefergy==22.1.1 From fe89b663e77f7a46fd2921f3e9c3d41ddda80487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 5 Jan 2023 23:45:29 +0100 Subject: [PATCH 089/173] Fix lacrosse_view fetching of latest data (#85117) lacrosse_view: fixed fetching of latest data When using datetime.utcnow(), it only replaces timezone information with UTC making the actual time offset by the timezone. When you are in UTC- timezones, it makes no issue as the offset is in the future, but when in UTC+, the last hour(s) of data are missing. This commits swtiches to time.time() as UTC timestamp is actually what the API expects. It also reduces the window to one hour what noticeably improves the API performance. --- .../components/lacrosse_view/coordinator.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/lacrosse_view/coordinator.py b/homeassistant/components/lacrosse_view/coordinator.py index 5361f94d04f..8dcbd8a2e5e 100644 --- a/homeassistant/components/lacrosse_view/coordinator.py +++ b/homeassistant/components/lacrosse_view/coordinator.py @@ -1,7 +1,8 @@ """DataUpdateCoordinator for LaCrosse View.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta +from time import time from lacrosse_view import HTTPError, LaCrosse, Location, LoginError, Sensor @@ -30,7 +31,7 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): ) -> None: """Initialize DataUpdateCoordinator for LaCrosse View.""" self.api = api - self.last_update = datetime.utcnow() + self.last_update = time() self.username = entry.data["username"] self.password = entry.data["password"] self.hass = hass @@ -45,26 +46,22 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): async def _async_update_data(self) -> list[Sensor]: """Get the data for LaCrosse View.""" - now = datetime.utcnow() + now = int(time()) - if self.last_update < now - timedelta(minutes=59): # Get new token + if self.last_update < now - 59 * 60: # Get new token once in a hour self.last_update = now try: await self.api.login(self.username, self.password) except LoginError as error: raise ConfigEntryAuthFailed from error - # Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request) - yesterday = now - timedelta(days=1) - yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0) - yesterday_timestamp = datetime.timestamp(yesterday) - try: + # Fetch last hour of data sensors = await self.api.get_sensors( location=Location(id=self.id, name=self.name), tz=self.hass.config.time_zone, - start=str(int(yesterday_timestamp)), - end=str(int(datetime.timestamp(now))), + start=str(now - 3600), + end=str(now), ) except HTTPError as error: raise ConfigEntryNotReady from error From 563ad02c65264edeefddf941d4934bb28e828cac Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 5 Jan 2023 03:05:46 +0100 Subject: [PATCH 090/173] Bump bthome-ble to 2.4.1 (#85153) fix https://github.com/home-assistant/core/issues/85142 fixes undefined --- homeassistant/components/bthome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 7a879608fc4..1be63f5f486 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -17,7 +17,7 @@ "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==2.4.0"], + "requirements": ["bthome-ble==2.4.1"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index cf138171729..7792d48739d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -488,7 +488,7 @@ brunt==1.2.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==2.4.0 +bthome-ble==2.4.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 941538838b0..8b9827a2cd3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -392,7 +392,7 @@ brother==2.1.1 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==2.4.0 +bthome-ble==2.4.1 # homeassistant.components.buienradar buienradar==1.0.5 From 3c2b7c0d6951908f35fa7f33ff76a2f38a39b12e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 Jan 2023 11:24:38 +0100 Subject: [PATCH 091/173] Bump hatasmota to 0.6.2 (#85182) --- homeassistant/components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 6e3e69f59fe..df01f719cec 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.6.1"], + "requirements": ["hatasmota==0.6.2"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 4b84bb8f86a..74402a51586 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, PERCENTAGE, + POWER_VOLT_AMPERE_REACTIVE, SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UnitOfApparentPower, @@ -217,8 +218,10 @@ SENSOR_UNIT_MAP = { hc.LIGHT_LUX: LIGHT_LUX, hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS, hc.PERCENTAGE: PERCENTAGE, + hc.POWER_FACTOR: None, hc.POWER_WATT: UnitOfPower.WATT, hc.PRESSURE_HPA: UnitOfPressure.HPA, + hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE, hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS, hc.SIGNAL_STRENGTH_DECIBELS_MILLIWATT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT, hc.SPEED_KILOMETERS_PER_HOUR: UnitOfSpeed.KILOMETERS_PER_HOUR, diff --git a/requirements_all.txt b/requirements_all.txt index 7792d48739d..682b8781edf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -858,7 +858,7 @@ hass-nabucasa==0.61.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.6.1 +hatasmota==0.6.2 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b9827a2cd3..c5be8d9c6d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -647,7 +647,7 @@ habitipy==0.2.0 hass-nabucasa==0.61.0 # homeassistant.components.tasmota -hatasmota==0.6.1 +hatasmota==0.6.2 # homeassistant.components.jewish_calendar hdate==0.10.4 From 8034faadcad20b570f7b167696368e5fd29941ba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 5 Jan 2023 17:02:17 +0100 Subject: [PATCH 092/173] Remove invalid AQI unit from Environment Canada (#85183) --- homeassistant/components/environment_canada/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index d3848086bf5..e7eceb8dadc 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -228,7 +228,6 @@ AQHI_SENSOR = ECSensorEntityDescription( key="aqhi", name="AQHI", device_class=SensorDeviceClass.AQI, - native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, value_fn=_get_aqhi_value, ) From d73b86132b61e686944e12f36537b661ea71356f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 5 Jan 2023 13:00:46 +0100 Subject: [PATCH 093/173] Adjust valid energy units (#85190) --- homeassistant/components/energy/sensor.py | 8 ++++---- homeassistant/components/energy/validate.py | 15 ++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 1509eb10afe..5ad4c74a6cf 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -41,20 +41,20 @@ SUPPORTED_STATE_CLASSES = { SensorStateClass.TOTAL_INCREASING, } VALID_ENERGY_UNITS: set[str] = { - UnitOfEnergy.WATT_HOUR, + UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, - UnitOfEnergy.GIGA_JOULE, + UnitOfEnergy.WATT_HOUR, } VALID_ENERGY_UNITS_GAS = { - UnitOfVolume.CUBIC_FEET, UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, *VALID_ENERGY_UNITS, } VALID_VOLUME_UNITS_WATER: set[str] = { - UnitOfVolume.CUBIC_FEET, UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, UnitOfVolume.GALLONS, UnitOfVolume.LITERS, diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index ea799fcdf06..55d11f5f04d 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -22,10 +22,10 @@ from .const import DOMAIN ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,) ENERGY_USAGE_UNITS = { sensor.SensorDeviceClass.ENERGY: ( + UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.WATT_HOUR, - UnitOfEnergy.GIGA_JOULE, ) } ENERGY_PRICE_UNITS = tuple( @@ -39,12 +39,16 @@ GAS_USAGE_DEVICE_CLASSES = ( ) GAS_USAGE_UNITS = { sensor.SensorDeviceClass.ENERGY: ( - UnitOfEnergy.WATT_HOUR, + UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, - UnitOfEnergy.GIGA_JOULE, + UnitOfEnergy.WATT_HOUR, + ), + sensor.SensorDeviceClass.GAS: ( + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, ), - sensor.SensorDeviceClass.GAS: (UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET), } GAS_PRICE_UNITS = tuple( f"/{unit}" for units in GAS_USAGE_UNITS.values() for unit in units @@ -54,8 +58,9 @@ GAS_PRICE_UNIT_ERROR = "entity_unexpected_unit_gas_price" WATER_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.WATER,) WATER_USAGE_UNITS = { sensor.SensorDeviceClass.WATER: ( - UnitOfVolume.CUBIC_METERS, + UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, UnitOfVolume.GALLONS, UnitOfVolume.LITERS, ), From 26ea02aa8ffe7f382bcd75c77a65a71eaf3439d1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 5 Jan 2023 21:30:52 +0100 Subject: [PATCH 094/173] Remove invalid device class for RSSI sensors (#85191) * Remove invalid device class for RRSI sensors * Restore state class --- homeassistant/components/zha/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index a66f3aa1fe8..ff95b66d101 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -755,7 +755,6 @@ class RSSISensor(Sensor, id_suffix="rssi"): """RSSI sensor for a device.""" _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False _attr_should_poll = True # BaseZhaEntity defaults to False From 59d6f827c368ffb4298c1b27d128f9b64d88dc10 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 5 Jan 2023 20:28:13 +0100 Subject: [PATCH 095/173] Fix device class for DSMR gas sensors providing energy readings (#85202) --- homeassistant/components/dsmr/sensor.py | 16 +++++++ tests/components/dsmr/test_sensor.py | 55 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 36f734273f2..a77577302d5 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -28,6 +28,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + UnitOfEnergy, UnitOfVolume, ) from homeassistant.core import CoreState, Event, HomeAssistant, callback @@ -591,6 +592,21 @@ class DSMREntity(SensorEntity): """Entity is only available if there is a telegram.""" return self.telegram is not None + @property + def device_class(self) -> SensorDeviceClass | None: + """Return the device class of this entity.""" + device_class = super().device_class + + # Override device class for gas sensors providing energy units, like + # kWh, MWh, GJ, etc. In those cases, the class should be energy, not gas + with suppress(ValueError): + if device_class == SensorDeviceClass.GAS and UnitOfEnergy( + str(self.native_unit_of_measurement) + ): + return SensorDeviceClass.ENERGY + + return device_class + @property def native_value(self) -> StateType: """Return the state of sensor, if available, translate if needed.""" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index ee0ffa5db5f..ac4b9587ec7 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -26,6 +26,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, VOLUME_CUBIC_METERS, + UnitOfEnergy, UnitOfPower, ) from homeassistant.helpers import entity_registry as er @@ -804,3 +805,57 @@ async def test_reconnect(hass, dsmr_connection_fixture): await hass.config_entries.async_unload(mock_entry.entry_id) assert mock_entry.state == config_entries.ConfigEntryState.NOT_LOADED + + +async def test_gas_meter_providing_energy_reading(hass, dsmr_connection_fixture): + """Test that gas providing energy readings use the correct device class.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.obis_references import GAS_METER_READING + from dsmr_parser.objects import MBusObject + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + } + entry_options = { + "time_between_update": 0, + } + + telegram = { + GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(123.456), "unit": UnitOfEnergy.GIGA_JOULE}, + ] + ), + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + telegram_callback(telegram) + await asyncio.sleep(0) + + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") + assert gas_consumption.state == "123.456" + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.GIGA_JOULE + ) From fa4c2500015cc1f78e589a15242ed298d03e3404 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Jan 2023 10:29:13 -1000 Subject: [PATCH 096/173] Improve error reporting when switchbot auth fails (#85244) * Improve error reporting when switchbot auth fails related issue #85243 * bump * coverage --- homeassistant/components/switchbot/config_flow.py | 6 +++++- homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/strings.json | 2 +- .../components/switchbot/translations/en.json | 15 ++------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switchbot/test_config_flow.py | 3 ++- 7 files changed, 13 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 6ba0e463718..933d8ac3c56 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -166,6 +166,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the SwitchBot API auth step.""" errors = {} assert self._discovered_adv is not None + description_placeholders = {} if user_input is not None: try: key_details = await self.hass.async_add_executor_job( @@ -176,8 +177,10 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): ) except SwitchbotAccountConnectionError as ex: raise AbortFlow("cannot_connect") from ex - except SwitchbotAuthenticationError: + except SwitchbotAuthenticationError as ex: + _LOGGER.debug("Authentication failed: %s", ex, exc_info=True) errors = {"base": "auth_failed"} + description_placeholders = {"error_detail": str(ex)} else: return await self.async_step_lock_key(key_details) @@ -195,6 +198,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): ), description_placeholders={ "name": name_from_discovery(self._discovered_adv), + **description_placeholders, }, ) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index c7c50e5cf6e..c38573f82ca 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.36.1"], + "requirements": ["PySwitchbot==0.36.2"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 08fd960334a..c25769bee41 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -40,7 +40,7 @@ }, "error": { "encryption_key_invalid": "Key ID or Encryption key is invalid", - "auth_failed": "Authentication failed" + "auth_failed": "Authentication failed: {error_detail}" }, "abort": { "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index b5658e58d6b..d7065138051 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -8,7 +8,7 @@ "unknown": "Unexpected error" }, "error": { - "auth_failed": "Authentication failed", + "auth_failed": "Authentication failed: {error_detail}", "encryption_key_invalid": "Key ID or Encryption key is invalid" }, "flow_title": "{name} ({address})", @@ -47,18 +47,7 @@ "data": { "address": "Device address" } - }, - "lock_key": { - "description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation.", - "data": { - "key_id": "Key ID", - "encryption_key": "Encryption key" } - } - }, - "error": { - "key_id_invalid": "Key ID or Encryption key is invalid", - "encryption_key_invalid": "Key ID or Encryption key is invalid" } }, "options": { @@ -70,4 +59,4 @@ } } } -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 682b8781edf..217d9dddc0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.1 +PySwitchbot==0.36.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5be8d9c6d9..7f856633ed7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.1 +PySwitchbot==0.36.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 1a3db48f192..a8cccbeb31d 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -481,7 +481,7 @@ async def test_user_setup_wolock_auth(hass): with patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", - side_effect=SwitchbotAuthenticationError, + side_effect=SwitchbotAuthenticationError("error from api"), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -494,6 +494,7 @@ async def test_user_setup_wolock_auth(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {"base": "auth_failed"} + assert "error from api" in result["description_placeholders"]["error_detail"] with patch_async_setup_entry() as mock_setup_entry, patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", From ee88f34a91b9caad6624a0c9b1ba963cb248eb29 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 5 Jan 2023 22:06:40 +0100 Subject: [PATCH 097/173] bump reolink-aio to 0.1.2 (#85247) --- 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 2c0deafca45..9ea4422203b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.1.1"], + "requirements": ["reolink-aio==0.1.2"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", "loggers": ["reolink-aio"] diff --git a/requirements_all.txt b/requirements_all.txt index 217d9dddc0b..477825e0822 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2190,7 +2190,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.1 +reolink-aio==0.1.2 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f856633ed7..e7ef6e1483f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1529,7 +1529,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.1 +reolink-aio==0.1.2 # homeassistant.components.python_script restrictedpython==5.2 From edfd83c3a70cc810e2c5da18d0cfd7b28ed27920 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 6 Jan 2023 03:05:37 +0100 Subject: [PATCH 098/173] Bump bimmer_connected to 0.12.0 (#85255) * Bump bimmer_connected to 0.12.0 * Fix mypy * Remove not needed code Co-authored-by: rikroe --- .../bmw_connected_drive/device_tracker.py | 2 ++ .../bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/bmw_connected_drive/__init__.py | 18 ------------------ 5 files changed, 5 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index c94d9b5b678..12d29736183 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -69,6 +69,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity): return ( self.vehicle.vehicle_location.location[0] if self.vehicle.is_vehicle_tracking_enabled + and self.vehicle.vehicle_location.location else None ) @@ -78,6 +79,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity): return ( self.vehicle.vehicle_location.location[1] if self.vehicle.is_vehicle_tracking_enabled + and self.vehicle.vehicle_location.location else None ) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 98b6861fd49..c03bdf6a26f 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.10.4"], + "requirements": ["bimmer_connected==0.12.0"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 477825e0822..20c80512c2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ beautifulsoup4==4.11.1 bellows==0.34.5 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.4 +bimmer_connected==0.12.0 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7ef6e1483f..f1bbc843baa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,7 +349,7 @@ beautifulsoup4==4.11.1 bellows==0.34.5 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.4 +bimmer_connected==0.12.0 # homeassistant.components.bluetooth bleak-retry-connector==2.13.0 diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index c2bb65b3fa7..81b3bb9fff3 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -4,7 +4,6 @@ import json from pathlib import Path from bimmer_connected.account import MyBMWAccount -from bimmer_connected.api.utils import log_to_to_file from homeassistant import config_entries from homeassistant.components.bmw_connected_drive.const import ( @@ -64,15 +63,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None: } fetched_at = utcnow() - # simulate storing fingerprints - if account.config.log_response_path: - for brand in ["bmw", "mini"]: - log_to_to_file( - json.dumps(vehicles[brand]), - account.config.log_response_path, - f"vehicles_v2_{brand}", - ) - # Create a vehicle with base + specific state as provided by state/VIN API for vehicle_base in [vehicle for brand in vehicles.values() for vehicle in brand]: vehicle_state_path = ( @@ -93,14 +83,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None: fetched_at, ) - # simulate storing fingerprints - if account.config.log_response_path: - log_to_to_file( - json.dumps(vehicle_state), - account.config.log_response_path, - f"state_{vehicle_base['vin']}", - ) - async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: """Mock a fully setup config entry and all components based on fixtures.""" From 2840821594fda614c2f076315bbdb3b5d3f7179e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 Jan 2023 13:44:10 -1000 Subject: [PATCH 099/173] Reject the WiFI AP when considering to update a shelly config entry from zeroconf (#85265) Reject the WiFI AP IP when considering to update a shelly config entry from zeroconf fixes #85180 --- .../components/shelly/config_flow.py | 14 +++++++++- tests/components/shelly/test_config_flow.py | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 8679edf5382..70d2c2492e8 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -53,6 +53,8 @@ BLE_SCANNER_OPTIONS = [ selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"), ] +INTERNAL_WIFI_AP_IP = "192.168.33.1" + async def validate_input( hass: HomeAssistant, @@ -217,7 +219,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): current_entry := await self.async_set_unique_id(mac) ) and current_entry.data[CONF_HOST] == host: await async_reconnect_soon(self.hass, current_entry) - self._abort_if_unique_id_configured({CONF_HOST: host}) + if host == INTERNAL_WIFI_AP_IP: + # If the device is broadcasting the internal wifi ap ip + # we can't connect to it, so we should not update the + # entry with the new host as it will be unreachable + # + # This is a workaround for a bug in the firmware 0.12 (and older?) + # which should be removed once the firmware is fixed + # and the old version is no longer in use + self._abort_if_unique_id_configured() + else: + self._abort_if_unique_id_configured({CONF_HOST: host}) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 6795049a207..1c0a32853e1 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Shelly config flow.""" from __future__ import annotations +from dataclasses import replace from unittest.mock import AsyncMock, Mock, patch from aioshelly.exceptions import ( @@ -12,6 +13,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf +from homeassistant.components.shelly import config_flow from homeassistant.components.shelly.const import ( CONF_BLE_SCANNER_MODE, DOMAIN, @@ -704,6 +706,30 @@ async def test_zeroconf_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_zeroconf_with_wifi_ap_ip(hass): + """Test we ignore the Wi-FI AP IP.""" + + entry = MockConfigEntry( + domain="shelly", unique_id="test-mac", data={"host": "2.2.2.2"} + ) + entry.add_to_hass(hass) + + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=replace(DISCOVERY_INFO, host=config_flow.INTERNAL_WIFI_AP_IP), + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Test config entry was not updated with the wifi ap ip + assert entry.data["host"] == "2.2.2.2" + + async def test_zeroconf_firmware_unsupported(hass): """Test we abort if device firmware is unsupported.""" with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported): From f0f2c12d91f7d3b71b98f18652d418c3343b1e53 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 5 Jan 2023 22:10:41 -0500 Subject: [PATCH 100/173] Fix Fully Kiosk service call config entry handling (#85275) * Make sure we're getting the fully_kiosk config entry * Make sure we're getting the fully_kiosk config entry --- .../components/fully_kiosk/services.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fully_kiosk/services.py b/homeassistant/components/fully_kiosk/services.py index 3d63bf4f23c..de269af891a 100644 --- a/homeassistant/components/fully_kiosk/services.py +++ b/homeassistant/components/fully_kiosk/services.py @@ -47,10 +47,19 @@ async def async_setup_services(hass: HomeAssistant) -> None: for target in call.data[ATTR_DEVICE_ID]: device = registry.async_get(target) if device: - coordinator = hass.data[DOMAIN][list(device.config_entries)[0]] - # fully_method(coordinator.fully, *args, **kwargs) would make - # test_services.py fail. - await getattr(coordinator.fully, fully_method.__name__)(*args, **kwargs) + for key in device.config_entries: + entry = hass.config_entries.async_get_entry(key) + if not entry: + continue + if entry.domain != DOMAIN: + continue + coordinator = hass.data[DOMAIN][key] + # fully_method(coordinator.fully, *args, **kwargs) would make + # test_services.py fail. + await getattr(coordinator.fully, fully_method.__name__)( + *args, **kwargs + ) + break async def async_load_url(call: ServiceCall) -> None: """Load a URL on the Fully Kiosk Browser.""" From 33bb9c230bef79c8563d36f489c31863677c03af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Jan 2023 22:24:17 -0500 Subject: [PATCH 101/173] Bumped version to 2023.1.1 --- 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 a606656f579..7d65c6b44a3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 7d577d96705..4bb8a5f2911 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.0" +version = "2023.1.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 7e6b0877738fd4112eb6bb85c01d51cbd0a6ca8f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 6 Jan 2023 09:27:02 +0100 Subject: [PATCH 102/173] Allow SensorDeviceClass.POWER_FACTOR unit None (#85287) Co-authored-by: Erik Montnemery --- homeassistant/components/number/__init__.py | 2 +- homeassistant/components/sensor/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index de2580eab75..dfb923a8d01 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -213,7 +213,7 @@ class NumberDeviceClass(StrEnum): POWER_FACTOR = "power_factor" """Power factor. - Unit of measurement: `%` + Unit of measurement: `%`, `None` """ POWER = "power" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 7beac83f059..5a96036f22b 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -309,7 +309,7 @@ class SensorDeviceClass(StrEnum): POWER_FACTOR = "power_factor" """Power factor. - Unit of measurement: `%` + Unit of measurement: `%`, `None` """ POWER = "power" @@ -521,7 +521,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.POWER_FACTOR: {PERCENTAGE}, + SensorDeviceClass.POWER_FACTOR: {PERCENTAGE, None}, SensorDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT}, SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), From 7396bcc585dd00a72733c4d56516c8844de70d29 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 7 Jan 2023 04:46:33 -0500 Subject: [PATCH 103/173] Retry ZHA config entry setup when `ENETUNREACH` is caught (#84615) * The config entry is not ready on `ENETUNREACH` * Use new `TransientConnectionError` from zigpy --- homeassistant/components/zha/core/gateway.py | 4 ++++ tests/components/zha/test_gateway.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 6a02a21781d..ffd005e8edc 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -17,6 +17,7 @@ from zigpy.application import ControllerApplication from zigpy.config import CONF_DEVICE import zigpy.device import zigpy.endpoint +import zigpy.exceptions import zigpy.group from zigpy.types.named import EUI64 @@ -24,6 +25,7 @@ from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.system_log import LogEntry, _figure_out_source 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.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo @@ -172,6 +174,8 @@ class ZHAGateway: self.application_controller = await app_controller_cls.new( app_config, auto_form=True, start_radio=True ) + except zigpy.exceptions.TransientConnectionError as exc: + raise ConfigEntryNotReady from exc except Exception as exc: # pylint: disable=broad-except _LOGGER.warning( "Couldn't start %s coordinator (attempt %s of %s)", diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index bc49b04d86a..4eb95a8441b 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -3,12 +3,14 @@ import asyncio from unittest.mock import AsyncMock, MagicMock, patch import pytest +import zigpy.exceptions import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting from homeassistant.components.zha.core.group import GroupMember from homeassistant.const import Platform +from homeassistant.exceptions import ConfigEntryNotReady from .common import async_find_group_entity_id, get_zha_gateway from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -259,3 +261,20 @@ async def test_gateway_initialize_failure(hass, device_light_1, coordinator): await zha_gateway.async_initialize() assert mock_new.call_count == 3 + + +@patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) +async def test_gateway_initialize_failure_transient(hass, device_light_1, coordinator): + """Test ZHA failing to initialize the gateway but with a transient error.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + + with patch( + "bellows.zigbee.application.ControllerApplication.new", + side_effect=[RuntimeError(), zigpy.exceptions.TransientConnectionError()], + ) as mock_new: + with pytest.raises(ConfigEntryNotReady): + await zha_gateway.async_initialize() + + # Initialization immediately stops and is retried after TransientConnectionError + assert mock_new.call_count == 2 From 764550f2e1044adedaea0e35048ba625894e35c9 Mon Sep 17 00:00:00 2001 From: Tom Puttemans Date: Fri, 6 Jan 2023 12:55:32 +0100 Subject: [PATCH 104/173] Fix dsmr_reader peak hour consumption unit of measurement (#85301) --- homeassistant/components/dsmr_reader/definitions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 0721819e312..cc0c851ebda 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -560,8 +560,8 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( DSMRReaderSensorEntityDescription( key="dsmr/consumption/quarter-hour-peak-electricity/average_delivered", name="Previous quarter-hour peak usage", - device_class=SensorDeviceClass.ENERGY, - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.KILO_WATT, ), DSMRReaderSensorEntityDescription( key="dsmr/consumption/quarter-hour-peak-electricity/read_at_start", From a781fcca86b5cc14d63b2a1de65f7b3d052de3a7 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 6 Jan 2023 20:52:24 +0100 Subject: [PATCH 105/173] Bump reolink-aio to 0.1.3 (#85309) bump reolink-aio to 0.1.3 --- 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 9ea4422203b..dfa8dfe8e6b 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.1.2"], + "requirements": ["reolink-aio==0.1.3"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", "loggers": ["reolink-aio"] diff --git a/requirements_all.txt b/requirements_all.txt index 20c80512c2d..ed8319848a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2190,7 +2190,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.2 +reolink-aio==0.1.3 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1bbc843baa..b80218d903e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1529,7 +1529,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.2 +reolink-aio==0.1.3 # homeassistant.components.python_script restrictedpython==5.2 From cf06f3b81d3b0c7dbd022ed9b69983d8a0803df7 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sat, 7 Jan 2023 05:53:04 -0600 Subject: [PATCH 106/173] Bump life360 package to 5.5.0 (#85322) Improve debug output & redact sensitive info from log. Fix bug that was masking some HTTP errors. Retry HTTP errors 502, 503 & 504, which have been observed to happen every once in a while, resulting in fewer unnecessary unavailable states. --- 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 eb3290e41e1..9fc13b1998e 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/life360", "codeowners": ["@pnbruckner"], - "requirements": ["life360==5.3.0"], + "requirements": ["life360==5.5.0"], "iot_class": "cloud_polling", "loggers": ["life360"] } diff --git a/requirements_all.txt b/requirements_all.txt index ed8319848a3..b5fb06a0b9b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1029,7 +1029,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.3.0 +life360==5.5.0 # homeassistant.components.osramlightify lightify==1.0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b80218d903e..d62e27dfc1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -767,7 +767,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.3.0 +life360==5.5.0 # homeassistant.components.logi_circle logi_circle==0.2.3 From 00e563f1b8f696e2b1fffc853c38e1d11d0a4d85 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 8 Jan 2023 00:14:25 +0100 Subject: [PATCH 107/173] Switch play pause method in philips js (#85343) fixes undefined --- homeassistant/components/philips_js/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index d9ea06c2f2c..684dafbc750 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -210,7 +210,7 @@ class PhilipsTVMediaPlayer( async def async_media_play_pause(self) -> None: """Send pause command to media player.""" if self._tv.quirk_playpause_spacebar: - await self._tv.sendUnicode(" ") + await self._tv.sendKey("Confirm") else: await self._tv.sendKey("PlayPause") await self._async_update_soon() @@ -509,6 +509,8 @@ class PhilipsTVMediaPlayer( self._media_title = self._sources.get(self._tv.source_id) self._media_channel = None + self._attr_assumed_state = True + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" From b30d4ef7cfd5ce88c5202f0ae6a3d57780102ef5 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 6 Jan 2023 23:01:36 -0500 Subject: [PATCH 108/173] Bump ZHA dependencies (#85355) * Bump ZHA dependencies * Deprecated `foundation.Command` -> `foundation.GeneralCommand` --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/zha/common.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 7f8568f1ab3..9adddc97720 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.34.5", + "bellows==0.34.6", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.90", "zigpy-deconz==0.19.2", - "zigpy==0.52.3", + "zigpy==0.53.0", "zigpy-xbee==0.16.2", "zigpy-zigate==0.10.3", "zigpy-znp==0.9.2" diff --git a/requirements_all.txt b/requirements_all.txt index b5fb06a0b9b..fd2522266ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -419,7 +419,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.34.5 +bellows==0.34.6 # homeassistant.components.bmw_connected_drive bimmer_connected==0.12.0 @@ -2668,7 +2668,7 @@ zigpy-zigate==0.10.3 zigpy-znp==0.9.2 # homeassistant.components.zha -zigpy==0.52.3 +zigpy==0.53.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d62e27dfc1e..e4ec396c893 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -346,7 +346,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.34.5 +bellows==0.34.6 # homeassistant.components.bmw_connected_drive bimmer_connected==0.12.0 @@ -1869,7 +1869,7 @@ zigpy-zigate==0.10.3 zigpy-znp==0.9.2 # homeassistant.components.zha -zigpy==0.52.3 +zigpy==0.53.0 # homeassistant.components.zwave_js zwave-js-server-python==0.44.0 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index a6a533acc71..53935caa435 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -77,7 +77,7 @@ def update_attribute_cache(cluster): attrid = zigpy.types.uint16_t(attrid) attrs.append(make_attribute(attrid, value)) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.GeneralCommand.Report_Attributes) hdr.frame_control.disable_default_response = True msg = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Report_Attributes].schema( attribute_reports=attrs From e5ba423d6d8926fb486d32bdfe450a6faa81a25d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Jan 2023 19:51:20 -1000 Subject: [PATCH 109/173] Add note to SwitchBot locks that usernames are case sensitive (#85359) --- homeassistant/components/switchbot/strings.json | 2 +- homeassistant/components/switchbot/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index c25769bee41..d263aed3dd5 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -24,7 +24,7 @@ } }, "lock_auth": { - "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key.", + "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key. Usernames and passwords are case sensitive.", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index d7065138051..fed02f12a38 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -21,7 +21,7 @@ "password": "Password", "username": "Username" }, - "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key." + "description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key. Usernames and passwords are case sensitive." }, "lock_choose_method": { "description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.", From 5ff7b3bb1a2e747d2a192b2c2fac2ddcb751f67d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Jan 2023 19:50:50 -1000 Subject: [PATCH 110/173] Bump pySwitchbot to 0.36.3 (#85360) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index c38573f82ca..b543e7f15e7 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.36.2"], + "requirements": ["PySwitchbot==0.36.3"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index fd2522266ef..b70d4c740b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.2 +PySwitchbot==0.36.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4ec396c893..503a98100e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.2 +PySwitchbot==0.36.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 85c9f9facfd7a28e4faed5ec3eecf07cc2467715 Mon Sep 17 00:00:00 2001 From: Lutz Lengemann Date: Sun, 8 Jan 2023 13:23:33 +0100 Subject: [PATCH 111/173] Increase Hydrawise default scan interval (#85398) Increasing default scan interval Fixes #83540 --- homeassistant/components/hydrawise/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index da413cce5ab..7074f86e4a8 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -28,7 +28,7 @@ DATA_HYDRAWISE = "hydrawise" DOMAIN = "hydrawise" DEFAULT_WATERING_TIME = 15 -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=120) SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update" From 10cb2e31c4b74016cf984280f735e9321e69ea27 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 7 Jan 2023 15:28:37 -0800 Subject: [PATCH 112/173] Bump ical to 4.2.9 (#85401) --- 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 b277611dfdb..2d70700facb 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -3,7 +3,7 @@ "name": "Local Calendar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_calendar", - "requirements": ["ical==4.2.8"], + "requirements": ["ical==4.2.9"], "codeowners": ["@allenporter"], "iot_class": "local_polling", "loggers": ["ical"] diff --git a/requirements_all.txt b/requirements_all.txt index b70d4c740b6..528b03ea48f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -930,7 +930,7 @@ ibm-watson==5.2.2 ibmiotf==0.3.4 # homeassistant.components.local_calendar -ical==4.2.8 +ical==4.2.9 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 503a98100e9..dd8f0bf1cee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -695,7 +695,7 @@ iaqualink==0.5.0 ibeacon_ble==1.0.1 # homeassistant.components.local_calendar -ical==4.2.8 +ical==4.2.9 # homeassistant.components.ping icmplib==3.0 From caf15534bb6c4a085aff1d7a08a524e9f1aa3e43 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 8 Jan 2023 15:35:23 -0800 Subject: [PATCH 113/173] Bump gcal_sync to 4.1.1 (#85453) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 100e128c8e3..9d2d96812a5 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==4.1.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.1.1", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 528b03ea48f..d6cadbfd26a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -744,7 +744,7 @@ gTTS==2.2.4 gassist-text==0.0.7 # homeassistant.components.google -gcal-sync==4.1.0 +gcal-sync==4.1.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd8f0bf1cee..315fc975e5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -560,7 +560,7 @@ gTTS==2.2.4 gassist-text==0.0.7 # homeassistant.components.google -gcal-sync==4.1.0 +gcal-sync==4.1.1 # homeassistant.components.geocaching geocachingapi==0.2.1 From 834847988d4d17a716598133c65c4ad49e805f06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 Jan 2023 20:24:25 -0500 Subject: [PATCH 114/173] Bumped version to 2023.1.2 --- 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 7d65c6b44a3..5aab387caa8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 4bb8a5f2911..5c6450bd8e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.1" +version = "2023.1.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 940b5d62b4a2c30e0a154e119e3e9cac895e59ff Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 8 Jan 2023 21:59:21 -0500 Subject: [PATCH 115/173] Bump pyunifiprotect to 4.6.0 (#85483) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index c7259356b66..e30818bd42f 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -4,7 +4,7 @@ "integration_type": "hub", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.5.2", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.6.0", "unifi-discovery==1.1.7"], "dependencies": ["http", "repairs"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index d6cadbfd26a..8cf864fb8dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2109,7 +2109,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.5.2 +pyunifiprotect==4.6.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 315fc975e5b..df66cd543c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1475,7 +1475,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.5.2 +pyunifiprotect==4.6.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From c73830439f52a9c9d75443c94680682d9ac174df Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 11:39:20 +0100 Subject: [PATCH 116/173] Remove invalid Signal Strength device class from NETGEAR (#85510) --- homeassistant/components/netgear/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 510b97c37e3..e5529361a75 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -58,7 +58,6 @@ SENSOR_TYPES = { key="signal", name="signal strength", native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, ), "ssid": SensorEntityDescription( From 9f9cdb62eb3ca34f2ef41873c737c16194b904a4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 9 Jan 2023 12:15:36 +0100 Subject: [PATCH 117/173] Restore Netgear signal strength icon (#85512) --- homeassistant/components/netgear/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index e5529361a75..9b3b3b23e33 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -59,6 +59,7 @@ SENSOR_TYPES = { name="signal strength", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:wifi", ), "ssid": SensorEntityDescription( key="ssid", From c844276e9547e4d83cc8a35cd6b40be3143e6383 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 9 Jan 2023 23:59:17 +0100 Subject: [PATCH 118/173] Remove invalid state class in Subaru sensor (#85520) --- homeassistant/components/subaru/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index 046c3b4393e..c5b2b86fda4 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -131,7 +131,6 @@ EV_SENSORS = [ key=sc.EV_TIME_TO_FULLY_CHARGED_UTC, device_class=SensorDeviceClass.TIMESTAMP, name="EV time to full charge", - state_class=SensorStateClass.MEASUREMENT, ), ] From 731ca046f6db2345aaf1d5513d2d94cba09b62c7 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Mon, 9 Jan 2023 11:35:15 -0500 Subject: [PATCH 119/173] Bump sense_energy to 0.11.1 (#85533) fixes undefined --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index 86e8f2fc2ca..723cad2b792 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_kasa", "name": "Emulated Kasa", "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", - "requirements": ["sense_energy==0.11.0"], + "requirements": ["sense_energy==0.11.1"], "codeowners": ["@kbickar"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 158ef7cae61..424ae34b16d 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.11.0"], + "requirements": ["sense_energy==0.11.1"], "codeowners": ["@kbickar"], "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8cf864fb8dd..0bf39083223 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2269,7 +2269,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.11.0 +sense_energy==0.11.1 # homeassistant.components.sensirion_ble sensirion-ble==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df66cd543c8..63ecbb82fde 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1578,7 +1578,7 @@ securetar==2022.2.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.11.0 +sense_energy==0.11.1 # homeassistant.components.sensirion_ble sensirion-ble==0.0.1 From 1a042c2dadcb2f7b664de44bb4a97742fdc31d91 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 9 Jan 2023 17:00:21 -0500 Subject: [PATCH 120/173] Bump pyunifiprotect to 4.6.1 (#85547) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 +- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/media_source.py | 2 +- homeassistant/components/unifiprotect/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/conftest.py | 1 + 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index bf53dc8d206..4a3b76581ba 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -337,7 +337,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( name="Doorbell", device_class=BinarySensorDeviceClass.OCCUPANCY, icon="mdi:doorbell-video", - ufp_required_field="feature_flags.has_chime", + ufp_required_field="feature_flags.is_doorbell", ufp_value="is_ringing", ufp_event_obj="last_ring_event", ), diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index e30818bd42f..de622497a3d 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -4,7 +4,7 @@ "integration_type": "hub", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.6.0", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.6.1", "unifi-discovery==1.1.7"], "dependencies": ["http", "repairs"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 81054d9aff5..a064295bebb 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -770,7 +770,7 @@ class ProtectMediaSource(MediaSource): if camera is None: raise BrowseError(f"Unknown Camera ID: {camera_id}") name = camera.name or camera.market_name or camera.type - is_doorbell = camera.feature_flags.has_chime + is_doorbell = camera.feature_flags.is_doorbell has_smart = camera.feature_flags.has_smart_detect thumbnail_url: str | None = None diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 1851b1ea776..4be8b489de9 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -206,7 +206,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( name="Last Doorbell Ring", device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:doorbell-video", - ufp_required_field="feature_flags.has_chime", + ufp_required_field="feature_flags.is_doorbell", ufp_value="last_ring", entity_registry_enabled_default=False, ), diff --git a/requirements_all.txt b/requirements_all.txt index 0bf39083223..199b74cd4ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2109,7 +2109,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.6.0 +pyunifiprotect==4.6.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63ecbb82fde..3705d250c73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1475,7 +1475,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.6.0 +pyunifiprotect==4.6.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 77aa9622f9e..ea270e28fcc 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -214,6 +214,7 @@ def doorbell_fixture(camera: Camera, fixed_now: datetime): doorbell.feature_flags.has_lcd_screen = True doorbell.feature_flags.has_speaker = True doorbell.feature_flags.has_privacy_mask = True + doorbell.feature_flags.is_doorbell = True doorbell.feature_flags.has_chime = True doorbell.feature_flags.has_smart_detect = True doorbell.feature_flags.has_package_camera = True From e7babb4266575e40b091b74e199687d18a18064e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jan 2023 12:16:14 -1000 Subject: [PATCH 121/173] Do not check ble scanner state for sleepy shelly devices (#85566) fixes #85563 --- homeassistant/components/shelly/coordinator.py | 3 ++- tests/components/shelly/test_init.py | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index aeb3dcf7ccf..12a46ee3ef9 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -509,7 +509,8 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): This will be executed on connect or when the config entry is updated. """ - await self._async_connect_ble_scanner() + if not self.entry.data.get(CONF_SLEEP_PERIOD): + await self._async_connect_ble_scanner() async def _async_connect_ble_scanner(self) -> None: """Connect BLE scanner.""" diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 0697c6fb613..3675186b9ba 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -213,7 +213,7 @@ async def test_entry_unload_not_connected(hass, mock_rpc_device, monkeypatch): assert entry.state is ConfigEntryState.LOADED -async def test_entry_unload_not_connected_but_we_with_we_are( +async def test_entry_unload_not_connected_but_we_think_we_are( hass, mock_rpc_device, monkeypatch ): """Test entry unload when not connected but we think we are still connected.""" @@ -238,3 +238,17 @@ async def test_entry_unload_not_connected_but_we_with_we_are( assert not mock_stop_scanner.call_count assert entry.state is ConfigEntryState.LOADED + + +async def test_no_attempt_to_stop_scanner_with_sleepy_devices(hass, mock_rpc_device): + """Test we do not try to stop the scanner if its disabled with a sleepy device.""" + with patch( + "homeassistant.components.shelly.coordinator.async_stop_scanner", + ) as mock_stop_scanner: + entry = await init_integration(hass, 2, sleep_period=7200) + assert entry.state is ConfigEntryState.LOADED + assert not mock_stop_scanner.call_count + + mock_rpc_device.mock_update() + await hass.async_block_till_done() + assert not mock_stop_scanner.call_count From ebab2bd0f97f620aa2de3514dbe5520647000b28 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 10 Jan 2023 01:48:39 -0700 Subject: [PATCH 122/173] Remove no-longer-needed invalid API key monitor for OpenUV (#85573) * Remove no-longer-needed invalid API key monitor for OpenUV * Handle re-auth cancellation * Use automatic API status check --- homeassistant/components/openuv/__init__.py | 12 +-- .../components/openuv/config_flow.py | 1 - .../components/openuv/coordinator.py | 81 ++++--------------- homeassistant/components/openuv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 21 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 3e65f33d8c5..cb8d1bffceb 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -31,7 +31,7 @@ from .const import ( DOMAIN, LOGGER, ) -from .coordinator import InvalidApiKeyMonitor, OpenUvCoordinator +from .coordinator import OpenUvCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -45,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data.get(CONF_LONGITUDE, hass.config.longitude), altitude=entry.data.get(CONF_ELEVATION, hass.config.elevation), session=websession, + check_status_before_request=True, ) async def async_update_protection_data() -> dict[str, Any]: @@ -53,16 +54,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: high = entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW) return await client.uv_protection_window(low=low, high=high) - invalid_api_key_monitor = InvalidApiKeyMonitor(hass, entry) - coordinators: dict[str, OpenUvCoordinator] = { coordinator_name: OpenUvCoordinator( hass, + entry=entry, name=coordinator_name, latitude=client.latitude, longitude=client.longitude, update_method=update_method, - invalid_api_key_monitor=invalid_api_key_monitor, ) for coordinator_name, update_method in ( (DATA_UV, client.uv_index), @@ -70,16 +69,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) } - # We disable the client's request retry abilities here to avoid a lengthy (and - # blocking) startup; then, if the initial update is successful, we re-enable client - # request retries: - client.disable_request_retries() init_tasks = [ coordinator.async_config_entry_first_refresh() for coordinator in coordinators.values() ] await asyncio.gather(*init_tasks) - client.enable_request_retries() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinators diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 3951a6ffb08..d78fa84c8c5 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -103,7 +103,6 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Verify the credentials and create/re-auth the entry.""" websession = aiohttp_client.async_get_clientsession(self.hass) client = Client(data.api_key, 0, 0, session=websession) - client.disable_request_retries() try: await client.uv_index() diff --git a/homeassistant/components/openuv/coordinator.py b/homeassistant/components/openuv/coordinator.py index f89a9c696a8..7472f213f82 100644 --- a/homeassistant/components/openuv/coordinator.py +++ b/homeassistant/components/openuv/coordinator.py @@ -1,15 +1,14 @@ """Define an update coordinator for OpenUV.""" from __future__ import annotations -import asyncio from collections.abc import Awaitable, Callable from typing import Any, cast from pyopenuv.errors import InvalidApiKeyError, OpenUvError from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -18,64 +17,6 @@ from .const import LOGGER DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60 -class InvalidApiKeyMonitor: - """Define a monitor for failed API calls (due to bad keys) across coordinators.""" - - DEFAULT_FAILED_API_CALL_THRESHOLD = 5 - - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: - """Initialize.""" - self._count = 1 - self._lock = asyncio.Lock() - self._reauth_flow_manager = ReauthFlowManager(hass, entry) - self.entry = entry - - async def async_increment(self) -> None: - """Increment the counter.""" - async with self._lock: - self._count += 1 - if self._count > self.DEFAULT_FAILED_API_CALL_THRESHOLD: - LOGGER.info("Starting reauth after multiple failed API calls") - self._reauth_flow_manager.start_reauth() - - async def async_reset(self) -> None: - """Reset the counter.""" - async with self._lock: - self._count = 0 - self._reauth_flow_manager.cancel_reauth() - - -class ReauthFlowManager: - """Define an OpenUV reauth flow manager.""" - - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: - """Initialize.""" - self.entry = entry - self.hass = hass - - @callback - def _get_active_reauth_flow(self) -> FlowResult | None: - """Get an active reauth flow (if it exists).""" - return next( - iter(self.entry.async_get_active_flows(self.hass, {SOURCE_REAUTH})), - None, - ) - - @callback - def cancel_reauth(self) -> None: - """Cancel a reauth flow (if appropriate).""" - if reauth_flow := self._get_active_reauth_flow(): - LOGGER.debug("API seems to have recovered; canceling reauth flow") - self.hass.config_entries.flow.async_abort(reauth_flow["flow_id"]) - - @callback - def start_reauth(self) -> None: - """Start a reauth flow (if appropriate).""" - if not self._get_active_reauth_flow(): - LOGGER.debug("Multiple API failures in a row; starting reauth flow") - self.entry.async_start_reauth(self.hass) - - class OpenUvCoordinator(DataUpdateCoordinator): """Define an OpenUV data coordinator.""" @@ -86,11 +27,11 @@ class OpenUvCoordinator(DataUpdateCoordinator): self, hass: HomeAssistant, *, + entry: ConfigEntry, name: str, latitude: str, longitude: str, update_method: Callable[[], Awaitable[dict[str, Any]]], - invalid_api_key_monitor: InvalidApiKeyMonitor, ) -> None: """Initialize.""" super().__init__( @@ -106,7 +47,7 @@ class OpenUvCoordinator(DataUpdateCoordinator): ), ) - self._invalid_api_key_monitor = invalid_api_key_monitor + self._entry = entry self.latitude = latitude self.longitude = longitude @@ -115,10 +56,18 @@ class OpenUvCoordinator(DataUpdateCoordinator): try: data = await self.update_method() except InvalidApiKeyError as err: - await self._invalid_api_key_monitor.async_increment() - raise UpdateFailed(str(err)) from err + raise ConfigEntryAuthFailed("Invalid API key") from err except OpenUvError as err: raise UpdateFailed(str(err)) from err - await self._invalid_api_key_monitor.async_reset() + # OpenUV uses HTTP 403 to indicate both an invalid API key and an API key that + # has hit its daily/monthly limit; both cases will result in a reauth flow. If + # coordinator update succeeds after a reauth flow has been started, terminate + # it: + if reauth_flow := next( + iter(self._entry.async_get_active_flows(self.hass, {SOURCE_REAUTH})), + None, + ): + self.hass.config_entries.flow.async_abort(reauth_flow["flow_id"]) + return cast(dict[str, Any], data["result"]) diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index 5e89f495b03..ef367b94dac 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -3,7 +3,7 @@ "name": "OpenUV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openuv", - "requirements": ["pyopenuv==2022.04.0"], + "requirements": ["pyopenuv==2023.01.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyopenuv"], diff --git a/requirements_all.txt b/requirements_all.txt index 199b74cd4ad..884677bd758 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1803,7 +1803,7 @@ pyoctoprintapi==0.1.9 pyombi==0.1.10 # homeassistant.components.openuv -pyopenuv==2022.04.0 +pyopenuv==2023.01.0 # homeassistant.components.opnsense pyopnsense==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3705d250c73..422fb154345 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1283,7 +1283,7 @@ pynzbgetapi==0.2.0 pyoctoprintapi==0.1.9 # homeassistant.components.openuv -pyopenuv==2022.04.0 +pyopenuv==2023.01.0 # homeassistant.components.opnsense pyopnsense==0.2.0 From b5d1421dfdf8871e90a00e05c34d0b04f00a29c4 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 10 Jan 2023 10:46:36 +0200 Subject: [PATCH 123/173] Bump aioshelly to 5.2.1 to fix Task exception was never retrieved (#85575) Bump aioshelly to 5.2.1 --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 911b0cf8c7c..b28218c3cfa 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==5.2.0"], + "requirements": ["aioshelly==5.2.1"], "dependencies": ["bluetooth", "http"], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 884677bd758..e4941f8a993 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.2.0 +aioshelly==5.2.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 422fb154345..1c1e3490ff8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.2.0 +aioshelly==5.2.1 # homeassistant.components.skybell aioskybell==22.7.0 From 62a003a0531a2e3b68a82aa19fbcab4c9592bb0e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 10 Jan 2023 10:15:55 -0800 Subject: [PATCH 124/173] Bump gcal-sync to 4.1.2 (#85631) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 9d2d96812a5..596a64f5eef 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==4.1.1", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.1.2", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index e4941f8a993..88c7962a02b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -744,7 +744,7 @@ gTTS==2.2.4 gassist-text==0.0.7 # homeassistant.components.google -gcal-sync==4.1.1 +gcal-sync==4.1.2 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c1e3490ff8..5a6cecf7e16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -560,7 +560,7 @@ gTTS==2.2.4 gassist-text==0.0.7 # homeassistant.components.google -gcal-sync==4.1.1 +gcal-sync==4.1.2 # homeassistant.components.geocaching geocachingapi==0.2.1 From fe7d32dc5dd92ac36fbb0407213aea26a0913950 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 10 Jan 2023 19:48:06 +0100 Subject: [PATCH 125/173] Bump hatasmota to 0.6.3 (#85633) --- homeassistant/components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index df01f719cec..4541e43dd31 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.6.2"], + "requirements": ["hatasmota==0.6.3"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 88c7962a02b..ebe405ba71d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -858,7 +858,7 @@ hass-nabucasa==0.61.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.6.2 +hatasmota==0.6.3 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a6cecf7e16..002a0ee1e3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -647,7 +647,7 @@ habitipy==0.2.0 hass-nabucasa==0.61.0 # homeassistant.components.tasmota -hatasmota==0.6.2 +hatasmota==0.6.3 # homeassistant.components.jewish_calendar hdate==0.10.4 From e47364f34d2e33daaf39935d870a948ffa45b0bc Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 10 Jan 2023 20:28:12 +0100 Subject: [PATCH 126/173] Update frontend to 20230110.0 (#85640) --- 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 0091d5dcf98..b940afead24 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20230104.0"], + "requirements": ["home-assistant-frontend==20230110.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c585c3c69e4..717d2f3e9a4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ dbus-fast==1.82.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230104.0 +home-assistant-frontend==20230110.0 httpx==0.23.2 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index ebe405ba71d..111afd8cf3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -888,7 +888,7 @@ hole==0.8.0 holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20230104.0 +home-assistant-frontend==20230110.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 002a0ee1e3e..0845fd0913b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -668,7 +668,7 @@ hole==0.8.0 holidays==0.17.2 # homeassistant.components.frontend -home-assistant-frontend==20230104.0 +home-assistant-frontend==20230110.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 4bdf87d383bef1b08e1c424dadf2923fa2458768 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 10 Jan 2023 14:52:06 -0500 Subject: [PATCH 127/173] Bumped version to 2023.1.3 --- 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 5aab387caa8..d7bd1fece49 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 5c6450bd8e5..e61e5b6e824 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.2" +version = "2023.1.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6df4fc67080050236a5a83897b42d011723e7278 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Thu, 12 Jan 2023 00:43:04 +0100 Subject: [PATCH 128/173] Upgrade aionanoleaf to 0.2.1 (#83669) --- homeassistant/components/nanoleaf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 1ce3210a206..8cc690f4fc0 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -3,7 +3,7 @@ "name": "Nanoleaf", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nanoleaf", - "requirements": ["aionanoleaf==0.2.0"], + "requirements": ["aionanoleaf==0.2.1"], "zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."], "homekit": { "models": ["NL29", "NL42", "NL47", "NL48", "NL52", "NL59"] diff --git a/requirements_all.txt b/requirements_all.txt index 111afd8cf3c..a391b7f1b3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,7 +220,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.14.4 # homeassistant.components.nanoleaf -aionanoleaf==0.2.0 +aionanoleaf==0.2.1 # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0845fd0913b..ad63662ba8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -198,7 +198,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.14.4 # homeassistant.components.nanoleaf -aionanoleaf==0.2.0 +aionanoleaf==0.2.1 # homeassistant.components.notion aionotion==3.0.2 From c3859f91706a7fdbbded2fc3505f5149cf572b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 12 Jan 2023 07:47:38 +0200 Subject: [PATCH 129/173] Improve Huawei LTE SSDP inclusion (#85572) * Probe Huawei LTE API for device support on SSDP match More or less as expected, the loosening of SSDP/UPnP data matches done in #81643 started to yield false positives, as in #85402. Coming up with robust matches solely based on the SSDP/UPnP data still does not seem possible, so keep the matches as loose as they were made, but additionally invoke a probe request on the API to determine if the device looks like a supported one. * Probe only after unique id checks Prevents throwaway probes for discoveries already in progress. * Fix SSDP result URL test, add missing assert on it --- .../components/huawei_lte/config_flow.py | 18 +++++++++ .../components/huawei_lte/strings.json | 3 +- .../huawei_lte/translations/en.json | 4 +- .../components/huawei_lte/test_config_flow.py | 37 ++++++++++++++++--- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index d1ab0547801..2319cc9a3ed 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -250,6 +250,24 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured(updates={CONF_URL: url}) + def _is_supported_device() -> bool: + """ + See if we are looking at a possibly supported device. + + Matching solely on SSDP data does not yield reliable enough results. + """ + try: + with Connection(url=url, timeout=CONNECTION_TIMEOUT) as conn: + basic_info = Client(conn).device.basic_information() + except ResponseErrorException: # API compatible error + return True + except Exception: # API incompatible error # pylint: disable=broad-except + return False + return isinstance(basic_info, dict) # Crude content check + + if not await self.hass.async_add_executor_job(_is_supported_device): + return self.async_abort(reason="unsupported_device") + self.context.update( { "title_placeholders": { diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index dbc30510d13..3875433888d 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "unsupported_device": "Unsupported device" }, "error": { "connection_timeout": "Connection timeout", diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 134a5372f71..42d28a26871 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Not a Huawei LTE device", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "unsupported_device": "Unsupported device" }, "error": { "connection_timeout": "Connection timeout", diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index d29c0554bca..9e723204b33 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -211,9 +211,14 @@ async def test_success(hass, login_requests_mock): @pytest.mark.parametrize( - ("upnp_data", "expected_result"), + ("requests_mock_request_kwargs", "upnp_data", "expected_result"), ( ( + { + "method": ANY, + "url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information", + "text": "Mock device", + }, { ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", ssdp.ATTR_UPNP_SERIAL: "00000000", @@ -225,6 +230,11 @@ async def test_success(hass, login_requests_mock): }, ), ( + { + "method": ANY, + "url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information", + "text": "100002", + }, { ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", # No ssdp.ATTR_UPNP_SERIAL @@ -235,19 +245,36 @@ async def test_success(hass, login_requests_mock): "errors": {}, }, ), + ( + { + "method": ANY, + "url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information", + "exc": Exception("Something unexpected"), + }, + { + # Does not matter + }, + { + "type": data_entry_flow.FlowResultType.ABORT, + "reason": "unsupported_device", + }, + ), ), ) -async def test_ssdp(hass, upnp_data, expected_result): +async def test_ssdp( + hass, login_requests_mock, requests_mock_request_kwargs, upnp_data, expected_result +): """Test SSDP discovery initiates config properly.""" - url = "http://192.168.100.1/" + url = FIXTURE_USER_INPUT[CONF_URL][:-1] # strip trailing slash for appending port context = {"source": config_entries.SOURCE_SSDP} + login_requests_mock.request(**requests_mock_request_kwargs) result = await hass.config_entries.flow.async_init( DOMAIN, context=context, data=ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="upnp:rootdevice", - ssdp_location="http://192.168.100.1:60957/rootDesc.xml", + ssdp_location=f"{url}:60957/rootDesc.xml", upnp={ ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", ssdp.ATTR_UPNP_MANUFACTURER: "Huawei", @@ -264,7 +291,7 @@ async def test_ssdp(hass, upnp_data, expected_result): for k, v in expected_result.items(): assert result[k] == v if result.get("data_schema"): - result["data_schema"]({})[CONF_URL] == url + assert result["data_schema"]({})[CONF_URL] == url + "/" @pytest.mark.parametrize( From 1e852e761c4e79897ca8d1ceda8c8ca88d4a8deb Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 11 Jan 2023 22:02:02 -0500 Subject: [PATCH 130/173] Remove oauth2client dependency in Google Sheets (#85637) Remove oauth2client dependency --- .../google_sheets/application_credentials.py | 10 +++----- .../google_sheets/test_config_flow.py | 23 ++++++++++--------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/google_sheets/application_credentials.py b/homeassistant/components/google_sheets/application_credentials.py index 415ab5947bf..f10f6891125 100644 --- a/homeassistant/components/google_sheets/application_credentials.py +++ b/homeassistant/components/google_sheets/application_credentials.py @@ -1,6 +1,4 @@ """application_credentials platform for Google Sheets.""" -import oauth2client - from homeassistant.components.application_credentials import AuthorizationServer from homeassistant.core import HomeAssistant @@ -8,17 +6,15 @@ from homeassistant.core import HomeAssistant async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: """Return authorization server.""" return AuthorizationServer( - oauth2client.GOOGLE_AUTH_URI, - oauth2client.GOOGLE_TOKEN_URI, + "https://accounts.google.com/o/oauth2/v2/auth", + "https://oauth2.googleapis.com/token", ) async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: """Return description placeholders for the credentials dialog.""" return { - "oauth_consent_url": ( - "https://console.cloud.google.com/apis/credentials/consent" - ), + "oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent", "more_info_url": "https://www.home-assistant.io/integrations/google_sheets/", "oauth_creds_url": "https://console.cloud.google.com/apis/credentials", } diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py index e74602dc8a1..7f434e19953 100644 --- a/tests/components/google_sheets/test_config_flow.py +++ b/tests/components/google_sheets/test_config_flow.py @@ -4,7 +4,6 @@ from collections.abc import Generator from unittest.mock import Mock, patch from gspread import GSpreadException -import oauth2client import pytest from homeassistant import config_entries @@ -21,6 +20,8 @@ from tests.common import MockConfigEntry CLIENT_ID = "1234" CLIENT_SECRET = "5678" +GOOGLE_AUTH_URI = "https://accounts.google.com/o/oauth2/v2/auth" +GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token" SHEET_ID = "google-sheet-id" TITLE = "Google Sheets" @@ -66,7 +67,7 @@ async def test_full_flow( ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" "&access_type=offline&prompt=consent" @@ -83,7 +84,7 @@ async def test_full_flow( mock_client.return_value.create = mock_create aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -133,7 +134,7 @@ async def test_create_sheet_error( ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" "&access_type=offline&prompt=consent" @@ -150,7 +151,7 @@ async def test_create_sheet_error( mock_client.return_value.create = mock_create aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -202,7 +203,7 @@ async def test_reauth( }, ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" "&access_type=offline&prompt=consent" @@ -218,7 +219,7 @@ async def test_reauth( mock_client.return_value.open_by_key = mock_open aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "updated-access-token", @@ -283,7 +284,7 @@ async def test_reauth_abort( }, ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" "&access_type=offline&prompt=consent" @@ -300,7 +301,7 @@ async def test_reauth_abort( mock_client.return_value.open_by_key = mock_open aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "updated-access-token", @@ -346,7 +347,7 @@ async def test_already_configured( ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/drive.file" "&access_type=offline&prompt=consent" @@ -363,7 +364,7 @@ async def test_already_configured( mock_client.return_value.create = mock_create aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", From d0f95d84b4601cc5b4990d6d82d9448936a36255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 11 Jan 2023 18:44:55 +0200 Subject: [PATCH 131/173] Upgrade huawei-lte-api to 1.6.11 (#85669) --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index f0997e2e165..1bd81536aa5 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ - "huawei-lte-api==1.6.7", + "huawei-lte-api==1.6.11", "stringcase==1.2.0", "url-normalize==1.4.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index a391b7f1b3e..fa67309f57c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -906,7 +906,7 @@ horimote==0.4.1 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.7 +huawei-lte-api==1.6.11 # homeassistant.components.hydrawise hydrawiser==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad63662ba8c..e6e1e73ed89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -683,7 +683,7 @@ homepluscontrol==0.0.5 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.7 +huawei-lte-api==1.6.11 # homeassistant.components.hyperion hyperion-py==0.7.5 From 45d14739c59743cbf9f58c2e49f1b0559ce6543e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 12 Jan 2023 03:15:28 +0100 Subject: [PATCH 132/173] Update pyTibber to 0.26.8 (#85702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tibber, update pyTibber Signed-off-by: Daniel Hjelseth Høyer * Tibber, update pyTibber Signed-off-by: Daniel Hjelseth Høyer Signed-off-by: Daniel Hjelseth Høyer --- 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 2082d6ddf30..115f3ed7d2e 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.26.7"], + "requirements": ["pyTibber==0.26.8"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index fa67309f57c..85609083f51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1439,7 +1439,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.7 +pyTibber==0.26.8 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6e1e73ed89..bf9b37e4540 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1039,7 +1039,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.7 +pyTibber==0.26.8 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From 2789747b0f153152248cade446550b3aa45a7338 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 9 Jan 2023 03:58:06 -0700 Subject: [PATCH 133/173] Bump pylitterbot to 2023.1.0 (#85484) --- 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 f81e663f302..ea656a3488e 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2022.12.0"], + "requirements": ["pylitterbot==2023.1.0"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 85609083f51..1c78073896e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1719,7 +1719,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.12.0 +pylitterbot==2023.1.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf9b37e4540..ab21e7c4deb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1220,7 +1220,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.12.0 +pylitterbot==2023.1.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 From 856f68252b1e2ad5f33c74e9ed92afb437cb752e Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Wed, 28 Dec 2022 12:30:48 -0700 Subject: [PATCH 134/173] Use built in polling for litterrobot update entity (#84678) * Use built in polling * Define scan interval --- .../components/litterrobot/update.py | 44 +++++-------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/litterrobot/update.py b/homeassistant/components/litterrobot/update.py index d6475ea486b..2ed46220a8c 100644 --- a/homeassistant/components/litterrobot/update.py +++ b/homeassistant/components/litterrobot/update.py @@ -1,8 +1,7 @@ """Support for Litter-Robot updates.""" from __future__ import annotations -from collections.abc import Callable -from datetime import datetime, timedelta +from datetime import timedelta from typing import Any from pylitterbot import LitterRobot4 @@ -17,12 +16,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.start import async_at_start from .const import DOMAIN from .entity import LitterRobotEntity, LitterRobotHub +SCAN_INTERVAL = timedelta(days=1) + FIRMWARE_UPDATE_ENTITY = UpdateEntityDescription( key="firmware", name="Firmware", @@ -43,7 +42,7 @@ async def async_setup_entry( for robot in robots if isinstance(robot, LitterRobot4) ] - async_add_entities(entities) + async_add_entities(entities, True) class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): @@ -53,16 +52,6 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS ) - def __init__( - self, - robot: LitterRobot4, - hub: LitterRobotHub, - description: UpdateEntityDescription, - ) -> None: - """Initialize a Litter-Robot update entity.""" - super().__init__(robot, hub, description) - self._poll_unsub: Callable[[], None] | None = None - @property def installed_version(self) -> str: """Version installed and in use.""" @@ -73,10 +62,13 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): """Update installation progress.""" return self.robot.firmware_update_triggered - async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None: - """Update the entity.""" - self._poll_unsub = None + @property + def should_poll(self) -> bool: + """Set polling to True.""" + return True + async def async_update(self) -> None: + """Update the entity.""" if await self.robot.has_firmware_update(): latest_version = await self.robot.get_latest_firmware() else: @@ -84,16 +76,6 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): if self._attr_latest_version != self.installed_version: self._attr_latest_version = latest_version - self.async_write_ha_state() - - self._poll_unsub = async_call_later( - self.hass, timedelta(days=1), self._async_update - ) - - async def async_added_to_hass(self) -> None: - """Set up a listener for the entity.""" - await super().async_added_to_hass() - self.async_on_remove(async_at_start(self.hass, self._async_update)) async def async_install( self, version: str | None, backup: bool, **kwargs: Any @@ -103,9 +85,3 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): if not await self.robot.update_firmware(): message = f"Unable to start firmware update on {self.robot.name}" raise HomeAssistantError(message) - - async def async_will_remove_from_hass(self) -> None: - """Call when entity will be removed.""" - if self._poll_unsub: - self._poll_unsub() - self._poll_unsub = None From 2e9ea0c934b4cb518817acca7bad1fd8571d96e3 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Wed, 11 Jan 2023 19:53:06 -0700 Subject: [PATCH 135/173] Fix Litter-Robot 4 firmware versions reported while updating (#85710) --- .../components/litterrobot/manifest.json | 2 +- .../components/litterrobot/update.py | 15 +++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litterrobot/test_update.py | 33 ++++++++++++++++++- 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index ea656a3488e..a6c392f4f62 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2023.1.0"], + "requirements": ["pylitterbot==2023.1.1"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", diff --git a/homeassistant/components/litterrobot/update.py b/homeassistant/components/litterrobot/update.py index 2ed46220a8c..845b42efaee 100644 --- a/homeassistant/components/litterrobot/update.py +++ b/homeassistant/components/litterrobot/update.py @@ -69,19 +69,20 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity): async def async_update(self) -> None: """Update the entity.""" - if await self.robot.has_firmware_update(): - latest_version = await self.robot.get_latest_firmware() - else: - latest_version = self.installed_version - - if self._attr_latest_version != self.installed_version: + # If the robot has a firmware update already in progress, checking for the + # latest firmware informs that an update has already been triggered, no + # firmware information is returned and we won't know the latest version. + if not self.robot.firmware_update_triggered: + latest_version = await self.robot.get_latest_firmware(True) + if not await self.robot.has_firmware_update(): + latest_version = self.robot.firmware self._attr_latest_version = latest_version async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" - if await self.robot.has_firmware_update(): + if await self.robot.has_firmware_update(True): if not await self.robot.update_firmware(): message = f"Unable to start firmware update on {self.robot.name}" raise HomeAssistantError(message) diff --git a/requirements_all.txt b/requirements_all.txt index 1c78073896e..91f8a093284 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1719,7 +1719,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2023.1.0 +pylitterbot==2023.1.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab21e7c4deb..e7b260a5b67 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1220,7 +1220,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2023.1.0 +pylitterbot==2023.1.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 diff --git a/tests/components/litterrobot/test_update.py b/tests/components/litterrobot/test_update.py index f4311992c8e..4940ec64824 100644 --- a/tests/components/litterrobot/test_update.py +++ b/tests/components/litterrobot/test_update.py @@ -11,7 +11,13 @@ from homeassistant.components.update import ( SERVICE_INSTALL, UpdateDeviceClass, ) -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -28,6 +34,7 @@ async def test_robot_with_no_update( """Tests the update entity was set up.""" robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0] robot.has_firmware_update = AsyncMock(return_value=False) + robot.get_latest_firmware = AsyncMock(return_value=None) entry = await setup_integration( hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN @@ -79,3 +86,27 @@ async def test_robot_with_update( ) await hass.async_block_till_done() assert robot.update_firmware.call_count == 1 + + +async def test_robot_with_update_already_in_progress( + hass: HomeAssistant, mock_account_with_litterrobot_4: MagicMock +): + """Tests the update entity was set up.""" + robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0] + robot._update_data( # pylint:disable=protected-access + {"isFirmwareUpdateTriggered": True}, partial=True + ) + + entry = await setup_integration( + hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN + ) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert state.attributes[ATTR_INSTALLED_VERSION] == OLD_FIRMWARE + assert state.attributes[ATTR_LATEST_VERSION] is None + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 32fc0e03a50e2d711210ee437a11d19889051e22 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Jan 2023 11:59:44 +0100 Subject: [PATCH 136/173] Use jemalloc in Docker builds (#85738) --- Dockerfile | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 03bd9131ea0..b80e86fb33c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,22 +11,45 @@ WORKDIR /usr/src COPY requirements.txt homeassistant/ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ RUN \ - pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -r homeassistant/requirements.txt --use-deprecated=legacy-resolver + pip3 install \ + --no-cache-dir \ + --no-index \ + --only-binary=:all: \ + --find-links "${WHEELS_LINKS}" \ + --use-deprecated=legacy-resolver \ + -r homeassistant/requirements.txt + COPY requirements_all.txt home_assistant_frontend-* homeassistant/ RUN \ if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \ - pip3 install --no-cache-dir --no-index homeassistant/home_assistant_frontend-*.whl; \ + pip3 install \ + --no-cache-dir \ + --no-index \ + homeassistant/home_assistant_frontend-*.whl; \ fi \ - && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -r homeassistant/requirements_all.txt --use-deprecated=legacy-resolver + && \ + LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ + MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ + pip3 install \ + --no-cache-dir \ + --no-index \ + --only-binary=:all: \ + --find-links "${WHEELS_LINKS}" \ + --use-deprecated=legacy-resolver \ + -r homeassistant/requirements_all.txt ## Setup Home Assistant Core COPY . homeassistant/ RUN \ - pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -e ./homeassistant --use-deprecated=legacy-resolver \ - && python3 -m compileall homeassistant/homeassistant + pip3 install \ + --no-cache-dir \ + --no-index \ + --only-binary=:all: \ + --find-links "${WHEELS_LINKS}" \ + --use-deprecated=legacy-resolver \ + -e ./homeassistant \ + && python3 -m compileall \ + homeassistant/homeassistant # Home Assistant S6-Overlay COPY rootfs / From 197634503fac8792dd4f186fb4e2a5713ceb57f2 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 12 Jan 2023 20:11:27 +0200 Subject: [PATCH 137/173] Bump aiowebostv to 0.3.0 (#85756) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 2547663be28..b4e761b067a 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.2.1"], + "requirements": ["aiowebostv==0.3.0"], "codeowners": ["@bendavid", "@thecode"], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 91f8a093284..8e6e063ed8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -297,7 +297,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.2.1 +aiowebostv==0.3.0 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7b260a5b67..c6e06374bcb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -272,7 +272,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.2.1 +aiowebostv==0.3.0 # homeassistant.components.yandex_transport aioymaps==1.2.2 From 6581bad7ce89ab429d3e6829b3f8d301cfc185b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 12 Jan 2023 13:20:23 -0500 Subject: [PATCH 138/173] Bumped version to 2023.1.4 --- 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 d7bd1fece49..2e8992e14b1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index e61e5b6e824..ec14caa7515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.3" +version = "2023.1.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1d5ecdd4eae00f7e4e2d657fe3e2c3d74be8920c Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 17 Jan 2023 03:34:42 +0100 Subject: [PATCH 139/173] Make API key mandatory for PI-Hole (#85885) add reauth, so make api-key mandatory --- homeassistant/components/pi_hole/__init__.py | 42 +++-- .../components/pi_hole/binary_sensor.py | 14 -- .../components/pi_hole/config_flow.py | 177 ++++++++++-------- homeassistant/components/pi_hole/const.py | 3 - homeassistant/components/pi_hole/strings.json | 17 +- .../components/pi_hole/translations/en.json | 19 +- tests/components/pi_hole/__init__.py | 37 +++- tests/components/pi_hole/test_config_flow.py | 111 ++++++----- tests/components/pi_hole/test_init.py | 83 +++----- 9 files changed, 252 insertions(+), 251 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 714547ba961..ba7949c0c30 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -17,7 +17,8 @@ from homeassistant.const import ( CONF_VERIFY_SSL, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo @@ -64,6 +65,13 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.SENSOR, + Platform.SWITCH, + Platform.UPDATE, +] + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Pi-hole integration.""" @@ -103,11 +111,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: location = entry.data[CONF_LOCATION] api_key = entry.data.get(CONF_API_KEY) - # For backward compatibility - if CONF_STATISTICS_ONLY not in entry.data: - hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_STATISTICS_ONLY: not api_key} - ) + # remove obsolet CONF_STATISTICS_ONLY from entry.data + if CONF_STATISTICS_ONLY in entry.data: + entry_data = entry.data.copy() + entry_data.pop(CONF_STATISTICS_ONLY) + hass.config_entries.async_update_entry(entry, data=entry_data) + + # start reauth to force api key is present + if CONF_API_KEY not in entry.data: + raise ConfigEntryAuthFailed _LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) @@ -125,8 +137,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await api.get_data() await api.get_versions() + _LOGGER.debug("async_update_data() api.data: %s", api.data) except HoleError as err: raise UpdateFailed(f"Failed to communicate with API: {err}") from err + if not isinstance(api.data, dict): + raise ConfigEntryAuthFailed coordinator = DataUpdateCoordinator( hass, @@ -142,30 +157,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - await hass.config_entries.async_forward_entry_setups(entry, _async_platforms(entry)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Pi-hole entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - entry, _async_platforms(entry) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok -@callback -def _async_platforms(entry: ConfigEntry) -> list[Platform]: - """Return platforms to be loaded / unloaded.""" - platforms = [Platform.BINARY_SENSOR, Platform.UPDATE, Platform.SENSOR] - if not entry.data[CONF_STATISTICS_ONLY]: - platforms.append(Platform.SWITCH) - return platforms - - class PiHoleEntity(CoordinatorEntity): """Representation of a Pi-hole entity.""" diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py index e887f2ea12f..7d0d9034fad 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -15,8 +15,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PiHoleEntity from .const import ( BINARY_SENSOR_TYPES, - BINARY_SENSOR_TYPES_STATISTICS_ONLY, - CONF_STATISTICS_ONLY, DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN, @@ -42,18 +40,6 @@ async def async_setup_entry( for description in BINARY_SENSOR_TYPES ] - if entry.data[CONF_STATISTICS_ONLY]: - binary_sensors += [ - PiHoleBinarySensor( - hole_data[DATA_KEY_API], - hole_data[DATA_KEY_COORDINATOR], - name, - entry.entry_id, - description, - ) - for description in BINARY_SENSOR_TYPES_STATISTICS_ONLY - ] - async_add_entities(binary_sensors, True) diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 40f4555e7d2..637f906b9ee 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the Pi-hole integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -26,7 +27,6 @@ from .const import ( DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, - DEFAULT_STATISTICS_ONLY, DEFAULT_VERIFY_SSL, DOMAIN, ) @@ -47,65 +47,29 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by the user.""" - return await self.async_step_init(user_input) - - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initiated by import.""" - return await self.async_step_init(user_input, is_import=True) - - async def async_step_init( - self, user_input: dict[str, Any] | None, is_import: bool = False - ) -> FlowResult: - """Handle init step of a flow.""" errors = {} if user_input is not None: - host = ( - user_input[CONF_HOST] - if is_import - else f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}" - ) - name = user_input[CONF_NAME] - location = user_input[CONF_LOCATION] - tls = user_input[CONF_SSL] - verify_tls = user_input[CONF_VERIFY_SSL] - endpoint = f"{host}/{location}" + self._config = { + CONF_HOST: f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}", + CONF_NAME: user_input[CONF_NAME], + CONF_LOCATION: user_input[CONF_LOCATION], + CONF_SSL: user_input[CONF_SSL], + CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + CONF_API_KEY: user_input[CONF_API_KEY], + } - if await self._async_endpoint_existed(endpoint): - return self.async_abort(reason="already_configured") - - try: - await self._async_try_connect(host, location, tls, verify_tls) - except HoleError as ex: - _LOGGER.debug("Connection failed: %s", ex) - if is_import: - _LOGGER.error("Failed to import: %s", ex) - return self.async_abort(reason="cannot_connect") - errors["base"] = "cannot_connect" - else: - self._config = { - CONF_HOST: host, - CONF_NAME: name, - CONF_LOCATION: location, - CONF_SSL: tls, - CONF_VERIFY_SSL: verify_tls, + self._async_abort_entries_match( + { + CONF_HOST: f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}", + CONF_LOCATION: user_input[CONF_LOCATION], } - if is_import: - api_key = user_input.get(CONF_API_KEY) - return self.async_create_entry( - title=name, - data={ - **self._config, - CONF_STATISTICS_ONLY: api_key is None, - CONF_API_KEY: api_key, - }, - ) - self._config[CONF_STATISTICS_ONLY] = user_input[CONF_STATISTICS_ONLY] - if self._config[CONF_STATISTICS_ONLY]: - return self.async_create_entry(title=name, data=self._config) - return await self.async_step_api_key() + ) + + if not (errors := await self._async_try_connect()): + return self.async_create_entry( + title=user_input[CONF_NAME], data=self._config + ) user_input = user_input or {} return self.async_show_form( @@ -116,6 +80,7 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required( CONF_PORT, default=user_input.get(CONF_PORT, 80) ): vol.Coerce(int), + vol.Required(CONF_API_KEY): str, vol.Required( CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) ): str, @@ -123,12 +88,6 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_LOCATION, default=user_input.get(CONF_LOCATION, DEFAULT_LOCATION), ): str, - vol.Required( - CONF_STATISTICS_ONLY, - default=user_input.get( - CONF_STATISTICS_ONLY, DEFAULT_STATISTICS_ONLY - ), - ): bool, vol.Required( CONF_SSL, default=user_input.get(CONF_SSL, DEFAULT_SSL), @@ -142,24 +101,94 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_api_key( - self, user_input: dict[str, Any] | None = None + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle a flow initiated by import.""" + + host = user_input[CONF_HOST] + name = user_input[CONF_NAME] + location = user_input[CONF_LOCATION] + tls = user_input[CONF_SSL] + verify_tls = user_input[CONF_VERIFY_SSL] + endpoint = f"{host}/{location}" + + if await self._async_endpoint_existed(endpoint): + return self.async_abort(reason="already_configured") + + try: + await self._async_try_connect_legacy(host, location, tls, verify_tls) + except HoleError as ex: + _LOGGER.debug("Connection failed: %s", ex) + _LOGGER.error("Failed to import: %s", ex) + return self.async_abort(reason="cannot_connect") + self._config = { + CONF_HOST: host, + CONF_NAME: name, + CONF_LOCATION: location, + CONF_SSL: tls, + CONF_VERIFY_SSL: verify_tls, + } + api_key = user_input.get(CONF_API_KEY) + return self.async_create_entry( + title=name, + data={ + **self._config, + CONF_STATISTICS_ONLY: api_key is None, + CONF_API_KEY: api_key, + }, + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._config = dict(entry_data) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, + user_input: dict[str, Any] | None = None, ) -> FlowResult: - """Handle step to setup API key.""" + """Perform reauth confirm upon an API authentication error.""" + errors = {} if user_input is not None: - return self.async_create_entry( - title=self._config[CONF_NAME], - data={ - **self._config, - CONF_API_KEY: user_input.get(CONF_API_KEY, ""), - }, - ) + self._config = {**self._config, CONF_API_KEY: user_input[CONF_API_KEY]} + if not (errors := await self._async_try_connect()): + entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + assert entry + self.hass.config_entries.async_update_entry(entry, data=self._config) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.context["entry_id"]) + ) + return self.async_abort(reason="reauth_successful") return self.async_show_form( - step_id="api_key", - data_schema=vol.Schema({vol.Optional(CONF_API_KEY): str}), + step_id="reauth_confirm", + description_placeholders={ + CONF_HOST: self._config[CONF_HOST], + CONF_LOCATION: self._config[CONF_LOCATION], + }, + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, ) + async def _async_try_connect(self) -> dict[str, str]: + session = async_get_clientsession(self.hass, self._config[CONF_VERIFY_SSL]) + pi_hole = Hole( + self._config[CONF_HOST], + session, + location=self._config[CONF_LOCATION], + tls=self._config[CONF_SSL], + api_token=self._config[CONF_API_KEY], + ) + try: + await pi_hole.get_data() + except HoleError as ex: + _LOGGER.debug("Connection failed: %s", ex) + return {"base": "cannot_connect"} + if not isinstance(pi_hole.data, dict): + return {CONF_API_KEY: "invalid_auth"} + return {} + async def _async_endpoint_existed(self, endpoint: str) -> bool: existing_endpoints = [ f"{entry.data.get(CONF_HOST)}/{entry.data.get(CONF_LOCATION)}" @@ -167,7 +196,7 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ] return endpoint in existing_endpoints - async def _async_try_connect( + async def _async_try_connect_legacy( self, host: str, location: str, tls: bool, verify_tls: bool ) -> None: session = async_get_clientsession(self.hass, verify_tls) diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index c73660faedb..a9bc5824ad9 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -154,9 +154,6 @@ BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( }, state_value=lambda api: bool(api.versions["FTL_update"]), ), -) - -BINARY_SENSOR_TYPES_STATISTICS_ONLY: tuple[PiHoleBinarySensorEntityDescription, ...] = ( PiHoleBinarySensorEntityDescription( key="status", name="Status", diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json index e911779d5d7..120ab8cb80a 100644 --- a/homeassistant/components/pi_hole/strings.json +++ b/homeassistant/components/pi_hole/strings.json @@ -8,28 +8,25 @@ "name": "[%key:common::config_flow::data::name%]", "location": "[%key:common::config_flow::data::location%]", "api_key": "[%key:common::config_flow::data::api_key%]", - "statistics_only": "Statistics Only", "ssl": "[%key:common::config_flow::data::ssl%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" } }, - "api_key": { + "reauth_confirm": { + "title": "PI-Hole [%key:common::config_flow::title::reauth%]", + "description": "Please enter a new api key for PI-Hole at {host}/{location}", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" - } - }, - "issues": { - "deprecated_yaml": { - "title": "The PI-Hole YAML configuration is being removed", - "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 4333838ae64..815182731c2 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -1,16 +1,20 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" }, "step": { - "api_key": { + "reauth_confirm": { "data": { "api_key": "API Key" - } + }, + "description": "Please enter a new api key for PI-Hole at {host}/{location}", + "title": "PI-Hole Reauthenticate Integration" }, "user": { "data": { @@ -20,16 +24,9 @@ "name": "Name", "port": "Port", "ssl": "Uses an SSL certificate", - "statistics_only": "Statistics Only", "verify_ssl": "Verify SSL certificate" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Configuring PI-Hole using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the PI-Hole YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The PI-Hole YAML configuration is being removed" - } } } \ No newline at end of file diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 57ea89fc7e0..49e15391f8c 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -3,7 +3,13 @@ from unittest.mock import AsyncMock, MagicMock, patch from hole.exceptions import HoleError -from homeassistant.components.pi_hole.const import CONF_STATISTICS_ONLY +from homeassistant.components.pi_hole.const import ( + CONF_STATISTICS_ONLY, + DEFAULT_LOCATION, + DEFAULT_NAME, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, +) from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -47,7 +53,16 @@ API_KEY = "apikey" SSL = False VERIFY_SSL = True -CONF_DATA = { +CONFIG_DATA_DEFAULTS = { + CONF_HOST: f"{HOST}:{PORT}", + CONF_LOCATION: DEFAULT_LOCATION, + CONF_NAME: DEFAULT_NAME, + CONF_SSL: DEFAULT_SSL, + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, + CONF_API_KEY: API_KEY, +} + +CONFIG_DATA = { CONF_HOST: f"{HOST}:{PORT}", CONF_LOCATION: LOCATION, CONF_NAME: NAME, @@ -56,34 +71,35 @@ CONF_DATA = { CONF_VERIFY_SSL: VERIFY_SSL, } -CONF_CONFIG_FLOW_USER = { +CONFIG_FLOW_USER = { CONF_HOST: HOST, CONF_PORT: PORT, + CONF_API_KEY: API_KEY, CONF_LOCATION: LOCATION, CONF_NAME: NAME, - CONF_STATISTICS_ONLY: False, CONF_SSL: SSL, CONF_VERIFY_SSL: VERIFY_SSL, } -CONF_CONFIG_FLOW_API_KEY = { +CONFIG_FLOW_API_KEY = { CONF_API_KEY: API_KEY, } -CONF_CONFIG_ENTRY = { +CONFIG_ENTRY = { CONF_HOST: f"{HOST}:{PORT}", CONF_LOCATION: LOCATION, CONF_NAME: NAME, - CONF_STATISTICS_ONLY: False, CONF_API_KEY: API_KEY, CONF_SSL: SSL, CONF_VERIFY_SSL: VERIFY_SSL, } +CONFIG_ENTRY_IMPORTED = {**CONFIG_ENTRY, CONF_STATISTICS_ONLY: False} + SWITCH_ENTITY_ID = "switch.pi_hole" -def _create_mocked_hole(raise_exception=False, has_versions=True): +def _create_mocked_hole(raise_exception=False, has_versions=True, has_data=True): mocked_hole = MagicMock() type(mocked_hole).get_data = AsyncMock( side_effect=HoleError("") if raise_exception else None @@ -93,7 +109,10 @@ def _create_mocked_hole(raise_exception=False, has_versions=True): ) type(mocked_hole).enable = AsyncMock() type(mocked_hole).disable = AsyncMock() - mocked_hole.data = ZERO_DATA + if has_data: + mocked_hole.data = ZERO_DATA + else: + mocked_hole.data = [] if has_versions: mocked_hole.versions = SAMPLE_VERSIONS else: diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index bc86922c89f..65f21418bad 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -2,28 +2,26 @@ import logging from unittest.mock import patch -from homeassistant.components.pi_hole.const import CONF_STATISTICS_ONLY, DOMAIN +from homeassistant.components.pi_hole.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import ( - CONF_CONFIG_ENTRY, - CONF_CONFIG_FLOW_API_KEY, - CONF_CONFIG_FLOW_USER, - CONF_DATA, + CONFIG_DATA, + CONFIG_DATA_DEFAULTS, + CONFIG_ENTRY, + CONFIG_ENTRY_IMPORTED, + CONFIG_FLOW_USER, NAME, + ZERO_DATA, _create_mocked_hole, _patch_config_flow_hole, + _patch_init_hole, ) - -def _flow_next(hass, flow_id): - return next( - flow - for flow in hass.config_entries.flow.async_progress() - if flow["flow_id"] == flow_id - ) +from tests.common import MockConfigEntry def _patch_setup(): @@ -33,41 +31,41 @@ def _patch_setup(): ) -async def test_flow_import(hass, caplog): +async def test_flow_import(hass: HomeAssistant, caplog): """Test import flow.""" mocked_hole = _create_mocked_hole() with _patch_config_flow_hole(mocked_hole), _patch_setup(): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_DATA ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME - assert result["data"] == CONF_CONFIG_ENTRY + assert result["data"] == CONFIG_ENTRY_IMPORTED # duplicated server result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_DATA ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_flow_import_invalid(hass, caplog): +async def test_flow_import_invalid(hass: HomeAssistant, caplog): """Test import flow with invalid server.""" mocked_hole = _create_mocked_hole(True) with _patch_config_flow_hole(mocked_hole), _patch_setup(): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_DATA ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len([x for x in caplog.records if x.levelno == logging.ERROR]) == 1 -async def test_flow_user(hass): +async def test_flow_user(hass: HomeAssistant): """Test user initialized flow.""" - mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_setup(): + mocked_hole = _create_mocked_hole(has_data=False) + with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -75,69 +73,68 @@ async def test_flow_user(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} - _flow_next(hass, result["flow_id"]) result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONF_CONFIG_FLOW_USER, + user_input=CONFIG_FLOW_USER, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "api_key" - assert result["errors"] is None - _flow_next(hass, result["flow_id"]) + assert result["step_id"] == "user" + assert result["errors"] == {CONF_API_KEY: "invalid_auth"} + mocked_hole.data = ZERO_DATA result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONF_CONFIG_FLOW_API_KEY, + user_input=CONFIG_FLOW_USER, ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME - assert result["data"] == CONF_CONFIG_ENTRY + assert result["data"] == CONFIG_ENTRY # duplicated server result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data=CONF_CONFIG_FLOW_USER, + data=CONFIG_FLOW_USER, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_flow_statistics_only(hass): - """Test user initialized flow with statistics only.""" - mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_setup(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - _flow_next(hass, result["flow_id"]) - - user_input = {**CONF_CONFIG_FLOW_USER} - user_input[CONF_STATISTICS_ONLY] = True - config_entry_data = {**CONF_CONFIG_ENTRY} - config_entry_data[CONF_STATISTICS_ONLY] = True - config_entry_data.pop(CONF_API_KEY) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=user_input, - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == NAME - assert result["data"] == config_entry_data - - async def test_flow_user_invalid(hass): """Test user initialized flow with invalid server.""" mocked_hole = _create_mocked_hole(True) with _patch_config_flow_hole(mocked_hole), _patch_setup(): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW_USER + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_reauth(hass: HomeAssistant): + """Test reauth flow.""" + mocked_hole = _create_mocked_hole(has_data=False) + entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_DATA_DEFAULTS) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole), _patch_config_flow_hole(mocked_hole): + assert not await hass.config_entries.async_setup(entry.entry_id) + + flows = hass.config_entries.flow.async_progress() + + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + assert flows[0]["context"]["entry_id"] == entry.entry_id + + mocked_hole.data = ZERO_DATA + + result = await hass.config_entries.flow.async_configure( + flows[0]["flow_id"], + user_input={CONF_API_KEY: "newkey"}, + ) + + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_API_KEY] == "newkey" diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index dce3773acdc..75d9dd27aee 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -7,27 +7,16 @@ from hole.exceptions import HoleError from homeassistant.components import pi_hole, switch from homeassistant.components.pi_hole.const import ( CONF_STATISTICS_ONLY, - DEFAULT_LOCATION, - DEFAULT_NAME, - DEFAULT_SSL, - DEFAULT_VERIFY_SSL, SERVICE_DISABLE, SERVICE_DISABLE_ATTR_DURATION, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_API_KEY, - CONF_HOST, - CONF_LOCATION, - CONF_NAME, - CONF_SSL, - CONF_VERIFY_SSL, -) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY, CONF_HOST +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import ( - CONF_CONFIG_ENTRY, - CONF_DATA, + CONFIG_DATA_DEFAULTS, SWITCH_ENTITY_ID, _create_mocked_hole, _patch_config_flow_hole, @@ -37,7 +26,7 @@ from . import ( from tests.common import MockConfigEntry -async def test_setup_minimal_config(hass): +async def test_setup_minimal_config(hass: HomeAssistant): """Tests component setup with minimal config.""" mocked_hole = _create_mocked_hole() with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): @@ -88,7 +77,7 @@ async def test_setup_minimal_config(hass): assert state.state == "off" -async def test_setup_name_config(hass): +async def test_setup_name_config(hass: HomeAssistant): """Tests component setup with a custom name.""" mocked_hole = _create_mocked_hole() with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): @@ -106,7 +95,7 @@ async def test_setup_name_config(hass): ) -async def test_switch(hass, caplog): +async def test_switch(hass: HomeAssistant, caplog): """Test Pi-hole switch.""" mocked_hole = _create_mocked_hole() with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): @@ -154,7 +143,7 @@ async def test_switch(hass, caplog): assert errors[-1].message == "Unable to disable Pi-hole: Error2" -async def test_disable_service_call(hass): +async def test_disable_service_call(hass: HomeAssistant): """Test disable service call with no Pi-hole named.""" mocked_hole = _create_mocked_hole() with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): @@ -180,21 +169,14 @@ async def test_disable_service_call(hass): await hass.async_block_till_done() - mocked_hole.disable.assert_called_once_with(1) + mocked_hole.disable.assert_called_with(1) -async def test_unload(hass): +async def test_unload(hass: HomeAssistant): """Test unload entities.""" entry = MockConfigEntry( domain=pi_hole.DOMAIN, - data={ - CONF_NAME: DEFAULT_NAME, - CONF_HOST: "pi.hole", - CONF_LOCATION: DEFAULT_LOCATION, - CONF_SSL: DEFAULT_SSL, - CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, - CONF_STATISTICS_ONLY: True, - }, + data={**CONFIG_DATA_DEFAULTS, CONF_HOST: "pi.hole"}, ) entry.add_to_hass(hass) mocked_hole = _create_mocked_hole() @@ -208,32 +190,25 @@ async def test_unload(hass): assert entry.entry_id not in hass.data[pi_hole.DOMAIN] -async def test_migrate(hass): - """Test migrate from old config entry.""" - entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA) - entry.add_to_hass(hass) - +async def test_remove_obsolete(hass: HomeAssistant): + """Test removing obsolete config entry parameters.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert entry.data == CONF_CONFIG_ENTRY - - -async def test_migrate_statistics_only(hass): - """Test migrate from old config entry with statistics only.""" - conf_data = {**CONF_DATA} - conf_data[CONF_API_KEY] = "" - entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=conf_data) + entry = MockConfigEntry( + domain=pi_hole.DOMAIN, data={**CONFIG_DATA_DEFAULTS, CONF_STATISTICS_ONLY: True} + ) entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) + assert CONF_STATISTICS_ONLY not in entry.data + +async def test_missing_api_key(hass: HomeAssistant): + """Tests start reauth flow if api key is missing.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - config_entry_data = {**CONF_CONFIG_ENTRY} - config_entry_data[CONF_STATISTICS_ONLY] = True - config_entry_data[CONF_API_KEY] = "" - assert entry.data == config_entry_data + data = CONFIG_DATA_DEFAULTS.copy() + data.pop(CONF_API_KEY) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=data) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert not await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_ERROR From cb27cfe7dda21b381d5aa14cbb70140be7e754c8 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 13 Jan 2023 20:07:09 +0100 Subject: [PATCH 140/173] Reolink check for admin (#85570) Co-authored-by: Martin Hjelmare fixes undefined --- homeassistant/components/reolink/__init__.py | 7 +- .../components/reolink/config_flow.py | 62 ++++++++++--- .../components/reolink/exceptions.py | 6 ++ homeassistant/components/reolink/host.py | 7 ++ homeassistant/components/reolink/strings.json | 13 ++- .../components/reolink/translations/en.json | 15 +++- tests/components/reolink/test_config_flow.py | 88 +++++++++++++++++-- 7 files changed, 171 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/reolink/exceptions.py diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index a4daba45ba7..6f7ab9d68b7 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -14,10 +14,11 @@ from reolink_aio.exceptions import ApiError, InvalidContentTypeError from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN +from .exceptions import UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -40,16 +41,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: if not await host.async_init(): + await host.stop() raise ConfigEntryNotReady( f"Error while trying to setup {host.api.host}:{host.api.port}: " "failed to obtain data from device." ) + except UserNotAdmin as err: + raise ConfigEntryAuthFailed(err) from UserNotAdmin except ( ClientConnectorError, asyncio.TimeoutError, ApiError, InvalidContentTypeError, ) as err: + await host.stop() raise ConfigEntryNotReady( f'Error while trying to setup {host.api.host}:{host.api.port}: "{str(err)}".' ) from err diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 31f1a10dc1e..fdbbf201756 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -1,19 +1,21 @@ """Config flow for the Reolink camera component.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any from reolink_aio.exceptions import ApiError, CredentialsInvalidError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, exceptions 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.helpers import config_validation as cv from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_PROTOCOL, DOMAIN +from .exceptions import UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) @@ -53,6 +55,13 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize.""" + self._host: str | None = None + self._username: str = "admin" + self._password: str | None = None + self._reauth: bool = False + @staticmethod @callback def async_get_options_flow( @@ -61,16 +70,37 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Options callback for Reolink.""" return ReolinkOptionsFlowHandler(config_entry) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an authentication error or no admin privileges.""" + self._host = entry_data[CONF_HOST] + self._username = entry_data[CONF_USERNAME] + self._password = entry_data[CONF_PASSWORD] + self._reauth = True + 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 not None: + return await self.async_step_user() + return self.async_show_form(step_id="reauth_confirm") + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" errors = {} - placeholders = {} + placeholders = {"error": ""} if user_input is not None: + host = ReolinkHost(self.hass, user_input, DEFAULT_OPTIONS) try: - host = await async_obtain_host_settings(self.hass, user_input) + await async_obtain_host_settings(host) + except UserNotAdmin: + errors[CONF_USERNAME] = "not_admin" + placeholders["username"] = host.api.username + placeholders["userlevel"] = host.api.user_level except CannotConnect: errors[CONF_HOST] = "cannot_connect" except CredentialsInvalidError: @@ -87,7 +117,17 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_PORT] = host.api.port user_input[CONF_USE_HTTPS] = host.api.use_https - await self.async_set_unique_id(host.unique_id, raise_on_progress=False) + existing_entry = await self.async_set_unique_id( + host.unique_id, raise_on_progress=False + ) + if existing_entry and self._reauth: + if self.hass.config_entries.async_update_entry( + existing_entry, data=user_input + ): + await self.hass.config_entries.async_reload( + existing_entry.entry_id + ) + return self.async_abort(reason="reauth_successful") self._abort_if_unique_id_configured(updates=user_input) return self.async_create_entry( @@ -98,9 +138,9 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema = vol.Schema( { - vol.Required(CONF_USERNAME, default="admin"): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME, default=self._username): str, + vol.Required(CONF_PASSWORD, default=self._password): str, + vol.Required(CONF_HOST, default=self._host): str, } ) if errors: @@ -119,20 +159,14 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -async def async_obtain_host_settings( - hass: core.HomeAssistant, user_input: dict -) -> ReolinkHost: +async def async_obtain_host_settings(host: ReolinkHost) -> None: """Initialize the Reolink host and get the host information.""" - host = ReolinkHost(hass, user_input, DEFAULT_OPTIONS) - try: if not await host.async_init(): raise CannotConnect finally: await host.stop() - return host - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/reolink/exceptions.py b/homeassistant/components/reolink/exceptions.py new file mode 100644 index 00000000000..ad95625cfa7 --- /dev/null +++ b/homeassistant/components/reolink/exceptions.py @@ -0,0 +1,6 @@ +"""Exceptions for the Reolink Camera integration.""" +from homeassistant.exceptions import HomeAssistantError + + +class UserNotAdmin(HomeAssistantError): + """Raised when user is not admin.""" diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index fc5e4947afa..5c744f0c5fd 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_TIMEOUT +from .exceptions import UserNotAdmin _LOGGER = logging.getLogger(__name__) @@ -68,6 +69,12 @@ class ReolinkHost: if self._api.mac_address is None: return False + if not self._api.is_admin: + await self.stop() + raise UserNotAdmin( + f"User '{self._api.username}' has authorization level '{self._api.user_level}', only admin users can change camera settings" + ) + enable_onvif = None enable_rtmp = None enable_rtsp = None diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 88211774240..1c82a43c8a2 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -2,6 +2,7 @@ "config": { "step": { "user": { + "description": "{error}", "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", @@ -9,16 +10,22 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Reolink integration needs to re-authenticate your connection details" } }, "error": { - "api_error": "API error occurred: {error}", + "api_error": "API error occurred", "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}" + "not_admin": "User needs to be admin, user ''{username}'' has authorisation level ''{userlevel}''", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/reolink/translations/en.json b/homeassistant/components/reolink/translations/en.json index 028f61ed8c7..beb366e8b39 100644 --- a/homeassistant/components/reolink/translations/en.json +++ b/homeassistant/components/reolink/translations/en.json @@ -1,15 +1,21 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { - "api_error": "API error occurred: {error}", + "api_error": "API error occurred", "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error: {error}" + "not_admin": "User needs to be admin, user ''{username}'' has authorisation level ''{userlevel}''", + "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "description": "The Reolink integration needs to re-authenticate your connection details", + "title": "Reauthenticate Integration" + }, "user": { "data": { "host": "Host", @@ -17,7 +23,8 @@ "port": "Port", "use_https": "Enable HTTPS", "username": "Username" - } + }, + "description": "{error}" } } }, diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index b69fab9797f..fc6672718b9 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -24,7 +24,7 @@ TEST_NVR_NAME = "test_reolink_name" TEST_USE_HTTPS = True -def get_mock_info(error=None, host_data_return=True): +def get_mock_info(error=None, host_data_return=True, user_level="admin"): """Return a mock gateway info instance.""" host_mock = Mock() if error is None: @@ -40,6 +40,8 @@ def get_mock_info(error=None, host_data_return=True): host_mock.nvr_name = TEST_NVR_NAME host_mock.port = TEST_PORT host_mock.use_https = TEST_USE_HTTPS + host_mock.is_admin = user_level == "admin" + host_mock.user_level = user_level return host_mock @@ -110,7 +112,22 @@ async def test_config_flow_errors(hass): assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {"host": "cannot_connect"} + assert result["errors"] == {CONF_HOST: "cannot_connect"} + + host_mock = get_mock_info(user_level="guest") + with patch("homeassistant.components.reolink.host.Host", return_value=host_mock): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_HOST: TEST_HOST, + }, + ) + + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_USERNAME: "not_admin"} host_mock = get_mock_info(error=json.JSONDecodeError("test_error", "test", 1)) with patch("homeassistant.components.reolink.host.Host", return_value=host_mock): @@ -125,7 +142,7 @@ async def test_config_flow_errors(hass): assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {"host": "unknown"} + assert result["errors"] == {CONF_HOST: "unknown"} host_mock = get_mock_info(error=CredentialsInvalidError("Test error")) with patch("homeassistant.components.reolink.host.Host", return_value=host_mock): @@ -140,7 +157,7 @@ async def test_config_flow_errors(hass): assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {"host": "invalid_auth"} + assert result["errors"] == {CONF_HOST: "invalid_auth"} host_mock = get_mock_info(error=ApiError("Test error")) with patch("homeassistant.components.reolink.host.Host", return_value=host_mock): @@ -155,7 +172,7 @@ async def test_config_flow_errors(hass): assert result["type"] is data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {"host": "api_error"} + assert result["errors"] == {CONF_HOST: "api_error"} result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -261,3 +278,64 @@ async def test_change_connection_settings(hass): assert config_entry.data[CONF_HOST] == TEST_HOST2 assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 + + +async def test_reauth(hass): + """Test a reauth flow.""" + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id=format_mac(TEST_MAC), + data={ + CONF_HOST: TEST_HOST, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: TEST_PORT, + const.CONF_USE_HTTPS: TEST_USE_HTTPS, + }, + options={ + const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + }, + title=TEST_NVR_NAME, + ) + 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.flow.async_init( + const.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "title_placeholders": {"name": TEST_NVR_NAME}, + "unique_id": format_mac(TEST_MAC), + }, + data=config_entry.data, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: TEST_HOST2, + CONF_USERNAME: TEST_USERNAME2, + CONF_PASSWORD: TEST_PASSWORD2, + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + assert config_entry.data[CONF_HOST] == TEST_HOST2 + assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 + assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 From 8beb043d62483c29dc81e4a2dce78b40a2d77ac2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 16 Jan 2023 09:25:06 +0100 Subject: [PATCH 141/173] Remove sky connect config entry if USB stick is not plugged in (#85765) * Remove sky connect config entry if USB stick is not plugged in * Tweak cleanup * Give some stuff more cromulent names * Do the needful * Add tests * Tweak --- .../homeassistant_sky_connect/__init__.py | 48 ++++++++++++---- homeassistant/components/usb/__init__.py | 42 +++++++++++++- .../test_hardware.py | 8 ++- .../homeassistant_sky_connect/test_init.py | 29 +++++++++- tests/components/usb/test_init.py | 56 +++++++++++++++++++ 5 files changed, 164 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 08d54bdef12..af6df6b519d 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -16,7 +16,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon get_zigbee_socket, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN @@ -25,12 +25,10 @@ from .util import get_usb_service_info _LOGGER = logging.getLogger(__name__) -async def _multi_pan_addon_info( - hass: HomeAssistant, entry: ConfigEntry -) -> AddonInfo | None: - """Return AddonInfo if the multi-PAN addon is enabled for our SkyConnect.""" +async def _wait_multi_pan_addon(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Wait for multi-PAN info to be available.""" if not is_hassio(hass): - return None + return addon_manager: AddonManager = get_addon_manager(hass) try: @@ -50,7 +48,18 @@ async def _multi_pan_addon_info( ) raise ConfigEntryNotReady - if addon_info.state == AddonState.NOT_INSTALLED: + +async def _multi_pan_addon_info( + hass: HomeAssistant, entry: ConfigEntry +) -> AddonInfo | None: + """Return AddonInfo if the multi-PAN addon is enabled for our SkyConnect.""" + if not is_hassio(hass): + return None + + addon_manager: AddonManager = get_addon_manager(hass) + addon_info: AddonInfo = await addon_manager.async_get_addon_info() + + if addon_info.state != AddonState.RUNNING: return None usb_dev = entry.data["device"] @@ -62,8 +71,8 @@ async def _multi_pan_addon_info( return addon_info -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up a Home Assistant Sky Connect config entry.""" +async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Finish Home Assistant Sky Connect config entry setup.""" matcher = usb.USBCallbackMatcher( domain=DOMAIN, vid=entry.data["vid"].upper(), @@ -74,8 +83,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) if not usb.async_is_plugged_in(hass, matcher): - # The USB dongle is not plugged in - raise ConfigEntryNotReady + # The USB dongle is not plugged in, remove the config entry + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return addon_info = await _multi_pan_addon_info(hass, entry) @@ -86,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: context={"source": "usb"}, data=usb_info, ) - return True + return hw_discovery_data = { "name": "Sky Connect Multi-PAN", @@ -101,6 +111,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data=hw_discovery_data, ) + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Home Assistant Sky Connect config entry.""" + + await _wait_multi_pan_addon(hass, entry) + + @callback + def async_usb_scan_done() -> None: + """Handle usb discovery started.""" + hass.async_create_task(_async_usb_scan_done(hass, entry)) + + unsub_usb = usb.async_register_initial_scan_callback(hass, async_usb_scan_done) + entry.async_on_unload(unsub_usb) + return True diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 0f81d2e42d6..17d6f679cf0 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -61,6 +61,18 @@ def async_register_scan_request_callback( return discovery.async_register_scan_request_callback(callback) +@hass_callback +def async_register_initial_scan_callback( + hass: HomeAssistant, callback: CALLBACK_TYPE +) -> CALLBACK_TYPE: + """Register to receive a callback when the initial USB scan is done. + + If the initial scan is already done, the callback is called immediately. + """ + discovery: USBDiscovery = hass.data[DOMAIN] + return discovery.async_register_initial_scan_callback(callback) + + @hass_callback def async_is_plugged_in(hass: HomeAssistant, matcher: USBCallbackMatcher) -> bool: """Return True is a USB device is present.""" @@ -186,6 +198,8 @@ class USBDiscovery: self.observer_active = False self._request_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None self._request_callbacks: list[CALLBACK_TYPE] = [] + self.initial_scan_done = False + self._initial_scan_callbacks: list[CALLBACK_TYPE] = [] async def async_setup(self) -> None: """Set up USB Discovery.""" @@ -249,7 +263,7 @@ class USBDiscovery: self, _callback: CALLBACK_TYPE, ) -> CALLBACK_TYPE: - """Register a callback.""" + """Register a scan request callback.""" self._request_callbacks.append(_callback) @hass_callback @@ -258,6 +272,26 @@ class USBDiscovery: return _async_remove_callback + @hass_callback + def async_register_initial_scan_callback( + self, + callback: CALLBACK_TYPE, + ) -> CALLBACK_TYPE: + """Register an initial scan callback.""" + if self.initial_scan_done: + callback() + return lambda: None + + self._initial_scan_callbacks.append(callback) + + @hass_callback + def _async_remove_callback() -> None: + if callback not in self._initial_scan_callbacks: + return + self._initial_scan_callbacks.remove(callback) + + return _async_remove_callback + @hass_callback def _async_process_discovered_usb_device(self, device: USBDevice) -> None: """Process a USB discovery.""" @@ -307,6 +341,12 @@ class USBDiscovery: async def _async_scan_serial(self) -> None: """Scan serial ports.""" self._async_process_ports(await self.hass.async_add_executor_job(comports)) + if self.initial_scan_done: + return + + self.initial_scan_done = True + while self._initial_scan_callbacks: + self._initial_scan_callbacks.pop()() async def _async_scan(self) -> None: """Scan for USB devices and notify callbacks to scan as well.""" diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 01f0e6ac5d7..09e650388c5 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -2,9 +2,10 @@ from unittest.mock import patch from homeassistant.components.homeassistant_sky_connect.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, MockModule, mock_integration +from tests.common import MockConfigEntry CONFIG_ENTRY_DATA = { "device": "bla_device", @@ -29,7 +30,8 @@ async def test_hardware_info( hass: HomeAssistant, hass_ws_client, addon_store_info ) -> None: """Test we can get the board info.""" - mock_integration(hass, MockModule("usb")) + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) # Setup the config entry config_entry = MockConfigEntry( diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index ebf1c74d9e0..c47066e8bc9 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -9,7 +9,8 @@ from homeassistant.components import zha from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant +from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -55,6 +56,9 @@ async def test_setup_entry( num_flows, ) -> None: """Test setup of a config entry, including setup of zha.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # Setup the config entry config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, @@ -100,6 +104,9 @@ async def test_setup_zha( mock_zha_config_flow_setup, hass: HomeAssistant, addon_store_info ) -> None: """Test zha gets the right config.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # Setup the config entry config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, @@ -146,6 +153,9 @@ async def test_setup_zha_multipan( hass: HomeAssistant, addon_info, addon_running ) -> None: """Test zha gets the right config.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + addon_info.return_value["options"]["device"] = CONFIG_ENTRY_DATA["device"] # Setup the config entry @@ -197,6 +207,9 @@ async def test_setup_zha_multipan_other_device( mock_zha_config_flow_setup, hass: HomeAssistant, addon_info, addon_running ) -> None: """Test zha gets the right config.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + addon_info.return_value["options"]["device"] = "/dev/not_our_sky_connect" # Setup the config entry @@ -258,16 +271,23 @@ async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", return_value=False, ) as mock_is_plugged_in: - assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + # USB discovery starts, config entry should be removed + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert len(mock_is_plugged_in.mock_calls) == 1 - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 async def test_setup_entry_addon_info_fails( hass: HomeAssistant, addon_store_info ) -> None: """Test setup of a config entry when fetching addon info fails.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + addon_store_info.side_effect = HassioAPIError("Boom") # Setup the config entry @@ -296,6 +316,9 @@ async def test_setup_entry_addon_not_running( hass: HomeAssistant, addon_installed, start_addon ) -> None: """Test the addon is started if it is not running.""" + assert await async_setup_component(hass, "usb", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # Setup the config entry config_entry = MockConfigEntry( data=CONFIG_ENTRY_DATA, diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index c7196fed0c5..ab9f00a6a5b 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -936,3 +936,59 @@ async def test_web_socket_triggers_discovery_request_callbacks(hass, hass_ws_cli assert response["success"] await hass.async_block_till_done() assert len(mock_callback.mock_calls) == 1 + + +async def test_initial_scan_callback(hass, hass_ws_client): + """Test it's possible to register a callback when the initial scan is done.""" + mock_callback_1 = Mock() + mock_callback_2 = Mock() + + with patch("pyudev.Context", side_effect=ImportError), patch( + "homeassistant.components.usb.async_get_usb", return_value=[] + ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + cancel_1 = usb.async_register_initial_scan_callback(hass, mock_callback_1) + assert len(mock_callback_1.mock_calls) == 0 + + await hass.async_block_till_done() + assert len(mock_callback_1.mock_calls) == 0 + + # This triggers the initial scan + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert len(mock_callback_1.mock_calls) == 1 + + # A callback registered now should be called immediately. The old callback + # should not be called again + cancel_2 = usb.async_register_initial_scan_callback(hass, mock_callback_2) + assert len(mock_callback_1.mock_calls) == 1 + assert len(mock_callback_2.mock_calls) == 1 + + # Calling the cancels should be allowed even if the callback has been called + cancel_1() + cancel_2() + + +async def test_cancel_initial_scan_callback(hass, hass_ws_client): + """Test it's possible to cancel an initial scan callback.""" + mock_callback = Mock() + + with patch("pyudev.Context", side_effect=ImportError), patch( + "homeassistant.components.usb.async_get_usb", return_value=[] + ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + cancel = usb.async_register_initial_scan_callback(hass, mock_callback) + assert len(mock_callback.mock_calls) == 0 + + await hass.async_block_till_done() + assert len(mock_callback.mock_calls) == 0 + cancel() + + # This triggers the initial scan + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert len(mock_callback.mock_calls) == 0 From caa8f9e49b2e001600b7aef6ca1a61097df3f3d5 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Thu, 12 Jan 2023 21:10:45 +0200 Subject: [PATCH 142/173] Remove WAQI unsupported UOM (#85768) fixes undefined --- homeassistant/components/waqi/sensor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index c9cc527387a..e91e3da5aa5 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -24,7 +24,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -53,7 +52,6 @@ KEY_TO_ATTR = { ATTRIBUTION = "Data provided by the World Air Quality Index project" ATTR_ICON = "mdi:cloud" -ATTR_UNIT = "AQI" CONF_LOCATIONS = "locations" CONF_STATIONS = "stations" @@ -62,7 +60,7 @@ SCAN_INTERVAL = timedelta(minutes=5) TIMEOUT = 10 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( { vol.Optional(CONF_STATIONS): cv.ensure_list, vol.Required(CONF_TOKEN): cv.string, @@ -110,7 +108,6 @@ class WaqiSensor(SensorEntity): """Implementation of a WAQI sensor.""" _attr_icon = ATTR_ICON - _attr_native_unit_of_measurement = ATTR_UNIT _attr_device_class = SensorDeviceClass.AQI _attr_state_class = SensorStateClass.MEASUREMENT From 502fea5f95897379db1c231e6016b2496540686d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Jan 2023 12:17:00 -1000 Subject: [PATCH 143/173] Bump pySwitchbot to 0.36.4 (#85777) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b543e7f15e7..05dca82b3f9 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.36.3"], + "requirements": ["PySwitchbot==0.36.4"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8e6e063ed8c..e3c774edd87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.3 +PySwitchbot==0.36.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6e06374bcb..64bf47763b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.36.3 +PySwitchbot==0.36.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 2447e246779828d7349d1b2908eff9b972aaecab Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 12 Jan 2023 15:20:16 -0800 Subject: [PATCH 144/173] Remove oauth2client dependency in Google Assistant SDK (#85785) Remove import oauth2client, inline 2 constants --- .../application_credentials.py | 6 ++---- .../google_assistant_sdk/test_config_flow.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/application_credentials.py b/homeassistant/components/google_assistant_sdk/application_credentials.py index 74cefb14b65..c8a7922bc7a 100644 --- a/homeassistant/components/google_assistant_sdk/application_credentials.py +++ b/homeassistant/components/google_assistant_sdk/application_credentials.py @@ -1,6 +1,4 @@ """application_credentials platform for Google Assistant SDK.""" -import oauth2client - from homeassistant.components.application_credentials import AuthorizationServer from homeassistant.core import HomeAssistant @@ -8,8 +6,8 @@ from homeassistant.core import HomeAssistant async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: """Return authorization server.""" return AuthorizationServer( - oauth2client.GOOGLE_AUTH_URI, - oauth2client.GOOGLE_TOKEN_URI, + "https://accounts.google.com/o/oauth2/v2/auth", + "https://oauth2.googleapis.com/token", ) diff --git a/tests/components/google_assistant_sdk/test_config_flow.py b/tests/components/google_assistant_sdk/test_config_flow.py index af5f0e73c75..56386df6824 100644 --- a/tests/components/google_assistant_sdk/test_config_flow.py +++ b/tests/components/google_assistant_sdk/test_config_flow.py @@ -1,8 +1,6 @@ """Test the Google Assistant SDK config flow.""" from unittest.mock import patch -import oauth2client - from homeassistant import config_entries from homeassistant.components.google_assistant_sdk.const import DOMAIN from homeassistant.core import HomeAssistant @@ -12,6 +10,8 @@ from .conftest import CLIENT_ID, ComponentSetup from tests.common import MockConfigEntry +GOOGLE_AUTH_URI = "https://accounts.google.com/o/oauth2/v2/auth" +GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token" TITLE = "Google Assistant SDK" @@ -35,7 +35,7 @@ async def test_full_flow( ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/assistant-sdk-prototype" "&access_type=offline&prompt=consent" @@ -47,7 +47,7 @@ async def test_full_flow( assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -112,7 +112,7 @@ async def test_reauth( }, ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/assistant-sdk-prototype" "&access_type=offline&prompt=consent" @@ -123,7 +123,7 @@ async def test_reauth( assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "updated-access-token", @@ -181,7 +181,7 @@ async def test_single_instance_allowed( ) assert result["url"] == ( - f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}&scope=https://www.googleapis.com/auth/assistant-sdk-prototype" "&access_type=offline&prompt=consent" @@ -193,7 +193,7 @@ async def test_single_instance_allowed( assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( - oauth2client.GOOGLE_TOKEN_URI, + GOOGLE_TOKEN_URI, json={ "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", From b0153c7debcf2371f3d65f00ea89882ae3f3f8ff Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 13 Jan 2023 23:50:02 +0200 Subject: [PATCH 145/173] Fix WebOS TV image fetch SSL verify failure (#85841) --- .../components/webostv/media_player.py | 27 ++++++++ tests/components/webostv/test_media_player.py | 66 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 339124142b1..1d7c92741a8 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -1,14 +1,18 @@ """Support for interface with an LG webOS Smart TV.""" from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable, Coroutine from contextlib import suppress from datetime import timedelta from functools import wraps +from http import HTTPStatus import logging +from ssl import SSLContext from typing import Any, TypeVar, cast from aiowebostv import WebOsClient, WebOsTvPairError +import async_timeout from typing_extensions import Concatenate, ParamSpec from homeassistant import util @@ -28,6 +32,7 @@ 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.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -466,3 +471,25 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): async def async_command(self, command: str, **kwargs: Any) -> None: """Send a command.""" await self._client.request(command, payload=kwargs.get(ATTR_PAYLOAD)) + + async def _async_fetch_image(self, url: str) -> tuple[bytes | None, str | None]: + """Retrieve an image. + + webOS uses self-signed certificates, thus we need to use an empty + SSLContext to bypass validation errors if url starts with https. + """ + content = None + ssl_context = None + if url.startswith("https"): + 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() + + if content is None: + _LOGGER.warning("Error retrieving proxied image from %s", url) + + return content, None diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 70549f5d4e8..e4e2e2ba45f 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -1,6 +1,7 @@ """The tests for the LG webOS media player platform.""" import asyncio from datetime import timedelta +from http import HTTPStatus from unittest.mock import Mock import pytest @@ -697,3 +698,68 @@ async def test_supported_features_ignore_cache(hass, client): attrs = hass.states.get(ENTITY_ID).attributes assert attrs[ATTR_SUPPORTED_FEATURES] == supported + + +async def test_get_image_http( + hass, client, hass_client_no_auth, aioclient_mock, monkeypatch +): + """Test get image via http.""" + url = "http://something/valid_icon" + monkeypatch.setitem(client.apps[LIVE_TV_APP_ID], "icon", url) + await setup_webostv(hass) + await client.mock_state_update() + + attrs = hass.states.get(ENTITY_ID).attributes + assert "entity_picture_local" not in attrs + + aioclient_mock.get(url, text="image") + client = await hass_client_no_auth() + + resp = await client.get(attrs["entity_picture"]) + content = await resp.read() + + assert content == b"image" + + +async def test_get_image_http_error( + hass, client, hass_client_no_auth, aioclient_mock, caplog, monkeypatch +): + """Test get image via http error.""" + url = "http://something/icon_error" + monkeypatch.setitem(client.apps[LIVE_TV_APP_ID], "icon", url) + await setup_webostv(hass) + await client.mock_state_update() + + attrs = hass.states.get(ENTITY_ID).attributes + assert "entity_picture_local" not in attrs + + aioclient_mock.get(url, exc=asyncio.TimeoutError()) + client = await hass_client_no_auth() + + resp = await client.get(attrs["entity_picture"]) + content = await resp.read() + + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + assert f"Error retrieving proxied image from {url}" in caplog.text + assert content == b"" + + +async def test_get_image_https( + hass, client, hass_client_no_auth, aioclient_mock, monkeypatch +): + """Test get image via http.""" + url = "https://something/valid_icon_https" + monkeypatch.setitem(client.apps[LIVE_TV_APP_ID], "icon", url) + await setup_webostv(hass) + await client.mock_state_update() + + attrs = hass.states.get(ENTITY_ID).attributes + assert "entity_picture_local" not in attrs + + aioclient_mock.get(url, text="https_image") + client = await hass_client_no_auth() + + resp = await client.get(attrs["entity_picture"]) + content = await resp.read() + + assert content == b"https_image" From d61b9152860f94315ccd24c8e485c9e75e61b58f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Jan 2023 14:03:16 -1000 Subject: [PATCH 146/173] Bump aiohomekit to 2.4.4 (#85853) fixes https://github.com/home-assistant/core/issues/85400 fixes https://github.com/home-assistant/core/issues/84023 --- 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 195e3330c7c..aa343b045ce 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.4.3"], + "requirements": ["aiohomekit==2.4.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index e3c774edd87..d68f8b67459 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -177,7 +177,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.3 +aiohomekit==2.4.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64bf47763b7..a7e9747c28d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.3 +aiohomekit==2.4.4 # homeassistant.components.emulated_hue # homeassistant.components.http From fcf53668c50fa64dcd0002df101302b9f79993c7 Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Sun, 15 Jan 2023 12:12:27 +0100 Subject: [PATCH 147/173] Skip over files without mime type in Jellyfin (#85874) * Skip over files without mime type * Skip over tracks without mime type --- .../components/jellyfin/media_source.py | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index dfb5bd82924..b81b5d81445 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -1,7 +1,9 @@ """The Media Source implementation for the Jellyfin integration.""" from __future__ import annotations +import logging import mimetypes +import os from typing import Any from jellyfin_apiclient_python.api import jellyfin_url @@ -41,6 +43,8 @@ from .const import ( ) from .models import JellyfinData +_LOGGER = logging.getLogger(__name__) + async def async_get_media_source(hass: HomeAssistant) -> MediaSource: """Set up Jellyfin media source.""" @@ -75,6 +79,9 @@ class JellyfinSource(MediaSource): stream_url = self._get_stream_url(media_item) mime_type = _media_mime_type(media_item) + # Media Sources without a mime type have been filtered out during library creation + assert mime_type is not None + return PlayMedia(stream_url, mime_type) async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: @@ -240,7 +247,11 @@ class JellyfinSource(MediaSource): k.get(ITEM_KEY_INDEX_NUMBER, None), ), ) - return [self._build_track(track) for track in tracks] + return [ + self._build_track(track) + for track in tracks + if _media_mime_type(track) is not None + ] def _build_track(self, track: dict[str, Any]) -> BrowseMediaSource: """Return a single track as a browsable media source.""" @@ -289,7 +300,11 @@ class JellyfinSource(MediaSource): """Return all movies in the movie library.""" movies = await self._get_children(library_id, ITEM_TYPE_MOVIE) movies = sorted(movies, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return] - return [self._build_movie(movie) for movie in movies] + return [ + self._build_movie(movie) + for movie in movies + if _media_mime_type(movie) is not None + ] def _build_movie(self, movie: dict[str, Any]) -> BrowseMediaSource: """Return a single movie as a browsable media source.""" @@ -349,20 +364,24 @@ class JellyfinSource(MediaSource): raise BrowseError(f"Unsupported media type {media_type}") -def _media_mime_type(media_item: dict[str, Any]) -> str: +def _media_mime_type(media_item: dict[str, Any]) -> str | None: """Return the mime type of a media item.""" if not media_item.get(ITEM_KEY_MEDIA_SOURCES): - raise BrowseError("Unable to determine mime type for item without media source") + _LOGGER.debug("Unable to determine mime type for item without media source") + return None media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0] if MEDIA_SOURCE_KEY_PATH not in media_source: - raise BrowseError("Unable to determine mime type for media source without path") + _LOGGER.debug("Unable to determine mime type for media source without path") + return None path = media_source[MEDIA_SOURCE_KEY_PATH] mime_type, _ = mimetypes.guess_type(path) if mime_type is None: - raise BrowseError(f"Unable to determine mime type for path {path}") + _LOGGER.debug( + "Unable to determine mime type for path %s", os.path.basename(path) + ) return mime_type From fa09eba165a98c374af4628f906bc197c7656ab7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 1 Jan 2023 06:19:31 -0800 Subject: [PATCH 148/173] Bump google-nest-sdm to 2.1.2 (#84926) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index f0e86456fd7..58db88599fa 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -5,7 +5,7 @@ "dependencies": ["ffmpeg", "http", "application_credentials"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.1.0"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.1.2"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index d68f8b67459..252b436ba2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -794,7 +794,7 @@ google-cloud-pubsub==2.13.11 google-cloud-texttospeech==2.12.3 # homeassistant.components.nest -google-nest-sdm==2.1.0 +google-nest-sdm==2.1.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7e9747c28d..1737d0202c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -601,7 +601,7 @@ goodwe==0.2.18 google-cloud-pubsub==2.13.11 # homeassistant.components.nest -google-nest-sdm==2.1.0 +google-nest-sdm==2.1.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 627ded42f5994f39c96696fc87a9e793555214b1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 14 Jan 2023 19:30:21 -0800 Subject: [PATCH 149/173] Bump google-nest-sdm to 2.2.2 (#85899) * Bump google-nest-sdm to 2.2.0 * Bump nest to 2.2.1 * Bump google-nest-sdm to 2.2.2 --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 58db88599fa..0d02e00dbbf 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -5,7 +5,7 @@ "dependencies": ["ffmpeg", "http", "application_credentials"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.1.2"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.2.2"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 252b436ba2c..b310f080b08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -794,7 +794,7 @@ google-cloud-pubsub==2.13.11 google-cloud-texttospeech==2.12.3 # homeassistant.components.nest -google-nest-sdm==2.1.2 +google-nest-sdm==2.2.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1737d0202c8..3746bb667f0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -601,7 +601,7 @@ goodwe==0.2.18 google-cloud-pubsub==2.13.11 # homeassistant.components.nest -google-nest-sdm==2.1.2 +google-nest-sdm==2.2.2 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 9a6aaea9db6e8480b3a21d2b6295889a81a48546 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 16 Jan 2023 10:52:43 -0800 Subject: [PATCH 150/173] Add a timeout during OAuth token exchange and additional debug logging (#85911) --- homeassistant/components/nest/config_flow.py | 2 + .../helpers/config_entry_oauth2_flow.py | 23 +++++-- homeassistant/strings.json | 1 + .../helpers/test_config_entry_oauth2_flow.py | 61 ++++++++++++++++++- 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index a837290249e..6c1768d9855 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -211,6 +211,7 @@ class NestFlowHandler( async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Complete OAuth setup and finish pubsub or finish.""" + _LOGGER.debug("Finishing post-oauth configuration") assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" self._data.update(data) if self.source == SOURCE_REAUTH: @@ -459,6 +460,7 @@ class NestFlowHandler( async def async_step_finish(self, data: dict[str, Any] | None = None) -> FlowResult: """Create an entry for the SDM flow.""" + _LOGGER.debug("Creating/updating configuration entry") assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" # Update existing config entry when in the reauth flow. if entry := self._async_reauth_entry(): diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 4b135ae6a2f..0a6356d310d 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -41,6 +41,9 @@ MY_AUTH_CALLBACK_PATH = "https://my.home-assistant.io/redirect/oauth" CLOCK_OUT_OF_SYNC_MAX_SEC = 20 +OAUTH_AUTHORIZE_URL_TIMEOUT_SEC = 30 +OAUTH_TOKEN_TIMEOUT_SEC = 30 + class AbstractOAuth2Implementation(ABC): """Base class to abstract OAuth2 authentication.""" @@ -194,6 +197,7 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): if self.client_secret is not None: data["client_secret"] = self.client_secret + _LOGGER.debug("Sending token request to %s", self.token_url) resp = await session.post(self.token_url, data=data) if resp.status >= 400 and _LOGGER.isEnabledFor(logging.DEBUG): body = await resp.text() @@ -283,9 +287,10 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): return self.async_external_step_done(next_step_id=next_step) try: - async with async_timeout.timeout(10): + async with async_timeout.timeout(OAUTH_AUTHORIZE_URL_TIMEOUT_SEC): url = await self.async_generate_authorize_url() - except asyncio.TimeoutError: + except asyncio.TimeoutError as err: + _LOGGER.error("Timeout generating authorize url: %s", err) return self.async_abort(reason="authorize_url_timeout") except NoURLAvailableError: return self.async_abort( @@ -303,7 +308,17 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Create config entry from external data.""" - token = await self.flow_impl.async_resolve_external_data(self.external_data) + _LOGGER.debug("Creating config entry from external data") + + try: + async with async_timeout.timeout(OAUTH_TOKEN_TIMEOUT_SEC): + token = await self.flow_impl.async_resolve_external_data( + self.external_data + ) + except asyncio.TimeoutError as err: + _LOGGER.error("Timeout resolving OAuth token: %s", err) + return self.async_abort(reason="oauth2_timeout") + # Force int for non-compliant oauth2 providers try: token["expires_in"] = int(token["expires_in"]) @@ -436,7 +451,7 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView): await hass.config_entries.flow.async_configure( flow_id=state["flow_id"], user_input=user_input ) - + _LOGGER.debug("Resumed OAuth configuration flow") return web.Response( headers={"content-type": "text/html"}, text="", diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 1c38fb6d064..c00b51ed6cf 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -71,6 +71,7 @@ "no_devices_found": "No devices found on the network", "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages.", "oauth2_error": "Received invalid token data.", + "oauth2_timeout": "Timeout resolving OAuth token.", "oauth2_missing_configuration": "The component is not configured. Please follow the documentation.", "oauth2_missing_credentials": "The integration requires application credentials.", "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 652ce69e57d..f64525ecdd3 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -134,8 +134,9 @@ async def test_abort_if_authorization_timeout( flow = flow_handler() flow.hass = hass - with patch.object( - local_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_timeout.timeout", + side_effect=asyncio.TimeoutError, ): result = await flow.async_step_user() @@ -278,6 +279,62 @@ async def test_abort_if_oauth_rejected( assert result["description_placeholders"] == {"error": "access_denied"} +async def test_abort_on_oauth_timeout_error( + hass, + flow_handler, + local_impl, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, +): + """Check timeout during oauth token exchange.""" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "pick_implementation" + + # Pick implementation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"implementation": TEST_DOMAIN} + ) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=read+write" + ) + + 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" + + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_timeout.timeout", + side_effect=asyncio.TimeoutError, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "oauth2_timeout" + + async def test_step_discovery(hass, flow_handler, local_impl): """Check flow triggers from discovery.""" flow_handler.async_register_implementation(hass, local_impl) From a318576c4f7a2eb820f351e55c4dbfcc4d3341bf Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 15 Jan 2023 13:26:28 +0200 Subject: [PATCH 151/173] Bump aiowebostv to 0.3.1 to fix support for older devices (#85916) Bump aiowebostv to 0.3.1 --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index b4e761b067a..da05a974710 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.3.0"], + "requirements": ["aiowebostv==0.3.1"], "codeowners": ["@bendavid", "@thecode"], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index b310f080b08..4698678cb61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -297,7 +297,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.3.0 +aiowebostv==0.3.1 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3746bb667f0..f7f024c6863 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -272,7 +272,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.3.0 +aiowebostv==0.3.1 # homeassistant.components.yandex_transport aioymaps==1.2.2 From b459261ef2cad523ffc8455f9858e26333eb021d Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 15 Jan 2023 13:29:11 +0200 Subject: [PATCH 152/173] Fix webOS TV SSDP discovery missing friendly name (#85917) --- homeassistant/components/webostv/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index 85da9250539..d04e8a54121 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -116,7 +116,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): host = urlparse(discovery_info.ssdp_location).hostname assert host self._host = host - self._name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] + self._name = discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, DEFAULT_NAME) uuid = discovery_info.upnp[ssdp.ATTR_UPNP_UDN] assert uuid From aa43acb443f9c1980c73d357675431a6fe5d0539 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 15 Jan 2023 20:14:36 +0200 Subject: [PATCH 153/173] Update webOS TV codeowners (#85959) --- CODEOWNERS | 4 ++-- homeassistant/components/webostv/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 464bb252a2e..8fe0ce4e831 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1293,8 +1293,8 @@ build.json @home-assistant/supervisor /tests/components/weather/ @home-assistant/core /homeassistant/components/webhook/ @home-assistant/core /tests/components/webhook/ @home-assistant/core -/homeassistant/components/webostv/ @bendavid @thecode -/tests/components/webostv/ @bendavid @thecode +/homeassistant/components/webostv/ @thecode +/tests/components/webostv/ @thecode /homeassistant/components/websocket_api/ @home-assistant/core /tests/components/websocket_api/ @home-assistant/core /homeassistant/components/wemo/ @esev diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index da05a974710..8c957bd3a09 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", "requirements": ["aiowebostv==0.3.1"], - "codeowners": ["@bendavid", "@thecode"], + "codeowners": ["@thecode"], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", "iot_class": "local_push", From 4138e518ef6ae50ba49dc1fc5667296a603b556f Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 16 Jan 2023 20:51:00 +0200 Subject: [PATCH 154/173] Bump aiowebostv to 0.3.2 (#86031) fixes undefined --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 8c957bd3a09..26fca61efe0 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.3.1"], + "requirements": ["aiowebostv==0.3.2"], "codeowners": ["@thecode"], "ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 4698678cb61..9305f61cebb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -297,7 +297,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.3.1 +aiowebostv==0.3.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7f024c6863..c4a8886141c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -272,7 +272,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.3.1 +aiowebostv==0.3.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 From 96578f3f892721bdefb2ed31464e8f6645ae4448 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Jan 2023 11:21:49 -1000 Subject: [PATCH 155/173] Handle ignored shelly entries when discovering via zeroconf (#86039) fixes https://github.com/home-assistant/core/issues/85879 --- .../components/shelly/config_flow.py | 2 +- tests/components/shelly/test_config_flow.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 70d2c2492e8..f6be4a254c6 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -217,7 +217,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Abort and reconnect soon if the device with the mac address is already configured.""" if ( current_entry := await self.async_set_unique_id(mac) - ) and current_entry.data[CONF_HOST] == host: + ) and current_entry.data.get(CONF_HOST) == host: await async_reconnect_soon(self.hass, current_entry) if host == INTERNAL_WIFI_AP_IP: # If the device is broadcasting the internal wifi ap ip diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1c0a32853e1..7338747cbaf 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -706,6 +706,30 @@ async def test_zeroconf_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_zeroconf_ignored(hass): + """Test zeroconf when the device was previously ignored.""" + + entry = MockConfigEntry( + domain="shelly", + unique_id="test-mac", + data={}, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + async def test_zeroconf_with_wifi_ap_ip(hass): """Test we ignore the Wi-FI AP IP.""" From 5656129b604c51b6ef82047ba8d636b0aec2fcde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 16 Jan 2023 22:16:34 +0100 Subject: [PATCH 156/173] Update pyTibber to 0.26.8 (#86044) --- 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 115f3ed7d2e..403b0f2b4fc 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.26.8"], + "requirements": ["pyTibber==0.26.9"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 9305f61cebb..155cc3a9830 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1439,7 +1439,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.8 +pyTibber==0.26.9 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4a8886141c..33da328a6da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1039,7 +1039,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.8 +pyTibber==0.26.9 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From 6a7e6ad0fdbcccfd761f5b4e09a15c9c1f545866 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Jan 2023 21:47:10 -0500 Subject: [PATCH 157/173] Bumped version to 2023.1.5 --- 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 2e8992e14b1..d9eb4bb8534 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index ec14caa7515..c1d8bb8caa2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.4" +version = "2023.1.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 669e6202adb134943a8a9ae785750ac7c27d36e0 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 10 Jan 2023 09:44:53 +0100 Subject: [PATCH 158/173] bump reolink-aio to 0.2.1 (#85571) --- 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 dfa8dfe8e6b..6fb26ea60fe 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.1.3"], + "requirements": ["reolink-aio==0.2.1"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", "loggers": ["reolink-aio"] diff --git a/requirements_all.txt b/requirements_all.txt index 155cc3a9830..5e812d80058 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2190,7 +2190,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.3 +reolink-aio==0.2.1 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33da328a6da..214c868043c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1529,7 +1529,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.1.3 +reolink-aio==0.2.1 # homeassistant.components.python_script restrictedpython==5.2 From d935f9400dfa1f5f7dee481805a627c2c10b0ddf Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 17 Jan 2023 15:39:42 +0100 Subject: [PATCH 159/173] Fix Matter unique_id generation (#86046) * bae entity unique id on Operational Instance Name standard * Update homeassistant/components/matter/entity.py Co-authored-by: Stefan Agner * also adjust unique id for devices * final adjustment * remove assert on server_info * move device info to init * fabric_id_hex * use DeviceInfo instead of dict * fix test Co-authored-by: Stefan Agner --- homeassistant/components/matter/adapter.py | 65 +++++++++++++--------- homeassistant/components/matter/entity.py | 22 ++++---- homeassistant/components/matter/helpers.py | 28 ++++++++++ tests/components/matter/test_adapter.py | 5 +- 4 files changed, 81 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index b573ed0a3fc..b07b489e029 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -1,11 +1,15 @@ """Matter to Home Assistant adapter.""" from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from chip.clusters import Objects as all_clusters from matter_server.common.models.events import EventType -from matter_server.common.models.node_device import AbstractMatterNodeDevice +from matter_server.common.models.node_device import ( + AbstractMatterNodeDevice, + MatterBridgedNodeDevice, +) +from matter_server.common.models.server_information import ServerInfo from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -15,6 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, LOGGER from .device_platform import DEVICE_PLATFORM +from .helpers import get_device_id if TYPE_CHECKING: from matter_server.client import MatterClient @@ -66,31 +71,49 @@ class MatterAdapter: bridge_unique_id: str | None = None if node.aggregator_device_type_instance is not None and ( - node_info := node.root_device_type_instance.get_cluster(all_clusters.Basic) + node.root_device_type_instance.get_cluster(all_clusters.Basic) ): - self._create_device_registry( - node_info, node_info.nodeLabel or "Hub device", None + # create virtual (parent) device for bridge node device + bridge_device = MatterBridgedNodeDevice( + node.aggregator_device_type_instance ) - bridge_unique_id = node_info.uniqueID + self._create_device_registry(bridge_device) + server_info = cast(ServerInfo, self.matter_client.server_info) + bridge_unique_id = get_device_id(server_info, bridge_device) for node_device in node.node_devices: self._setup_node_device(node_device, bridge_unique_id) def _create_device_registry( self, - info: all_clusters.Basic | all_clusters.BridgedDeviceBasic, - name: str, - bridge_unique_id: str | None, + node_device: AbstractMatterNodeDevice, + bridge_unique_id: str | None = None, ) -> None: """Create a device registry entry.""" + server_info = cast(ServerInfo, self.matter_client.server_info) + node_unique_id = get_device_id( + server_info, + node_device, + ) + basic_info = node_device.device_info() + device_type_instances = node_device.device_type_instances() + + name = basic_info.nodeLabel + if not name and isinstance(node_device, MatterBridgedNodeDevice): + # fallback name for Bridge + name = "Hub device" + elif not name and device_type_instances: + # fallback name based on device type + name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node_device.node().node_id}" + dr.async_get(self.hass).async_get_or_create( name=name, config_entry_id=self.config_entry.entry_id, - identifiers={(DOMAIN, info.uniqueID)}, - hw_version=info.hardwareVersionString, - sw_version=info.softwareVersionString, - manufacturer=info.vendorName, - model=info.productName, + identifiers={(DOMAIN, node_unique_id)}, + hw_version=basic_info.hardwareVersionString, + sw_version=basic_info.softwareVersionString, + manufacturer=basic_info.vendorName, + model=basic_info.productName, via_device=(DOMAIN, bridge_unique_id) if bridge_unique_id else None, ) @@ -98,17 +121,9 @@ class MatterAdapter: self, node_device: AbstractMatterNodeDevice, bridge_unique_id: str | None ) -> None: """Set up a node device.""" - node = node_device.node() - basic_info = node_device.device_info() - device_type_instances = node_device.device_type_instances() - - name = basic_info.nodeLabel - if not name and device_type_instances: - name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node.node_id}" - - self._create_device_registry(basic_info, name, bridge_unique_id) - - for instance in device_type_instances: + self._create_device_registry(node_device, bridge_unique_id) + # run platform discovery from device type instances + for instance in node_device.device_type_instances(): created = False for platform, devices in DEVICE_PLATFORM.items(): diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 4f28c1d2369..fd839dcca5e 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -5,16 +5,18 @@ from abc import abstractmethod from collections.abc import Callable from dataclasses import dataclass import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from matter_server.common.models.device_type_instance import MatterDeviceTypeInstance from matter_server.common.models.events import EventType from matter_server.common.models.node_device import AbstractMatterNodeDevice +from matter_server.common.models.server_information import ServerInfo from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from .const import DOMAIN +from .helpers import get_device_id, get_operational_instance_id if TYPE_CHECKING: from matter_server.client import MatterClient @@ -55,24 +57,20 @@ class MatterEntity(Entity): self._node_device = node_device self._device_type_instance = device_type_instance self.entity_description = entity_description - node = device_type_instance.node self._unsubscribes: list[Callable] = [] # for fast lookups we create a mapping to the attribute paths - self._attributes_map: dict[type, str] = {} - server_info = matter_client.server_info # The server info is set when the client connects to the server. - assert server_info is not None + self._attributes_map: dict[type, str] = {} + server_info = cast(ServerInfo, self.matter_client.server_info) + # create unique_id based on "Operational Instance Name" and endpoint/device type self._attr_unique_id = ( - f"{server_info.compressed_fabric_id}-" - f"{node.unique_id}-" + f"{get_operational_instance_id(server_info, self._node_device.node())}-" f"{device_type_instance.endpoint}-" f"{device_type_instance.device_type.device_type}" ) - - @property - def device_info(self) -> DeviceInfo | None: - """Return device info for device registry.""" - return {"identifiers": {(DOMAIN, self._node_device.device_info().uniqueID)}} + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, get_device_id(server_info, node_device))} + ) async def async_added_to_hass(self) -> None: """Handle being added to Home Assistant.""" diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 479b1d824ad..8dd20538a39 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -10,6 +10,10 @@ from homeassistant.core import HomeAssistant, callback from .const import DOMAIN if TYPE_CHECKING: + from matter_server.common.models.node import MatterNode + from matter_server.common.models.node_device import AbstractMatterNodeDevice + from matter_server.common.models.server_information import ServerInfo + from .adapter import MatterAdapter @@ -29,3 +33,27 @@ def get_matter(hass: HomeAssistant) -> MatterAdapter: # In case of the config entry we need to fix this. matter_entry_data: MatterEntryData = next(iter(hass.data[DOMAIN].values())) return matter_entry_data.adapter + + +def get_operational_instance_id( + server_info: ServerInfo, + node: MatterNode, +) -> str: + """Return `Operational Instance Name` for given MatterNode.""" + fabric_id_hex = f"{server_info.compressed_fabric_id:016X}" + node_id_hex = f"{node.node_id:016X}" + # operational instance id matches the mdns advertisement for the node + # this is the recommended ID to recognize a unique matter node (within a fabric) + return f"{fabric_id_hex}-{node_id_hex}" + + +def get_device_id( + server_info: ServerInfo, + node_device: AbstractMatterNodeDevice, +) -> str: + """Return HA device_id for the given MatterNodeDevice.""" + operational_instance_id = get_operational_instance_id( + server_info, node_device.node() + ) + # append nodedevice(type) to differentiate between a root node and bridge within HA devices. + return f"{operational_instance_id}-{node_device.__class__.__name__}" diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index 6bd341b0f2f..c89b45e4c0b 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -27,8 +27,9 @@ async def test_device_registry_single_node_device( ) dev_reg = dr.async_get(hass) - - entry = dev_reg.async_get_device({(DOMAIN, "mock-onoff-light")}) + entry = dev_reg.async_get_device( + {(DOMAIN, "00000000000004D2-0000000000000001-MatterNodeDevice")} + ) assert entry is not None assert entry.name == "Mock OnOff Light" From 15a35004ddd0b624cec8f26af95f926665f07725 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Jan 2023 19:09:12 +0100 Subject: [PATCH 160/173] Code styling tweaks to the Matter integration (#86096) --- homeassistant/components/matter/adapter.py | 5 ++++- homeassistant/components/matter/entity.py | 2 +- homeassistant/components/matter/helpers.py | 11 ++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index b07b489e029..16e7d456212 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -104,7 +104,10 @@ class MatterAdapter: name = "Hub device" elif not name and device_type_instances: # fallback name based on device type - name = f"{device_type_instances[0].device_type.__doc__[:-1]} {node_device.node().node_id}" + name = ( + f"{device_type_instances[0].device_type.__doc__[:-1]}" + f" {node_device.node().node_id}" + ) dr.async_get(self.hass).async_get_or_create( name=name, diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index fd839dcca5e..f239cec0342 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -113,7 +113,7 @@ class MatterEntity(Entity): @callback def get_matter_attribute(self, attribute: type) -> MatterAttribute | None: - """Lookup MatterAttribute instance on device instance by providing the attribute class.""" + """Lookup MatterAttribute on device by providing the attribute class.""" return next( ( x diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 8dd20538a39..5abf81ee608 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -29,8 +29,8 @@ class MatterEntryData: def get_matter(hass: HomeAssistant) -> MatterAdapter: """Return MatterAdapter instance.""" # NOTE: This assumes only one Matter connection/fabric can exist. - # Shall we support connecting to multiple servers in the client or by config entries? - # In case of the config entry we need to fix this. + # Shall we support connecting to multiple servers in the client or by + # config entries? In case of the config entry we need to fix this. matter_entry_data: MatterEntryData = next(iter(hass.data[DOMAIN].values())) return matter_entry_data.adapter @@ -42,8 +42,8 @@ def get_operational_instance_id( """Return `Operational Instance Name` for given MatterNode.""" fabric_id_hex = f"{server_info.compressed_fabric_id:016X}" node_id_hex = f"{node.node_id:016X}" - # operational instance id matches the mdns advertisement for the node - # this is the recommended ID to recognize a unique matter node (within a fabric) + # Operational instance id matches the mDNS advertisement for the node + # this is the recommended ID to recognize a unique matter node (within a fabric). return f"{fabric_id_hex}-{node_id_hex}" @@ -55,5 +55,6 @@ def get_device_id( operational_instance_id = get_operational_instance_id( server_info, node_device.node() ) - # append nodedevice(type) to differentiate between a root node and bridge within HA devices. + # Append nodedevice(type) to differentiate between a root node + # and bridge within Home Assistant devices. return f"{operational_instance_id}-{node_device.__class__.__name__}" From 67f7a9ea78181a32db87c8dafb59691084d1d0fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Jan 2023 09:48:21 -1000 Subject: [PATCH 161/173] Bump govee-ble to 0.21.1 (#86103) fixes https://github.com/home-assistant/core/issues/85580 --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index ad54aa4bc43..aaa659d1667 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -73,7 +73,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.21.0"], + "requirements": ["govee-ble==0.21.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco", "@PierreAronnax"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 5e812d80058..3f6fecf57da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -803,7 +803,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.21.0 +govee-ble==0.21.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 214c868043c..fa5c72da0af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -607,7 +607,7 @@ google-nest-sdm==2.2.2 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.21.0 +govee-ble==0.21.1 # homeassistant.components.gree greeclimate==1.3.0 From 2c127c00d4e4c84b88449e9b96cd8486da89a44d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 18 Jan 2023 17:17:33 +0100 Subject: [PATCH 162/173] Adjust device registry for Matter devices (#86108) * adjust device registry * ignore test unique id * update test * ditch uniqueid + prefix serial * adjust test * add tests * fix switch test * prefix all identifiers * Update homeassistant/components/matter/adapter.py Co-authored-by: Martin Hjelmare * no underscore in id * fix test Co-authored-by: Martin Hjelmare --- homeassistant/components/matter/adapter.py | 26 ++++++++++------- homeassistant/components/matter/const.py | 4 +++ homeassistant/components/matter/entity.py | 5 ++-- .../fixtures/nodes/on-off-plugin-unit.json | 2 +- .../matter/fixtures/nodes/onoff-light.json | 2 +- tests/components/matter/test_adapter.py | 29 ++++++++++++++++++- tests/components/matter/test_switch.py | 10 +++---- 7 files changed, 57 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 16e7d456212..08763e38327 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, LOGGER +from .const import DOMAIN, ID_TYPE_DEVICE_ID, ID_TYPE_SERIAL, LOGGER from .device_platform import DEVICE_PLATFORM from .helpers import get_device_id @@ -91,10 +91,7 @@ class MatterAdapter: ) -> None: """Create a device registry entry.""" server_info = cast(ServerInfo, self.matter_client.server_info) - node_unique_id = get_device_id( - server_info, - node_device, - ) + basic_info = node_device.device_info() device_type_instances = node_device.device_type_instances() @@ -103,16 +100,23 @@ class MatterAdapter: # fallback name for Bridge name = "Hub device" elif not name and device_type_instances: - # fallback name based on device type - name = ( - f"{device_type_instances[0].device_type.__doc__[:-1]}" - f" {node_device.node().node_id}" - ) + # use the productName if no node label is present + name = basic_info.productName + + node_device_id = get_device_id( + server_info, + node_device, + ) + identifiers = {(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")} + # if available, we also add the serialnumber as identifier + if basic_info.serialNumber and "test" not in basic_info.serialNumber.lower(): + # prefix identifier with 'serial_' to be able to filter it + identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info.serialNumber}")) dr.async_get(self.hass).async_get_or_create( name=name, config_entry_id=self.config_entry.entry_id, - identifiers={(DOMAIN, node_unique_id)}, + identifiers=identifiers, hw_version=basic_info.hardwareVersionString, sw_version=basic_info.softwareVersionString, manufacturer=basic_info.vendorName, diff --git a/homeassistant/components/matter/const.py b/homeassistant/components/matter/const.py index c5ec1173ac0..e7f96bd2448 100644 --- a/homeassistant/components/matter/const.py +++ b/homeassistant/components/matter/const.py @@ -8,3 +8,7 @@ CONF_USE_ADDON = "use_addon" DOMAIN = "matter" LOGGER = logging.getLogger(__package__) + +# prefixes to identify device identifier id types +ID_TYPE_DEVICE_ID = "deviceid" +ID_TYPE_SERIAL = "serial" diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index f239cec0342..820d0f72846 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -15,7 +15,7 @@ from matter_server.common.models.server_information import ServerInfo from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from .const import DOMAIN +from .const import DOMAIN, ID_TYPE_DEVICE_ID from .helpers import get_device_id, get_operational_instance_id if TYPE_CHECKING: @@ -68,8 +68,9 @@ class MatterEntity(Entity): f"{device_type_instance.endpoint}-" f"{device_type_instance.device_type.device_type}" ) + node_device_id = get_device_id(server_info, node_device) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, get_device_id(server_info, node_device))} + identifiers={(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")} ) async def async_added_to_hass(self) -> None: diff --git a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json index cbbe39b1f09..e26450a9a28 100644 --- a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json +++ b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json @@ -175,7 +175,7 @@ "attribute_id": 5, "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", "attribute_name": "NodeLabel", - "value": "Mock OnOff Plugin Unit" + "value": "" }, "0/40/6": { "node_id": 1, diff --git a/tests/components/matter/fixtures/nodes/onoff-light.json b/tests/components/matter/fixtures/nodes/onoff-light.json index cc6521aa2e3..340d7cb71c9 100644 --- a/tests/components/matter/fixtures/nodes/onoff-light.json +++ b/tests/components/matter/fixtures/nodes/onoff-light.json @@ -469,7 +469,7 @@ "attribute_id": 15, "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", "attribute_name": "SerialNumber", - "value": "TEST_SN" + "value": "12345678" }, "0/40/16": { "node_id": 1, diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index c89b45e4c0b..f83f21dc5e0 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -28,10 +28,13 @@ async def test_device_registry_single_node_device( dev_reg = dr.async_get(hass) entry = dev_reg.async_get_device( - {(DOMAIN, "00000000000004D2-0000000000000001-MatterNodeDevice")} + {(DOMAIN, "deviceid_00000000000004D2-0000000000000001-MatterNodeDevice")} ) assert entry is not None + # test serial id present as additional identifier + assert (DOMAIN, "serial_12345678") in entry.identifiers + assert entry.name == "Mock OnOff Light" assert entry.manufacturer == "Nabu Casa" assert entry.model == "Mock Light" @@ -39,6 +42,30 @@ async def test_device_registry_single_node_device( assert entry.sw_version == "v1.0" +async def test_device_registry_single_node_device_alt( + hass: HomeAssistant, + matter_client: MagicMock, +) -> None: + """Test additional device with different attribute values.""" + await setup_integration_with_node_fixture( + hass, + "on-off-plugin-unit", + matter_client, + ) + + dev_reg = dr.async_get(hass) + entry = dev_reg.async_get_device( + {(DOMAIN, "deviceid_00000000000004D2-0000000000000001-MatterNodeDevice")} + ) + assert entry is not None + + # test name is derived from productName (because nodeLabel is absent) + assert entry.name == "Mock OnOffPluginUnit (powerplug/switch)" + + # test serial id NOT present as additional identifier + assert (DOMAIN, "serial_TEST_SN") not in entry.identifiers + + @pytest.mark.skip("Waiting for a new test fixture") async def test_device_registry_bridge( hass: HomeAssistant, diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py index a79edd6010b..9fe225b1b13 100644 --- a/tests/components/matter/test_switch.py +++ b/tests/components/matter/test_switch.py @@ -30,7 +30,7 @@ async def test_turn_on( switch_node: MatterNode, ) -> None: """Test turning on a switch.""" - state = hass.states.get("switch.mock_onoff_plugin_unit") + state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") assert state assert state.state == "off" @@ -38,7 +38,7 @@ async def test_turn_on( "switch", "turn_on", { - "entity_id": "switch.mock_onoff_plugin_unit", + "entity_id": "switch.mock_onoffpluginunit_powerplug_switch", }, blocking=True, ) @@ -53,7 +53,7 @@ async def test_turn_on( set_node_attribute(switch_node, 1, 6, 0, True) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("switch.mock_onoff_plugin_unit") + state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") assert state assert state.state == "on" @@ -64,7 +64,7 @@ async def test_turn_off( switch_node: MatterNode, ) -> None: """Test turning off a switch.""" - state = hass.states.get("switch.mock_onoff_plugin_unit") + state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") assert state assert state.state == "off" @@ -72,7 +72,7 @@ async def test_turn_off( "switch", "turn_off", { - "entity_id": "switch.mock_onoff_plugin_unit", + "entity_id": "switch.mock_onoffpluginunit_powerplug_switch", }, blocking=True, ) From 58beab1b596376a3bc22027829386a9d46f931a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 Jan 2023 16:06:37 -1000 Subject: [PATCH 163/173] Fix live logbook stalling when there are no historical events with a high commit interval (#86110) * Force live logbook to send an empty message to indicate no results Since the sync task can take a while if the recorder is busy, the logbook will appear to hang if we do not send the first partial message even if its empty. This work is in preparation for a higher database commit interval where this issue is most obvious. The historical only path did not have this issue because it never had to wait for the db sync. * update tests --- .../components/logbook/websocket_api.py | 10 ++++++++-- tests/components/logbook/test_websocket_api.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 8d0dd49bff8..04b288d523b 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -83,6 +83,7 @@ async def _async_send_historical_events( formatter: Callable[[int, Any], dict[str, Any]], event_processor: EventProcessor, partial: bool, + force_send: bool = False, ) -> dt | None: """Select historical data from the database and deliver it to the websocket. @@ -116,7 +117,7 @@ async def _async_send_historical_events( # if its the last one (not partial) so # consumers of the api know their request was # answered but there were no results - if last_event_time or not partial: + if last_event_time or not partial or force_send: connection.send_message(message) return last_event_time @@ -150,7 +151,7 @@ async def _async_send_historical_events( # if its the last one (not partial) so # consumers of the api know their request was # answered but there were no results - if older_query_last_event_time or not partial: + if older_query_last_event_time or not partial or force_send: connection.send_message(older_message) # Returns the time of the newest event @@ -384,6 +385,11 @@ async def ws_event_stream( messages.event_message, event_processor, partial=True, + # Force a send since the wait for the sync task + # can take a a while if the recorder is busy and + # we want to make sure the client is not still spinning + # because it is waiting for the first message + force_send=True, ) live_stream.task = asyncio.create_task( diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 5b16c98998c..91d1a95f75b 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -1817,6 +1817,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["id"] == 7 assert msg["type"] == TYPE_RESULT assert msg["success"] + await async_wait_recording_done(hass) # There are no answers to our initial query # so we get an empty reply. This is to ensure @@ -1828,6 +1829,15 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"]["events"] == [] + assert "partial" in msg["event"] + await async_wait_recording_done(hass) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + assert "partial" not in msg["event"] + await async_wait_recording_done(hass) hass.states.async_set("binary_sensor.should_not_appear", STATE_ON) hass.states.async_set("binary_sensor.should_not_appear", STATE_OFF) @@ -1942,6 +1952,14 @@ async def test_logbook_stream_match_multiple_entities( assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"]["events"] == [] + assert "partial" in msg["event"] + await async_wait_recording_done(hass) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + assert "partial" not in msg["event"] await async_wait_recording_done(hass) hass.states.async_set("binary_sensor.should_not_appear", STATE_ON) From 8dd0752bd0d148add734edb746738a6a7ea6e4d5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 19 Jan 2023 01:11:40 +0200 Subject: [PATCH 164/173] Fix Shelly sleeping Gen2 device updates (#86198) --- homeassistant/components/shelly/coordinator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 12a46ee3ef9..96852512334 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -475,6 +475,11 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): async def _async_disconnected(self) -> None: """Handle device disconnected.""" + # Sleeping devices send data and disconnects + # There are no disconnect events for sleeping devices + if self.entry.data.get(CONF_SLEEP_PERIOD): + return + async with self._connection_lock: if not self.connected: # Already disconnected return From 82a13740b33904e106fabcf1407c4587ffb9bb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Thu, 19 Jan 2023 22:07:08 +0100 Subject: [PATCH 165/173] Update allowlisted OAuth redirect URIs for Wear OS in China (#86247) --- homeassistant/components/auth/indieauth.py | 1 + tests/components/auth/test_indieauth.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 478f7ab2831..ec8431366ab 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -47,6 +47,7 @@ async def verify_redirect_uri( if client_id == "https://home-assistant.io/android" and redirect_uri in ( "homeassistant://auth-callback", "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", + "https://wear.googleapis-cn.com/3p_auth/io.homeassistant.companion.android", ): return True diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index 17d1fa927a0..43bd6b71fe5 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -190,9 +190,19 @@ async def test_verify_redirect_uri_android_ios(client_id): client_id, "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", ) + assert await indieauth.verify_redirect_uri( + None, + client_id, + "https://wear.googleapis-cn.com/3p_auth/io.homeassistant.companion.android", + ) else: assert not await indieauth.verify_redirect_uri( None, client_id, "https://wear.googleapis.com/3p_auth/io.homeassistant.companion.android", ) + assert not await indieauth.verify_redirect_uri( + None, + client_id, + "https://wear.googleapis-cn.com/3p_auth/io.homeassistant.companion.android", + ) From aa7e05153807de1b2fc6d1767fcdec654d36ffd2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Jan 2023 16:09:03 -0500 Subject: [PATCH 166/173] Bumped version to 2023.1.6 --- 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 d9eb4bb8534..d14ce310238 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index c1d8bb8caa2..6fd631b63c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.5" +version = "2023.1.6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e0d0dc05e4c251b410eaf44d0710f2ad0bd21ba5 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 22 Jan 2023 17:20:12 +0100 Subject: [PATCH 167/173] Support password less PI-Hole installations (#86183) --- homeassistant/components/pi_hole/__init__.py | 4 -- .../components/pi_hole/config_flow.py | 26 +++++++- homeassistant/components/pi_hole/strings.json | 6 +- .../components/pi_hole/translations/en.json | 6 +- tests/components/pi_hole/__init__.py | 19 +++++- tests/components/pi_hole/test_config_flow.py | 64 +++++++++++++------ tests/components/pi_hole/test_init.py | 15 +---- 7 files changed, 95 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ba7949c0c30..b33b9438354 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -117,10 +117,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data.pop(CONF_STATISTICS_ONLY) hass.config_entries.async_update_entry(entry, data=entry_data) - # start reauth to force api key is present - if CONF_API_KEY not in entry.data: - raise ConfigEntryAuthFailed - _LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) session = async_get_clientsession(hass, verify_tls) diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 637f906b9ee..519ef5a7628 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -56,7 +56,6 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_LOCATION: user_input[CONF_LOCATION], CONF_SSL: user_input[CONF_SSL], CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], - CONF_API_KEY: user_input[CONF_API_KEY], } self._async_abort_entries_match( @@ -71,6 +70,9 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title=user_input[CONF_NAME], data=self._config ) + if CONF_API_KEY in errors: + return await self.async_step_api_key() + user_input = user_input or {} return self.async_show_form( step_id="user", @@ -80,7 +82,6 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required( CONF_PORT, default=user_input.get(CONF_PORT, 80) ): vol.Coerce(int), - vol.Required(CONF_API_KEY): str, vol.Required( CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) ): str, @@ -101,6 +102,25 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_api_key( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle step to setup API key.""" + errors = {} + if user_input is not None: + self._config[CONF_API_KEY] = user_input[CONF_API_KEY] + if not (errors := await self._async_try_connect()): + return self.async_create_entry( + title=self._config[CONF_NAME], + data=self._config, + ) + + return self.async_show_form( + step_id="api_key", + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, + ) + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Handle a flow initiated by import.""" @@ -178,7 +198,7 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): session, location=self._config[CONF_LOCATION], tls=self._config[CONF_SSL], - api_token=self._config[CONF_API_KEY], + api_token=self._config.get(CONF_API_KEY), ) try: await pi_hole.get_data() diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json index 120ab8cb80a..2f04b8fe47e 100644 --- a/homeassistant/components/pi_hole/strings.json +++ b/homeassistant/components/pi_hole/strings.json @@ -7,11 +7,15 @@ "port": "[%key:common::config_flow::data::port%]", "name": "[%key:common::config_flow::data::name%]", "location": "[%key:common::config_flow::data::location%]", - "api_key": "[%key:common::config_flow::data::api_key%]", "ssl": "[%key:common::config_flow::data::ssl%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" } }, + "api_key": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, "reauth_confirm": { "title": "PI-Hole [%key:common::config_flow::title::reauth%]", "description": "Please enter a new api key for PI-Hole at {host}/{location}", diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 815182731c2..940e3950281 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -9,6 +9,11 @@ "invalid_auth": "Invalid authentication" }, "step": { + "api_key": { + "data": { + "api_key": "API Key" + } + }, "reauth_confirm": { "data": { "api_key": "API Key" @@ -18,7 +23,6 @@ }, "user": { "data": { - "api_key": "API Key", "host": "Host", "location": "Location", "name": "Name", diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 49e15391f8c..8658fc84ffe 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -74,7 +74,6 @@ CONFIG_DATA = { CONFIG_FLOW_USER = { CONF_HOST: HOST, CONF_PORT: PORT, - CONF_API_KEY: API_KEY, CONF_LOCATION: LOCATION, CONF_NAME: NAME, CONF_SSL: SSL, @@ -85,7 +84,7 @@ CONFIG_FLOW_API_KEY = { CONF_API_KEY: API_KEY, } -CONFIG_ENTRY = { +CONFIG_ENTRY_WITH_API_KEY = { CONF_HOST: f"{HOST}:{PORT}", CONF_LOCATION: LOCATION, CONF_NAME: NAME, @@ -94,7 +93,15 @@ CONFIG_ENTRY = { CONF_VERIFY_SSL: VERIFY_SSL, } -CONFIG_ENTRY_IMPORTED = {**CONFIG_ENTRY, CONF_STATISTICS_ONLY: False} +CONFIG_ENTRY_WITHOUT_API_KEY = { + CONF_HOST: f"{HOST}:{PORT}", + CONF_LOCATION: LOCATION, + CONF_NAME: NAME, + CONF_SSL: SSL, + CONF_VERIFY_SSL: VERIFY_SSL, +} + +CONFIG_ENTRY_IMPORTED = {**CONFIG_ENTRY_WITH_API_KEY, CONF_STATISTICS_ONLY: False} SWITCH_ENTITY_ID = "switch.pi_hole" @@ -128,3 +135,9 @@ def _patch_config_flow_hole(mocked_hole): return patch( "homeassistant.components.pi_hole.config_flow.Hole", return_value=mocked_hole ) + + +def _patch_setup_hole(): + return patch( + "homeassistant.components.pi_hole.async_setup_entry", return_value=True + ) diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 65f21418bad..8c38803de64 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -1,6 +1,5 @@ """Test pi_hole config flow.""" import logging -from unittest.mock import patch from homeassistant.components.pi_hole.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER @@ -11,30 +10,26 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( CONFIG_DATA, CONFIG_DATA_DEFAULTS, - CONFIG_ENTRY, CONFIG_ENTRY_IMPORTED, + CONFIG_ENTRY_WITH_API_KEY, + CONFIG_ENTRY_WITHOUT_API_KEY, + CONFIG_FLOW_API_KEY, CONFIG_FLOW_USER, NAME, ZERO_DATA, _create_mocked_hole, _patch_config_flow_hole, _patch_init_hole, + _patch_setup_hole, ) from tests.common import MockConfigEntry -def _patch_setup(): - return patch( - "homeassistant.components.pi_hole.async_setup_entry", - return_value=True, - ) - - async def test_flow_import(hass: HomeAssistant, caplog): """Test import flow.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_setup(): + with _patch_config_flow_hole(mocked_hole), _patch_setup_hole(): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_DATA ) @@ -53,7 +48,7 @@ async def test_flow_import(hass: HomeAssistant, caplog): async def test_flow_import_invalid(hass: HomeAssistant, caplog): """Test import flow with invalid server.""" mocked_hole = _create_mocked_hole(True) - with _patch_config_flow_hole(mocked_hole), _patch_setup(): + with _patch_config_flow_hole(mocked_hole), _patch_setup_hole(): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_DATA ) @@ -62,10 +57,10 @@ async def test_flow_import_invalid(hass: HomeAssistant, caplog): assert len([x for x in caplog.records if x.levelno == logging.ERROR]) == 1 -async def test_flow_user(hass: HomeAssistant): - """Test user initialized flow.""" +async def test_flow_user_with_api_key(hass: HomeAssistant): + """Test user initialized flow with api key needed.""" mocked_hole = _create_mocked_hole(has_data=False) - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): + with _patch_config_flow_hole(mocked_hole), _patch_setup_hole() as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -79,17 +74,26 @@ async def test_flow_user(hass: HomeAssistant): user_input=CONFIG_FLOW_USER, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "api_key" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_API_KEY: "some_key"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "api_key" assert result["errors"] == {CONF_API_KEY: "invalid_auth"} mocked_hole.data = ZERO_DATA result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONFIG_FLOW_USER, + user_input=CONFIG_FLOW_API_KEY, ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME - assert result["data"] == CONFIG_ENTRY + assert result["data"] == CONFIG_ENTRY_WITH_API_KEY + mock_setup.assert_called_once() # duplicated server result = await hass.config_entries.flow.async_init( @@ -101,10 +105,32 @@ async def test_flow_user(hass: HomeAssistant): assert result["reason"] == "already_configured" -async def test_flow_user_invalid(hass): +async def test_flow_user_without_api_key(hass: HomeAssistant): + """Test user initialized flow without api key needed.""" + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_setup_hole() as mock_setup: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONFIG_FLOW_USER, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == CONFIG_ENTRY_WITHOUT_API_KEY + mock_setup.assert_called_once() + + +async def test_flow_user_invalid(hass: HomeAssistant): """Test user initialized flow with invalid server.""" mocked_hole = _create_mocked_hole(True) - with _patch_config_flow_hole(mocked_hole), _patch_setup(): + with _patch_config_flow_hole(mocked_hole): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_FLOW_USER ) diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index 75d9dd27aee..b863898db25 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -10,8 +10,7 @@ from homeassistant.components.pi_hole.const import ( SERVICE_DISABLE, SERVICE_DISABLE_ATTR_DURATION, ) -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY, CONF_HOST +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -200,15 +199,3 @@ async def test_remove_obsolete(hass: HomeAssistant): with _patch_init_hole(mocked_hole): assert await hass.config_entries.async_setup(entry.entry_id) assert CONF_STATISTICS_ONLY not in entry.data - - -async def test_missing_api_key(hass: HomeAssistant): - """Tests start reauth flow if api key is missing.""" - mocked_hole = _create_mocked_hole() - data = CONFIG_DATA_DEFAULTS.copy() - data.pop(CONF_API_KEY) - entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=data) - entry.add_to_hass(hass) - with _patch_init_hole(mocked_hole): - assert not await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ConfigEntryState.SETUP_ERROR From 72dae914fcca9c131f3d827227c7db26dcc10ec7 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 20 Jan 2023 03:26:51 +0100 Subject: [PATCH 168/173] Bump odp-amsterdam to v5.0.1 (#86252) Bump package version to v5.0.1 --- homeassistant/components/garages_amsterdam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/garages_amsterdam/manifest.json b/homeassistant/components/garages_amsterdam/manifest.json index 1d6e91293db..a889ed062a8 100644 --- a/homeassistant/components/garages_amsterdam/manifest.json +++ b/homeassistant/components/garages_amsterdam/manifest.json @@ -3,7 +3,7 @@ "name": "Garages Amsterdam", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/garages_amsterdam", - "requirements": ["odp-amsterdam==5.0.0"], + "requirements": ["odp-amsterdam==5.0.1"], "codeowners": ["@klaasnicolaas"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 3f6fecf57da..ce2dc8322ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1220,7 +1220,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==5.0.0 +odp-amsterdam==5.0.1 # homeassistant.components.oem oemthermostat==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa5c72da0af..3c77014a07f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -892,7 +892,7 @@ oauth2client==4.1.3 objgraph==3.5.0 # homeassistant.components.garages_amsterdam -odp-amsterdam==5.0.0 +odp-amsterdam==5.0.1 # homeassistant.components.omnilogic omnilogic==0.4.5 From 9669b286c493c098f664d36694b4cb10e54f97ba Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 20 Jan 2023 21:27:31 +0200 Subject: [PATCH 169/173] Fix Shelly sleeping Gen2 - do not refresh from zeroconf discovery (#86296) --- .../components/shelly/coordinator.py | 3 +- tests/components/shelly/test_config_flow.py | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 96852512334..9029f18eb22 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -660,7 +660,8 @@ async def async_reconnect_soon( ) -> None: """Try to reconnect soon.""" if ( - not hass.is_stopping + not entry.data.get(CONF_SLEEP_PERIOD) + and not hass.is_stopping and entry.state == config_entries.ConfigEntryState.LOADED and (entry_data := get_entry_data(hass).get(entry.entry_id)) and (coordinator := entry_data.rpc) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 7338747cbaf..fbaf2d98ba2 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1187,3 +1187,39 @@ async def test_zeroconf_already_configured_triggers_refresh( mock_rpc_device.mock_disconnected() await hass.async_block_till_done() assert len(mock_rpc_device.initialize.mock_calls) == 2 + + +async def test_zeroconf_sleeping_device_not_triggers_refresh( + hass, mock_rpc_device, monkeypatch, caplog +): + """Test zeroconf discovery does not triggers refresh for sleeping device.""" + entry = MockConfigEntry( + domain="shelly", + unique_id="AABBCCDDEEFF", + data={"host": "1.1.1.1", "gen": 2, "sleep_period": 1000, "model": "SHSW-1"}, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_rpc_device.mock_update() + + assert "online, resuming setup" in caplog.text + + with patch( + "aioshelly.common.get_info", + return_value={"mac": "AABBCCDDEEFF", "type": "SHSW-1", "auth": False}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + monkeypatch.setattr(mock_rpc_device, "connected", False) + mock_rpc_device.mock_disconnected() + await hass.async_block_till_done() + assert len(mock_rpc_device.initialize.mock_calls) == 0 + assert "device did not update" not in caplog.text From 71d54da6730adf47782d98e7b4383bbac1efcc32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 12:05:06 -1000 Subject: [PATCH 170/173] Bump recommended esphome version for bluetooth proxies to 2022.12.4 (#86308) This will fix an MTU issue reported with airthings and other devices. needs https://github.com/esphome/esphome/pull/4323 --- homeassistant/components/esphome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index bfee5658679..b0f046ef1eb 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -65,7 +65,7 @@ CONF_NOISE_PSK = "noise_psk" _LOGGER = logging.getLogger(__name__) _R = TypeVar("_R") -STABLE_BLE_VERSION_STR = "2022.12.0" +STABLE_BLE_VERSION_STR = "2022.12.4" STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) PROJECT_URLS = { "esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", From 974601cc2e2f620b5fdd87f0203b25bc395107ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 21 Jan 2023 00:01:17 +0100 Subject: [PATCH 171/173] Update pyTibber to 0.26.11 (#86316) --- 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 403b0f2b4fc..1636c5da4bd 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.26.9"], + "requirements": ["pyTibber==0.26.11"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index ce2dc8322ec..c906fbbd94e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1439,7 +1439,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.9 +pyTibber==0.26.11 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c77014a07f..1547c3dffcb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1039,7 +1039,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.9 +pyTibber==0.26.11 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From 79a3d2e6f630d837ead4dda51de122cdccabf323 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 23 Jan 2023 03:28:17 +1100 Subject: [PATCH 172/173] Pass frag_duration as integer (#86375) fixes undefined --- homeassistant/components/stream/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 308340f74a6..aefbbf698f1 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -192,7 +192,7 @@ class StreamMuxer: # of the range, hoping that the parts stay pretty well bounded, and we adjust the part # durations a bit in the hls metadata so that everything "looks" ok. "frag_duration": str( - self._stream_settings.part_target_duration * 9e5 + int(self._stream_settings.part_target_duration * 9e5) ), } if self._stream_settings.ll_hls From 71c4588747a390462784e5667132822011b4bbd1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Jan 2023 17:48:00 +0100 Subject: [PATCH 173/173] Bumped version to 2023.1.7 --- 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 d14ce310238..c738d7fca6d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "6" +PATCH_VERSION: Final = "7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 6fd631b63c6..7f970771255 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.6" +version = "2023.1.7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"