From 123aafd772663e663fcc959eb037254105617b19 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Jan 2023 18:39:20 +0100 Subject: [PATCH 001/187] Bumped version to 2023.2.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 44cec9fb861..e5025c35e20 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 = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 48ebcd4146e..aa0af6037c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0.dev0" +version = "2023.2.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From a1416b904492005e5a2b623622a698d6d347c6f4 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 25 Jan 2023 20:45:50 +0100 Subject: [PATCH 002/187] Print expected device class units in error log (#86125) --- homeassistant/components/sensor/__init__.py | 2 ++ tests/components/sensor/test_init.py | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 6dd745861e0..351976db162 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -665,6 +665,7 @@ class SensorEntity(Entity): ( "Entity %s (%s) is using native unit of measurement '%s' which " "is not a valid unit for the device class ('%s') it is using; " + "expected one of %s; " "Please update your configuration if your entity is manually " "configured, otherwise %s" ), @@ -672,6 +673,7 @@ class SensorEntity(Entity): type(self), native_unit_of_measurement, device_class, + [str(unit) if unit else "no unit of measurement" for unit in units], report_issue, ) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index cd62228f83a..e65ae8ae7b8 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -9,7 +9,11 @@ import pytest from pytest import approx from homeassistant.components.number import NumberDeviceClass -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.components.sensor import ( + DEVICE_CLASS_UNITS, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, @@ -1403,13 +1407,17 @@ async def test_device_classes_with_invalid_unit_of_measurement( device_class=device_class, native_unit_of_measurement="INVALID!", ) - + units = [ + str(unit) if unit else "no unit of measurement" + for unit in DEVICE_CLASS_UNITS.get(device_class, set()) + ] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() assert ( "is using native unit of measurement 'INVALID!' which is not a valid " - f"unit for the device class ('{device_class}') it is using" + f"unit for the device class ('{device_class}') it is using; " + f"expected one of {units}" ) in caplog.text From e1c8dff536ddcfd5ada6bc4a365a6d4799389cd2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Jan 2023 14:50:16 -0500 Subject: [PATCH 003/187] Fix oauth2 error (#86634) --- homeassistant/helpers/config_entry_oauth2_flow.py | 12 +++++++++--- tests/helpers/test_config_entry_oauth2_flow.py | 7 +++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 0a6356d310d..552fa29eb86 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -13,7 +13,7 @@ from collections.abc import Awaitable, Callable import logging import secrets import time -from typing import Any, cast +from typing import Any, Optional, cast from aiohttp import client, web import async_timeout @@ -437,7 +437,10 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView): state = _decode_jwt(hass, request.query["state"]) if state is None: - return web.Response(text="Invalid state") + return web.Response( + text="Invalid state. Is My Home Assistant configured to go to the right instance?", + status=400, + ) user_input: dict[str, Any] = {"state": state} @@ -538,7 +541,10 @@ def _encode_jwt(hass: HomeAssistant, data: dict) -> str: @callback def _decode_jwt(hass: HomeAssistant, encoded: str) -> dict | None: """JWT encode data.""" - secret = cast(str, hass.data.get(DATA_JWT_SECRET)) + secret = cast(Optional[str], hass.data.get(DATA_JWT_SECRET)) + + if secret is None: + return None try: return jwt.decode(encoded, secret, algorithms=["HS256"]) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index f64525ecdd3..3b94f3d80c1 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -726,3 +726,10 @@ async def test_oauth_session_refresh_failure( session = config_entry_oauth2_flow.OAuth2Session(hass, config_entry, local_impl) with pytest.raises(aiohttp.client_exceptions.ClientResponseError): await session.async_request("post", "https://example.com") + + +async def test_oauth2_without_secret_init(local_impl, hass_client_no_auth): + """Check authorize callback without secret initalizated.""" + client = await hass_client_no_auth() + resp = await client.get("/auth/external/callback?code=abcd&state=qwer") + assert resp.status == 400 From 9ca04dbfa10576960ba7eb161ef85d6fefd0f8e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Jan 2023 21:33:30 -0500 Subject: [PATCH 004/187] Google Assistant: unset agent on unload (#86635) --- homeassistant/components/google_assistant_sdk/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index 257562dc536..93699321eda 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -98,6 +98,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for service_name in hass.services.async_services()[DOMAIN]: hass.services.async_remove(DOMAIN, service_name) + if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False): + conversation.async_unset_agent(hass, entry) + return True From a6fdf1d09aa9d29e2c27457e59f110f0b86c19ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Jan 2023 15:14:59 -1000 Subject: [PATCH 005/187] Correct units on mopeka battery voltage sensor (#86663) --- homeassistant/components/mopeka/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mopeka/sensor.py b/homeassistant/components/mopeka/sensor.py index f885ec89544..26965cd9a14 100644 --- a/homeassistant/components/mopeka/sensor.py +++ b/homeassistant/components/mopeka/sensor.py @@ -19,6 +19,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfElectricPotential, UnitOfLength, UnitOfTemperature, ) @@ -41,7 +42,7 @@ SENSOR_DESCRIPTIONS = { "battery_voltage": SensorEntityDescription( key="battery_voltage", device_class=SensorDeviceClass.VOLTAGE, - native_unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, From ea2bf34647303f3db7e071740a93f414d4c40e73 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 25 Jan 2023 22:15:09 -0500 Subject: [PATCH 006/187] Bump ZHA quirks lib (#86669) --- 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 a8a7ffc7c06..0392368070f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.34.6", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.91", + "zha-quirks==0.0.92", "zigpy-deconz==0.19.2", "zigpy==0.53.0", "zigpy-xbee==0.16.2", diff --git a/requirements_all.txt b/requirements_all.txt index 863f775ef16..a9776bbc7f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2693,7 +2693,7 @@ zeroconf==0.47.1 zeversolar==0.2.0 # homeassistant.components.zha -zha-quirks==0.0.91 +zha-quirks==0.0.92 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8256fd40621..8c0de222d9c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1906,7 +1906,7 @@ zeroconf==0.47.1 zeversolar==0.2.0 # homeassistant.components.zha -zha-quirks==0.0.91 +zha-quirks==0.0.92 # homeassistant.components.zha zigpy-deconz==0.19.2 From 07a1259db9ed61d8b15646ee9445d04aeeb26d72 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Jan 2023 22:17:19 -0500 Subject: [PATCH 007/187] Add error handling for OpenAI (#86671) * Add error handling for OpenAI * Simplify area filtering * better prompt --- .../openai_conversation/__init__.py | 43 +++++++++---------- .../components/openai_conversation/const.py | 23 ++++++---- .../openai_conversation/test_init.py | 43 +++++++++++++++---- 3 files changed, 70 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 78cdc927c10..3f71537a9d2 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from functools import partial import logging -from typing import cast import openai from openai import error @@ -13,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, TemplateError -from homeassistant.helpers import area_registry, device_registry, intent, template +from homeassistant.helpers import area_registry, intent, template from homeassistant.util import ulid from .const import DEFAULT_MODEL, DEFAULT_PROMPT @@ -97,15 +96,26 @@ class OpenAIAgent(conversation.AbstractConversationAgent): _LOGGER.debug("Prompt for %s: %s", model, prompt) - result = await self.hass.async_add_executor_job( - partial( - openai.Completion.create, - engine=model, - prompt=prompt, - max_tokens=150, - user=conversation_id, + try: + result = await self.hass.async_add_executor_job( + partial( + openai.Completion.create, + engine=model, + prompt=prompt, + max_tokens=150, + user=conversation_id, + ) ) - ) + except error.OpenAIError as err: + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem talking to OpenAI: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + _LOGGER.debug("Response %s", result) response = result["choices"][0]["text"].strip() self.history[conversation_id] = prompt + response @@ -122,20 +132,9 @@ class OpenAIAgent(conversation.AbstractConversationAgent): def _async_generate_prompt(self) -> str: """Generate a prompt for the user.""" - dev_reg = device_registry.async_get(self.hass) return template.Template(DEFAULT_PROMPT, self.hass).async_render( { "ha_name": self.hass.config.location_name, - "areas": [ - area - for area in area_registry.async_get(self.hass).areas.values() - # Filter out areas without devices - if any( - not dev.disabled_by - for dev in device_registry.async_entries_for_area( - dev_reg, cast(str, area.id) - ) - ) - ], + "areas": list(area_registry.async_get(self.hass).areas.values()), } ) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index 035a02a5b2e..edad9574f7b 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -3,19 +3,26 @@ DOMAIN = "openai_conversation" CONF_PROMPT = "prompt" DEFAULT_MODEL = "text-davinci-003" -DEFAULT_PROMPT = """ -You are a conversational AI for a smart home named {{ ha_name }}. -If a user wants to control a device, reject the request and suggest using the Home Assistant UI. +DEFAULT_PROMPT = """This smart home is controlled by Home Assistant. An overview of the areas and the devices in this smart home: -{% for area in areas %} +{%- for area in areas %} +{%- set area_info = namespace(printed=false) %} +{%- for device in area_devices(area.name) -%} +{%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") %} +{%- if not area_info.printed %} + {{ area.name }}: -{% for device in area_devices(area.name) -%} -{%- if not device_attr(device, "disabled_by") %} -- {{ device_attr(device, "name") }} ({{ device_attr(device, "model") }} by {{ device_attr(device, "manufacturer") }}) +{%- set area_info.printed = true %} +{%- endif %} +- {{ device_attr(device, "name") }}{% if device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %} {%- endif %} {%- endfor %} -{% endfor %} +{%- endfor %} + +Answer the users questions about the world truthfully. + +If the user wants to control a device, reject the request and suggest using the Home Assistant UI. Now finish this conversation: diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index 6597d81bffb..eb6afebd80e 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -1,14 +1,20 @@ """Tests for the OpenAI integration.""" from unittest.mock import patch +from openai import error + from homeassistant.components import conversation from homeassistant.core import Context -from homeassistant.helpers import device_registry +from homeassistant.helpers import area_registry, device_registry, intent async def test_default_prompt(hass, mock_init_component): """Test that the default prompt works.""" device_reg = device_registry.async_get(hass) + area_reg = area_registry.async_get(hass) + + for i in range(3): + area_reg.async_create(f"{i}Empty Area") device_reg.async_get_or_create( config_entry_id="1234", @@ -18,12 +24,22 @@ async def test_default_prompt(hass, mock_init_component): model="Test Model", suggested_area="Test Area", ) + for i in range(3): + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", f"{i}abcd")}, + name="Test Service", + manufacturer="Test Manufacturer", + model="Test Model", + suggested_area="Test Area", + entry_type=device_registry.DeviceEntryType.SERVICE, + ) device_reg.async_get_or_create( config_entry_id="1234", connections={("test", "5678")}, name="Test Device 2", manufacturer="Test Manufacturer 2", - model="Test Model 2", + model="Device 2", suggested_area="Test Area 2", ) device_reg.async_get_or_create( @@ -31,7 +47,7 @@ async def test_default_prompt(hass, mock_init_component): connections={("test", "9876")}, name="Test Device 3", manufacturer="Test Manufacturer 3", - model="Test Model 3", + model="Test Model 3A", suggested_area="Test Area 2", ) @@ -40,20 +56,20 @@ async def test_default_prompt(hass, mock_init_component): assert ( mock_create.mock_calls[0][2]["prompt"] - == """You are a conversational AI for a smart home named test home. -If a user wants to control a device, reject the request and suggest using the Home Assistant UI. + == """This smart home is controlled by Home Assistant. An overview of the areas and the devices in this smart home: Test Area: - -- Test Device (Test Model by Test Manufacturer) +- Test Device (Test Model) Test Area 2: +- Test Device 2 +- Test Device 3 (Test Model 3A) -- Test Device 2 (Test Model 2 by Test Manufacturer 2) -- Test Device 3 (Test Model 3 by Test Manufacturer 3) +Answer the users questions about the world truthfully. +If the user wants to control a device, reject the request and suggest using the Home Assistant UI. Now finish this conversation: @@ -61,3 +77,12 @@ Smart home: How can I assist? User: hello Smart home: """ ) + + +async def test_error_handling(hass, mock_init_component): + """Test that the default prompt works.""" + with patch("openai.Completion.create", side_effect=error.ServiceUnavailableError): + result = await conversation.async_converse(hass, "hello", None, Context()) + + assert result.response.response_type == intent.IntentResponseType.ERROR, result + assert result.response.error_code == "unknown", result From 8f684e962a6cb57143af70707f784c5ddecd2e90 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Jan 2023 23:00:35 -0500 Subject: [PATCH 008/187] Bumped version to 2023.2.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 e5025c35e20..fdfd223ff89 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 = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index aa0af6037c5..bc20cf67ded 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b0" +version = "2023.2.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bd1371680f2aa10c14c1ecc468d818a3eb3f7661 Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Thu, 26 Jan 2023 12:38:10 +0100 Subject: [PATCH 009/187] Add device registration to the Nuki component (#79806) * Add device registration to the Nuki component * Name is always given by the API * implement pvizeli's suggestions * switch device_registry to snake_case * fix entity naming * unify manufacturer names --- homeassistant/components/nuki/__init__.py | 171 +++++++++++------- .../components/nuki/binary_sensor.py | 5 - homeassistant/components/nuki/lock.py | 5 - 3 files changed, 105 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 4598d43b4dc..20309339451 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -13,7 +13,7 @@ from homeassistant import exceptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -41,37 +41,6 @@ def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOp return bridge.locks, bridge.openers -def _update_devices(devices: list[NukiDevice]) -> dict[str, set[str]]: - """ - Update the Nuki devices. - - Returns: - A dict with the events to be fired. The event type is the key and the device ids are the value - """ - - events: dict[str, set[str]] = defaultdict(set) - - for device in devices: - for level in (False, True): - try: - if isinstance(device, NukiOpener): - last_ring_action_state = device.ring_action_state - - device.update(level) - - if not last_ring_action_state and device.ring_action_state: - events["ring"].add(device.nuki_id) - else: - device.update(level) - except RequestException: - continue - - if device.state not in ERROR_STATES: - break - - return events - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Nuki entry.""" @@ -101,42 +70,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except RequestException as err: raise exceptions.ConfigEntryNotReady from err - async def async_update_data() -> None: - """Fetch data from Nuki bridge.""" - try: - # Note: asyncio.TimeoutError and aiohttp.ClientError are already - # handled by the data update coordinator. - async with async_timeout.timeout(10): - events = await hass.async_add_executor_job( - _update_devices, locks + openers - ) - except InvalidCredentialsException as err: - raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err - except RequestException as err: - raise UpdateFailed(f"Error communicating with Bridge: {err}") from err - - ent_reg = er.async_get(hass) - for event, device_ids in events.items(): - for device_id in device_ids: - entity_id = ent_reg.async_get_entity_id( - Platform.LOCK, DOMAIN, device_id - ) - event_data = { - "entity_id": entity_id, - "type": event, - } - hass.bus.async_fire("nuki_event", event_data) - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - # Name of the data. For logging purposes. - name="nuki devices", - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=UPDATE_INTERVAL, + # Device registration for the bridge + info = bridge.info() + bridge_id = parse_id(info["ids"]["hardwareId"]) + dev_reg = device_registry.async_get(hass) + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, bridge_id)}, + manufacturer="Nuki Home Solutions GmbH", + name=f"Nuki Bridge {bridge_id}", + model="Hardware Bridge", + sw_version=info["versions"]["firmwareVersion"], ) + coordinator = NukiCoordinator(hass, bridge, locks, openers) + hass.data[DOMAIN][entry.entry_id] = { DATA_COORDINATOR: coordinator, DATA_BRIDGE: bridge, @@ -178,3 +126,94 @@ class NukiEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator) self._nuki_device = nuki_device + + @property + def device_info(self): + """Device info for Nuki entities.""" + return { + "identifiers": {(DOMAIN, parse_id(self._nuki_device.nuki_id))}, + "name": self._nuki_device.name, + "manufacturer": "Nuki Home Solutions GmbH", + "model": self._nuki_device.device_type_str.capitalize(), + "sw_version": self._nuki_device.firmware_version, + "via_device": (DOMAIN, self.coordinator.bridge_id), + } + + +class NukiCoordinator(DataUpdateCoordinator): + """Data Update Coordinator for the Nuki integration.""" + + def __init__(self, hass, bridge, locks, openers): + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="nuki devices", + # Polling interval. Will only be polled if there are subscribers. + update_interval=UPDATE_INTERVAL, + ) + self.bridge = bridge + self.locks = locks + self.openers = openers + + @property + def bridge_id(self): + """Return the parsed id of the Nuki bridge.""" + return parse_id(self.bridge.info()["ids"]["hardwareId"]) + + async def _async_update_data(self) -> None: + """Fetch data from Nuki bridge.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + events = await self.hass.async_add_executor_job( + self.update_devices, self.locks + self.openers + ) + except InvalidCredentialsException as err: + raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err + except RequestException as err: + raise UpdateFailed(f"Error communicating with Bridge: {err}") from err + + ent_reg = entity_registry.async_get(self.hass) + for event, device_ids in events.items(): + for device_id in device_ids: + entity_id = ent_reg.async_get_entity_id( + Platform.LOCK, DOMAIN, device_id + ) + event_data = { + "entity_id": entity_id, + "type": event, + } + self.hass.bus.async_fire("nuki_event", event_data) + + def update_devices(self, devices: list[NukiDevice]) -> dict[str, set[str]]: + """ + Update the Nuki devices. + + Returns: + A dict with the events to be fired. The event type is the key and the device ids are the value + """ + + events: dict[str, set[str]] = defaultdict(set) + + for device in devices: + for level in (False, True): + try: + if isinstance(device, NukiOpener): + last_ring_action_state = device.ring_action_state + + device.update(level) + + if not last_ring_action_state and device.ring_action_state: + events["ring"].add(device.nuki_id) + else: + device.update(level) + except RequestException: + continue + + if device.state not in ERROR_STATES: + break + + return events diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 6c03cef3664..ae861609e1a 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -36,11 +36,6 @@ class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.DOOR - @property - def name(self): - """Return the name of the lock.""" - return self._nuki_device.name - @property def unique_id(self) -> str: """Return a unique ID.""" diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 4a8643e77aa..87cc6f40846 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -69,11 +69,6 @@ class NukiDeviceEntity(NukiEntity, LockEntity, ABC): _attr_supported_features = LockEntityFeature.OPEN - @property - def name(self) -> str | None: - """Return the name of the lock.""" - return self._nuki_device.name - @property def unique_id(self) -> str | None: """Return a unique ID.""" From 8cb8ecdae971edd35bb25acde8603d0b1a7afccb Mon Sep 17 00:00:00 2001 From: Patrick ZAJDA Date: Thu, 26 Jan 2023 13:50:19 +0100 Subject: [PATCH 010/187] Migrate Nuki to new entity naming style (#80021) Co-authored-by: Pascal Vizeli --- homeassistant/components/nuki/binary_sensor.py | 2 ++ homeassistant/components/nuki/lock.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index ae861609e1a..6e73d9c3208 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -34,6 +34,8 @@ async def async_setup_entry( class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): """Representation of a Nuki Lock Doorsensor.""" + _attr_has_entity_name = True + _attr_name = "Door sensor" _attr_device_class = BinarySensorDeviceClass.DOOR @property diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 87cc6f40846..45fbf726e7a 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -67,6 +67,7 @@ async def async_setup_entry( class NukiDeviceEntity(NukiEntity, LockEntity, ABC): """Representation of a Nuki device.""" + _attr_has_entity_name = True _attr_supported_features = LockEntityFeature.OPEN @property From c8c3f4bef6ab7abe045ebbc133886990f2ac407f Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Thu, 26 Jan 2023 11:53:20 +0400 Subject: [PATCH 011/187] Update ndms2_client to 0.1.2 (#86624) fix https://github.com/home-assistant/core/issues/86379 fixes undefined --- homeassistant/components/keenetic_ndms2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index be1ffd1f0b2..acb92dffe59 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,7 +3,7 @@ "name": "Keenetic NDMS2 Router", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", - "requirements": ["ndms2_client==0.1.1"], + "requirements": ["ndms2_client==0.1.2"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", diff --git a/requirements_all.txt b/requirements_all.txt index a9776bbc7f2..04f1557d25d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1168,7 +1168,7 @@ mycroftapi==2.0 nad_receiver==0.3.0 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.1.1 +ndms2_client==0.1.2 # homeassistant.components.ness_alarm nessclient==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c0de222d9c..0a1d47aac41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -867,7 +867,7 @@ mutagen==1.46.0 mutesync==0.0.1 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.1.1 +ndms2_client==0.1.2 # homeassistant.components.ness_alarm nessclient==0.10.0 From 41add96bab63d4f9df93fb759bc7b8d66f94eebd Mon Sep 17 00:00:00 2001 From: MHFDoge Date: Thu, 26 Jan 2023 05:55:07 -0600 Subject: [PATCH 012/187] Add known webostv button to list (#86674) Add known button to list. --- homeassistant/components/webostv/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml index 0fb3cd1ae16..1985857d128 100644 --- a/homeassistant/components/webostv/services.yaml +++ b/homeassistant/components/webostv/services.yaml @@ -17,7 +17,7 @@ button: description: >- Name of the button to press. Known possible values are LEFT, RIGHT, DOWN, UP, HOME, MENU, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, - MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, + MUTE, RED, GREEN, BLUE, YELLOW, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, PLAY, PAUSE, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 required: true example: "LEFT" From ba82f138216fc311edb44a54bd5ce08d70a2c0b6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Jan 2023 05:04:15 -0500 Subject: [PATCH 013/187] Make openai conversation prompt template more readable + test case (#86676) --- .../components/openai_conversation/const.py | 16 ++++++++-------- .../components/openai_conversation/test_init.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index edad9574f7b..34516cbb109 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -7,17 +7,17 @@ DEFAULT_PROMPT = """This smart home is controlled by Home Assistant. An overview of the areas and the devices in this smart home: {%- for area in areas %} -{%- set area_info = namespace(printed=false) %} -{%- for device in area_devices(area.name) -%} -{%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") %} -{%- if not area_info.printed %} + {%- set area_info = namespace(printed=false) %} + {%- for device in area_devices(area.name) -%} + {%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") %} + {%- if not area_info.printed %} {{ area.name }}: -{%- set area_info.printed = true %} -{%- endif %} + {%- set area_info.printed = true %} + {%- endif %} - {{ device_attr(device, "name") }}{% if device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %} -{%- endif %} -{%- endfor %} + {%- endif %} + {%- endfor %} {%- endfor %} Answer the users questions about the world truthfully. diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index eb6afebd80e..ac5be9f6115 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -50,6 +50,17 @@ async def test_default_prompt(hass, mock_init_component): model="Test Model 3A", suggested_area="Test Area 2", ) + device = device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "9876-disabled")}, + name="Test Device 3", + manufacturer="Test Manufacturer 3", + model="Test Model 3A", + suggested_area="Test Area 2", + ) + device_reg.async_update_device( + device.id, disabled_by=device_registry.DeviceEntryDisabler.USER + ) with patch("openai.Completion.create") as mock_create: await conversation.async_converse(hass, "hello", None, Context()) From 22afc7c7fbc7e6242dd53c736f743d5f8da4a564 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 26 Jan 2023 11:44:01 +0100 Subject: [PATCH 014/187] Fix missing interface key in deCONZ logbook (#86684) fixes undefined --- homeassistant/components/deconz/logbook.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 67801b84344..39fe7e98a56 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -17,6 +17,10 @@ from .device_trigger import ( CONF_BUTTON_2, CONF_BUTTON_3, CONF_BUTTON_4, + CONF_BUTTON_5, + CONF_BUTTON_6, + CONF_BUTTON_7, + CONF_BUTTON_8, CONF_CLOSE, CONF_DIM_DOWN, CONF_DIM_UP, @@ -95,6 +99,10 @@ INTERFACES = { CONF_BUTTON_2: "Button 2", CONF_BUTTON_3: "Button 3", CONF_BUTTON_4: "Button 4", + CONF_BUTTON_5: "Button 5", + CONF_BUTTON_6: "Button 6", + CONF_BUTTON_7: "Button 7", + CONF_BUTTON_8: "Button 8", CONF_SIDE_1: "Side 1", CONF_SIDE_2: "Side 2", CONF_SIDE_3: "Side 3", From 1dc3bb6eb16f33b6e7a1ede5026a8d6d5265dd82 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 Jan 2023 11:11:03 +0100 Subject: [PATCH 015/187] Terminate strings at NUL when recording states and events (#86687) --- homeassistant/components/recorder/core.py | 6 ++- .../components/recorder/db_schema.py | 17 +++++++-- homeassistant/helpers/json.py | 34 +++++++++++++++++ tests/components/recorder/db_schema_30.py | 9 ++++- tests/components/recorder/test_init.py | 37 +++++++++++++++++++ tests/helpers/test_json.py | 17 +++++++++ 6 files changed, 113 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 20a98af4b2f..a97eed8eff6 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -836,7 +836,9 @@ class Recorder(threading.Thread): return try: - shared_data_bytes = EventData.shared_data_bytes_from_event(event) + shared_data_bytes = EventData.shared_data_bytes_from_event( + event, self.dialect_name + ) except JSON_ENCODE_EXCEPTIONS as ex: _LOGGER.warning("Event is not JSON serializable: %s: %s", event, ex) return @@ -869,7 +871,7 @@ class Recorder(threading.Thread): try: dbstate = States.from_event(event) shared_attrs_bytes = StateAttributes.shared_attrs_bytes_from_event( - event, self._exclude_attributes_by_domain + event, self._exclude_attributes_by_domain, self.dialect_name ) except JSON_ENCODE_EXCEPTIONS as ex: _LOGGER.warning( diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 1b5ac87c24a..47b9658b053 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -43,11 +43,12 @@ from homeassistant.helpers.json import ( JSON_DECODE_EXCEPTIONS, JSON_DUMP, json_bytes, + json_bytes_strip_null, json_loads, ) import homeassistant.util.dt as dt_util -from .const import ALL_DOMAIN_EXCLUDE_ATTRS +from .const import ALL_DOMAIN_EXCLUDE_ATTRS, SupportedDialect from .models import StatisticData, StatisticMetaData, process_timestamp # SQLAlchemy Schema @@ -251,8 +252,12 @@ class EventData(Base): # type: ignore[misc,valid-type] ) @staticmethod - def shared_data_bytes_from_event(event: Event) -> bytes: + def shared_data_bytes_from_event( + event: Event, dialect: SupportedDialect | None + ) -> bytes: """Create shared_data from an event.""" + if dialect == SupportedDialect.POSTGRESQL: + return json_bytes_strip_null(event.data) return json_bytes(event.data) @staticmethod @@ -416,7 +421,9 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] @staticmethod def shared_attrs_bytes_from_event( - event: Event, exclude_attrs_by_domain: dict[str, set[str]] + event: Event, + exclude_attrs_by_domain: dict[str, set[str]], + dialect: SupportedDialect | None, ) -> bytes: """Create shared_attrs from a state_changed event.""" state: State | None = event.data.get("new_state") @@ -427,6 +434,10 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] exclude_attrs = ( exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS ) + if dialect == SupportedDialect.POSTGRESQL: + return json_bytes_strip_null( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) return json_bytes( {k: v for k, v in state.attributes.items() if k not in exclude_attrs} ) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 74a2f542910..2a499dc0d97 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -71,6 +71,40 @@ def json_bytes(data: Any) -> bytes: ) +def json_bytes_strip_null(data: Any) -> bytes: + """Dump json bytes after terminating strings at the first NUL.""" + + def process_dict(_dict: dict[Any, Any]) -> dict[Any, Any]: + """Strip NUL from items in a dict.""" + return {key: strip_null(o) for key, o in _dict.items()} + + def process_list(_list: list[Any]) -> list[Any]: + """Strip NUL from items in a list.""" + return [strip_null(o) for o in _list] + + def strip_null(obj: Any) -> Any: + """Strip NUL from an object.""" + if isinstance(obj, str): + return obj.split("\0", 1)[0] + if isinstance(obj, dict): + return process_dict(obj) + if isinstance(obj, list): + return process_list(obj) + return obj + + # We expect null-characters to be very rare, hence try encoding first and look + # for an escaped null-character in the output. + result = json_bytes(data) + if b"\\u0000" in result: + # We work on the processed result so we don't need to worry about + # Home Assistant extensions which allows encoding sets, tuples, etc. + data_processed = orjson.loads(result) + data_processed = strip_null(data_processed) + result = json_bytes(data_processed) + + return result + + def json_dumps(data: Any) -> str: """Dump json string. diff --git a/tests/components/recorder/db_schema_30.py b/tests/components/recorder/db_schema_30.py index 8854cd33a61..01c31807ff7 100644 --- a/tests/components/recorder/db_schema_30.py +++ b/tests/components/recorder/db_schema_30.py @@ -34,6 +34,7 @@ from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import aliased, declarative_base, relationship from sqlalchemy.orm.session import Session +from homeassistant.components.recorder.const import SupportedDialect from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_RESTORED, @@ -287,7 +288,9 @@ class EventData(Base): # type: ignore[misc,valid-type] ) @staticmethod - def shared_data_bytes_from_event(event: Event) -> bytes: + def shared_data_bytes_from_event( + event: Event, dialect: SupportedDialect | None + ) -> bytes: """Create shared_data from an event.""" return json_bytes(event.data) @@ -438,7 +441,9 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] @staticmethod def shared_attrs_bytes_from_event( - event: Event, exclude_attrs_by_domain: dict[str, set[str]] + event: Event, + exclude_attrs_by_domain: dict[str, set[str]], + dialect: SupportedDialect | None, ) -> bytes: """Create shared_attrs from a state_changed event.""" state: State | None = event.data.get("new_state") diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 8f32cfb6a62..c06865fb5a3 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -31,6 +31,7 @@ from homeassistant.components.recorder.const import ( EVENT_RECORDER_5MIN_STATISTICS_GENERATED, EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, KEEPALIVE_TIME, + SupportedDialect, ) from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, @@ -223,6 +224,42 @@ async def test_saving_state(recorder_mock, hass: HomeAssistant): assert state == _state_with_context(hass, entity_id) +@pytest.mark.parametrize( + "dialect_name, expected_attributes", + ( + (SupportedDialect.MYSQL, {"test_attr": 5, "test_attr_10": "silly\0stuff"}), + (SupportedDialect.POSTGRESQL, {"test_attr": 5, "test_attr_10": "silly"}), + (SupportedDialect.SQLITE, {"test_attr": 5, "test_attr_10": "silly\0stuff"}), + ), +) +async def test_saving_state_with_nul( + recorder_mock, hass: HomeAssistant, dialect_name, expected_attributes +): + """Test saving and restoring a state with nul in attributes.""" + entity_id = "test.recorder" + state = "restoring_from_db" + attributes = {"test_attr": 5, "test_attr_10": "silly\0stuff"} + + with patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name + ): + hass.states.async_set(entity_id, state, attributes) + await async_wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + db_states.append(db_state) + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + assert len(db_states) == 1 + assert db_states[0].event_id is None + + expected = _state_with_context(hass, entity_id) + expected.attributes = expected_attributes + assert state == expected + + async def test_saving_many_states( async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant ): diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 1e85338f152..92583fcfba8 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -10,6 +10,7 @@ from homeassistant import core from homeassistant.helpers.json import ( ExtendedJSONEncoder, JSONEncoder, + json_bytes_strip_null, json_dumps, json_dumps_sorted, ) @@ -118,3 +119,19 @@ def test_json_dumps_rgb_color_subclass(): rgb = RGBColor(4, 2, 1) assert json_dumps(rgb) == "[4,2,1]" + + +def test_json_bytes_strip_null(): + """Test stripping nul from strings.""" + + assert json_bytes_strip_null("\0") == b'""' + assert json_bytes_strip_null("silly\0stuff") == b'"silly"' + assert json_bytes_strip_null(["one", "two\0", "three"]) == b'["one","two","three"]' + assert ( + json_bytes_strip_null({"k1": "one", "k2": "two\0", "k3": "three"}) + == b'{"k1":"one","k2":"two","k3":"three"}' + ) + assert ( + json_bytes_strip_null([[{"k1": {"k2": ["silly\0stuff"]}}]]) + == b'[[{"k1":{"k2":["silly"]}}]]' + ) From d211603ba7128527aaf8120fea6ddae793d8f35c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 26 Jan 2023 08:27:44 -0500 Subject: [PATCH 016/187] Update Inovelli Blue Series switch support in ZHA (#86711) --- .../zha/core/channels/manufacturerspecific.py | 3 ++- homeassistant/components/zha/switch.py | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 427579cfb59..e6b88a6c9ad 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -224,7 +224,8 @@ class InovelliConfigEntityChannel(ZigbeeChannel): "switch_type": False, "button_delay": False, "smart_bulb_mode": False, - "double_tap_up_for_full_brightness": True, + "double_tap_up_for_max_brightness": True, + "double_tap_down_for_min_brightness": True, "led_color_when_on": True, "led_color_when_off": True, "led_intensity_when_on": True, diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 85a8a0959b3..f7a16c83b60 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -372,14 +372,26 @@ class InovelliSmartBulbMode(ZHASwitchConfigurationEntity, id_suffix="smart_bulb_ channel_names=CHANNEL_INOVELLI, ) class InovelliDoubleTapForFullBrightness( - ZHASwitchConfigurationEntity, id_suffix="double_tap_up_for_full_brightness" + ZHASwitchConfigurationEntity, id_suffix="double_tap_up_for_max_brightness" ): """Inovelli double tap for full brightness control.""" - _zcl_attribute: str = "double_tap_up_for_full_brightness" + _zcl_attribute: str = "double_tap_up_for_max_brightness" _attr_name: str = "Double tap full brightness" +@CONFIG_DIAGNOSTIC_MATCH( + channel_names=CHANNEL_INOVELLI, +) +class InovelliDoubleTapForMinBrightness( + ZHASwitchConfigurationEntity, id_suffix="double_tap_down_for_min_brightness" +): + """Inovelli double tap down for minimum brightness control.""" + + _zcl_attribute: str = "double_tap_down_for_min_brightness" + _attr_name: str = "Double tap minimum brightness" + + @CONFIG_DIAGNOSTIC_MATCH( channel_names=CHANNEL_INOVELLI, ) From 77bd23899fc200595dac219db5751f0f05085e0a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 26 Jan 2023 15:43:12 +0100 Subject: [PATCH 017/187] Bump python-matter-server to 2.0.2 (#86712) --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 91bf823e5f3..46fe45873b4 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==2.0.1"], + "requirements": ["python-matter-server==2.0.2"], "dependencies": ["websocket_api"], "codeowners": ["@home-assistant/matter"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 04f1557d25d..f424e8df2f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2072,7 +2072,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==2.0.1 +python-matter-server==2.0.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a1d47aac41..887f6d76710 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1468,7 +1468,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.matter -python-matter-server==2.0.1 +python-matter-server==2.0.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From cd59705c4b886f8bb98fee68bf90eeceaad09a3f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 26 Jan 2023 16:44:52 +0100 Subject: [PATCH 018/187] Remove gas device class from current sensor in dsmr_reader (#86725) --- homeassistant/components/dsmr_reader/definitions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index cc0c851ebda..ddf149d680f 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -209,7 +209,6 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/currently_delivered", name="Current gas usage", - device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, state_class=SensorStateClass.MEASUREMENT, ), From b464179eaca489ce3a05e44b8a728a3311ee8ca4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 26 Jan 2023 17:26:52 +0100 Subject: [PATCH 019/187] Fix state classes for duration device class (#86727) --- homeassistant/components/sensor/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 93fccbab124..4fb63140506 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -513,7 +513,7 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass | None] SensorDeviceClass.DATA_SIZE: set(SensorStateClass), SensorDeviceClass.DATE: set(), SensorDeviceClass.DISTANCE: set(SensorStateClass), - SensorDeviceClass.DURATION: set(), + SensorDeviceClass.DURATION: set(SensorStateClass), SensorDeviceClass.ENERGY: { SensorStateClass.TOTAL, SensorStateClass.TOTAL_INCREASING, From 4f2966674a0cc9f1e9e1f6c0a38de65366d770eb Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 27 Jan 2023 00:34:31 +0200 Subject: [PATCH 020/187] Bump aioshelly to 5.3.1 (#86751) --- 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 ea981b58ff2..ff5de472005 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.3.0"], + "requirements": ["aioshelly==5.3.1"], "dependencies": ["bluetooth", "http"], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index f424e8df2f4..91724ff8cf8 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.3.0 +aioshelly==5.3.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 887f6d76710..677fa5087d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -245,7 +245,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.3.0 +aioshelly==5.3.1 # homeassistant.components.skybell aioskybell==22.7.0 From c7665b479a01004710cae7b98b55829fb197c387 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Jan 2023 17:25:02 -0500 Subject: [PATCH 021/187] OpenAI: Fix device without model (#86754) --- .../components/openai_conversation/__init__.py | 1 + .../components/openai_conversation/const.py | 4 ++-- tests/components/openai_conversation/test_init.py | 13 +++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 3f71537a9d2..c9d92f554ee 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -73,6 +73,7 @@ class OpenAIAgent(conversation.AbstractConversationAgent): try: prompt = self._async_generate_prompt() except TemplateError as err: + _LOGGER.error("Error rendering prompt: %s", err) intent_response = intent.IntentResponse(language=user_input.language) intent_response.async_set_error( intent.IntentResponseErrorCode.UNKNOWN, diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index 34516cbb109..378548173b0 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -15,14 +15,14 @@ An overview of the areas and the devices in this smart home: {{ area.name }}: {%- set area_info.printed = true %} {%- endif %} -- {{ device_attr(device, "name") }}{% if device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %} +- {{ device_attr(device, "name") }}{% if device_attr(device, "model") and device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %} {%- endif %} {%- endfor %} {%- endfor %} Answer the users questions about the world truthfully. -If the user wants to control a device, reject the request and suggest using the Home Assistant UI. +If the user wants to control a device, reject the request and suggest using the Home Assistant app. Now finish this conversation: diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index ac5be9f6115..551d493df8e 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -50,6 +50,12 @@ async def test_default_prompt(hass, mock_init_component): model="Test Model 3A", suggested_area="Test Area 2", ) + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "qwer")}, + name="Test Device 4", + suggested_area="Test Area 2", + ) device = device_reg.async_get_or_create( config_entry_id="1234", connections={("test", "9876-disabled")}, @@ -63,7 +69,9 @@ async def test_default_prompt(hass, mock_init_component): ) with patch("openai.Completion.create") as mock_create: - await conversation.async_converse(hass, "hello", None, Context()) + result = await conversation.async_converse(hass, "hello", None, Context()) + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert ( mock_create.mock_calls[0][2]["prompt"] @@ -77,10 +85,11 @@ Test Area: Test Area 2: - Test Device 2 - Test Device 3 (Test Model 3A) +- Test Device 4 Answer the users questions about the world truthfully. -If the user wants to control a device, reject the request and suggest using the Home Assistant UI. +If the user wants to control a device, reject the request and suggest using the Home Assistant app. Now finish this conversation: From 8cbefd5f97a65d1951ee81dc22c668844c09bab3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 26 Jan 2023 16:14:46 -0700 Subject: [PATCH 022/187] Fix state class issues in Ambient PWS (#86758) fixes undefined --- homeassistant/components/ambient_station/sensor.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index eb01fd379b2..0fc6e7643db 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -128,7 +128,6 @@ SENSOR_DESCRIPTIONS = ( key=TYPE_AQI_PM25_24H, name="AQI PM2.5 24h avg", device_class=SensorDeviceClass.AQI, - state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_AQI_PM25_IN, @@ -140,7 +139,6 @@ SENSOR_DESCRIPTIONS = ( key=TYPE_AQI_PM25_IN_24H, name="AQI PM2.5 indoor 24h avg", device_class=SensorDeviceClass.AQI, - state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_BAROMABSIN, @@ -182,7 +180,7 @@ SENSOR_DESCRIPTIONS = ( name="Event rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_FEELSLIKE, @@ -287,7 +285,6 @@ SENSOR_DESCRIPTIONS = ( name="Last rain", icon="mdi:water", device_class=SensorDeviceClass.TIMESTAMP, - state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_DAY, @@ -315,7 +312,7 @@ SENSOR_DESCRIPTIONS = ( name="Monthly rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_PM25_24H, @@ -586,7 +583,7 @@ SENSOR_DESCRIPTIONS = ( name="Lifetime rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_UV, @@ -599,7 +596,7 @@ SENSOR_DESCRIPTIONS = ( name="Weekly rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_WINDDIR, From e20c7491c1fc87bd9beb27f24cd971aeee149927 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Jan 2023 21:45:42 -0500 Subject: [PATCH 023/187] ESPHome update: Store reference to runtime data, not one of its values (#86762) Store reference to runtime data, not one of its values --- homeassistant/components/esphome/update.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 0f4836d0c66..de7a7463191 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -72,15 +72,13 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): _attr_title = "ESPHome" _attr_name = "Firmware" - _device_info: ESPHomeDeviceInfo - def __init__( self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboard ) -> None: """Initialize the update entity.""" super().__init__(coordinator=coordinator) assert entry_data.device_info is not None - self._device_info = entry_data.device_info + self._entry_data = entry_data self._attr_unique_id = entry_data.device_info.mac_address self._attr_device_info = DeviceInfo( connections={ @@ -88,6 +86,12 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): } ) + @property + def _device_info(self) -> ESPHomeDeviceInfo: + """Return the device info.""" + assert self._entry_data.device_info is not None + return self._entry_data.device_info + @property def available(self) -> bool: """Return if update is available.""" From b7311dc6555314ef1da4294ec523c427313365ee Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 27 Jan 2023 15:45:51 +1300 Subject: [PATCH 024/187] Remove esphome password from config flow data if not needed (#86763) * Remove esphome password if not needed * Add test Co-authored-by: Paulus Schoutsen --- .../components/esphome/config_flow.py | 1 + tests/components/esphome/test_config_flow.py | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index ee8da40d0ba..61eb97a365b 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -153,6 +153,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): if self._device_info.uses_password: return await self.async_step_authenticate() + self._password = "" return self._async_get_entry() async def async_step_discovery_confirm( diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index dda9c88cd1d..a2f51f2526e 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -518,6 +518,54 @@ async def test_reauth_fixed_via_dashboard( assert len(mock_get_encryption_key.mock_calls) == 1 +async def test_reauth_fixed_via_dashboard_remove_password( + hass, mock_client, mock_zeroconf, mock_dashboard +): + """Test reauth fixed automatically via dashboard with password removed.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "hello", + CONF_DEVICE_NAME: "test", + }, + ) + entry.add_to_hass(hass) + + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + assert entry.data[CONF_PASSWORD] == "" + + assert len(mock_get_encryption_key.mock_calls) == 1 + + async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): """Test reauth initiation with invalid PSK.""" entry = MockConfigEntry( From 6397cc5d0408fd744b674a2553372852aaadffa9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Jan 2023 21:47:21 -0500 Subject: [PATCH 025/187] Bumped version to 2023.2.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 fdfd223ff89..59e7bf30396 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 = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index bc20cf67ded..1e76d932ae7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b1" +version = "2023.2.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0a6ce35e301b3f0c62a0e825ee8a0ef208644f74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jan 2023 17:39:45 -1000 Subject: [PATCH 026/187] Chunk MariaDB and Postgresql data migration to avoid running out of buffer space (#86680) * Chunk MariaDB data migration to avoid running out of buffer space This will make the migration slower but since the innodb_buffer_pool_size is using the defaul to 128M and not tuned to the db size there is a risk of running out of buffer space for large databases * Update homeassistant/components/recorder/migration.py * hard code since bandit thinks its an injection * Update homeassistant/components/recorder/migration.py * guard against manually modified data/corrupt db * adjust to 10k per chunk * adjust to 50k per chunk * memory still just fine at 250k * but slower * commit after each chunk to reduce lock pressure * adjust * set to 0 if null so we do not loop forever (this should only happen if the data is missing) * set to 0 if null so we do not loop forever (this should only happen if the data is missing) * tweak * tweak * limit cleanup * lower limit to give some more buffer * lower limit to give some more buffer * where required for sqlite * sqlite can wipe as many as needed with no limit * limit on mysql only * chunk postgres * fix limit * tweak * fix reference * fix * tweak for ram * postgres memory reduction * defer cleanup * fix * same order --- homeassistant/components/recorder/core.py | 4 +- .../components/recorder/migration.py | 165 +++++++++++++----- 2 files changed, 127 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index a97eed8eff6..62c1213a4a4 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1026,7 +1026,9 @@ class Recorder(threading.Thread): def _post_schema_migration(self, old_version: int, new_version: int) -> None: """Run post schema migration tasks.""" - migration.post_schema_migration(self.event_session, old_version, new_version) + migration.post_schema_migration( + self.engine, self.event_session, old_version, new_version + ) def _send_keep_alive(self) -> None: """Send a keep alive to keep the db connection open.""" diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 9bddf11fcad..746adf11c18 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text -from sqlalchemy.engine import Engine +from sqlalchemy.engine import CursorResult, Engine from sqlalchemy.exc import ( DatabaseError, InternalError, @@ -43,7 +43,7 @@ from .statistics import ( get_start_time, validate_db_schema as statistics_validate_db_schema, ) -from .tasks import PostSchemaMigrationTask +from .tasks import CommitTask, PostSchemaMigrationTask from .util import session_scope if TYPE_CHECKING: @@ -166,6 +166,9 @@ def migrate_schema( if current_version != SCHEMA_VERSION: instance.queue_task(PostSchemaMigrationTask(current_version, SCHEMA_VERSION)) + # Make sure the post schema migration task is committed in case + # the next task does not have commit_before = True + instance.queue_task(CommitTask()) def _create_index( @@ -846,8 +849,7 @@ def _apply_update( # noqa: C901 _create_index(session_maker, "events", "ix_events_event_type_time_fired_ts") _create_index(session_maker, "states", "ix_states_entity_id_last_updated_ts") _create_index(session_maker, "states", "ix_states_last_updated_ts") - with session_scope(session=session_maker()) as session: - _migrate_columns_to_timestamp(hass, session, engine) + _migrate_columns_to_timestamp(session_maker, engine) elif new_version == 32: # Migration is done in two steps to ensure we can start using # the new columns before we wipe the old ones. @@ -860,6 +862,7 @@ def _apply_update( # noqa: C901 def post_schema_migration( + engine: Engine, session: Session, old_version: int, new_version: int, @@ -878,62 +881,142 @@ def post_schema_migration( # In version 31 we migrated all the time_fired, last_updated, and last_changed # columns to be timestamps. In version 32 we need to wipe the old columns # since they are no longer used and take up a significant amount of space. - _wipe_old_string_time_columns(session) + _wipe_old_string_time_columns(engine, session) -def _wipe_old_string_time_columns(session: Session) -> None: +def _wipe_old_string_time_columns(engine: Engine, session: Session) -> None: """Wipe old string time columns to save space.""" # Wipe Events.time_fired since its been replaced by Events.time_fired_ts # Wipe States.last_updated since its been replaced by States.last_updated_ts # Wipe States.last_changed since its been replaced by States.last_changed_ts - session.execute(text("UPDATE events set time_fired=NULL;")) - session.execute(text("UPDATE states set last_updated=NULL, last_changed=NULL;")) - session.commit() + # + if engine.dialect.name == SupportedDialect.SQLITE: + session.execute(text("UPDATE events set time_fired=NULL;")) + session.commit() + session.execute(text("UPDATE states set last_updated=NULL, last_changed=NULL;")) + session.commit() + elif engine.dialect.name == SupportedDialect.MYSQL: + # + # Since this is only to save space we limit the number of rows we update + # to 10,000,000 per table since we do not want to block the database for too long + # or run out of innodb_buffer_pool_size on MySQL. The old data will eventually + # be cleaned up by the recorder purge if we do not do it now. + # + session.execute(text("UPDATE events set time_fired=NULL LIMIT 10000000;")) + session.commit() + session.execute( + text( + "UPDATE states set last_updated=NULL, last_changed=NULL " + " LIMIT 10000000;" + ) + ) + session.commit() + elif engine.dialect.name == SupportedDialect.POSTGRESQL: + # + # Since this is only to save space we limit the number of rows we update + # to 250,000 per table since we do not want to block the database for too long + # or run out ram with postgresql. The old data will eventually + # be cleaned up by the recorder purge if we do not do it now. + # + session.execute( + text( + "UPDATE events set time_fired=NULL " + "where event_id in " + "(select event_id from events where time_fired_ts is NOT NULL LIMIT 250000);" + ) + ) + session.commit() + session.execute( + text( + "UPDATE states set last_updated=NULL, last_changed=NULL " + "where state_id in " + "(select state_id from states where last_updated_ts is NOT NULL LIMIT 250000);" + ) + ) + session.commit() def _migrate_columns_to_timestamp( - hass: HomeAssistant, session: Session, engine: Engine + session_maker: Callable[[], Session], engine: Engine ) -> None: """Migrate columns to use timestamp.""" # Migrate all data in Events.time_fired to Events.time_fired_ts # Migrate all data in States.last_updated to States.last_updated_ts # Migrate all data in States.last_changed to States.last_changed_ts - connection = session.connection() + result: CursorResult | None = None if engine.dialect.name == SupportedDialect.SQLITE: - connection.execute( - text( - 'UPDATE events set time_fired_ts=strftime("%s",time_fired) + ' - "cast(substr(time_fired,-7) AS FLOAT);" + # With SQLite we do this in one go since it is faster + with session_scope(session=session_maker()) as session: + connection = session.connection() + connection.execute( + text( + 'UPDATE events set time_fired_ts=strftime("%s",time_fired) + ' + "cast(substr(time_fired,-7) AS FLOAT);" + ) ) - ) - connection.execute( - text( - 'UPDATE states set last_updated_ts=strftime("%s",last_updated) + ' - "cast(substr(last_updated,-7) AS FLOAT), " - 'last_changed_ts=strftime("%s",last_changed) + ' - "cast(substr(last_changed,-7) AS FLOAT);" + connection.execute( + text( + 'UPDATE states set last_updated_ts=strftime("%s",last_updated) + ' + "cast(substr(last_updated,-7) AS FLOAT), " + 'last_changed_ts=strftime("%s",last_changed) + ' + "cast(substr(last_changed,-7) AS FLOAT);" + ) ) - ) elif engine.dialect.name == SupportedDialect.MYSQL: - connection.execute( - text("UPDATE events set time_fired_ts=UNIX_TIMESTAMP(time_fired);") - ) - connection.execute( - text( - "UPDATE states set last_updated_ts=UNIX_TIMESTAMP(last_updated), " - "last_changed_ts=UNIX_TIMESTAMP(last_changed);" - ) - ) + # With MySQL we do this in chunks to avoid hitting the `innodb_buffer_pool_size` limit + # We also need to do this in a loop since we can't be sure that we have + # updated all rows in the table until the rowcount is 0 + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE events set time_fired_ts=" + "IF(time_fired is NULL,0,UNIX_TIMESTAMP(time_fired)) " + "where time_fired_ts is NULL " + "LIMIT 250000;" + ) + ) + result = None + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE states set last_updated_ts=" + "IF(last_updated is NULL,0,UNIX_TIMESTAMP(last_updated)), " + "last_changed_ts=UNIX_TIMESTAMP(last_changed) " + "where last_updated_ts is NULL " + "LIMIT 250000;" + ) + ) elif engine.dialect.name == SupportedDialect.POSTGRESQL: - connection.execute( - text("UPDATE events set time_fired_ts=EXTRACT(EPOCH FROM time_fired);") - ) - connection.execute( - text( - "UPDATE states set last_updated_ts=EXTRACT(EPOCH FROM last_updated), " - "last_changed_ts=EXTRACT(EPOCH FROM last_changed);" - ) - ) + # With Postgresql we do this in chunks to avoid using too much memory + # We also need to do this in a loop since we can't be sure that we have + # updated all rows in the table until the rowcount is 0 + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE events SET " + "time_fired_ts= " + "(case when time_fired is NULL then 0 else EXTRACT(EPOCH FROM time_fired) end) " + "WHERE event_id IN ( " + "SELECT event_id FROM events where time_fired_ts is NULL LIMIT 250000 " + " );" + ) + ) + result = None + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE states set last_updated_ts=" + "(case when last_updated is NULL then 0 else EXTRACT(EPOCH FROM last_updated) end), " + "last_changed_ts=EXTRACT(EPOCH FROM last_changed) " + "where state_id IN ( " + "SELECT state_id FROM states where last_updated_ts is NULL LIMIT 250000 " + " );" + ) + ) def _initialize_database(session: Session) -> bool: From 60b96f19b79be05e46e68c5a031ca92eb948c285 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Jan 2023 17:16:16 -1000 Subject: [PATCH 027/187] Fix Bluetooth discoveries missing between restarts (#86808) * Fix Bluetooth discoveries missing between restarts * do not load other integrations * coverage --- homeassistant/components/bluetooth/manager.py | 14 ++++++++++++++ tests/components/bluetooth/conftest.py | 15 +++++++++++++++ tests/components/bluetooth/test_base_scanner.py | 4 +++- tests/components/bluetooth/test_manager.py | 10 +++++++--- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index a8b890116d5..1523f41bf1f 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -209,6 +209,20 @@ class BluetoothManager: self._bluetooth_adapters, self.storage ) self.async_setup_unavailable_tracking() + seen: set[str] = set() + for address, service_info in itertools.chain( + self._connectable_history.items(), self._all_history.items() + ): + if address in seen: + continue + seen.add(address) + for domain in self._integration_matcher.match_domains(service_info): + discovery_flow.async_create_flow( + self.hass, + domain, + {"source": config_entries.SOURCE_BLUETOOTH}, + service_info, + ) @hass_callback def async_stop(self, event: Event) -> None: diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index ffa353fd0c4..59c5cc822df 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -186,3 +186,18 @@ def one_adapter_old_bluez(): }, ): yield + + +@pytest.fixture(name="disable_new_discovery_flows") +def disable_new_discovery_flows_fixture(): + """Fixture that disables new discovery flows. + + We want to disable new discovery flows as we are testing the + BluetoothManager and not the discovery flows. This fixture + will patch the discovery_flow.async_create_flow method to + ensure we do not load other integrations. + """ + with patch( + "homeassistant.components.bluetooth.manager.discovery_flow.async_create_flow" + ) as mock_create_flow: + yield mock_create_flow diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index 935b35b5863..5c6bbccd443 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -345,7 +345,9 @@ async def test_base_scanner_connecting_behavior(hass, enable_bluetooth): unsetup() -async def test_restore_history_remote_adapter(hass, hass_storage): +async def test_restore_history_remote_adapter( + hass, hass_storage, disable_new_discovery_flows +): """Test we can restore history for a remote adapter.""" data = hass_storage[storage.REMOTE_SCANNER_STORAGE_KEY] = json_loads( diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index d0ae23a5226..12e48313332 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -282,7 +282,9 @@ async def test_switching_adapters_based_on_stale( ) -async def test_restore_history_from_dbus(hass, one_adapter): +async def test_restore_history_from_dbus( + hass, one_adapter, disable_new_discovery_flows +): """Test we can restore history from dbus.""" address = "AA:BB:CC:CC:CC:FF" @@ -304,7 +306,7 @@ async def test_restore_history_from_dbus(hass, one_adapter): async def test_restore_history_from_dbus_and_remote_adapters( - hass, one_adapter, hass_storage + hass, one_adapter, hass_storage, disable_new_discovery_flows ): """Test we can restore history from dbus along with remote adapters.""" address = "AA:BB:CC:CC:CC:FF" @@ -337,10 +339,11 @@ async def test_restore_history_from_dbus_and_remote_adapters( assert ( bluetooth.async_ble_device_from_address(hass, "EB:0B:36:35:6F:A4") is not None ) + assert disable_new_discovery_flows.call_count > 1 async def test_restore_history_from_dbus_and_corrupted_remote_adapters( - hass, one_adapter, hass_storage + hass, one_adapter, hass_storage, disable_new_discovery_flows ): """Test we can restore history from dbus when the remote adapters data is corrupted.""" address = "AA:BB:CC:CC:CC:FF" @@ -371,6 +374,7 @@ async def test_restore_history_from_dbus_and_corrupted_remote_adapters( assert bluetooth.async_ble_device_from_address(hass, address) is not None assert bluetooth.async_ble_device_from_address(hass, "EB:0B:36:35:6F:A4") is None + assert disable_new_discovery_flows.call_count >= 1 async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable( From 29eb7e8f9e0607108a941d8035cc4205cc0d977e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 27 Jan 2023 20:00:44 +0100 Subject: [PATCH 028/187] Bump plugwise to v0.27.4 (#86812) fixes undefined --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index d5c3d41ce9b..e3ac555a00d 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.27.1"], + "requirements": ["plugwise==0.27.4"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 91724ff8cf8..a106d3c509e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1373,7 +1373,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.27.1 +plugwise==0.27.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 677fa5087d4..29f58cfde41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1003,7 +1003,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.27.1 +plugwise==0.27.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From bedf5fe6cdcbe7237cabbaefe0dedf142cdeea32 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 27 Jan 2023 21:38:25 -0500 Subject: [PATCH 029/187] Fix D-Link config flow auth (#86824) --- homeassistant/components/dlink/__init__.py | 2 +- homeassistant/components/dlink/config_flow.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dlink/__init__.py b/homeassistant/components/dlink/__init__.py index 528e5182b31..40fce4acf76 100644 --- a/homeassistant/components/dlink/__init__.py +++ b/homeassistant/components/dlink/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_USERNAME], entry.data[CONF_USE_LEGACY_PROTOCOL], ) - if not smartplug.authenticated and entry.data[CONF_USE_LEGACY_PROTOCOL]: + if not smartplug.authenticated and smartplug.use_legacy_protocol: raise ConfigEntryNotReady("Cannot connect/authenticate") hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SmartPlugData(smartplug) diff --git a/homeassistant/components/dlink/config_flow.py b/homeassistant/components/dlink/config_flow.py index 686448cfd81..4499e2efffc 100644 --- a/homeassistant/components/dlink/config_flow.py +++ b/homeassistant/components/dlink/config_flow.py @@ -131,6 +131,6 @@ class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except Exception as ex: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception: %s", ex) return "unknown" - if smartplug.authenticated: - return None - return "cannot_connect" + if not smartplug.authenticated and smartplug.use_legacy_protocol: + return "cannot_connect" + return None From d33373f6ee605f562e772ef07fd6fe5d2ebc7d31 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 27 Jan 2023 21:16:28 -0600 Subject: [PATCH 030/187] Check for missing ISY994 Z-Wave Properties (#86829) * Check for missing Z-Wave Properties * Fix black from mobile --- homeassistant/components/isy994/helpers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index e3e21dbfd4f..b190638fb35 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -313,7 +313,11 @@ def _generate_device_info(node: Node) -> DeviceInfo: model += f" ({node.type})" # Get extra information for Z-Wave Devices - if node.protocol == PROTO_ZWAVE and node.zwave_props.mfr_id != "0": + if ( + node.protocol == PROTO_ZWAVE + and node.zwave_props + and node.zwave_props.mfr_id != "0" + ): device_info[ ATTR_MANUFACTURER ] = f"Z-Wave MfrID:{int(node.zwave_props.mfr_id):#0{6}x}" From 69ed30f743e97862bdfc6fbbc762c150363c3e7b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Jan 2023 22:54:05 -0500 Subject: [PATCH 031/187] Bumped version to 2023.2.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 59e7bf30396..0cd0e9b5189 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 = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 1e76d932ae7..f4207bfb6a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b2" +version = "2023.2.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9adaf270640f5c7502f1e89fa9479141be074725 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Sun, 29 Jan 2023 00:49:29 +0100 Subject: [PATCH 032/187] Update frontend to 20230128.0 (#86838) --- 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 345daa57776..0c337ca1f6d 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==20230125.0"], + "requirements": ["home-assistant-frontend==20230128.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8aead1bbafb..44bc99ea03c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 hassil==0.2.5 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230125.0 +home-assistant-frontend==20230128.0 home-assistant-intents==2023.1.25 httpx==0.23.3 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index a106d3c509e..78c35f70556 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -907,7 +907,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230125.0 +home-assistant-frontend==20230128.0 # homeassistant.components.conversation home-assistant-intents==2023.1.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 29f58cfde41..13baca898c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230125.0 +home-assistant-frontend==20230128.0 # homeassistant.components.conversation home-assistant-intents==2023.1.25 From 6db9653a87031c8b431080b59ada6ddffb229e73 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 28 Jan 2023 22:03:58 -0500 Subject: [PATCH 033/187] Fix D-Link attributes (#86842) * Fix D-Link attributes * fix blocking call --- homeassistant/components/dlink/data.py | 6 +++--- homeassistant/components/dlink/switch.py | 23 ++++++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/dlink/data.py b/homeassistant/components/dlink/data.py index c9e3b20a0bf..b93cd219166 100644 --- a/homeassistant/components/dlink/data.py +++ b/homeassistant/components/dlink/data.py @@ -19,9 +19,9 @@ class SmartPlugData: """Initialize the data object.""" self.smartplug = smartplug self.state: str | None = None - self.temperature: str | None = None - self.current_consumption = None - self.total_consumption: str | None = None + self.temperature: str = "" + self.current_consumption: str = "" + self.total_consumption: str = "" self.available = False self._n_tried = 0 self._last_tried: datetime | None = None diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 9827c1c13a1..e6b9a4c7883 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -94,17 +94,22 @@ class SmartPlugSwitch(DLinkEntity, SwitchEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" - attrs: dict[str, Any] = {} - if self.data.temperature and self.data.temperature.isnumeric(): - attrs[ATTR_TEMPERATURE] = self.hass.config.units.temperature( + try: + temperature = self.hass.config.units.temperature( int(self.data.temperature), UnitOfTemperature.CELSIUS ) - else: - attrs[ATTR_TEMPERATURE] = None - if self.data.total_consumption and self.data.total_consumption.isnumeric(): - attrs[ATTR_TOTAL_CONSUMPTION] = float(self.data.total_consumption) - else: - attrs[ATTR_TOTAL_CONSUMPTION] = None + except ValueError: + temperature = None + + try: + total_consumption = float(self.data.total_consumption) + except ValueError: + total_consumption = None + + attrs = { + ATTR_TOTAL_CONSUMPTION: total_consumption, + ATTR_TEMPERATURE: temperature, + } return attrs From c9cf3c29f8884e151a91d495844edef12f2d318c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jan 2023 17:05:06 -1000 Subject: [PATCH 034/187] Improve websocket throughput of state changes (#86855) After the start event we tend to get an event storm of state changes which can get the websocket behind. #86854 will help with that a bit, but we can reduce the overhead to build a state diff when the attributes have not changed --- homeassistant/components/websocket_api/messages.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 5c01484f912..15965b37faa 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -161,12 +161,14 @@ def _state_diff( additions[COMPRESSED_STATE_CONTEXT]["id"] = new_state.context.id else: additions[COMPRESSED_STATE_CONTEXT] = new_state.context.id - old_attributes = old_state.attributes - for key, value in new_state.attributes.items(): - if old_attributes.get(key) != value: - additions.setdefault(COMPRESSED_STATE_ATTRIBUTES, {})[key] = value - if removed := set(old_attributes).difference(new_state.attributes): - diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: removed} + if (old_attributes := old_state.attributes) != ( + new_attributes := new_state.attributes + ): + for key, value in new_attributes.items(): + if old_attributes.get(key) != value: + additions.setdefault(COMPRESSED_STATE_ATTRIBUTES, {})[key] = value + if removed := set(old_attributes).difference(new_attributes): + diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: removed} return {ENTITY_EVENT_CHANGE: {new_state.entity_id: diff}} From 55b5b36c47c3cf6385f10701192952ca46928e28 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 29 Jan 2023 04:05:31 +0100 Subject: [PATCH 035/187] Fix tradfri air quality device class (#86861) --- homeassistant/components/tradfri/sensor.py | 2 +- tests/components/tradfri/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 3b92abfad77..689964cb151 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -89,9 +89,9 @@ SENSOR_DESCRIPTIONS_FAN: tuple[TradfriSensorEntityDescription, ...] = ( TradfriSensorEntityDescription( key="aqi", name="air quality", - device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:air-filter", value=_get_air_quality, ), TradfriSensorEntityDescription( diff --git a/tests/components/tradfri/test_sensor.py b/tests/components/tradfri/test_sensor.py index 6408613f4e3..10904b8ffa6 100644 --- a/tests/components/tradfri/test_sensor.py +++ b/tests/components/tradfri/test_sensor.py @@ -91,8 +91,8 @@ async def test_air_quality_sensor(hass, mock_gateway, mock_api_factory): assert sensor_1 is not None assert sensor_1.state == "42" assert sensor_1.attributes["unit_of_measurement"] == "µg/m³" - assert sensor_1.attributes["device_class"] == "aqi" assert sensor_1.attributes["state_class"] == "measurement" + assert "device_class" not in sensor_1.attributes async def test_filter_time_left_sensor(hass, mock_gateway, mock_api_factory): From 85d5ea2eca8be72c3ae656a315ec45ccc3d26690 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Jan 2023 17:06:07 -1000 Subject: [PATCH 036/187] Fix v32 schema migration when MySQL global.time_zone is configured with non-UTC timezone (#86867) * Fix v32 schema migration when MySQL timezone is not UTC * tweak --- homeassistant/components/recorder/migration.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 746adf11c18..af1446400c2 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -971,7 +971,9 @@ def _migrate_columns_to_timestamp( result = session.connection().execute( text( "UPDATE events set time_fired_ts=" - "IF(time_fired is NULL,0,UNIX_TIMESTAMP(time_fired)) " + "IF(time_fired is NULL,0," + "UNIX_TIMESTAMP(CONVERT_TZ(time_fired,'+00:00',@@global.time_zone))" + ") " "where time_fired_ts is NULL " "LIMIT 250000;" ) @@ -982,8 +984,11 @@ def _migrate_columns_to_timestamp( result = session.connection().execute( text( "UPDATE states set last_updated_ts=" - "IF(last_updated is NULL,0,UNIX_TIMESTAMP(last_updated)), " - "last_changed_ts=UNIX_TIMESTAMP(last_changed) " + "IF(last_updated is NULL,0," + "UNIX_TIMESTAMP(CONVERT_TZ(last_updated,'+00:00',@@global.time_zone)) " + "), " + "last_changed_ts=" + "UNIX_TIMESTAMP(CONVERT_TZ(last_changed,'+00:00',@@global.time_zone)) " "where last_updated_ts is NULL " "LIMIT 250000;" ) From 8a9de2671b3b9cda2a2808396f52bfaaf60ef896 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Jan 2023 22:07:57 -0500 Subject: [PATCH 037/187] Bumped version to 2023.2.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 0cd0e9b5189..2b85d085bba 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 = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index f4207bfb6a9..329efa00592 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b3" +version = "2023.2.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 63c218060bd2645d79ec4e8a5e062109372675a7 Mon Sep 17 00:00:00 2001 From: Tom Puttemans Date: Mon, 30 Jan 2023 14:56:57 +0100 Subject: [PATCH 038/187] Ignore empty payloads from DSMR Reader (#86841) * Ignore empty payloads from DSMR Reader * Simplify empty payload handling If the native value hasn't changed, requesting to store it won't have a performance impact. Co-authored-by: Franck Nijhof --------- Co-authored-by: Franck Nijhof --- homeassistant/components/dsmr_reader/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 7130380cbf5..72e24c52724 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -69,7 +69,10 @@ class DSMRSensor(SensorEntity): @callback def message_received(message): """Handle new MQTT messages.""" - if self.entity_description.state is not None: + if message.payload == "": + self._attr_native_value = None + elif self.entity_description.state is not None: + # Perform optional additional parsing self._attr_native_value = self.entity_description.state(message.payload) else: self._attr_native_value = message.payload From 71b13d8f3e234715489755c5811cd942bef76472 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 30 Jan 2023 08:18:56 -0500 Subject: [PATCH 039/187] Address Google mail late review (#86847) --- .../components/google_mail/__init__.py | 14 +++++-- .../components/google_mail/config_flow.py | 32 +++++++++------ homeassistant/components/google_mail/const.py | 1 + .../components/google_mail/entity.py | 2 + .../components/google_mail/manifest.json | 2 +- .../components/google_mail/notify.py | 9 ++--- .../components/google_mail/strings.json | 3 +- .../google_mail/translations/en.json | 3 +- homeassistant/generated/integrations.json | 2 +- tests/components/google_mail/conftest.py | 2 +- .../google_mail/fixtures/get_profile_2.json | 6 +++ .../google_mail/test_config_flow.py | 40 ++++++++++++++++--- tests/components/google_mail/test_init.py | 3 +- tests/components/google_mail/test_notify.py | 2 +- 14 files changed, 88 insertions(+), 33 deletions(-) create mode 100644 tests/components/google_mail/fixtures/get_profile_2.json diff --git a/homeassistant/components/google_mail/__init__.py b/homeassistant/components/google_mail/__init__.py index e769bc239f4..a24d5c17874 100644 --- a/homeassistant/components/google_mail/__init__.py +++ b/homeassistant/components/google_mail/__init__.py @@ -13,14 +13,22 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) +from homeassistant.helpers.typing import ConfigType from .api import AsyncConfigEntryAuth -from .const import DATA_AUTH, DOMAIN +from .const import DATA_AUTH, DATA_HASS_CONFIG, DOMAIN from .services import async_setup_services PLATFORMS = [Platform.NOTIFY, Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Google Mail platform.""" + hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google Mail from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) @@ -36,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err except ClientError as err: raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = auth + hass.data[DOMAIN][entry.entry_id] = auth hass.async_create_task( discovery.async_load_platform( @@ -44,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: Platform.NOTIFY, DOMAIN, {DATA_AUTH: auth, CONF_NAME: entry.title}, - {}, + hass.data[DOMAIN][DATA_HASS_CONFIG], ) ) diff --git a/homeassistant/components/google_mail/config_flow.py b/homeassistant/components/google_mail/config_flow.py index e7631199ddd..0552f57bf5c 100644 --- a/homeassistant/components/google_mail/config_flow.py +++ b/homeassistant/components/google_mail/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Any +from typing import Any, cast from google.oauth2.credentials import Credentials from googleapiclient.discovery import build @@ -57,23 +57,29 @@ class OAuth2FlowHandler( async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for the flow, or update existing entry.""" - if self.reauth_entry: - self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) - await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) - return self.async_abort(reason="reauth_successful") - credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) - - def _get_profile() -> dict[str, Any]: + def _get_profile() -> str: """Get profile from inside the executor.""" users = build( # pylint: disable=no-member "gmail", "v1", credentials=credentials ).users() - return users.getProfile(userId="me").execute() + return users.getProfile(userId="me").execute()["emailAddress"] - email = (await self.hass.async_add_executor_job(_get_profile))["emailAddress"] + credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) + email = await self.hass.async_add_executor_job(_get_profile) - await self.async_set_unique_id(email) - self._abort_if_unique_id_configured() + if not self.reauth_entry: + await self.async_set_unique_id(email) + self._abort_if_unique_id_configured() - return self.async_create_entry(title=email, data=data) + return self.async_create_entry(title=email, data=data) + + if self.reauth_entry.unique_id == email: + self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_abort( + reason="wrong_account", + description_placeholders={"email": cast(str, self.reauth_entry.unique_id)}, + ) diff --git a/homeassistant/components/google_mail/const.py b/homeassistant/components/google_mail/const.py index b9c2157e031..6e70ea9838c 100644 --- a/homeassistant/components/google_mail/const.py +++ b/homeassistant/components/google_mail/const.py @@ -16,6 +16,7 @@ ATTR_START = "start" ATTR_TITLE = "title" DATA_AUTH = "auth" +DATA_HASS_CONFIG = "hass_config" DEFAULT_ACCESS = [ "https://www.googleapis.com/auth/gmail.compose", "https://www.googleapis.com/auth/gmail.settings.basic", diff --git a/homeassistant/components/google_mail/entity.py b/homeassistant/components/google_mail/entity.py index bfa93f48107..5e447125e82 100644 --- a/homeassistant/components/google_mail/entity.py +++ b/homeassistant/components/google_mail/entity.py @@ -1,6 +1,7 @@ """Entity representing a Google Mail account.""" from __future__ import annotations +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from .api import AsyncConfigEntryAuth @@ -24,6 +25,7 @@ class GoogleMailEntity(Entity): f"{auth.oauth_session.config_entry.entry_id}_{description.key}" ) self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, auth.oauth_session.config_entry.entry_id)}, manufacturer=MANUFACTURER, name=auth.oauth_session.config_entry.unique_id, diff --git a/homeassistant/components/google_mail/manifest.json b/homeassistant/components/google_mail/manifest.json index 3693e8ac619..6e4757aa619 100644 --- a/homeassistant/components/google_mail/manifest.json +++ b/homeassistant/components/google_mail/manifest.json @@ -7,5 +7,5 @@ "requirements": ["google-api-python-client==2.71.0"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", - "integration_type": "device" + "integration_type": "service" } diff --git a/homeassistant/components/google_mail/notify.py b/homeassistant/components/google_mail/notify.py index 9abf75ea1e9..eba38c32491 100644 --- a/homeassistant/components/google_mail/notify.py +++ b/homeassistant/components/google_mail/notify.py @@ -3,10 +3,9 @@ from __future__ import annotations import base64 from email.message import EmailMessage -from typing import Any, cast +from typing import Any from googleapiclient.http import HttpRequest -import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, @@ -27,9 +26,9 @@ async def async_get_service( hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = None, -) -> GMailNotificationService: +) -> GMailNotificationService | None: """Get the notification service.""" - return GMailNotificationService(cast(DiscoveryInfoType, discovery_info)) + return GMailNotificationService(discovery_info) if discovery_info else None class GMailNotificationService(BaseNotificationService): @@ -61,6 +60,6 @@ class GMailNotificationService(BaseNotificationService): msg = users.drafts().create(userId=email["From"], body={ATTR_MESSAGE: body}) else: if not to_addrs: - raise vol.Invalid("recipient address required") + raise ValueError("recipient address required") msg = users.messages().send(userId=email["From"], body=body) await self.hass.async_add_executor_job(msg.execute) diff --git a/homeassistant/components/google_mail/strings.json b/homeassistant/components/google_mail/strings.json index eaebca01e5d..eb44bffb134 100644 --- a/homeassistant/components/google_mail/strings.json +++ b/homeassistant/components/google_mail/strings.json @@ -21,7 +21,8 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "wrong_account": "Wrong account: Please authenticate with {email}." }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/google_mail/translations/en.json b/homeassistant/components/google_mail/translations/en.json index 51d69638ed2..9f9495d0ec0 100644 --- a/homeassistant/components/google_mail/translations/en.json +++ b/homeassistant/components/google_mail/translations/en.json @@ -12,7 +12,8 @@ "oauth_error": "Received invalid token data.", "reauth_successful": "Re-authentication was successful", "timeout_connect": "Timeout establishing connection", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "wrong_account": "Wrong account: Please authenticate with {email}." }, "create_entry": { "default": "Successfully authenticated" diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 8beb5db0e6f..8ceabb723f8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2007,7 +2007,7 @@ "name": "Google Domains" }, "google_mail": { - "integration_type": "device", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling", "name": "Google Mail" diff --git a/tests/components/google_mail/conftest.py b/tests/components/google_mail/conftest.py index 3ecaf5b5d09..d0b7afae786 100644 --- a/tests/components/google_mail/conftest.py +++ b/tests/components/google_mail/conftest.py @@ -114,6 +114,6 @@ async def mock_setup_integration( ), ): assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() + await hass.async_block_till_done() yield func diff --git a/tests/components/google_mail/fixtures/get_profile_2.json b/tests/components/google_mail/fixtures/get_profile_2.json new file mode 100644 index 00000000000..3b36d576183 --- /dev/null +++ b/tests/components/google_mail/fixtures/get_profile_2.json @@ -0,0 +1,6 @@ +{ + "emailAddress": "example2@gmail.com", + "messagesTotal": 35308, + "threadsTotal": 33901, + "historyId": "4178212" +} diff --git a/tests/components/google_mail/test_config_flow.py b/tests/components/google_mail/test_config_flow.py index b0814c4b643..08f71368cc2 100644 --- a/tests/components/google_mail/test_config_flow.py +++ b/tests/components/google_mail/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from httplib2 import Response +import pytest from homeassistant import config_entries from homeassistant.components.google_mail.const import DOMAIN @@ -68,14 +69,36 @@ async def test_full_flow( ) +@pytest.mark.parametrize( + "fixture,abort_reason,placeholders,calls,access_token", + [ + ("get_profile", "reauth_successful", None, 1, "updated-access-token"), + ( + "get_profile_2", + "wrong_account", + {"email": "example@gmail.com"}, + 0, + "mock-access-token", + ), + ], +) async def test_reauth( hass: HomeAssistant, hass_client_no_auth, aioclient_mock: AiohttpClientMocker, current_request_with_host, config_entry: MockConfigEntry, + fixture: str, + abort_reason: str, + placeholders: dict[str, str], + calls: int, + access_token: str, ) -> None: - """Test the reauthentication case updates the existing config entry.""" + """Test the re-authentication case updates the correct config entry. + + Make sure we abort if the user selects the + wrong account on the consent screen. + """ config_entry.add_to_hass(hass) config_entry.async_start_reauth(hass) @@ -118,19 +141,26 @@ async def test_reauth( with patch( "homeassistant.components.google_mail.async_setup_entry", return_value=True - ) as mock_setup: + ) as mock_setup, patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture(f"google_mail/{fixture}.json"), encoding="UTF-8"), + ), + ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_setup.mock_calls) == 1 assert result.get("type") == "abort" - assert result.get("reason") == "reauth_successful" + assert result["reason"] == abort_reason + assert result["description_placeholders"] == placeholders + assert len(mock_setup.mock_calls) == calls assert config_entry.unique_id == TITLE assert "token" in config_entry.data # Verify access token is refreshed - assert config_entry.data["token"].get("access_token") == "updated-access-token" + assert config_entry.data["token"].get("access_token") == access_token assert config_entry.data["token"].get("refresh_token") == "mock-refresh-token" diff --git a/tests/components/google_mail/test_init.py b/tests/components/google_mail/test_init.py index b57547cfd70..239c7f9d51f 100644 --- a/tests/components/google_mail/test_init.py +++ b/tests/components/google_mail/test_init.py @@ -29,7 +29,7 @@ async def test_setup_success( await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert not len(hass.services.async_services().get(DOMAIN, {})) + assert not hass.services.async_services().get(DOMAIN) @pytest.mark.parametrize("expires_at", [time.time() - 3600], ids=["expired"]) @@ -125,6 +125,7 @@ async def test_device_info( entry = hass.config_entries.async_entries(DOMAIN)[0] device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + assert device.entry_type is dr.DeviceEntryType.SERVICE assert device.identifiers == {(DOMAIN, entry.entry_id)} assert device.manufacturer == "Google, Inc." assert device.name == "example@gmail.com" diff --git a/tests/components/google_mail/test_notify.py b/tests/components/google_mail/test_notify.py index c95d0fa8df3..1e9a174d81f 100644 --- a/tests/components/google_mail/test_notify.py +++ b/tests/components/google_mail/test_notify.py @@ -52,7 +52,7 @@ async def test_notify_voluptuous_error( """Test voluptuous error thrown when drafting email.""" await setup_integration() - with pytest.raises(Invalid) as ex: + with pytest.raises(ValueError) as ex: await hass.services.async_call( NOTIFY_DOMAIN, "example_gmail_com", From 0d27ee4fd8648e51571558422bfc907cdd4aaa13 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jan 2023 02:16:29 -1000 Subject: [PATCH 040/187] Cache the names and area lists in the default agent (#86874) * Cache the names and area lists in the default agent fixes #86803 * add coverage to make sure the entity cache busts * add areas test * cover the last line --- .../components/conversation/default_agent.py | 50 ++++- tests/components/conversation/test_init.py | 212 +++++++++++++++++- 2 files changed, 256 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 7be37062d13..68fa891bb88 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -71,6 +71,8 @@ class DefaultAgent(AbstractConversationAgent): # intent -> [sentences] self._config_intents: dict[str, Any] = {} + self._areas_list: TextSlotList | None = None + self._names_list: TextSlotList | None = None async def async_initialize(self, config_intents): """Initialize the default agent.""" @@ -81,6 +83,22 @@ class DefaultAgent(AbstractConversationAgent): if config_intents: self._config_intents = config_intents + self.hass.bus.async_listen( + area_registry.EVENT_AREA_REGISTRY_UPDATED, + self._async_handle_area_registry_changed, + run_immediately=True, + ) + self.hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + self._async_handle_entity_registry_changed, + run_immediately=True, + ) + self.hass.bus.async_listen( + core.EVENT_STATE_CHANGED, + self._async_handle_state_changed, + run_immediately=True, + ) + async def async_process(self, user_input: ConversationInput) -> ConversationResult: """Process a sentence.""" language = user_input.language or self.hass.config.language @@ -310,8 +328,29 @@ class DefaultAgent(AbstractConversationAgent): return lang_intents + @core.callback + def _async_handle_area_registry_changed(self, event: core.Event) -> None: + """Clear area area cache when the area registry has changed.""" + self._areas_list = None + + @core.callback + def _async_handle_entity_registry_changed(self, event: core.Event) -> None: + """Clear names list cache when an entity changes aliases.""" + if event.data["action"] == "update" and "aliases" not in event.data["changes"]: + return + self._names_list = None + + @core.callback + def _async_handle_state_changed(self, event: core.Event) -> None: + """Clear names list cache when a state is added or removed from the state machine.""" + if event.data.get("old_state") and event.data.get("new_state"): + return + self._names_list = None + def _make_areas_list(self) -> TextSlotList: """Create slot list mapping area names/aliases to area ids.""" + if self._areas_list is not None: + return self._areas_list registry = area_registry.async_get(self.hass) areas = [] for entry in registry.async_list_areas(): @@ -320,16 +359,18 @@ class DefaultAgent(AbstractConversationAgent): for alias in entry.aliases: areas.append((alias, entry.id)) - return TextSlotList.from_tuples(areas) + self._areas_list = TextSlotList.from_tuples(areas) + return self._areas_list def _make_names_list(self) -> TextSlotList: """Create slot list mapping entity names/aliases to entity ids.""" + if self._names_list is not None: + return self._names_list states = self.hass.states.async_all() registry = entity_registry.async_get(self.hass) names = [] for state in states: - domain = state.entity_id.split(".", maxsplit=1)[0] - context = {"domain": domain} + context = {"domain": state.domain} entry = registry.async_get(state.entity_id) if entry is not None: @@ -344,7 +385,8 @@ class DefaultAgent(AbstractConversationAgent): # Default name names.append((state.name, state.entity_id, context)) - return TextSlotList.from_tuples(names) + self._names_list = TextSlotList.from_tuples(names) + return self._names_list def _get_error_text( self, response_type: ResponseType, lang_intents: LanguageIntents diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index e79cd69475c..f4b386cbe4b 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -7,10 +7,15 @@ import pytest from homeassistant.components import conversation from homeassistant.components.cover import SERVICE_OPEN_COVER from homeassistant.core import DOMAIN as HASS_DOMAIN, Context -from homeassistant.helpers import entity_registry, intent +from homeassistant.helpers import ( + area_registry, + device_registry, + entity_registry, + intent, +) from homeassistant.setup import async_setup_component -from tests.common import async_mock_service +from tests.common import MockConfigEntry, async_mock_service class OrderBeerIntentHandler(intent.IntentHandler): @@ -75,6 +80,143 @@ async def test_http_processing_intent( } +async def test_http_processing_intent_entity_added( + hass, init_components, hass_client, hass_admin_user +): + """Test processing intent via HTTP API with entities added later. + + We want to ensure that adding an entity later busts the cache + so that the new entity is available as well as any aliases. + """ + er = entity_registry.async_get(hass) + er.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen") + er.async_update_entity("light.kitchen", aliases={"my cool light"}) + hass.states.async_set("light.kitchen", "off") + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on my cool light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on my cool light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Add an alias + er.async_get_or_create("light", "demo", "5678", suggested_object_id="late") + hass.states.async_set("light.late", "off", {"friendly_name": "friendly light"}) + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on friendly light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on friendly light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.late", "name": "friendly light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Now add an alias + er.async_update_entity("light.late", aliases={"late added light"}) + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on late added light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on late added light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.late", "name": "friendly light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Now delete the entity + er.async_remove("light.late") + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on late added light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data == { + "conversation_id": None, + "response": { + "card": {}, + "data": {"code": "no_intent_match"}, + "language": hass.config.language, + "response_type": "error", + "speech": { + "plain": { + "extra_data": None, + "speech": "Sorry, I couldn't understand " "that", + } + }, + }, + } + + @pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on")) async def test_turn_on_intent(hass, init_components, sentence): """Test calling the turn on intent.""" @@ -569,3 +711,69 @@ async def test_non_default_response(hass, init_components): ) ) assert result.response.speech["plain"]["speech"] == "Opened front door" + + +async def test_turn_on_area(hass, init_components): + """Test turning on an area.""" + er = entity_registry.async_get(hass) + dr = device_registry.async_get(hass) + ar = area_registry.async_get(hass) + entry = MockConfigEntry(domain="test") + + device = dr.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + kitchen_area = ar.async_create("kitchen") + dr.async_update_device(device.id, area_id=kitchen_area.id) + + er.async_get_or_create("light", "demo", "1234", suggested_object_id="stove") + er.async_update_entity( + "light.stove", aliases={"my stove light"}, area_id=kitchen_area.id + ) + hass.states.async_set("light.stove", "off") + + calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on lights in the kitchen"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == "turn_on" + assert call.data == {"entity_id": "light.stove"} + + basement_area = ar.async_create("basement") + dr.async_update_device(device.id, area_id=basement_area.id) + er.async_update_entity("light.stove", area_id=basement_area.id) + calls.clear() + + # Test that the area is updated + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on lights in the kitchen"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 0 + + # Test the new area works + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on lights in the basement"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == "turn_on" + assert call.data == {"entity_id": "light.stove"} From 07e9b0e98bc659b17f96e99d07157a7f66bc020b Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Sun, 29 Jan 2023 15:00:36 +0100 Subject: [PATCH 041/187] Add Bosch SHC description and host form strings (#86897) * Add description to setup SHC II. Add missing host info in reauth_confirm * Remove template value in en.json --- homeassistant/components/bosch_shc/strings.json | 7 +++++-- homeassistant/components/bosch_shc/translations/de.json | 7 +++++-- homeassistant/components/bosch_shc/translations/en.json | 7 +++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bosch_shc/strings.json b/homeassistant/components/bosch_shc/strings.json index 15fb061ef2b..2b5720f0849 100644 --- a/homeassistant/components/bosch_shc/strings.json +++ b/homeassistant/components/bosch_shc/strings.json @@ -14,11 +14,14 @@ } }, "confirm_discovery": { - "description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?" + "description": "Smart Home Controller I: Please press the front-side button until LED starts flashing.\nSmart Home Controller II: Press the function button shortly. Cloud and network lights start blinking orange.\nDevice is now ready to be paired.\n\nReady to continue to set up {model} @ {host} with Home Assistant?" }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "The bosch_shc integration needs to re-authenticate your account" + "description": "The bosch_shc integration needs to re-authenticate your account", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } } }, "error": { diff --git a/homeassistant/components/bosch_shc/translations/de.json b/homeassistant/components/bosch_shc/translations/de.json index 6ace3cedc95..9c0a9a359f9 100644 --- a/homeassistant/components/bosch_shc/translations/de.json +++ b/homeassistant/components/bosch_shc/translations/de.json @@ -14,7 +14,7 @@ "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Bitte dr\u00fccke die frontseitige Taste des Bosch Smart Home Controllers, bis die LED zu blinken beginnt.\nBist du bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?" + "description": "Smart Home Controller I: Bitte dr\u00fccke die frontseitige Taste, bis die LED zu blinken beginnt.\nSmart Home Controller II: Dr\u00fccke kurz die Funktionstaste. Die Cloud- und Netzwerkleuchten beginnen orange zu blinken.\nDas Ger\u00e4t ist nun f\u00fcr die Kopplung bereit.\n\nBist du bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?" }, "credentials": { "data": { @@ -23,7 +23,10 @@ }, "reauth_confirm": { "description": "Die bosch_shc Integration muss dein Konto neu authentifizieren", - "title": "Integration erneut authentifizieren" + "title": "Integration erneut authentifizieren", + "data": { + "host": "Host" + } }, "user": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/en.json b/homeassistant/components/bosch_shc/translations/en.json index ab5cde9ef27..ad7859e80e6 100644 --- a/homeassistant/components/bosch_shc/translations/en.json +++ b/homeassistant/components/bosch_shc/translations/en.json @@ -14,7 +14,7 @@ "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?" + "description": "Smart Home Controller I: Please press the front-side button until LED starts flashing.\nSmart Home Controller II: Press the function button shortly. Cloud and network lights start blinking orange.\nDevice is now ready to be paired.\n\nReady to continue to set up {model} @ {host} with Home Assistant?" }, "credentials": { "data": { @@ -23,7 +23,10 @@ }, "reauth_confirm": { "description": "The bosch_shc integration needs to re-authenticate your account", - "title": "Reauthenticate Integration" + "title": "Reauthenticate Integration", + "data": { + "host": "Host" + } }, "user": { "data": { From f14771ccf2108b2b87194890b4e7cfbcee50dad8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jan 2023 16:33:23 -1000 Subject: [PATCH 042/187] Fix old indices not being removed in schema migration leading to slow MySQL queries (#86917) fixes #83787 --- .../components/recorder/db_schema.py | 2 +- .../components/recorder/migration.py | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 47b9658b053..1fef18573ea 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -55,7 +55,7 @@ from .models import StatisticData, StatisticMetaData, process_timestamp # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 32 +SCHEMA_VERSION = 33 _StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index af1446400c2..85459e5acc9 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -505,12 +505,12 @@ def _apply_update( # noqa: C901 timestamp_type = "FLOAT" if new_version == 1: - _create_index(session_maker, "events", "ix_events_time_fired") + # This used to create ix_events_time_fired, but it was removed in version 32 + pass elif new_version == 2: # Create compound start/end index for recorder_runs _create_index(session_maker, "recorder_runs", "ix_recorder_runs_start_end") - # Create indexes for states - _create_index(session_maker, "states", "ix_states_last_updated") + # This used to create ix_states_last_updated bit it was removed in version 32 elif new_version == 3: # There used to be a new index here, but it was removed in version 4. pass @@ -529,8 +529,7 @@ def _apply_update( # noqa: C901 _drop_index(session_maker, "states", "states__state_changes") _drop_index(session_maker, "states", "states__significant_changes") _drop_index(session_maker, "states", "ix_states_entity_id_created") - - _create_index(session_maker, "states", "ix_states_entity_id_last_updated") + # This used to create ix_states_entity_id_last_updated, but it was removed in version 32 elif new_version == 5: # Create supporting index for States.event_id foreign key _create_index(session_maker, "states", "ix_states_event_id") @@ -541,20 +540,21 @@ def _apply_update( # noqa: C901 ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) _create_index(session_maker, "events", "ix_events_context_id") - _create_index(session_maker, "events", "ix_events_context_user_id") + # This used to create ix_events_context_user_id, but it was removed in version 28 _add_columns( session_maker, "states", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) _create_index(session_maker, "states", "ix_states_context_id") - _create_index(session_maker, "states", "ix_states_context_user_id") + # This used to create ix_states_context_user_id, but it was removed in version 28 elif new_version == 7: - _create_index(session_maker, "states", "ix_states_entity_id") + # There used to be a ix_states_entity_id index here, but it was removed in later schema + pass elif new_version == 8: _add_columns(session_maker, "events", ["context_parent_id CHARACTER(36)"]) _add_columns(session_maker, "states", ["old_state_id INTEGER"]) - _create_index(session_maker, "events", "ix_events_context_parent_id") + # This used to create ix_events_context_parent_id, but it was removed in version 28 elif new_version == 9: # We now get the context from events with a join # since its always there on state_changed events @@ -572,7 +572,7 @@ def _apply_update( # noqa: C901 # Redundant keys on composite index: # We already have ix_states_entity_id_last_updated _drop_index(session_maker, "states", "ix_states_entity_id") - _create_index(session_maker, "events", "ix_events_event_type_time_fired") + # This used to create ix_events_event_type_time_fired, but it was removed in version 32 _drop_index(session_maker, "events", "ix_events_event_type") elif new_version == 10: # Now done in step 11 @@ -857,6 +857,11 @@ def _apply_update( # noqa: C901 _drop_index(session_maker, "events", "ix_events_event_type_time_fired") _drop_index(session_maker, "states", "ix_states_last_updated") _drop_index(session_maker, "events", "ix_events_time_fired") + elif new_version == 33: + # This index is no longer used and can cause MySQL to use the wrong index + # when querying the states table. + # https://github.com/home-assistant/core/issues/83787 + _drop_index(session_maker, "states", "ix_states_entity_id") else: raise ValueError(f"No schema migration defined for version {new_version}") From 423acfa93b857a57d6ddbd519a97bc9658ec04a6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 30 Jan 2023 14:31:27 +0100 Subject: [PATCH 043/187] Drop minus sign on negative zero (#86939) * Drop minus sign on negative zero * Add tests --- homeassistant/components/sensor/__init__.py | 9 +++++++++ tests/components/sensor/test_init.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 351976db162..f61be0193f2 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -9,6 +9,7 @@ from datetime import date, datetime, timedelta, timezone from decimal import Decimal, InvalidOperation as DecimalInvalidOperation import logging from math import ceil, floor, log10 +import re from typing import Any, Final, cast, final from homeassistant.config_entries import ConfigEntry @@ -84,6 +85,8 @@ _LOGGER: Final = logging.getLogger(__name__) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +NEGATIVE_ZERO_PATTERN = re.compile(r"^-(0\.?0*)$") + SCAN_INTERVAL: Final = timedelta(seconds=30) __all__ = [ @@ -647,8 +650,14 @@ class SensorEntity(Entity): unit_of_measurement, ) value = f"{converted_numerical_value:.{precision}f}" + # This can be replaced with adding the z option when we drop support for + # Python 3.10 + value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value) elif precision is not None: value = f"{numerical_value:.{precision}f}" + # This can be replaced with adding the z option when we drop support for + # Python 3.10 + value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value) # Validate unit of measurement used for sensors with a device class if ( diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index e65ae8ae7b8..89501cf37df 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -538,6 +538,15 @@ async def test_custom_unit( "29.921", # Native precision is 3 "1013.24", # One digit of precision removed when converting ), + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.INHG, + UnitOfPressure.HPA, + -0.0001, + 3, + "0.000", # Native precision is 3 + "0.00", # One digit of precision removed when converting + ), ], ) async def test_native_precision_scaling( @@ -595,6 +604,14 @@ async def test_native_precision_scaling( "1000.000", "1000.0000", ), + ( + SensorDeviceClass.DISTANCE, + UnitOfLength.KILOMETERS, + 1, + -0.04, + "-0.040", + "0.0", # Make sure minus is dropped + ), ], ) async def test_custom_precision_native_precision( From a491bfe84c4070badb612cb8fbd55609fec1024d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 09:13:30 -0500 Subject: [PATCH 044/187] Bumped version to 2023.2.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 2b85d085bba..44128d3f474 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 = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 329efa00592..381c0c34dfc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b4" +version = "2023.2.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 3f717ae8545e952cceaff0611adf238cb8a69a3b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 30 Jan 2023 19:15:11 +0100 Subject: [PATCH 045/187] Fix MQTT discovery failing after bad config update (#86935) * Fix MQTT discovery failing after bad config update * Update last discovery payload after update success * Improve test, correct update assignment * send_discovery_done to finally-catch vol.Error * Just use try..finally * Remove extra line * use elif to avoid log confusion --- homeassistant/components/mqtt/mixins.py | 16 +++++-- tests/components/mqtt/test_discovery.py | 57 ++++++++++++++++++++++++- tests/components/mqtt/test_tag.py | 46 ++++++++++++++++++++ 3 files changed, 114 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 51d7ca401ca..e90301875e4 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -744,8 +744,12 @@ class MqttDiscoveryDeviceUpdate(ABC): self.log_name, discovery_hash, ) - await self.async_update(discovery_payload) - if not discovery_payload: + try: + await self.async_update(discovery_payload) + finally: + send_discovery_done(self.hass, self._discovery_data) + self._discovery_data[ATTR_DISCOVERY_PAYLOAD] = discovery_payload + elif not discovery_payload: # Unregister and clean up the current discovery instance stop_discovery_updates( self.hass, self._discovery_data, self._remove_discovery_updated @@ -869,15 +873,19 @@ class MqttDiscoveryUpdate(Entity): _LOGGER.info("Removing component: %s", self.entity_id) self._cleanup_discovery_on_remove() await _async_remove_state_and_registry_entry(self) + send_discovery_done(self.hass, self._discovery_data) elif self._discovery_update: if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: # Non-empty, changed payload: Notify component _LOGGER.info("Updating component: %s", self.entity_id) - await self._discovery_update(payload) + try: + await self._discovery_update(payload) + finally: + send_discovery_done(self.hass, self._discovery_data) else: # Non-empty, unchanged payload: Ignore to avoid changing states _LOGGER.debug("Ignoring unchanged update for: %s", self.entity_id) - send_discovery_done(self.hass, self._discovery_data) + send_discovery_done(self.hass, self._discovery_data) if discovery_hash: assert self._discovery_data is not None diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 89a56903c3b..251c0af24a6 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -6,6 +6,7 @@ import re from unittest.mock import AsyncMock, call, patch import pytest +from voluptuous import MultipleInvalid from homeassistant import config_entries from homeassistant.components import mqtt @@ -1494,7 +1495,7 @@ async def test_clean_up_registry_monitoring( async def test_unique_id_collission_has_priority( hass, mqtt_mock_entry_no_yaml_config, entity_reg ): - """Test tehe unique_id collision detection has priority over registry disabled items.""" + """Test the unique_id collision detection has priority over registry disabled items.""" await mqtt_mock_entry_no_yaml_config() config = { "name": "sbfspot_12345", @@ -1534,3 +1535,57 @@ async def test_unique_id_collission_has_priority( assert entity_reg.async_get("sensor.sbfspot_12345_1") is not None # Verify the second entity is not created because it is not unique assert entity_reg.async_get("sensor.sbfspot_12345_2") is None + + +@pytest.mark.xfail(raises=MultipleInvalid) +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_update_with_bad_config_not_breaks_discovery( + hass: ha.HomeAssistant, mqtt_mock_entry_no_yaml_config, entity_reg +) -> None: + """Test a bad update does not break discovery.""" + await mqtt_mock_entry_no_yaml_config() + # discover a sensor + config1 = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/state", + } + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/config", + json.dumps(config1), + ) + await hass.async_block_till_done() + assert hass.states.get("sensor.sbfspot_12345") is not None + # update with a breaking config + config2 = { + "name": "sbfspot_12345", + "availability": 1, + "state_topic": "homeassistant_test/sensor/sbfspot_0/state", + } + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/config", + json.dumps(config2), + ) + await hass.async_block_till_done() + # update the state topic + config3 = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/new_state_topic", + } + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/config", + json.dumps(config3), + ) + await hass.async_block_till_done() + + # Send an update for the state + async_fire_mqtt_message( + hass, + "homeassistant_test/sensor/sbfspot_0/new_state_topic", + "new_value", + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.sbfspot_12345").state == "new_value" diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index ed33ed0dcdf..9276f9658b6 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -4,10 +4,12 @@ import json from unittest.mock import ANY, patch import pytest +from voluptuous import MultipleInvalid from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -805,6 +807,50 @@ async def test_cleanup_device_with_entity2( assert device_entry is None +@pytest.mark.xfail(raises=MultipleInvalid) +async def test_update_with_bad_config_not_breaks_discovery( + hass: HomeAssistant, + mqtt_mock_entry_no_yaml_config, + tag_mock, +) -> None: + """Test a bad update does not break discovery.""" + await mqtt_mock_entry_no_yaml_config() + config1 = { + "topic": "test-topic", + "device": {"identifiers": ["helloworld"]}, + } + config2 = { + "topic": "test-topic", + "device": {"bad_key": "some bad value"}, + } + + config3 = { + "topic": "test-topic-update", + "device": {"identifiers": ["helloworld"]}, + } + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + data3 = json.dumps(config3) + + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data1) + await hass.async_block_till_done() + + # Update with bad identifier + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data2) + await hass.async_block_till_done() + + # Topic update + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data3) + await hass.async_block_till_done() + + # Fake tag scan. + async_fire_mqtt_message(hass, "test-topic-update", "12345") + + await hass.async_block_till_done() + tag_mock.assert_called_once_with(ANY, "12345", ANY) + + async def test_unload_entry(hass, device_reg, mqtt_mock, tag_mock, tmp_path) -> None: """Test unloading the MQTT entry.""" From 2e26a40bba097e7316c6cbf3d0506778c683d5aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Jan 2023 08:00:34 -1000 Subject: [PATCH 046/187] Speed up live history setup if there is no pending data to commit (#86942) --- homeassistant/components/recorder/core.py | 2 ++ tests/components/logbook/test_websocket_api.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 62c1213a4a4..ddca32e6970 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1044,6 +1044,8 @@ class Recorder(threading.Thread): async def async_block_till_done(self) -> None: """Async version of block_till_done.""" + if self._queue.empty() and not self._event_session_has_pending_writes(): + return event = asyncio.Event() self.queue_task(SynchronizeTask(event)) await event.wait() diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 91d1a95f75b..805213cf3bf 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2303,6 +2303,11 @@ async def test_recorder_is_far_behind(recorder_mock, hass, hass_ws_client, caplo hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "1"}) await hass.async_block_till_done() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" From 171acc22ca6f0520291ae9c23e0e386717c4c162 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 30 Jan 2023 16:15:53 +0100 Subject: [PATCH 047/187] Fix ThreeWayHandle sensor in Overkiz integration (#86953) Fix typo in sensor.py Fixes https://github.com/home-assistant/core/issues/85913 --- homeassistant/components/overkiz/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index e6a0e04deb7..7e7c8ad2625 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -386,7 +386,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ key=OverkizState.CORE_THREE_WAY_HANDLE_DIRECTION, name="Three way handle direction", device_class=SensorDeviceClass.ENUM, - options=["open", "tilt", "close"], + options=["open", "tilt", "closed"], translation_key="three_way_handle_direction", ), ] From 0713e034b9f6530a7bf8815faccf30ed74474457 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Jan 2023 07:38:33 -1000 Subject: [PATCH 048/187] Silence spurious warnings about removing ix_states_entity_id with newer installs (#86961) * Silence spurious warnings about removing ix_states_entity_id with newer installs https://ptb.discord.com/channels/330944238910963714/427516175237382144/1069648035459641465 * Silence spurious warnings about removing ix_states_entity_id with newer installs https://ptb.discord.com/channels/330944238910963714/427516175237382144/1069648035459641465 --- homeassistant/components/recorder/migration.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 85459e5acc9..5b1f048aead 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -274,9 +274,13 @@ def _drop_index( "Finished dropping index %s from table %s", index_name, table_name ) else: - if index_name == "ix_states_context_parent_id": - # Was only there on nightly so we do not want + if index_name in ("ix_states_entity_id", "ix_states_context_parent_id"): + # ix_states_context_parent_id was only there on nightly so we do not want # to generate log noise or issues about it. + # + # ix_states_entity_id was only there for users who upgraded from schema + # version 8 or earlier. Newer installs will not have it so we do not + # want to generate log noise or issues about it. return _LOGGER.warning( From 0b015d46c330937e8819e4e0ca65b08c10c44c7a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 14:08:19 -0500 Subject: [PATCH 049/187] Fix some mobile app sensor registration/update issues (#86965) Co-authored-by: Franck Nijhof --- homeassistant/components/mobile_app/sensor.py | 6 +- .../components/mobile_app/webhook.py | 42 +++++----- tests/components/mobile_app/test_webhook.py | 80 +++++++++++++++++++ 3 files changed, 107 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 0d2c05df518..fc325b1b6e9 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for mobile_app.""" from __future__ import annotations +from datetime import date, datetime from typing import Any from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass @@ -10,6 +11,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util from .const import ( @@ -99,7 +101,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): self._config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement @property - def native_value(self): + def native_value(self) -> StateType | date | datetime: """Return the state of the sensor.""" if (state := self._config[ATTR_SENSOR_STATE]) in (None, STATE_UNKNOWN): return None @@ -122,7 +124,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): return state @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement this sensor expresses itself in.""" return self._config.get(ATTR_SENSOR_UOM) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 7a86755bc5d..6476b681256 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -22,9 +22,7 @@ from homeassistant.components import ( notify as hass_notify, tag, ) -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES as BINARY_SENSOR_CLASSES, -) +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.device_tracker import ( ATTR_BATTERY, @@ -33,10 +31,7 @@ from homeassistant.components.device_tracker import ( ATTR_LOCATION_NAME, ) from homeassistant.components.frontend import MANIFEST_JSON -from homeassistant.components.sensor import ( - DEVICE_CLASSES as SENSOR_CLASSES, - STATE_CLASSES as SENSOSR_STATE_CLASSES, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -58,7 +53,7 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA +from homeassistant.helpers.entity import EntityCategory from homeassistant.util.decorator import Registry from .const import ( @@ -131,8 +126,7 @@ WEBHOOK_COMMANDS: Registry[ str, Callable[[HomeAssistant, ConfigEntry, Any], Coroutine[Any, Any, Response]] ] = Registry() -COMBINED_CLASSES = set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES) -SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] +SENSOR_TYPES = (ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR) WEBHOOK_PAYLOAD_SCHEMA = vol.Schema( { @@ -507,19 +501,27 @@ def _extract_sensor_unique_id(webhook_id: str, unique_id: str) -> str: vol.All( { vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( - vol.Lower, vol.In(COMBINED_CLASSES) + vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.Any( + None, + vol.All(vol.Lower, vol.Coerce(BinarySensorDeviceClass)), + vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)), ), vol.Required(ATTR_SENSOR_NAME): cv.string, vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - vol.Optional(ATTR_SENSOR_UOM): cv.string, + vol.Optional(ATTR_SENSOR_UOM): vol.Any(None, cv.string), vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any( - None, bool, str, int, float + None, bool, int, float, str + ), + vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): vol.Any( + None, vol.Coerce(EntityCategory) + ), + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any( + None, cv.icon + ), + vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.Any( + None, vol.Coerce(SensorStateClass) ), - vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.In(SENSOSR_STATE_CLASSES), vol.Optional(ATTR_SENSOR_DISABLED): bool, }, _validate_state_class_sensor, @@ -619,8 +621,10 @@ async def webhook_update_sensor_states( sensor_schema_full = vol.Schema( { vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, str, int, float), + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any( + None, cv.icon + ), + vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, int, float, str), vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, } diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 996471c939f..a6ab5797a11 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -979,6 +979,32 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client): entry = ent_reg.async_get("sensor.test_1_battery_state") assert entry.disabled_by is None + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "New Name 2", + "state": 100, + "type": "sensor", + "unique_id": "abcd", + "state_class": None, + "device_class": None, + "entity_category": None, + "icon": None, + "unit_of_measurement": None, + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.original_name == "Test 1 New Name 2" + assert entry.device_class is None + assert entry.unit_of_measurement is None + assert entry.entity_category is None + assert entry.original_icon is None + async def test_webhook_handle_conversation_process( hass, create_registrations, webhook_client, mock_agent @@ -1017,3 +1043,57 @@ async def test_webhook_handle_conversation_process( }, "conversation_id": None, } + + +async def test_sending_sensor_state(hass, create_registrations, webhook_client, caplog): + """Test that we can register and send sensor state as number and None.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Battery State", + "state": 100, + "type": "sensor", + "unique_id": "abcd", + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + ent_reg = er.async_get(hass) + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.original_name == "Test 1 Battery State" + assert entry.device_class is None + assert entry.unit_of_measurement is None + assert entry.entity_category is None + assert entry.original_icon == "mdi:cellphone" + assert entry.disabled_by is None + + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_1_battery_state") + assert state is not None + assert state.state == "100" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": { + "state": 50.0000, + "type": "sensor", + "unique_id": "abcd", + }, + }, + ) + + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_1_battery_state") + assert state is not None + assert state.state == "50.0" From 81de0bba22c14e5ac4182f8b882af40d52cdc2d7 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 30 Jan 2023 12:25:22 -0600 Subject: [PATCH 050/187] Performance improvements for Assist (#86966) * Move hassil recognize into executor * Bump hassil to 0.2.6 * Disable template parsing in name/area lists * Don't iterate over hass.config.components directly --- .../components/conversation/default_agent.py | 18 +++++++++++------- .../components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 68fa891bb88..2702372a660 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -127,7 +127,9 @@ class DefaultAgent(AbstractConversationAgent): "name": self._make_names_list(), } - result = recognize(user_input.text, lang_intents.intents, slot_lists=slot_lists) + result = await self.hass.async_add_executor_job( + recognize, user_input.text, lang_intents.intents, slot_lists + ) if result is None: _LOGGER.debug("No intent was matched for '%s'", user_input.text) return _make_error_result( @@ -214,13 +216,15 @@ class DefaultAgent(AbstractConversationAgent): async def async_get_or_load_intents(self, language: str) -> LanguageIntents | None: """Load all intents of a language with lock.""" + hass_components = set(self.hass.config.components) async with self._lang_lock[language]: return await self.hass.async_add_executor_job( - self._get_or_load_intents, - language, + self._get_or_load_intents, language, hass_components ) - def _get_or_load_intents(self, language: str) -> LanguageIntents | None: + def _get_or_load_intents( + self, language: str, hass_components: set[str] + ) -> LanguageIntents | None: """Load all intents for language (run inside executor).""" lang_intents = self._lang_intents.get(language) @@ -233,7 +237,7 @@ class DefaultAgent(AbstractConversationAgent): # Check if any new components have been loaded intents_changed = False - for component in self.hass.config.components: + for component in hass_components: if component in loaded_components: continue @@ -359,7 +363,7 @@ class DefaultAgent(AbstractConversationAgent): for alias in entry.aliases: areas.append((alias, entry.id)) - self._areas_list = TextSlotList.from_tuples(areas) + self._areas_list = TextSlotList.from_tuples(areas, allow_template=False) return self._areas_list def _make_names_list(self) -> TextSlotList: @@ -385,7 +389,7 @@ class DefaultAgent(AbstractConversationAgent): # Default name names.append((state.name, state.entity_id, context)) - self._names_list = TextSlotList.from_tuples(names) + self._names_list = TextSlotList.from_tuples(names, allow_template=False) return self._names_list def _get_error_text( diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index e7445b190e8..ad49516fe57 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -2,7 +2,7 @@ "domain": "conversation", "name": "Conversation", "documentation": "https://www.home-assistant.io/integrations/conversation", - "requirements": ["hassil==0.2.5", "home-assistant-intents==2023.1.25"], + "requirements": ["hassil==0.2.6", "home-assistant-intents==2023.1.25"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 44bc99ea03c..373253def9c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ cryptography==39.0.0 dbus-fast==1.84.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 -hassil==0.2.5 +hassil==0.2.6 home-assistant-bluetooth==1.9.2 home-assistant-frontend==20230128.0 home-assistant-intents==2023.1.25 diff --git a/requirements_all.txt b/requirements_all.txt index 78c35f70556..4afb7ae4436 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -874,7 +874,7 @@ hass-nabucasa==0.61.0 hass_splunk==0.1.1 # homeassistant.components.conversation -hassil==0.2.5 +hassil==0.2.6 # homeassistant.components.tasmota hatasmota==0.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13baca898c4..fccfe27d37a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -666,7 +666,7 @@ habitipy==0.2.0 hass-nabucasa==0.61.0 # homeassistant.components.conversation -hassil==0.2.5 +hassil==0.2.6 # homeassistant.components.tasmota hatasmota==0.6.3 From 0702314dcba072572ddf9cebb34287c48c0ea96b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 14:39:37 -0500 Subject: [PATCH 051/187] Bumped version to 2023.2.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 44128d3f474..9f185951d1a 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 = 2 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 381c0c34dfc..24f24ba97a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b5" +version = "2023.2.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c7b944ca758da8ef2222ed349558549f9f2d0406 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 26 Jan 2023 09:48:49 -0600 Subject: [PATCH 052/187] Use device area id in intent matching (#86678) * Use device area id when matching * Normalize whitespace in response * Add extra test entity --- .../components/conversation/default_agent.py | 18 ++++---- homeassistant/helpers/intent.py | 22 +++++++++- tests/helpers/test_intent.py | 42 ++++++++++++++++++- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 2702372a660..c897d2e3b87 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -180,17 +180,19 @@ class DefaultAgent(AbstractConversationAgent): ).get(response_key) if response_str: response_template = template.Template(response_str, self.hass) - intent_response.async_set_speech( - response_template.async_render( - { - "slots": { - entity_name: entity_value.text or entity_value.value - for entity_name, entity_value in result.entities.items() - } + speech = response_template.async_render( + { + "slots": { + entity_name: entity_value.text or entity_value.value + for entity_name, entity_value in result.entities.items() } - ) + } ) + # Normalize whitespace + speech = " ".join(speech.strip().split()) + intent_response.async_set_speech(speech) + return ConversationResult( response=intent_response, conversation_id=conversation_id ) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index f7ee7008b68..511c2b2c009 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -20,7 +20,7 @@ from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from . import area_registry, config_validation as cv, entity_registry +from . import area_registry, config_validation as cv, device_registry, entity_registry _LOGGER = logging.getLogger(__name__) _SlotsType = dict[str, Any] @@ -159,6 +159,7 @@ def async_match_states( states: Iterable[State] | None = None, entities: entity_registry.EntityRegistry | None = None, areas: area_registry.AreaRegistry | None = None, + devices: device_registry.DeviceRegistry | None = None, ) -> Iterable[State]: """Find states that match the constraints.""" if states is None: @@ -206,11 +207,28 @@ def async_match_states( assert area is not None, f"No area named {area_name}" if area is not None: + if devices is None: + devices = device_registry.async_get(hass) + + entity_area_ids: dict[str, str | None] = {} + for _state, entity in states_and_entities: + if entity is None: + continue + + if entity.area_id: + # Use entity's area id first + entity_area_ids[entity.id] = entity.area_id + elif entity.device_id: + # Fall back to device area if not set on entity + device = devices.async_get(entity.device_id) + if device is not None: + entity_area_ids[entity.id] = device.area_id + # Filter by area states_and_entities = [ (state, entity) for state, entity in states_and_entities - if (entity is not None) and (entity.area_id == area.id) + if (entity is not None) and (entity_area_ids.get(entity.id) == area.id) ] if name is not None: diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index f190f41072f..11a54b3b529 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -9,6 +9,7 @@ from homeassistant.core import State from homeassistant.helpers import ( area_registry, config_validation as cv, + device_registry, entity_registry, intent, ) @@ -41,7 +42,7 @@ async def test_async_match_states(hass): entities.async_update_entity(state1.entity_id, area_id=area_kitchen.id) entities.async_get_or_create( - "switch", "demo", "1234", suggested_object_id="bedroom" + "switch", "demo", "5678", suggested_object_id="bedroom" ) entities.async_update_entity( state2.entity_id, @@ -92,6 +93,45 @@ async def test_async_match_states(hass): ) +async def test_match_device_area(hass): + """Test async_match_state with a device in an area.""" + areas = area_registry.async_get(hass) + area_kitchen = areas.async_get_or_create("kitchen") + area_bedroom = areas.async_get_or_create("bedroom") + + devices = device_registry.async_get(hass) + kitchen_device = devices.async_get_or_create( + config_entry_id="1234", connections=set(), identifiers={("demo", "id-1234")} + ) + devices.async_update_device(kitchen_device.id, area_id=area_kitchen.id) + + state1 = State( + "light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} + ) + state2 = State( + "light.bedroom", "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"} + ) + state3 = State( + "light.living_room", "on", attributes={ATTR_FRIENDLY_NAME: "living room light"} + ) + entities = entity_registry.async_get(hass) + entities.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen") + entities.async_update_entity(state1.entity_id, device_id=kitchen_device.id) + + entities.async_get_or_create("light", "demo", "5678", suggested_object_id="bedroom") + entities.async_update_entity(state2.entity_id, area_id=area_bedroom.id) + + # Match on area/domain + assert [state1] == list( + intent.async_match_states( + hass, + domains={"light"}, + area_name="kitchen", + states=[state1, state2, state3], + ) + ) + + def test_async_validate_slots(): """Test async_validate_slots of IntentHandler.""" handler1 = MockIntentHandler({vol.Required("name"): cv.string}) From ba966bd0f7fcc64f37004d134c689364c4409a96 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 30 Jan 2023 18:04:00 -0500 Subject: [PATCH 053/187] Honeywell auto mode invalid attribute (#86728) fixes undefined --- homeassistant/components/honeywell/climate.py | 69 +++++++++++-------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 97d6c2f8881..4a773201171 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -169,10 +169,17 @@ class HoneywellUSThermostat(ClimateEntity): @property def min_temp(self) -> float: """Return the minimum temperature.""" - if self.hvac_mode in [HVACMode.COOL, HVACMode.HEAT_COOL]: + if self.hvac_mode == HVACMode.COOL: return self._device.raw_ui_data["CoolLowerSetptLimit"] if self.hvac_mode == HVACMode.HEAT: return self._device.raw_ui_data["HeatLowerSetptLimit"] + if self.hvac_mode == HVACMode.HEAT_COOL: + return min( + [ + self._device.raw_ui_data["CoolLowerSetptLimit"], + self._device.raw_ui_data["HeatLowerSetptLimit"], + ] + ) return DEFAULT_MIN_TEMP @property @@ -180,8 +187,15 @@ class HoneywellUSThermostat(ClimateEntity): """Return the maximum temperature.""" if self.hvac_mode == HVACMode.COOL: return self._device.raw_ui_data["CoolUpperSetptLimit"] - if self.hvac_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL]: + if self.hvac_mode == HVACMode.HEAT: return self._device.raw_ui_data["HeatUpperSetptLimit"] + if self.hvac_mode == HVACMode.HEAT_COOL: + return max( + [ + self._device.raw_ui_data["CoolUpperSetptLimit"], + self._device.raw_ui_data["HeatUpperSetptLimit"], + ] + ) return DEFAULT_MAX_TEMP @property @@ -257,42 +271,43 @@ class HoneywellUSThermostat(ClimateEntity): # Get current mode mode = self._device.system_mode # Set hold if this is not the case - if getattr(self._device, f"hold_{mode}", None) is False: - # Get next period key - next_period_key = f"{mode.capitalize()}NextPeriod" - # Get next period raw value - next_period = self._device.raw_ui_data.get(next_period_key) + if self._device.hold_heat is False and self._device.hold_cool is False: # Get next period time - hour, minute = divmod(next_period * 15, 60) + hour_heat, minute_heat = divmod( + self._device.raw_ui_data["HEATNextPeriod"] * 15, 60 + ) + hour_cool, minute_cool = divmod( + self._device.raw_ui_data["COOLNextPeriod"] * 15, 60 + ) # Set hold time if mode in COOLING_MODES: - await self._device.set_hold_cool(datetime.time(hour, minute)) - elif mode in HEATING_MODES: - await self._device.set_hold_heat(datetime.time(hour, minute)) + await self._device.set_hold_cool( + datetime.time(hour_cool, minute_cool) + ) + if mode in HEATING_MODES: + await self._device.set_hold_heat( + datetime.time(hour_heat, minute_heat) + ) - # Set temperature - if mode in COOLING_MODES: + # Set temperature if not in auto + elif mode == "cool": await self._device.set_setpoint_cool(temperature) - elif mode in HEATING_MODES: + elif mode == "heat": await self._device.set_setpoint_heat(temperature) + elif mode == "auto": + if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): + await self._device.set_setpoint_cool(temperature) + if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): + await self._device.set_setpoint_heat(temperature) - except AIOSomecomfort.SomeComfortError: - _LOGGER.error("Temperature %.1f out of range", temperature) + except AIOSomecomfort.SomeComfortError as err: + _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if {HVACMode.COOL, HVACMode.HEAT} & set(self._hvac_mode_map): await self._set_temperature(**kwargs) - try: - if HVACMode.HEAT_COOL in self._hvac_mode_map: - if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): - await self._device.set_setpoint_cool(temperature) - if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): - await self._device.set_setpoint_heat(temperature) - except AIOSomecomfort.SomeComfortError as err: - _LOGGER.error("Invalid temperature %s: %s", temperature, err) - async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" await self._device.set_fan_mode(self._fan_mode_map[fan_mode]) @@ -322,7 +337,7 @@ class HoneywellUSThermostat(ClimateEntity): if mode in COOLING_MODES: await self._device.set_hold_cool(True) await self._device.set_setpoint_cool(self._cool_away_temp) - elif mode in HEATING_MODES: + if mode in HEATING_MODES: await self._device.set_hold_heat(True) await self._device.set_setpoint_heat(self._heat_away_temp) @@ -349,7 +364,7 @@ class HoneywellUSThermostat(ClimateEntity): # Set permanent hold if mode in COOLING_MODES: await self._device.set_hold_cool(True) - elif mode in HEATING_MODES: + if mode in HEATING_MODES: await self._device.set_hold_heat(True) except AIOSomecomfort.SomeComfortError: From 565a9735fc9424d685a2ae38a923c4db92e11fa4 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 30 Jan 2023 16:21:34 -0500 Subject: [PATCH 054/187] ZHA config flow cleanup (#86742) fixes undefined --- homeassistant/components/zha/config_flow.py | 25 ++++- homeassistant/components/zha/radio_manager.py | 4 +- homeassistant/components/zha/strings.json | 8 +- .../components/zha/translations/en.json | 10 +- tests/components/zha/test_config_flow.py | 103 +++++++++++++++--- 5 files changed, 125 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 8f4c9ee4336..a92a2c13a76 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -37,6 +37,7 @@ DECONZ_DOMAIN = "deconz" FORMATION_STRATEGY = "formation_strategy" FORMATION_FORM_NEW_NETWORK = "form_new_network" +FORMATION_FORM_INITIAL_NETWORK = "form_initial_network" FORMATION_REUSE_SETTINGS = "reuse_settings" FORMATION_CHOOSE_AUTOMATIC_BACKUP = "choose_automatic_backup" FORMATION_UPLOAD_MANUAL_BACKUP = "upload_manual_backup" @@ -270,8 +271,21 @@ class BaseZhaFlow(FlowHandler): strategies.append(FORMATION_REUSE_SETTINGS) strategies.append(FORMATION_UPLOAD_MANUAL_BACKUP) - strategies.append(FORMATION_FORM_NEW_NETWORK) + # Do not show "erase network settings" if there are none to erase + if self._radio_mgr.current_settings is None: + strategies.append(FORMATION_FORM_INITIAL_NETWORK) + else: + strategies.append(FORMATION_FORM_NEW_NETWORK) + + # Automatically form a new network if we're onboarding with a brand new radio + if not onboarding.async_is_onboarded(self.hass) and set(strategies) == { + FORMATION_UPLOAD_MANUAL_BACKUP, + FORMATION_FORM_INITIAL_NETWORK, + }: + return await self.async_step_form_initial_network() + + # Otherwise, let the user choose return self.async_show_menu( step_id="choose_formation_strategy", menu_options=strategies, @@ -283,6 +297,13 @@ class BaseZhaFlow(FlowHandler): """Reuse the existing network settings on the stick.""" return await self._async_create_radio_entry() + async def async_step_form_initial_network( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Form an initial network.""" + # This step exists only for translations, it does nothing new + return await self.async_step_form_new_network(user_input) + async def async_step_form_new_network( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -439,7 +460,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN return self.async_abort(reason="single_instance_allowed") # Without confirmation, discovery can automatically progress into parts of the - # config flow logic that interacts with hardware! + # config flow logic that interacts with hardware. if user_input is not None or not onboarding.async_is_onboarded(self.hass): # Probe the radio type if we don't have one yet if ( diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 9b7493b9bd3..c68e65fd48f 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -11,7 +11,7 @@ from typing import Any import voluptuous as vol from zigpy.application import ControllerApplication import zigpy.backups -from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH +from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH, CONF_NWK_BACKUP_ENABLED from zigpy.exceptions import NetworkNotFormed from homeassistant import config_entries @@ -126,6 +126,7 @@ class ZhaRadioManager: app_config[CONF_DATABASE] = database_path app_config[CONF_DEVICE] = self.device_settings + app_config[CONF_NWK_BACKUP_ENABLED] = False app_config = self.radio_type.controller.SCHEMA(app_config) app = await self.radio_type.controller.new( @@ -206,6 +207,7 @@ class ZhaRadioManager: # The list of backups will always exist self.backups = app.backups.backups.copy() + self.backups.sort(reverse=True, key=lambda b: b.backup_time) return backup diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 36bd5a38ecc..132f6ed9d95 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -31,7 +31,8 @@ "title": "Network Formation", "description": "Choose the network settings for your radio.", "menu_options": { - "form_new_network": "Erase network settings and form a new network", + "form_new_network": "Erase network settings and create a new network", + "form_initial_network": "Create a network", "reuse_settings": "Keep radio network settings", "choose_automatic_backup": "Restore an automatic backup", "upload_manual_backup": "Upload a manual backup" @@ -86,11 +87,11 @@ }, "intent_migrate": { "title": "Migrate to a new radio", - "description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?" + "description": "Before plugging in your new radio, your old radio needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?" }, "instruct_unplug": { "title": "Unplug your old radio", - "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it." + "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.\n\nYou can now plug in your new radio." }, "choose_serial_port": { "title": "[%key:component::zha::config::step::choose_serial_port::title%]", @@ -120,6 +121,7 @@ "description": "[%key:component::zha::config::step::choose_formation_strategy::description%]", "menu_options": { "form_new_network": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::form_new_network%]", + "form_initial_network": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::form_initial_network%]", "reuse_settings": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::reuse_settings%]", "choose_automatic_backup": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::choose_automatic_backup%]", "upload_manual_backup": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::upload_manual_backup%]" diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index a9d13a7a3a0..59e8004ad39 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -22,7 +22,8 @@ "description": "Choose the network settings for your radio.", "menu_options": { "choose_automatic_backup": "Restore an automatic backup", - "form_new_network": "Erase network settings and form a new network", + "form_initial_network": "Create a network", + "form_new_network": "Erase network settings and create a new network", "reuse_settings": "Keep radio network settings", "upload_manual_backup": "Upload a manual backup" }, @@ -174,7 +175,8 @@ "description": "Choose the network settings for your radio.", "menu_options": { "choose_automatic_backup": "Restore an automatic backup", - "form_new_network": "Erase network settings and form a new network", + "form_initial_network": "Create a network", + "form_new_network": "Erase network settings and create a new network", "reuse_settings": "Keep radio network settings", "upload_manual_backup": "Upload a manual backup" }, @@ -192,11 +194,11 @@ "title": "Reconfigure ZHA" }, "instruct_unplug": { - "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.", + "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.\n\nYou can now plug in your new radio.", "title": "Unplug your old radio" }, "intent_migrate": { - "description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?", + "description": "Before plugging in your new radio, your old radio needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?", "title": "Migrate to a new radio" }, "manual_pick_radio_type": { diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index d457e0b6b8c..acff888dfde 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for ZHA config flow.""" import copy +from datetime import timedelta import json from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch import uuid @@ -67,12 +68,27 @@ def mock_app(): @pytest.fixture -def backup(): - """Zigpy network backup with non-default settings.""" - backup = zigpy.backups.NetworkBackup() - backup.node_info.ieee = zigpy.types.EUI64.convert("AA:BB:CC:DD:11:22:33:44") +def make_backup(): + """Zigpy network backup factory that creates unique backups with each call.""" + num_calls = 0 - return backup + def inner(*, backup_time_offset=0): + nonlocal num_calls + + backup = zigpy.backups.NetworkBackup() + backup.backup_time += timedelta(seconds=backup_time_offset) + backup.node_info.ieee = zigpy.types.EUI64.convert(f"AABBCCDDEE{num_calls:06X}") + num_calls += 1 + + return backup + + return inner + + +@pytest.fixture +def backup(make_backup): + """Zigpy network backup with non-default settings.""" + return make_backup() def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): @@ -1101,6 +1117,56 @@ async def test_formation_strategy_form_new_network(pick_radio, mock_app, hass): assert result2["type"] == FlowResultType.CREATE_ENTRY +async def test_formation_strategy_form_initial_network(pick_radio, mock_app, hass): + """Test forming a new network, with no previous settings on the radio.""" + mock_app.load_network_info = AsyncMock(side_effect=NetworkNotFormed()) + + result, port = await pick_radio(RadioType.ezsp) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_FORM_INITIAL_NETWORK}, + ) + await hass.async_block_till_done() + + # A new network will be formed + mock_app.form_network.assert_called_once() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + + +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_onboarding_auto_formation_new_hardware(mock_app, hass): + """Test auto network formation with new hardware during onboarding.""" + mock_app.load_network_info = AsyncMock(side_effect=NetworkNotFormed()) + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zigbee radio", + manufacturer="test", + ) + + with patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "zigbee radio" + assert result["data"] == { + "device": { + "baudrate": 115200, + "flow_control": None, + "path": "/dev/ttyZIGBEE", + }, + CONF_RADIO_TYPE: "znp", + } + + async def test_formation_strategy_reuse_settings(pick_radio, mock_app, hass): """Test reusing existing network settings.""" result, port = await pick_radio(RadioType.ezsp) @@ -1298,13 +1364,13 @@ def test_format_backup_choice(): ) @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_formation_strategy_restore_automatic_backup_ezsp( - pick_radio, mock_app, hass + pick_radio, mock_app, make_backup, hass ): """Test restoring an automatic backup (EZSP radio).""" mock_app.backups.backups = [ - MagicMock(), - MagicMock(), - MagicMock(), + make_backup(), + make_backup(), + make_backup(), ] backup = mock_app.backups.backups[1] # pick the second one backup.is_compatible_with = MagicMock(return_value=False) @@ -1347,13 +1413,13 @@ async def test_formation_strategy_restore_automatic_backup_ezsp( @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) @pytest.mark.parametrize("is_advanced", [True, False]) async def test_formation_strategy_restore_automatic_backup_non_ezsp( - is_advanced, pick_radio, mock_app, hass + is_advanced, pick_radio, mock_app, make_backup, hass ): """Test restoring an automatic backup (non-EZSP radio).""" mock_app.backups.backups = [ - MagicMock(), - MagicMock(), - MagicMock(), + make_backup(backup_time_offset=5), + make_backup(backup_time_offset=-3), + make_backup(backup_time_offset=2), ] backup = mock_app.backups.backups[1] # pick the second one backup.is_compatible_with = MagicMock(return_value=False) @@ -1375,13 +1441,20 @@ async def test_formation_strategy_restore_automatic_backup_non_ezsp( assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "choose_automatic_backup" - # We must prompt for overwriting the IEEE address + # We don't prompt for overwriting the IEEE address, since only EZSP needs this assert config_flow.OVERWRITE_COORDINATOR_IEEE not in result2["data_schema"].schema + # The backup choices are ordered by date + assert result2["data_schema"].schema["choose_automatic_backup"].container == [ + f"choice:{mock_app.backups.backups[0]!r}", + f"choice:{mock_app.backups.backups[2]!r}", + f"choice:{mock_app.backups.backups[1]!r}", + ] + result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={ - config_flow.CHOOSE_AUTOMATIC_BACKUP: "choice:" + repr(backup), + config_flow.CHOOSE_AUTOMATIC_BACKUP: f"choice:{backup!r}", }, ) From 6a1710063aecbb8a35284bbd7e6e83170dc6c540 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 30 Jan 2023 22:42:32 +0100 Subject: [PATCH 055/187] Catch AndroidTV exception on setup (#86819) fixes undefined --- .../components/androidtv/__init__.py | 34 +++++++++++++++++-- .../components/androidtv/media_player.py | 22 ++---------- tests/components/androidtv/patchers.py | 4 +-- .../components/androidtv/test_media_player.py | 16 +++++---- 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index c2d83ab05e8..d10b1161da6 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -6,6 +6,13 @@ import os from typing import Any from adb_shell.auth.keygen import keygen +from adb_shell.exceptions import ( + AdbTimeoutError, + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, +) from androidtv.adb_manager.adb_manager_sync import ADBPythonSync, PythonRSASigner from androidtv.setup_async import ( AndroidTVAsync, @@ -43,6 +50,18 @@ from .const import ( SIGNAL_CONFIG_ENTITY, ) +ADB_PYTHON_EXCEPTIONS: tuple = ( + AdbTimeoutError, + BrokenPipeError, + ConnectionResetError, + ValueError, + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, +) +ADB_TCP_EXCEPTIONS: tuple = (ConnectionResetError, RuntimeError) + PLATFORMS = [Platform.MEDIA_PLAYER] RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] @@ -132,9 +151,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Android TV platform.""" state_det_rules = entry.options.get(CONF_STATE_DETECTION_RULES) - aftv, error_message = await async_connect_androidtv( - hass, entry.data, state_detection_rules=state_det_rules - ) + if CONF_ADB_SERVER_IP not in entry.data: + exceptions = ADB_PYTHON_EXCEPTIONS + else: + exceptions = ADB_TCP_EXCEPTIONS + + try: + aftv, error_message = await async_connect_androidtv( + hass, entry.data, state_detection_rules=state_det_rules + ) + except exceptions as exc: + raise ConfigEntryNotReady(exc) from exc + if not aftv: raise ConfigEntryNotReady(error_message) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index d77c755110f..f4be6d6eea5 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -7,13 +7,6 @@ import functools import logging from typing import Any, Concatenate, ParamSpec, TypeVar -from adb_shell.exceptions import ( - AdbTimeoutError, - InvalidChecksumError, - InvalidCommandError, - InvalidResponseError, - TcpTimeoutException, -) from androidtv.constants import APPS, KEYS from androidtv.exceptions import LockNotAcquiredException import voluptuous as vol @@ -42,7 +35,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import get_androidtv_mac +from . import ADB_PYTHON_EXCEPTIONS, ADB_TCP_EXCEPTIONS, get_androidtv_mac from .const import ( ANDROID_DEV, ANDROID_DEV_OPT, @@ -252,19 +245,10 @@ class ADBDevice(MediaPlayerEntity): # ADB exceptions to catch if not aftv.adb_server_ip: # Using "adb_shell" (Python ADB implementation) - self.exceptions = ( - AdbTimeoutError, - BrokenPipeError, - ConnectionResetError, - ValueError, - InvalidChecksumError, - InvalidCommandError, - InvalidResponseError, - TcpTimeoutException, - ) + self.exceptions = ADB_PYTHON_EXCEPTIONS else: # Using "pure-python-adb" (communicate with ADB server) - self.exceptions = (ConnectionResetError, RuntimeError) + self.exceptions = ADB_TCP_EXCEPTIONS # Property attributes self._attr_extra_state_attributes = { diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 31e9a9c82c3..5ebd95ccacd 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -111,7 +111,7 @@ def patch_connect(success): } -def patch_shell(response=None, error=False, mac_eth=False): +def patch_shell(response=None, error=False, mac_eth=False, exc=None): """Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods.""" async def shell_success(self, cmd, *args, **kwargs): @@ -128,7 +128,7 @@ def patch_shell(response=None, error=False, mac_eth=False): async def shell_fail_python(self, cmd, *args, **kwargs): """Mock the `AdbDeviceTcpAsyncFake.shell` method when it fails.""" self.shell_cmd = cmd - raise ValueError + raise exc or ValueError async def shell_fail_server(self, cmd): """Mock the `DeviceAsyncFake.shell` method when it fails.""" diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 5aaf0e1b9c8..a0d6230ed6b 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -2,6 +2,7 @@ import logging from unittest.mock import Mock, patch +from adb_shell.exceptions import TcpTimeoutException as AdbShellTimeoutException from androidtv.constants import APPS as ANDROIDTV_APPS, KEYS from androidtv.exceptions import LockNotAcquiredException import pytest @@ -538,25 +539,28 @@ async def test_select_source_firetv(hass, source, expected_arg, method_patch): @pytest.mark.parametrize( - "config", + ["config", "connect"], [ - CONFIG_ANDROIDTV_DEFAULT, - CONFIG_FIRETV_DEFAULT, + (CONFIG_ANDROIDTV_DEFAULT, False), + (CONFIG_FIRETV_DEFAULT, False), + (CONFIG_ANDROIDTV_DEFAULT, True), + (CONFIG_FIRETV_DEFAULT, True), ], ) -async def test_setup_fail(hass, config): +async def test_setup_fail(hass, config, connect): """Test that the entity is not created when the ADB connection is not established.""" patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) - with patchers.patch_connect(False)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF + with patchers.patch_connect(connect)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF, error=True, exc=AdbShellTimeoutException )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) is False await hass.async_block_till_done() await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) + assert config_entry.state == ConfigEntryState.SETUP_RETRY assert state is None From d39d4d6b7f38897e393f408d3eb7a45830df7c14 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 30 Jan 2023 22:10:55 +0100 Subject: [PATCH 056/187] Uses PolledSmartEnergySummation for ZLinky (#86960) --- homeassistant/components/zha/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index bd6161cbb13..eb952d44610 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -511,7 +511,7 @@ class PolledSmartEnergySummation(SmartEnergySummation): models={"ZLinky_TIC"}, ) class Tier1SmartEnergySummation( - SmartEnergySummation, id_suffix="tier1_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier1_summation_delivered" ): """Tier 1 Smart Energy Metering summation sensor.""" @@ -524,7 +524,7 @@ class Tier1SmartEnergySummation( models={"ZLinky_TIC"}, ) class Tier2SmartEnergySummation( - SmartEnergySummation, id_suffix="tier2_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier2_summation_delivered" ): """Tier 2 Smart Energy Metering summation sensor.""" @@ -537,7 +537,7 @@ class Tier2SmartEnergySummation( models={"ZLinky_TIC"}, ) class Tier3SmartEnergySummation( - SmartEnergySummation, id_suffix="tier3_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier3_summation_delivered" ): """Tier 3 Smart Energy Metering summation sensor.""" @@ -550,7 +550,7 @@ class Tier3SmartEnergySummation( models={"ZLinky_TIC"}, ) class Tier4SmartEnergySummation( - SmartEnergySummation, id_suffix="tier4_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier4_summation_delivered" ): """Tier 4 Smart Energy Metering summation sensor.""" @@ -563,7 +563,7 @@ class Tier4SmartEnergySummation( models={"ZLinky_TIC"}, ) class Tier5SmartEnergySummation( - SmartEnergySummation, id_suffix="tier5_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier5_summation_delivered" ): """Tier 5 Smart Energy Metering summation sensor.""" @@ -576,7 +576,7 @@ class Tier5SmartEnergySummation( models={"ZLinky_TIC"}, ) class Tier6SmartEnergySummation( - SmartEnergySummation, id_suffix="tier6_summation_delivered" + PolledSmartEnergySummation, id_suffix="tier6_summation_delivered" ): """Tier 6 Smart Energy Metering summation sensor.""" From dc50a6899aca55d96c93438fd9d7c47c3e694a5e Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 30 Jan 2023 22:43:58 +0100 Subject: [PATCH 057/187] Fix error on empty location in ssdp messages (#86970) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index acb2cfd4405..460e50d18e4 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.33.0", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.33.1", "getmac==0.8.2"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index f141b2c1519..f7407195964 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.33.0"], + "requirements": ["async-upnp-client==0.33.1"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index e1c326ba364..96dd8093cfc 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -8,7 +8,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.1.0", - "async-upnp-client==0.33.0" + "async-upnp-client==0.33.1" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 10f7a8e49a2..4f3c56965c7 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.33.0"], + "requirements": ["async-upnp-client==0.33.1"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index ff683c883bb..c5a872cd207 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.33.0", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.33.1", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index c72a9401c52..62106f99d0d 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.10", "async-upnp-client==0.33.0"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.33.1"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 373253def9c..3b29316b5b9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.13 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.33.0 +async-upnp-client==0.33.1 async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 attrs==22.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4afb7ae4436..461216d072e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -371,7 +371,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.33.0 +async-upnp-client==0.33.1 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fccfe27d37a..0c2bbec2397 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -328,7 +328,7 @@ arcam-fmj==1.0.1 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.33.0 +async-upnp-client==0.33.1 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 From 6a9f06d36ea18f97f903cf1cce898e16533d1097 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 30 Jan 2023 22:14:48 +0100 Subject: [PATCH 058/187] Ensure a proper scope_id is given for IPv6 addresses when initializing the SSDP component (#86975) fixes undefined --- homeassistant/components/ssdp/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 18ed063c8bc..c2f56bb7b4a 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -202,7 +202,9 @@ async def async_build_source_set(hass: HomeAssistant) -> set[IPv4Address | IPv6A return { source_ip for source_ip in await network.async_get_enabled_source_ips(hass) - if not source_ip.is_loopback and not source_ip.is_global + if not source_ip.is_loopback + and not source_ip.is_global + and (source_ip.version == 6 and source_ip.scope_id or source_ip.version == 4) } From f6230e2d710edb00a21061f58c7925427c4aef1c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 30 Jan 2023 22:43:23 +0100 Subject: [PATCH 059/187] Allow any state class when using the precipitation device class (#86977) --- homeassistant/components/sensor/const.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 4fb63140506..c8402a28ffe 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -535,10 +535,7 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass | None] SensorDeviceClass.PM25: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.POWER_FACTOR: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.POWER: {SensorStateClass.MEASUREMENT}, - SensorDeviceClass.PRECIPITATION: { - SensorStateClass.TOTAL, - SensorStateClass.TOTAL_INCREASING, - }, + SensorDeviceClass.PRECIPITATION: set(SensorStateClass), SensorDeviceClass.PRECIPITATION_INTENSITY: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.PRESSURE: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.REACTIVE_POWER: {SensorStateClass.MEASUREMENT}, From 688bba15ac5f27043ef5710f179f3a760d83cac3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 31 Jan 2023 03:34:26 +0100 Subject: [PATCH 060/187] Update frontend to 20230130.0 (#86978) --- 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 0c337ca1f6d..eeb0a906bcf 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==20230128.0"], + "requirements": ["home-assistant-frontend==20230130.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3b29316b5b9..f869419026c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 hassil==0.2.6 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230128.0 +home-assistant-frontend==20230130.0 home-assistant-intents==2023.1.25 httpx==0.23.3 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index 461216d072e..bd03e3fef7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -907,7 +907,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230128.0 +home-assistant-frontend==20230130.0 # homeassistant.components.conversation home-assistant-intents==2023.1.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c2bbec2397..3d7fdfbc689 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230128.0 +home-assistant-frontend==20230130.0 # homeassistant.components.conversation home-assistant-intents==2023.1.25 From 01dea7773a6c88efab7500288fcfb31e94f85e16 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 30 Jan 2023 21:35:27 -0500 Subject: [PATCH 061/187] Bump ZHA dependencies (#86979) Bump ZHA dependency bellows from 0.34.6 to 0.34.7 --- 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 0392368070f..065f4aaf8a4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.34.6", + "bellows==0.34.7", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.92", diff --git a/requirements_all.txt b/requirements_all.txt index bd03e3fef7f..ecbd5a9a514 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.34.6 +bellows==0.34.7 # homeassistant.components.bmw_connected_drive bimmer_connected==0.12.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d7fdfbc689..f3bfce3cabf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.34.6 +bellows==0.34.7 # homeassistant.components.bmw_connected_drive bimmer_connected==0.12.0 From 29056f1bd79fb2fc1d6dca6e42b7bc9703c21281 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 16:46:42 -0500 Subject: [PATCH 062/187] Check dashboard when showing reauth form (#86980) --- .../components/esphome/config_flow.py | 10 ++-- tests/components/esphome/test_config_flow.py | 53 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 61eb97a365b..550697d5d12 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -92,11 +92,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._name = entry.title self._device_name = entry.data.get(CONF_DEVICE_NAME) - if await self._retrieve_encryption_key_from_dashboard(): - error = await self.fetch_device_info() - if error is None: - return await self._async_authenticate_or_add() - return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( @@ -105,6 +100,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle reauthorization flow.""" errors = {} + if await self._retrieve_encryption_key_from_dashboard(): + error = await self.fetch_device_info() + if error is None: + return await self._async_authenticate_or_add() + if user_input is not None: self._noise_psk = user_input[CONF_NOISE_PSK] error = await self.fetch_device_info() diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index a2f51f2526e..f7326d05b8f 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -566,6 +566,59 @@ async def test_reauth_fixed_via_dashboard_remove_password( assert len(mock_get_encryption_key.mock_calls) == 1 +async def test_reauth_fixed_via_dashboard_at_confirm( + hass, mock_client, mock_zeroconf, mock_dashboard +): + """Test reauth fixed automatically via dashboard at confirm step.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + ) + entry.add_to_hass(hass) + + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.FORM, result + assert result["step_id"] == "reauth_confirm" + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + # We just fetch the form + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + + assert len(mock_get_encryption_key.mock_calls) == 1 + + async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): """Test reauth initiation with invalid PSK.""" entry = MockConfigEntry( From 32a7ae61293410ccb5dd850df602616c08011387 Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 30 Jan 2023 20:36:51 -0600 Subject: [PATCH 063/187] Bump pyisy to 3.1.11 (#86981) * Bump pyisy to 3.1.10 * Bump pyisy to 3.1.11 --- 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 e1ba8e4e216..8fa77cd126c 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.1.9"], + "requirements": ["pyisy==3.1.11"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index ecbd5a9a514..b254ab847ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1702,7 +1702,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.9 +pyisy==3.1.11 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3bfce3cabf..5fe7fd1cf52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1221,7 +1221,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.9 +pyisy==3.1.11 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From edf02b70ea75d669b7b3aa11284396252bf54a2b Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 30 Jan 2023 22:46:25 -0600 Subject: [PATCH 064/187] Prioritize entity names over area names in Assist matching (#86982) * Refactor async_match_states * Check entity name after state, before aliases * Give entity name matches priority over area names * Don't force result to have area * Add area alias in tests * Move name/area list creation back * Clean up PR * More clean up --- .../components/conversation/default_agent.py | 39 ++++++-- homeassistant/helpers/intent.py | 89 +++++++++++++------ tests/components/conversation/test_init.py | 49 ++++++++++ tests/helpers/test_intent.py | 8 ++ 4 files changed, 148 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index c897d2e3b87..cabf9089b1c 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -11,7 +11,7 @@ import re from typing import IO, Any from hassil.intents import Intents, ResponseType, SlotList, TextSlotList -from hassil.recognize import recognize +from hassil.recognize import RecognizeResult, recognize_all from hassil.util import merge_dict from home_assistant_intents import get_intents import yaml @@ -128,7 +128,10 @@ class DefaultAgent(AbstractConversationAgent): } result = await self.hass.async_add_executor_job( - recognize, user_input.text, lang_intents.intents, slot_lists + self._recognize, + user_input, + lang_intents, + slot_lists, ) if result is None: _LOGGER.debug("No intent was matched for '%s'", user_input.text) @@ -197,6 +200,26 @@ class DefaultAgent(AbstractConversationAgent): response=intent_response, conversation_id=conversation_id ) + def _recognize( + self, + user_input: ConversationInput, + lang_intents: LanguageIntents, + slot_lists: dict[str, SlotList], + ) -> RecognizeResult | None: + """Search intents for a match to user input.""" + # Prioritize matches with entity names above area names + maybe_result: RecognizeResult | None = None + for result in recognize_all( + user_input.text, lang_intents.intents, slot_lists=slot_lists + ): + if "name" in result.entities: + return result + + # Keep looking in case an entity has the same name + maybe_result = result + + return maybe_result + async def async_reload(self, language: str | None = None): """Clear cached intents for a language.""" if language is None: @@ -373,19 +396,19 @@ class DefaultAgent(AbstractConversationAgent): if self._names_list is not None: return self._names_list states = self.hass.states.async_all() - registry = entity_registry.async_get(self.hass) + entities = entity_registry.async_get(self.hass) names = [] for state in states: context = {"domain": state.domain} - entry = registry.async_get(state.entity_id) - if entry is not None: - if entry.entity_category: + entity = entities.async_get(state.entity_id) + if entity is not None: + if entity.entity_category: # Skip configuration/diagnostic entities continue - if entry.aliases: - for alias in entry.aliases: + if entity.aliases: + for alias in entity.aliases: names.append((alias, state.entity_id, context)) # Default name diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 511c2b2c009..58252da4822 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -138,15 +138,62 @@ def _has_name( if name in (state.entity_id, state.name.casefold()): return True - # Check aliases - if (entity is not None) and entity.aliases: - for alias in entity.aliases: - if name == alias.casefold(): - return True + # Check name/aliases + if (entity is None) or (not entity.aliases): + return False + + for alias in entity.aliases: + if name == alias.casefold(): + return True return False +def _find_area( + id_or_name: str, areas: area_registry.AreaRegistry +) -> area_registry.AreaEntry | None: + """Find an area by id or name, checking aliases too.""" + area = areas.async_get_area(id_or_name) or areas.async_get_area_by_name(id_or_name) + if area is not None: + return area + + # Check area aliases + for maybe_area in areas.areas.values(): + if not maybe_area.aliases: + continue + + for area_alias in maybe_area.aliases: + if id_or_name == area_alias.casefold(): + return maybe_area + + return None + + +def _filter_by_area( + states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]], + area: area_registry.AreaEntry, + devices: device_registry.DeviceRegistry, +) -> Iterable[tuple[State, entity_registry.RegistryEntry | None]]: + """Filter state/entity pairs by an area.""" + entity_area_ids: dict[str, str | None] = {} + for _state, entity in states_and_entities: + if entity is None: + continue + + if entity.area_id: + # Use entity's area id first + entity_area_ids[entity.id] = entity.area_id + elif entity.device_id: + # Fall back to device area if not set on entity + device = devices.async_get(entity.device_id) + if device is not None: + entity_area_ids[entity.id] = device.area_id + + for state, entity in states_and_entities: + if (entity is not None) and (entity_area_ids.get(entity.id) == area.id): + yield (state, entity) + + @callback @bind_hass def async_match_states( @@ -200,45 +247,29 @@ def async_match_states( if areas is None: areas = area_registry.async_get(hass) - # id or name - area = areas.async_get_area(area_name) or areas.async_get_area_by_name( - area_name - ) + area = _find_area(area_name, areas) assert area is not None, f"No area named {area_name}" if area is not None: + # Filter by states/entities by area if devices is None: devices = device_registry.async_get(hass) - entity_area_ids: dict[str, str | None] = {} - for _state, entity in states_and_entities: - if entity is None: - continue - - if entity.area_id: - # Use entity's area id first - entity_area_ids[entity.id] = entity.area_id - elif entity.device_id: - # Fall back to device area if not set on entity - device = devices.async_get(entity.device_id) - if device is not None: - entity_area_ids[entity.id] = device.area_id - - # Filter by area - states_and_entities = [ - (state, entity) - for state, entity in states_and_entities - if (entity is not None) and (entity_area_ids.get(entity.id) == area.id) - ] + states_and_entities = list(_filter_by_area(states_and_entities, area, devices)) if name is not None: + if devices is None: + devices = device_registry.async_get(hass) + # Filter by name name = name.casefold() + # Check states for state, entity in states_and_entities: if _has_name(state, entity, name): yield state break + else: # Not filtered by name for state, _entity in states_and_entities: diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index f4b386cbe4b..54fed8a6139 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -6,6 +6,7 @@ import pytest from homeassistant.components import conversation from homeassistant.components.cover import SERVICE_OPEN_COVER +from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import DOMAIN as HASS_DOMAIN, Context from homeassistant.helpers import ( area_registry, @@ -777,3 +778,51 @@ async def test_turn_on_area(hass, init_components): assert call.domain == HASS_DOMAIN assert call.service == "turn_on" assert call.data == {"entity_id": "light.stove"} + + +async def test_light_area_same_name(hass, init_components): + """Test turning on a light with the same name as an area.""" + entities = entity_registry.async_get(hass) + devices = device_registry.async_get(hass) + areas = area_registry.async_get(hass) + entry = MockConfigEntry(domain="test") + + device = devices.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + kitchen_area = areas.async_create("kitchen") + devices.async_update_device(device.id, area_id=kitchen_area.id) + + kitchen_light = entities.async_get_or_create( + "light", "demo", "1234", original_name="kitchen light" + ) + entities.async_update_entity(kitchen_light.entity_id, area_id=kitchen_area.id) + hass.states.async_set( + kitchen_light.entity_id, "off", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} + ) + + ceiling_light = entities.async_get_or_create( + "light", "demo", "5678", original_name="ceiling light" + ) + entities.async_update_entity(ceiling_light.entity_id, area_id=kitchen_area.id) + hass.states.async_set( + ceiling_light.entity_id, "off", attributes={ATTR_FRIENDLY_NAME: "ceiling light"} + ) + + calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on kitchen light"}, + ) + await hass.async_block_till_done() + + # Should only turn on one light instead of all lights in the kitchen + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == "turn_on" + assert call.data == {"entity_id": kitchen_light.entity_id} diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index 11a54b3b529..14ada0b967d 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -27,6 +27,7 @@ async def test_async_match_states(hass): """Test async_match_state helper.""" areas = area_registry.async_get(hass) area_kitchen = areas.async_get_or_create("kitchen") + areas.async_update(area_kitchen.id, aliases={"food room"}) area_bedroom = areas.async_get_or_create("bedroom") state1 = State( @@ -68,6 +69,13 @@ async def test_async_match_states(hass): ) ) + # Test area alias + assert [state1] == list( + intent.async_match_states( + hass, name="kitchen light", area_name="food room", states=[state1, state2] + ) + ) + # Wrong area assert not list( intent.async_match_states( From 876022729641f85d60bcca2b7cdef7b276c44e6f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 21:32:52 -0500 Subject: [PATCH 065/187] ESPHome discovered dashboard checks reauth flows (#86993) --- homeassistant/components/esphome/dashboard.py | 12 +++-- tests/components/esphome/__init__.py | 1 + tests/components/esphome/conftest.py | 16 ++++-- tests/components/esphome/test_config_flow.py | 3 +- tests/components/esphome/test_dashboard.py | 54 ++++++++++++++++++- tests/components/esphome/test_diagnostics.py | 3 +- 6 files changed, 77 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 3ce07d683b9..052d6161cf9 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -8,7 +8,7 @@ import logging import aiohttp from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -52,8 +52,14 @@ async def async_set_dashboard_info( for entry in hass.config_entries.async_entries(DOMAIN) if entry.state == ConfigEntryState.LOADED ] - if reloads: - await asyncio.gather(*reloads) + # Re-auth flows will check the dashboard for encryption key when the form is requested + reauths = [ + hass.config_entries.flow.async_configure(flow["flow_id"]) + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN and flow["context"]["source"] == SOURCE_REAUTH + ] + if reloads or reauths: + await asyncio.gather(*reloads, *reauths) class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): diff --git a/tests/components/esphome/__init__.py b/tests/components/esphome/__init__.py index 764a06f3bb9..a44db03f841 100644 --- a/tests/components/esphome/__init__.py +++ b/tests/components/esphome/__init__.py @@ -3,3 +3,4 @@ DASHBOARD_SLUG = "mock-slug" DASHBOARD_HOST = "mock-host" DASHBOARD_PORT = 1234 +VALID_NOISE_PSK = "bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=" diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 6febe15389a..f53e513e6bb 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -7,7 +7,12 @@ from aioesphomeapi import APIClient, DeviceInfo import pytest from zeroconf import Zeroconf -from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, dashboard +from homeassistant.components.esphome import ( + CONF_DEVICE_NAME, + CONF_NOISE_PSK, + DOMAIN, + dashboard, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant @@ -27,9 +32,9 @@ def esphome_mock_async_zeroconf(mock_async_zeroconf): @pytest.fixture -def mock_config_entry() -> MockConfigEntry: +def mock_config_entry(hass) -> MockConfigEntry: """Return the default mocked config entry.""" - return MockConfigEntry( + config_entry = MockConfigEntry( title="ESPHome Device", domain=DOMAIN, data={ @@ -37,9 +42,12 @@ def mock_config_entry() -> MockConfigEntry: CONF_PORT: 6053, CONF_PASSWORD: "pwd", CONF_NOISE_PSK: "12345678123456781234567812345678", + CONF_DEVICE_NAME: "test", }, unique_id="11:22:33:44:55:aa", ) + config_entry.add_to_hass(hass) + return config_entry @pytest.fixture @@ -59,8 +67,6 @@ async def init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> MockConfigEntry: """Set up the ESPHome integration for testing.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index f7326d05b8f..92e2df5ca39 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -24,9 +24,10 @@ from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import FlowResultType +from . import VALID_NOISE_PSK + from tests.common import MockConfigEntry -VALID_NOISE_PSK = "bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=" INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM=" diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index 7a5486d5205..c14bb06d3d8 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -1,8 +1,13 @@ """Test ESPHome dashboard features.""" from unittest.mock import patch -from homeassistant.components.esphome import dashboard -from homeassistant.config_entries import ConfigEntryState +from aioesphomeapi import DeviceInfo + +from homeassistant.components.esphome import CONF_NOISE_PSK, dashboard +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.data_entry_flow import FlowResultType + +from . import VALID_NOISE_PSK async def test_new_info_reload_config_entries(hass, init_integration, mock_dashboard): @@ -20,3 +25,48 @@ async def test_new_info_reload_config_entries(hass, init_integration, mock_dashb await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) assert len(mock_setup.mock_calls) == 0 + + +async def test_new_dashboard_fix_reauth( + hass, mock_client, mock_config_entry, mock_dashboard +): + """Test config entries waiting for reauth are triggered.""" + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert len(mock_get_encryption_key.mock_calls) == 0 + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key, patch( + "homeassistant.components.esphome.async_setup_entry", return_value=True + ) as mock_setup: + await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) + await hass.async_block_till_done() + + assert len(mock_get_encryption_key.mock_calls) == 1 + assert len(mock_setup.mock_calls) == 1 + assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 959d49c4ee3..6a08a47e6eb 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -3,7 +3,7 @@ from aiohttp import ClientSession import pytest -from homeassistant.components.esphome import CONF_NOISE_PSK +from homeassistant.components.esphome import CONF_DEVICE_NAME, CONF_NOISE_PSK from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant @@ -25,6 +25,7 @@ async def test_diagnostics( assert isinstance(result, dict) assert result["config"]["data"] == { + CONF_DEVICE_NAME: "test", CONF_HOST: "192.168.1.2", CONF_PORT: 6053, CONF_PASSWORD: "**REDACTED**", From c9e86ccd380956064844322ab59309628841be06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 23:05:48 -0500 Subject: [PATCH 066/187] ESPHome handle remove password and no encryption (#86995) * ESPHome handle remove password and no encryption * Start reauth for invalid api password --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- homeassistant/components/esphome/__init__.py | 10 ++++- .../components/esphome/config_flow.py | 13 ++++++ tests/components/esphome/test_config_flow.py | 45 +++++++++++-------- tests/components/esphome/test_dashboard.py | 7 ++- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 8cefac41859..4c9d8b6362b 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -17,6 +17,7 @@ from aioesphomeapi import ( EntityInfo, EntityState, HomeassistantServiceCall, + InvalidAuthAPIError, InvalidEncryptionKeyAPIError, ReconnectLogic, RequiresEncryptionAPIError, @@ -347,7 +348,14 @@ async def async_setup_entry( # noqa: C901 async def on_connect_error(err: Exception) -> None: """Start reauth flow if appropriate connect error type.""" - if isinstance(err, (RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError)): + if isinstance( + err, + ( + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError, + InvalidAuthAPIError, + ), + ): entry.async_start_reauth(hass) reconnect_logic = ReconnectLogic( diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 550697d5d12..acc94bc7ea0 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -92,6 +92,19 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._name = entry.title self._device_name = entry.data.get(CONF_DEVICE_NAME) + # Device without encryption allows fetching device info. We can then check + # if the device is no longer using a password. If we did try with a password, + # we know setting password to empty will allow us to authenticate. + error = await self.fetch_device_info() + if ( + error is None + and self._password + and self._device_info + and not self._device_info.uses_password + ): + self._password = "" + return await self._async_authenticate_or_add() + return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 92e2df5ca39..b629e604410 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -519,23 +519,14 @@ async def test_reauth_fixed_via_dashboard( assert len(mock_get_encryption_key.mock_calls) == 1 -async def test_reauth_fixed_via_dashboard_remove_password( - hass, mock_client, mock_zeroconf, mock_dashboard +async def test_reauth_fixed_via_dashboard_add_encryption_remove_password( + hass, mock_client, mock_zeroconf, mock_dashboard, mock_config_entry ): """Test reauth fixed automatically via dashboard with password removed.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "127.0.0.1", - CONF_PORT: 6053, - CONF_PASSWORD: "hello", - CONF_DEVICE_NAME: "test", - }, + mock_client.device_info.side_effect = ( + InvalidAuthAPIError, + DeviceInfo(uses_password=False, name="test"), ) - entry.add_to_hass(hass) - - mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") mock_dashboard["configured"].append( { @@ -554,19 +545,37 @@ async def test_reauth_fixed_via_dashboard_remove_password( "esphome", context={ "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - "unique_id": entry.unique_id, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, }, ) assert result["type"] == FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" - assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK - assert entry.data[CONF_PASSWORD] == "" + assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + assert mock_config_entry.data[CONF_PASSWORD] == "" assert len(mock_get_encryption_key.mock_calls) == 1 +async def test_reauth_fixed_via_remove_password(hass, mock_client, mock_config_entry): + """Test reauth fixed automatically by seeing password removed.""" + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_PASSWORD] == "" + + async def test_reauth_fixed_via_dashboard_at_confirm( hass, mock_client, mock_zeroconf, mock_dashboard ): diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index c14bb06d3d8..0960556503a 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -1,7 +1,7 @@ """Test ESPHome dashboard features.""" from unittest.mock import patch -from aioesphomeapi import DeviceInfo +from aioesphomeapi import DeviceInfo, InvalidAuthAPIError from homeassistant.components.esphome import CONF_NOISE_PSK, dashboard from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState @@ -31,7 +31,10 @@ async def test_new_dashboard_fix_reauth( hass, mock_client, mock_config_entry, mock_dashboard ): """Test config entries waiting for reauth are triggered.""" - mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + mock_client.device_info.side_effect = ( + InvalidAuthAPIError, + DeviceInfo(uses_password=False, name="test"), + ) with patch( "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", From 2f896c5df8cfefce128a739c558c0f7d85e049f7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 23:47:52 -0500 Subject: [PATCH 067/187] Bumped version to 2023.2.0b7 --- 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 9f185951d1a..6c42db059aa 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 = 2 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 24f24ba97a4..6ea38a4e9fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b6" +version = "2023.2.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ac6fa3275b318328d3d767288e085dad7f10f170 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Tue, 31 Jan 2023 02:56:27 -0500 Subject: [PATCH 068/187] Bump env_canada to 0.5.27 (#86996) fixes undefined --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 83c7ca455cb..b7d3644d481 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.5.22"], + "requirements": ["env_canada==0.5.27"], "codeowners": ["@gwww", "@michaeldavie"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index b254ab847ac..255905f2e10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env_canada==0.5.22 +env_canada==0.5.27 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5fe7fd1cf52..4f68733a617 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -511,7 +511,7 @@ energyzero==0.3.1 enocean==0.50 # homeassistant.components.environment_canada -env_canada==0.5.22 +env_canada==0.5.27 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 From be69e9579c61334a99a218ca417befed4c325ccd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 00:05:59 -0500 Subject: [PATCH 069/187] Bump ESPHome Dashboard API 1.2.3 (#86997) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b07e0f3a476..2ae066266e9 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.1"], + "requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.3"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 255905f2e10..68f87342eff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -673,7 +673,7 @@ epson-projector==0.5.0 epsonprinter==0.0.9 # homeassistant.components.esphome -esphome-dashboard-api==1.2.1 +esphome-dashboard-api==1.2.3 # homeassistant.components.netgear_lte eternalegypt==0.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f68733a617..d0d82952715 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -523,7 +523,7 @@ ephem==4.1.2 epson-projector==0.5.0 # homeassistant.components.esphome -esphome-dashboard-api==1.2.1 +esphome-dashboard-api==1.2.3 # homeassistant.components.eufylife_ble eufylife_ble_client==0.1.7 From c34eb1ad9d2af2f88c12445874befc4e253d5d50 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Tue, 31 Jan 2023 09:32:39 +0100 Subject: [PATCH 070/187] Bump plugwise to v0.27.5 (#87001) fixes undefined --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index e3ac555a00d..a69aaae8eeb 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.27.4"], + "requirements": ["plugwise==0.27.5"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 68f87342eff..0ac0b988672 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1373,7 +1373,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.27.4 +plugwise==0.27.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0d82952715..6d293def0ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1003,7 +1003,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.27.4 +plugwise==0.27.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From 1859dcf99b84e773a6b69b379e5c87e55d195388 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Jan 2023 12:44:18 +0100 Subject: [PATCH 071/187] Only report invalid numeric value for sensors once (#87010) --- homeassistant/components/sensor/__init__.py | 31 +++++++++++---------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index f61be0193f2..35ffc1c3d2a 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -599,21 +599,22 @@ class SensorEntity(Entity): f"({type(value)})" ) from err # This should raise in Home Assistant Core 2023.4 - self._invalid_numeric_value_reported = True - report_issue = self._suggest_report_issue() - _LOGGER.warning( - "Sensor %s has device class %s, state class %s and unit %s " - "thus indicating it has a numeric value; however, it has the " - "non-numeric value: %s (%s); Please update your configuration " - "if your entity is manually configured, otherwise %s", - self.entity_id, - device_class, - state_class, - unit_of_measurement, - value, - type(value), - report_issue, - ) + if not self._invalid_numeric_value_reported: + self._invalid_numeric_value_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Sensor %s has device class %s, state class %s and unit %s " + "thus indicating it has a numeric value; however, it has the " + "non-numeric value: %s (%s); Please update your configuration " + "if your entity is manually configured, otherwise %s", + self.entity_id, + device_class, + state_class, + unit_of_measurement, + value, + type(value), + report_issue, + ) return value else: numerical_value = value From 1caca9117475b4cc69c59947cb5316850c49cace Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 31 Jan 2023 12:59:06 -0500 Subject: [PATCH 072/187] Honeywell Correct key name (#87018) * Correct key name * Logic error around setpoint and auto mode * Set tempurature and setpoints correctly * Only high/low in auto. --- homeassistant/components/honeywell/climate.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 4a773201171..0267eb32e47 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -274,10 +274,10 @@ class HoneywellUSThermostat(ClimateEntity): if self._device.hold_heat is False and self._device.hold_cool is False: # Get next period time hour_heat, minute_heat = divmod( - self._device.raw_ui_data["HEATNextPeriod"] * 15, 60 + self._device.raw_ui_data["HeatNextPeriod"] * 15, 60 ) hour_cool, minute_cool = divmod( - self._device.raw_ui_data["COOLNextPeriod"] * 15, 60 + self._device.raw_ui_data["CoolNextPeriod"] * 15, 60 ) # Set hold time if mode in COOLING_MODES: @@ -290,15 +290,10 @@ class HoneywellUSThermostat(ClimateEntity): ) # Set temperature if not in auto - elif mode == "cool": + if mode == "cool": await self._device.set_setpoint_cool(temperature) - elif mode == "heat": + if mode == "heat": await self._device.set_setpoint_heat(temperature) - elif mode == "auto": - if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): - await self._device.set_setpoint_cool(temperature) - if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): - await self._device.set_setpoint_heat(temperature) except AIOSomecomfort.SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) @@ -307,6 +302,14 @@ class HoneywellUSThermostat(ClimateEntity): """Set new target temperature.""" if {HVACMode.COOL, HVACMode.HEAT} & set(self._hvac_mode_map): await self._set_temperature(**kwargs) + try: + if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): + await self._device.set_setpoint_cool(temperature) + if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): + await self._device.set_setpoint_heat(temperature) + + except AIOSomecomfort.SomeComfortError as err: + _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" From 2f403b712c0e45ad6c93e9e409e8045bdd8d05ef Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 31 Jan 2023 10:23:03 -0600 Subject: [PATCH 073/187] Bump home-assistant-intents to 2023.1.31 (#87034) --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ad49516fe57..f44bcda8f03 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -2,7 +2,7 @@ "domain": "conversation", "name": "Conversation", "documentation": "https://www.home-assistant.io/integrations/conversation", - "requirements": ["hassil==0.2.6", "home-assistant-intents==2023.1.25"], + "requirements": ["hassil==0.2.6", "home-assistant-intents==2023.1.31"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f869419026c..689b8f28f20 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ hass-nabucasa==0.61.0 hassil==0.2.6 home-assistant-bluetooth==1.9.2 home-assistant-frontend==20230130.0 -home-assistant-intents==2023.1.25 +home-assistant-intents==2023.1.31 httpx==0.23.3 ifaddr==0.1.7 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0ac0b988672..246c2f8dbf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -910,7 +910,7 @@ holidays==0.18.0 home-assistant-frontend==20230130.0 # homeassistant.components.conversation -home-assistant-intents==2023.1.25 +home-assistant-intents==2023.1.31 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d293def0ff..a84f9ea6e98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -693,7 +693,7 @@ holidays==0.18.0 home-assistant-frontend==20230130.0 # homeassistant.components.conversation -home-assistant-intents==2023.1.25 +home-assistant-intents==2023.1.31 # homeassistant.components.home_connect homeconnect==0.7.2 From 3d6ced2a16d9f9812495b7f51e5bb4121efe678d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Jan 2023 13:42:07 -0600 Subject: [PATCH 074/187] Add a repair issue when using MariaDB is affected by MDEV-25020 (#87040) closes https://github.com/home-assistant/core/issues/83787 --- .../components/recorder/strings.json | 6 + .../components/recorder/translations/en.json | 6 + homeassistant/components/recorder/util.py | 72 ++++++++++- tests/components/recorder/test_util.py | 115 +++++++++++++++++- 4 files changed, 195 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index 9b616372adf..7af67f10e25 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -7,5 +7,11 @@ "database_engine": "Database Engine", "database_version": "Database Version" } + }, + "issues": { + "maria_db_range_index_regression": { + "title": "Update MariaDB to {min_version} or later resolve a significant performance issue", + "description": "Older versions of MariaDB suffer from a significant performance regression when retrieving history data or purging the database. Update to MariaDB version {min_version} or later and restart Home Assistant. If you are using the MariaDB core add-on, make sure to update it to the latest version." + } } } diff --git a/homeassistant/components/recorder/translations/en.json b/homeassistant/components/recorder/translations/en.json index c9ceffc7397..30c17b854ca 100644 --- a/homeassistant/components/recorder/translations/en.json +++ b/homeassistant/components/recorder/translations/en.json @@ -1,4 +1,10 @@ { + "issues": { + "maria_db_range_index_regression": { + "description": "Older versions of MariaDB suffer from a significant performance regression when retrieving history data or purging the database. Update to MariaDB version {min_version} or later and restart Home Assistant. If you are using the MariaDB core add-on, make sure to update it to the latest version.", + "title": "Update MariaDB to {min_version} or later resolve a significant performance issue" + } + }, "system_health": { "info": { "current_recorder_run": "Current Run Start Time", diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 0e2b1f4d517..0469a71009a 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -25,11 +25,11 @@ from sqlalchemy.orm.session import Session from sqlalchemy.sql.lambdas import StatementLambdaElement import voluptuous as vol -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, issue_registry as ir import homeassistant.util.dt as dt_util -from .const import DATA_INSTANCE, SQLITE_URL_PREFIX, SupportedDialect +from .const import DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX, SupportedDialect from .db_schema import ( TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES, @@ -51,9 +51,35 @@ QUERY_RETRY_WAIT = 0.1 SQLITE3_POSTFIXES = ["", "-wal", "-shm"] DEFAULT_YIELD_STATES_ROWS = 32768 +# Our minimum versions for each database +# +# Older MariaDB suffers https://jira.mariadb.org/browse/MDEV-25020 +# which is fixed in 10.5.17, 10.6.9, 10.7.5, 10.8.4 +# MIN_VERSION_MARIA_DB = AwesomeVersion( "10.3.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER ) +RECOMMENDED_MIN_VERSION_MARIA_DB = AwesomeVersion( + "10.5.17", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MARIA_DB_106 = AwesomeVersion( + "10.6.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +RECOMMENDED_MIN_VERSION_MARIA_DB_106 = AwesomeVersion( + "10.6.9", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MARIA_DB_107 = AwesomeVersion( + "10.7.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +RECOMMENDED_MIN_VERSION_MARIA_DB_107 = AwesomeVersion( + "10.7.5", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MARIA_DB_108 = AwesomeVersion( + "10.8.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +RECOMMENDED_MIN_VERSION_MARIA_DB_108 = AwesomeVersion( + "10.8.4", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) MIN_VERSION_MYSQL = AwesomeVersion( "8.0.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER ) @@ -408,6 +434,34 @@ def build_mysqldb_conv() -> dict: return {**conversions, FIELD_TYPE.DATETIME: _datetime_or_none} +@callback +def _async_create_mariadb_range_index_regression_issue( + hass: HomeAssistant, version: AwesomeVersion +) -> None: + """Create an issue for the index range regression in older MariaDB. + + The range scan issue was fixed in MariaDB 10.5.17, 10.6.9, 10.7.5, 10.8.4 and later. + """ + if version >= MARIA_DB_108: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_108 + elif version >= MARIA_DB_107: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_107 + elif version >= MARIA_DB_106: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_106 + else: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB + ir.async_create_issue( + hass, + DOMAIN, + "maria_db_range_index_regression", + is_fixable=False, + severity=ir.IssueSeverity.CRITICAL, + learn_more_url="https://jira.mariadb.org/browse/MDEV-25020", + translation_key="maria_db_range_index_regression", + translation_placeholders={"min_version": str(min_version)}, + ) + + def setup_connection_for_dialect( instance: Recorder, dialect_name: str, @@ -464,6 +518,18 @@ def setup_connection_for_dialect( _fail_unsupported_version( version or version_string, "MariaDB", MIN_VERSION_MARIA_DB ) + if version and ( + (version < RECOMMENDED_MIN_VERSION_MARIA_DB) + or (MARIA_DB_106 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_106) + or (MARIA_DB_107 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_107) + or (MARIA_DB_108 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_108) + ): + instance.hass.add_job( + _async_create_mariadb_range_index_regression_issue, + instance.hass, + version, + ) + else: if not version or version < MIN_VERSION_MYSQL: _fail_unsupported_version( diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index ecdd729a163..3f5ba6d40ef 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -14,7 +14,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util -from homeassistant.components.recorder.const import SQLITE_URL_PREFIX +from homeassistant.components.recorder.const import DOMAIN, SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import RecorderRuns from homeassistant.components.recorder.models import UnsupportedDialect from homeassistant.components.recorder.util import ( @@ -25,6 +25,7 @@ from homeassistant.components.recorder.util import ( ) from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry from homeassistant.util import dt as dt_util from .common import corrupt_db_file, run_information_with_session, wait_recording_done @@ -550,6 +551,118 @@ def test_warn_unsupported_dialect(caplog, dialect, message): assert message in caplog.text +@pytest.mark.parametrize( + "mysql_version,min_version", + [ + ( + "10.5.16-MariaDB", + "10.5.17", + ), + ( + "10.6.8-MariaDB", + "10.6.9", + ), + ( + "10.7.1-MariaDB", + "10.7.5", + ), + ( + "10.8.0-MariaDB", + "10.8.4", + ), + ], +) +async def test_issue_for_mariadb_with_MDEV_25020( + hass, caplog, mysql_version, min_version +): + """Test we create an issue for MariaDB versions affected. + + See https://jira.mariadb.org/browse/MDEV-25020. + """ + instance_mock = MagicMock() + instance_mock.hass = hass + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT VERSION()": + return [[mysql_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + await hass.async_add_executor_job( + util.setup_connection_for_dialect, + instance_mock, + "mysql", + dbapi_connection, + True, + ) + await hass.async_block_till_done() + + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "maria_db_range_index_regression") + assert issue is not None + assert issue.translation_placeholders == {"min_version": min_version} + + +@pytest.mark.parametrize( + "mysql_version", + [ + "10.5.17-MariaDB", + "10.6.9-MariaDB", + "10.7.5-MariaDB", + "10.8.4-MariaDB", + "10.9.1-MariaDB", + ], +) +async def test_no_issue_for_mariadb_with_MDEV_25020(hass, caplog, mysql_version): + """Test we do not create an issue for MariaDB versions not affected. + + See https://jira.mariadb.org/browse/MDEV-25020. + """ + instance_mock = MagicMock() + instance_mock.hass = hass + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT VERSION()": + return [[mysql_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + await hass.async_add_executor_job( + util.setup_connection_for_dialect, + instance_mock, + "mysql", + dbapi_connection, + True, + ) + await hass.async_block_till_done() + + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "maria_db_range_index_regression") + assert issue is None + + def test_basic_sanity_check(hass_recorder, recorder_db_url): """Test the basic sanity checks with a missing table.""" if recorder_db_url.startswith("mysql://"): From c7871d13cff4aea81b19ea0a9cabe545169d3665 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Jan 2023 20:37:11 +0100 Subject: [PATCH 075/187] Fix Yamaha MusicCast zone sleep select entity (#87041) --- .../components/yamaha_musiccast/const.py | 9 -------- .../components/yamaha_musiccast/manifest.json | 2 +- .../components/yamaha_musiccast/select.py | 23 ++----------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/const.py b/homeassistant/components/yamaha_musiccast/const.py index 5984d73980f..49234ac38ee 100644 --- a/homeassistant/components/yamaha_musiccast/const.py +++ b/homeassistant/components/yamaha_musiccast/const.py @@ -55,12 +55,3 @@ TRANSLATION_KEY_MAPPING = { "zone_LINK_CONTROL": "zone_link_control", "zone_LINK_AUDIO_DELAY": "zone_link_audio_delay", } - -ZONE_SLEEP_STATE_MAPPING = { - "off": "off", - "30 min": "30_min", - "60 min": "60_min", - "90 min": "90_min", - "120 min": "120_min", -} -STATE_ZONE_SLEEP_MAPPING = {val: key for key, val in ZONE_SLEEP_STATE_MAPPING.items()} diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 8c0b55def69..afcc64985dc 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -3,7 +3,7 @@ "name": "MusicCast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", - "requirements": ["aiomusiccast==0.14.4"], + "requirements": ["aiomusiccast==0.14.7"], "ssdp": [ { "manufacturer": "Yamaha Corporation" diff --git a/homeassistant/components/yamaha_musiccast/select.py b/homeassistant/components/yamaha_musiccast/select.py index 200d62bde32..a8ca6162c91 100644 --- a/homeassistant/components/yamaha_musiccast/select.py +++ b/homeassistant/components/yamaha_musiccast/select.py @@ -9,11 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, MusicCastCapabilityEntity, MusicCastDataUpdateCoordinator -from .const import ( - STATE_ZONE_SLEEP_MAPPING, - TRANSLATION_KEY_MAPPING, - ZONE_SLEEP_STATE_MAPPING, -) +from .const import TRANSLATION_KEY_MAPPING async def async_setup_entry( @@ -48,10 +44,6 @@ class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Select the given option.""" value = {val: key for key, val in self.capability.options.items()}[option] - # If the translation key is "zone_sleep", we need to translate - # Home Assistant state back to the MusicCast value - if self.translation_key == "zone_sleep": - value = STATE_ZONE_SLEEP_MAPPING[value] await self.capability.set(value) @property @@ -62,20 +54,9 @@ class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity): @property def options(self) -> list[str]: """Return the list possible options.""" - # If the translation key is "zone_sleep", we need to translate - # the options to make them compatible with Home Assistant - if self.translation_key == "zone_sleep": - return list(STATE_ZONE_SLEEP_MAPPING) return list(self.capability.options.values()) @property def current_option(self) -> str | None: """Return the currently selected option.""" - # If the translation key is "zone_sleep", we need to translate - # the value to make it compatible with Home Assistant - if ( - value := self.capability.current - ) is not None and self.translation_key == "zone_sleep": - return ZONE_SLEEP_STATE_MAPPING[value] - - return value + return self.capability.options.get(self.capability.current) diff --git a/requirements_all.txt b/requirements_all.txt index 246c2f8dbf8..ea93d704e08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -214,7 +214,7 @@ aiolyric==1.0.9 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.4 +aiomusiccast==0.14.7 # homeassistant.components.nanoleaf aionanoleaf==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a84f9ea6e98..2f107420066 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -195,7 +195,7 @@ aiolyric==1.0.9 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.4 +aiomusiccast==0.14.7 # homeassistant.components.nanoleaf aionanoleaf==0.2.1 From 0d3a368a1ff9680733a7a21be2a9ac322e5cc482 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 14:37:26 -0500 Subject: [PATCH 076/187] Improve JSON errors from HTTP view (#87042) --- homeassistant/components/http/view.py | 13 +++++++++++-- tests/components/http/test_view.py | 15 ++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 6ab3b2a84a4..32582dbdc92 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -20,7 +20,11 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import Context, is_callback -from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS, json_bytes +from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS, json_bytes, json_dumps +from homeassistant.util.json import ( + find_paths_unserializable_data, + format_unserializable_data, +) from .const import KEY_AUTHENTICATED, KEY_HASS @@ -54,7 +58,12 @@ class HomeAssistantView: try: msg = json_bytes(result) except JSON_ENCODE_EXCEPTIONS as err: - _LOGGER.error("Unable to serialize to JSON: %s\n%s", err, result) + _LOGGER.error( + "Unable to serialize to JSON. Bad data found at %s", + format_unserializable_data( + find_paths_unserializable_data(result, dump=json_dumps) + ), + ) raise HTTPInternalServerError from err response = web.Response( body=msg, diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index f6a2ff85d3a..4709806fedd 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -1,4 +1,5 @@ """Tests for Home Assistant View.""" +from decimal import Decimal from http import HTTPStatus import json from unittest.mock import AsyncMock, Mock @@ -32,18 +33,18 @@ def mock_request_with_stopping(): async def test_invalid_json(caplog): """Test trying to return invalid JSON.""" - view = HomeAssistantView() - with pytest.raises(HTTPInternalServerError): - view.json(rb"\ud800") + HomeAssistantView.json({"hello": Decimal("2.0")}) - assert "Unable to serialize to JSON" in caplog.text + assert ( + "Unable to serialize to JSON. Bad data found at $.hello=2.0(" + in caplog.text + ) -async def test_nan_serialized_to_null(caplog): +async def test_nan_serialized_to_null(): """Test nan serialized to null JSON.""" - view = HomeAssistantView() - response = view.json(float("NaN")) + response = HomeAssistantView.json(float("NaN")) assert json.loads(response.body.decode("utf-8")) is None From 0bae47c992cafec5868be6d5f4fbe8cb7ffe93ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 15:16:21 -0500 Subject: [PATCH 077/187] Bumped version to 2023.2.0b8 --- 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 6c42db059aa..16582aa0eeb 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 = 2 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 6ea38a4e9fd..4bac03d7ba4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b7" +version = "2023.2.0b8" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c43174ee4b87fd6175583d3f386fcc4bf346aeb5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Jan 2023 20:03:43 -0600 Subject: [PATCH 078/187] Ensure humidity is still exported to HomeKit when it is read-only (#87051) * Ensure humidity is still exported to HomeKit when is cannot be set We would only send humidity to HomeKit if the device supported changing the humidity * remove unrelated changes --- .../components/homekit/type_thermostats.py | 11 +++++++-- .../homekit/test_type_thermostats.py | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 24a137e4957..c34e9066160 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -188,8 +188,14 @@ class Thermostat(HomeAccessory): (CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) ) + if ( + ATTR_CURRENT_HUMIDITY in attributes + or features & ClimateEntityFeature.TARGET_HUMIDITY + ): + self.chars.append(CHAR_CURRENT_HUMIDITY) + if features & ClimateEntityFeature.TARGET_HUMIDITY: - self.chars.extend((CHAR_TARGET_HUMIDITY, CHAR_CURRENT_HUMIDITY)) + self.chars.append(CHAR_TARGET_HUMIDITY) serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) self.set_primary_service(serv_thermostat) @@ -253,7 +259,6 @@ class Thermostat(HomeAccessory): properties={PROP_MIN_VALUE: hc_min_temp, PROP_MAX_VALUE: hc_max_temp}, ) self.char_target_humidity = None - self.char_current_humidity = None if CHAR_TARGET_HUMIDITY in self.chars: self.char_target_humidity = serv_thermostat.configure_char( CHAR_TARGET_HUMIDITY, @@ -265,6 +270,8 @@ class Thermostat(HomeAccessory): # of 0-80% properties={PROP_MIN_VALUE: min_humidity}, ) + self.char_current_humidity = None + if CHAR_CURRENT_HUMIDITY in self.chars: self.char_current_humidity = serv_thermostat.configure_char( CHAR_CURRENT_HUMIDITY, value=50 ) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 1dd191f5303..1a18c7fe805 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -742,6 +742,29 @@ async def test_thermostat_humidity(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] == "35%" +async def test_thermostat_humidity_with_target_humidity(hass, hk_driver, events): + """Test if accessory and HA are updated accordingly with humidity without target hudmidity. + + This test is for thermostats that do not support target humidity but + have a current humidity sensor. + """ + entity_id = "climate.test" + + # support_auto = True + hass.states.async_set(entity_id, HVACMode.OFF, {ATTR_CURRENT_HUMIDITY: 40}) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.char_current_humidity.value == 40 + hass.states.async_set(entity_id, HVACMode.HEAT_COOL, {ATTR_CURRENT_HUMIDITY: 65}) + await hass.async_block_till_done() + assert acc.char_current_humidity.value == 65 + + async def test_thermostat_power_state(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" From fd3d76988e6a06e400ef4c2c8551acaa9fb85b66 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 22:13:41 -0500 Subject: [PATCH 079/187] Trigger update of ESPHome update entity when static info updates (#87058) Trigger update of update entity when static info updates --- homeassistant/components/esphome/__init__.py | 5 +- .../components/esphome/entry_data.py | 8 +++- homeassistant/components/esphome/update.py | 21 ++++++++- tests/components/esphome/test_update.py | 47 ++++++++++++++++++- 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 4c9d8b6362b..d2c987d56ba 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -646,9 +646,10 @@ async def platform_async_setup_entry( # Add entities to Home Assistant async_add_entities(add_entities) - signal = f"esphome_{entry.entry_id}_on_list" entry_data.cleanup_callbacks.append( - async_dispatcher_connect(hass, signal, async_list_entities) + async_dispatcher_connect( + hass, entry_data.signal_static_info_updated, async_list_entities + ) ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 24322fdac47..b7443eea211 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -107,6 +107,11 @@ class RuntimeEntryData: return self.device_info.friendly_name return self.name + @property + def signal_static_info_updated(self) -> str: + """Return the signal to listen to for updates on static info.""" + return f"esphome_{self.entry_id}_on_list" + @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: """Update the BLE connection limits.""" @@ -168,8 +173,7 @@ class RuntimeEntryData: await self._ensure_platforms_loaded(hass, entry, needed_platforms) # Then send dispatcher event - signal = f"esphome_{self.entry_id}_on_list" - async_dispatcher_send(hass, signal, infos) + async_dispatcher_send(hass, self.signal_static_info_updated, infos) @callback def async_subscribe_state_update( diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index de7a7463191..be525a6f3b8 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -5,7 +5,7 @@ import asyncio import logging from typing import Any, cast -from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo +from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo, EntityInfo from homeassistant.components.update import ( UpdateDeviceClass, @@ -13,7 +13,7 @@ from homeassistant.components.update import ( UpdateEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -115,6 +115,23 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): """URL to the full release notes of the latest version available.""" return "https://esphome.io/changelog/" + async def async_added_to_hass(self) -> None: + """Handle entity added to Home Assistant.""" + await super().async_added_to_hass() + + @callback + def _static_info_updated(infos: list[EntityInfo]) -> None: + """Handle static info update.""" + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._entry_data.signal_static_info_updated, + _static_info_updated, + ) + ) + async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index a263f4ab0cd..d7ad83697af 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -1,9 +1,11 @@ """Test ESPHome update entities.""" +import dataclasses from unittest.mock import Mock, patch import pytest from homeassistant.components.esphome.dashboard import async_get_dashboard +from homeassistant.helpers.dispatcher import async_dispatcher_send @pytest.fixture(autouse=True) @@ -57,8 +59,6 @@ async def test_update_entity( mock_dashboard["configured"] = devices_payload await async_get_dashboard(hass).async_refresh() - mock_config_entry.add_to_hass(hass) - with patch( "homeassistant.components.esphome.update.DomainData.get_entry_data", return_value=Mock(available=True, device_info=mock_device_info), @@ -93,3 +93,46 @@ async def test_update_entity( assert len(mock_upload.mock_calls) == 1 assert mock_upload.mock_calls[0][1][0] == "test.yaml" + + +async def test_update_static_info( + hass, + mock_config_entry, + mock_device_info, + mock_dashboard, +): + """Test ESPHome update entity.""" + mock_dashboard["configured"] = [ + { + "name": "test", + "current_version": "1.2.3", + }, + ] + await async_get_dashboard(hass).async_refresh() + + signal_static_info_updated = f"esphome_{mock_config_entry.entry_id}_on_list" + runtime_data = Mock( + available=True, + device_info=mock_device_info, + signal_static_info_updated=signal_static_info_updated, + ) + + with patch( + "homeassistant.components.esphome.update.DomainData.get_entry_data", + return_value=runtime_data, + ): + assert await hass.config_entries.async_forward_entry_setup( + mock_config_entry, "update" + ) + + state = hass.states.get("update.none_firmware") + assert state is not None + assert state.state == "on" + + runtime_data.device_info = dataclasses.replace( + runtime_data.device_info, esphome_version="1.2.3" + ) + async_dispatcher_send(hass, signal_static_info_updated, []) + + state = hass.states.get("update.none_firmware") + assert state.state == "off" From c786fe27d75c6ad7ffb836a38291b4e58f965b42 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 23:15:23 -0500 Subject: [PATCH 080/187] Guard what version we can install ESPHome updates with (#87059) * Guard what version we can install ESPHome updates with * Update homeassistant/components/esphome/dashboard.py --- homeassistant/components/esphome/dashboard.py | 15 +++++++++++ homeassistant/components/esphome/update.py | 3 ++- tests/components/esphome/test_dashboard.py | 26 +++++++++++++++++++ tests/components/esphome/test_update.py | 17 +++++++++--- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 052d6161cf9..7439f8946f6 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -6,6 +6,7 @@ from datetime import timedelta import logging import aiohttp +from awesomeversion import AwesomeVersion from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState @@ -83,6 +84,20 @@ class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): self.url = url self.api = ESPHomeDashboardAPI(url, session) + @property + def supports_update(self) -> bool: + """Return whether the dashboard supports updates.""" + if self.data is None: + raise RuntimeError("Data needs to be loaded first") + + if len(self.data) == 0: + return False + + esphome_version: str = next(iter(self.data.values()))["current_version"] + + # There is no January release + return AwesomeVersion(esphome_version) > AwesomeVersion("2023.1.0") + async def _async_update_data(self) -> dict: """Fetch device data.""" devices = await self.api.get_devices() diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index be525a6f3b8..7139c9e937f 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -68,7 +68,6 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): _attr_has_entity_name = True _attr_device_class = UpdateDeviceClass.FIRMWARE - _attr_supported_features = UpdateEntityFeature.INSTALL _attr_title = "ESPHome" _attr_name = "Firmware" @@ -85,6 +84,8 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): (dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address) } ) + if coordinator.supports_update: + self._attr_supported_features = UpdateEntityFeature.INSTALL @property def _device_info(self) -> ESPHomeDeviceInfo: diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index 0960556503a..ed2afb7e500 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -73,3 +73,29 @@ async def test_new_dashboard_fix_reauth( assert len(mock_get_encryption_key.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1 assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + + +async def test_dashboard_supports_update(hass, mock_dashboard): + """Test dashboard supports update.""" + dash = dashboard.async_get_dashboard(hass) + + # No data + assert not dash.supports_update + + # supported version + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + "current_version": "2023.2.0-dev", + } + ) + await dash.async_refresh() + + assert dash.supports_update + + # unsupported version + mock_dashboard["configured"][0]["current_version"] = "2023.1.0" + await dash.async_refresh() + + assert not dash.supports_update diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index d7ad83697af..9cfba03be9f 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant.components.esphome.dashboard import async_get_dashboard +from homeassistant.components.update import UpdateEntityFeature from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -22,12 +23,16 @@ def stub_reconnect(): [ { "name": "test", - "current_version": "1.2.3", + "current_version": "2023.2.0-dev", "configuration": "test.yaml", } ], "on", - {"latest_version": "1.2.3", "installed_version": "1.0.0"}, + { + "latest_version": "2023.2.0-dev", + "installed_version": "1.0.0", + "supported_features": UpdateEntityFeature.INSTALL, + }, ), ( [ @@ -37,12 +42,16 @@ def stub_reconnect(): }, ], "off", - {"latest_version": "1.0.0", "installed_version": "1.0.0"}, + { + "latest_version": "1.0.0", + "installed_version": "1.0.0", + "supported_features": 0, + }, ), ( [], "unavailable", - {}, + {"supported_features": 0}, ), ], ) From d57ce25287935b85240d04cd39a8f170248f75e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Jan 2023 23:16:09 -0500 Subject: [PATCH 081/187] Bumped version to 2023.2.0b9 --- 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 16582aa0eeb..38bda177007 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 = 2 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 4bac03d7ba4..e93ba800ff4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b8" +version = "2023.2.0b9" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From eabcfa419e8c60c72f34e6826de536e148dc76a4 Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 26 Jan 2023 16:00:54 -0500 Subject: [PATCH 082/187] Bump AIOAladdinConnect to 0.1.54 (#86749) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index e9556cb1b35..74f1b467968 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.53"], + "requirements": ["AIOAladdinConnect==0.1.54"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index ea93d704e08..93e87a2b85b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.53 +AIOAladdinConnect==0.1.54 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f107420066..c6fcb8eb23f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.53 +AIOAladdinConnect==0.1.54 # homeassistant.components.adax Adax-local==0.1.5 From fe541583a8b27fa4de224f7e68b37cb546758243 Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 1 Feb 2023 09:57:21 -0500 Subject: [PATCH 083/187] Bump AIOAladdinConnect to 0.1.55 (#87086) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 74f1b467968..3cfe7a14167 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.54"], + "requirements": ["AIOAladdinConnect==0.1.55"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 93e87a2b85b..a9129c5679d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.54 +AIOAladdinConnect==0.1.55 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6fcb8eb23f..686e12c5385 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.54 +AIOAladdinConnect==0.1.55 # homeassistant.components.adax Adax-local==0.1.5 From a678eee31bfc9182da2917cca2128c0f5213764d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Feb 2023 11:34:40 -0600 Subject: [PATCH 084/187] Reduce chance of queue overflow during schema migration (#87090) Co-authored-by: Franck Nijhof --- homeassistant/components/recorder/const.py | 2 +- tests/components/recorder/test_websocket_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 8db4b43e04e..379185bec7b 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -19,7 +19,7 @@ EVENT_RECORDER_HOURLY_STATISTICS_GENERATED = "recorder_hourly_statistics_generat CONF_DB_INTEGRITY_CHECK = "db_integrity_check" -MAX_QUEUE_BACKLOG = 40000 +MAX_QUEUE_BACKLOG = 65000 # The maximum number of rows (events) we purge in one delete statement diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 6aa76c19ec7..935594d5a5c 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -2033,7 +2033,7 @@ async def test_recorder_info(recorder_mock, hass, hass_ws_client): assert response["success"] assert response["result"] == { "backlog": 0, - "max_backlog": 40000, + "max_backlog": 65000, "migration_in_progress": False, "migration_is_live": False, "recording": True, From 65286d0544a47af4aad4fce4ef890278ffff9380 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Feb 2023 11:48:04 -0500 Subject: [PATCH 085/187] Fix Assist skipping entities that are hidden or have entity category (#87096) Skipping entities that are hidden or have entity category --- .../components/conversation/default_agent.py | 12 +++-- .../conversation/test_default_agent.py | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 tests/components/conversation/test_default_agent.py diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index cabf9089b1c..2756998b3a6 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -403,16 +403,20 @@ class DefaultAgent(AbstractConversationAgent): entity = entities.async_get(state.entity_id) if entity is not None: - if entity.entity_category: - # Skip configuration/diagnostic entities + if entity.entity_category or entity.hidden: + # Skip configuration/diagnostic/hidden entities continue if entity.aliases: for alias in entity.aliases: names.append((alias, state.entity_id, context)) - # Default name - names.append((state.name, state.entity_id, context)) + # Default name + names.append((state.name, state.entity_id, context)) + + else: + # Default name + names.append((state.name, state.entity_id, context)) self._names_list = TextSlotList.from_tuples(names, allow_template=False) return self._names_list diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py new file mode 100644 index 00000000000..591802f5888 --- /dev/null +++ b/tests/components/conversation/test_default_agent.py @@ -0,0 +1,44 @@ +"""Test for the default agent.""" +import pytest + +from homeassistant.components import conversation +from homeassistant.core import DOMAIN as HASS_DOMAIN, Context +from homeassistant.helpers import entity, entity_registry, intent +from homeassistant.setup import async_setup_component + +from tests.common import async_mock_service + + +@pytest.fixture +async def init_components(hass): + """Initialize relevant components with empty configs.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component(hass, "intent", {}) + + +@pytest.mark.parametrize( + "er_kwargs", + [ + {"hidden_by": entity_registry.RegistryEntryHider.USER}, + {"hidden_by": entity_registry.RegistryEntryHider.INTEGRATION}, + {"entity_category": entity.EntityCategory.CONFIG}, + {"entity_category": entity.EntityCategory.DIAGNOSTIC}, + ], +) +async def test_hidden_entities_skipped(hass, init_components, er_kwargs): + """Test we skip hidden entities.""" + + er = entity_registry.async_get(hass) + er.async_get_or_create( + "light", "demo", "1234", suggested_object_id="Test light", **er_kwargs + ) + hass.states.async_set("light.test_light", "off") + calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + result = await conversation.async_converse( + hass, "turn on test light", None, Context(), None + ) + + assert len(calls) == 0 + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH From 6c93b28374352d118532774d403cd09383a1308a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 1 Feb 2023 18:04:24 +0100 Subject: [PATCH 086/187] Update pyTibber to 0.26.12 (#87098) --- 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 1636c5da4bd..cb3c88532d9 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.11"], + "requirements": ["pyTibber==0.26.12"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index a9129c5679d..6429f03045c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1470,7 +1470,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.11 +pyTibber==0.26.12 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 686e12c5385..40115b07ddd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1073,7 +1073,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.11 +pyTibber==0.26.12 # homeassistant.components.dlink pyW215==0.7.0 From 7028aa7dace49152c64209591df0a3ec131d8070 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 1 Feb 2023 18:06:12 +0100 Subject: [PATCH 087/187] Update frontend to 20230201.0 (#87099) --- 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 eeb0a906bcf..b33668cde4c 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==20230130.0"], + "requirements": ["home-assistant-frontend==20230201.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 689b8f28f20..9101375adb3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 hassil==0.2.6 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230130.0 +home-assistant-frontend==20230201.0 home-assistant-intents==2023.1.31 httpx==0.23.3 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index 6429f03045c..6ccb4d457e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -907,7 +907,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230130.0 +home-assistant-frontend==20230201.0 # homeassistant.components.conversation home-assistant-intents==2023.1.31 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40115b07ddd..b91f5d6d144 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230130.0 +home-assistant-frontend==20230201.0 # homeassistant.components.conversation home-assistant-intents==2023.1.31 From eed15bb9fa1366150aa380c69c00ff056f0224ba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Feb 2023 18:41:13 +0100 Subject: [PATCH 088/187] Bumped version to 2023.2.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 38bda177007..ee5d60a32be 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 = 2 -PATCH_VERSION: Final = "0b9" +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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index e93ba800ff4..5c014aabd4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0b9" +version = "2023.2.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 46414978066b431b4b741ce86dffade7f5d2f0b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 29 Jan 2023 15:04:17 +0100 Subject: [PATCH 089/187] Bump isort from 5.11.4 to 5.12.0 (#86890) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83329d8dfcf..f7684e4d4d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -60,7 +60,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.11.4 + rev: 5.12.0 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 34b68588285..a852f1b3161 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -8,7 +8,7 @@ flake8-comprehensions==3.10.1 flake8-docstrings==1.6.0 flake8-noqa==1.3.0 flake8==6.0.0 -isort==5.11.4 +isort==5.12.0 mccabe==0.7.0 pycodestyle==2.10.0 pydocstyle==6.2.3 From 8a7e2922c26cb5a972967fcbf06ec72ca6f68fbe Mon Sep 17 00:00:00 2001 From: shbatm Date: Thu, 2 Feb 2023 13:27:30 -0600 Subject: [PATCH 090/187] Support ISY994 Z-Wave motorized blinds as cover (#87102) --- homeassistant/components/isy994/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 211939f8eb6..95e68c5fa11 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -255,7 +255,7 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { FILTER_STATES: ["open", "closed", "closing", "opening", "stopped"], FILTER_NODE_DEF_ID: ["DimmerMotorSwitch_ADV"], FILTER_INSTEON_TYPE: [TYPE_CATEGORY_COVER], - FILTER_ZWAVE_CAT: [], + FILTER_ZWAVE_CAT: ["106", "107"], }, Platform.LIGHT: { FILTER_UOM: ["51"], From b24d0a86eeb095b8972de4c6ee1f1b01641475a5 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 2 Feb 2023 04:56:18 +0100 Subject: [PATCH 091/187] Bump reolink_aio to 0.3.1 (#87118) --- 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 88e2e3b7730..3516241feec 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.3.0"], + "requirements": ["reolink-aio==0.3.1"], "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 6ccb4d457e5..7c44269a097 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2227,7 +2227,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.0 +reolink-aio==0.3.1 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b91f5d6d144..e1a21fcf16d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1572,7 +1572,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.0 +reolink-aio==0.3.1 # homeassistant.components.python_script restrictedpython==6.0 From 264b6d4f777f0e4b3974e4cf2f55d4fa39631a70 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 2 Feb 2023 11:24:06 +0100 Subject: [PATCH 092/187] Bump reolink-aio to 0.3.2 (#87121) --- homeassistant/components/reolink/__init__.py | 18 ++----- homeassistant/components/reolink/host.py | 50 ++++++++++--------- .../components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 34 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index c20aff637ec..56974d9ed83 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -9,14 +9,7 @@ import logging from aiohttp import ClientConnectorError import async_timeout -from reolink_aio.exceptions import ( - ApiError, - InvalidContentTypeError, - LoginError, - NoDataError, - ReolinkError, - UnexpectedDataError, -) +from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform @@ -48,17 +41,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await host.async_init() - except UserNotAdmin as err: + except (UserNotAdmin, CredentialsInvalidError) as err: + await host.stop() raise ConfigEntryAuthFailed(err) from err except ( ClientConnectorError, asyncio.TimeoutError, - ApiError, - InvalidContentTypeError, - LoginError, - NoDataError, ReolinkException, - UnexpectedDataError, + ReolinkError, ) as err: await host.stop() raise ConfigEntryNotReady( diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 3e0731ac8ce..582a52ae1ba 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -9,7 +9,7 @@ from typing import Any import aiohttp from aiohttp.web import Request from reolink_aio.api import Host -from reolink_aio.exceptions import ReolinkError +from reolink_aio.exceptions import ReolinkError, SubscriptionError from homeassistant.components import webhook from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -76,7 +76,6 @@ class ReolinkHost: raise ReolinkSetupException("Could not get mac address") if not self._api.is_admin: - await self.stop() raise UserNotAdmin( f"User '{self._api.username}' has authorization level " f"'{self._api.user_level}', only admin users can change camera settings" @@ -182,22 +181,19 @@ class ReolinkHost: ) return - if await self._api.subscribe(self._webhook_url): - _LOGGER.debug( - "Host %s: subscribed successfully to webhook %s", - self._api.host, - self._webhook_url, - ) - else: - raise ReolinkWebhookException( - f"Host {self._api.host}: webhook subscription failed" - ) + await self._api.subscribe(self._webhook_url) + + _LOGGER.debug( + "Host %s: subscribed successfully to webhook %s", + self._api.host, + self._webhook_url, + ) async def renew(self) -> None: """Renew the subscription of motion events (lease time is 15 minutes).""" try: await self._renew() - except ReolinkWebhookException as err: + except SubscriptionError as err: if not self._lost_subscription: self._lost_subscription = True _LOGGER.error( @@ -220,25 +216,33 @@ class ReolinkHost: return timer = self._api.renewtimer + _LOGGER.debug( + "Host %s:%s should renew subscription in: %i seconds", + self._api.host, + self._api.port, + timer, + ) if timer > SUBSCRIPTION_RENEW_THRESHOLD: return if timer > 0: - if await self._api.renew(): + try: + await self._api.renew() + except SubscriptionError as err: + _LOGGER.debug( + "Host %s: error renewing Reolink subscription, " + "trying to subscribe again: %s", + self._api.host, + err, + ) + else: _LOGGER.debug( "Host %s successfully renewed Reolink subscription", self._api.host ) return - _LOGGER.debug( - "Host %s: error renewing Reolink subscription, " - "trying to subscribe again", - self._api.host, - ) - if not await self._api.subscribe(self._webhook_url): - raise ReolinkWebhookException( - f"Host {self._api.host}: webhook re-subscription failed" - ) + await self._api.subscribe(self._webhook_url) + _LOGGER.debug( "Host %s: Reolink re-subscription successful after it was expired", self._api.host, diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 3516241feec..ab944314054 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.3.1"], + "requirements": ["reolink-aio==0.3.2"], "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 7c44269a097..f8abf9d975b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2227,7 +2227,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.1 +reolink-aio==0.3.2 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1a21fcf16d..18071e51a33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1572,7 +1572,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.1 +reolink-aio==0.3.2 # homeassistant.components.python_script restrictedpython==6.0 From ef8029ebbff4b1cb1c5eed20ab02e9ebf8f1cbf6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Feb 2023 10:38:21 +0100 Subject: [PATCH 093/187] Fix invalid state class in renault (#87135) --- homeassistant/components/renault/sensor.py | 2 +- tests/components/renault/const.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 679fbbd370f..52538d1e873 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -257,7 +257,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( device_class=SensorDeviceClass.ENERGY, name="Battery available energy", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), RenaultSensorEntityDescription( key="battery_temperature", diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 5db47c5d589..b15b00b825f 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -139,7 +139,7 @@ MOCK_VEHICLES = { ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_STATE: "31", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, @@ -368,7 +368,7 @@ MOCK_VEHICLES = { ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_STATE: "0", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, @@ -597,7 +597,7 @@ MOCK_VEHICLES = { ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_STATE: "31", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, From 517e89ab3caf13d8e0e69cf995081d0d505c6569 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Feb 2023 09:44:26 +0100 Subject: [PATCH 094/187] Add missing converters to recorder statistics (#87137) --- homeassistant/components/recorder/statistics.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4a39abd9f63..7a5fba6ea60 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -36,8 +36,12 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, + DataRateConverter, DistanceConverter, + ElectricCurrentConverter, + ElectricPotentialConverter, EnergyConverter, + InformationConverter, MassConverter, PowerConverter, PressureConverter, @@ -128,8 +132,15 @@ QUERY_STATISTIC_META = [ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { + **{unit: DataRateConverter for unit in DataRateConverter.VALID_UNITS}, **{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS}, + **{unit: ElectricCurrentConverter for unit in ElectricCurrentConverter.VALID_UNITS}, + **{ + unit: ElectricPotentialConverter + for unit in ElectricPotentialConverter.VALID_UNITS + }, **{unit: EnergyConverter for unit in EnergyConverter.VALID_UNITS}, + **{unit: InformationConverter for unit in InformationConverter.VALID_UNITS}, **{unit: MassConverter for unit in MassConverter.VALID_UNITS}, **{unit: PowerConverter for unit in PowerConverter.VALID_UNITS}, **{unit: PressureConverter for unit in PressureConverter.VALID_UNITS}, From 56a583e6acb8b5268b1c763dfcf1824030e09257 Mon Sep 17 00:00:00 2001 From: Dmitry Vlasov Date: Thu, 2 Feb 2023 12:36:38 +0300 Subject: [PATCH 095/187] Add missing supported features to Z-Wave.Me siren (#87141) --- homeassistant/components/zwave_me/siren.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_me/siren.py b/homeassistant/components/zwave_me/siren.py index f59face65d4..c6757f61ad7 100644 --- a/homeassistant/components/zwave_me/siren.py +++ b/homeassistant/components/zwave_me/siren.py @@ -1,7 +1,7 @@ """Representation of a sirenBinary.""" from typing import Any -from homeassistant.components.siren import SirenEntity +from homeassistant.components.siren import SirenEntity, SirenEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -41,6 +41,13 @@ async def async_setup_entry( class ZWaveMeSiren(ZWaveMeEntity, SirenEntity): """Representation of a ZWaveMe siren.""" + def __init__(self, controller, device): + """Initialize the device.""" + super().__init__(controller, device) + self._attr_supported_features = ( + SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF + ) + @property def is_on(self) -> bool: """Return the state of the siren.""" From a58e4e0f8844dc092924c67a70d104b56f2fa309 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 2 Feb 2023 21:21:41 +0100 Subject: [PATCH 096/187] Reolink unsubscribe webhook when first refresh fails (#87147) * catch ValueError on webhook async_register * add ONVIF to webhook_id * Unsubscribe webhook when ConfigEntryNotReady for async_config_entry_first_refresh * Revert catching ValueError --- homeassistant/components/reolink/__init__.py | 6 +++++- homeassistant/components/reolink/host.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 56974d9ed83..f15ce90427c 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -80,7 +80,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b update_interval=timedelta(seconds=DEVICE_UPDATE_INTERVAL), ) # Fetch initial data so we have data when entities subscribe - await coordinator_device_config_update.async_config_entry_first_refresh() + try: + await coordinator_device_config_update.async_config_entry_first_refresh() + except ConfigEntryNotReady as err: + await host.stop() + raise err hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData( host=host, diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 582a52ae1ba..e44623e1a1e 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -250,7 +250,7 @@ class ReolinkHost: async def register_webhook(self) -> None: """Register the webhook for motion events.""" - self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}" + self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}_ONVIF" event_id = self.webhook_id webhook.async_register( From be77d7daa5d0702073e27fcf781c03317b12e190 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Feb 2023 13:52:55 -0600 Subject: [PATCH 097/187] Fix statistics graphs not loading with data_rate, electric_current, voltage, information, and unitless units (#87202) * Add missing converts to recorder/statistics_during_period API This was resulting in the stats graphs not loading on the frontend * its in two places --- .../components/recorder/websocket_api.py | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index c63e5bc43ca..8733d675069 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -15,13 +15,18 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( + DataRateConverter, DistanceConverter, + ElectricCurrentConverter, + ElectricPotentialConverter, EnergyConverter, + InformationConverter, MassConverter, PowerConverter, PressureConverter, SpeedConverter, TemperatureConverter, + UnitlessRatioConverter, VolumeConverter, ) @@ -47,6 +52,24 @@ from .util import ( _LOGGER: logging.Logger = logging.getLogger(__package__) +UNIT_SCHEMA = vol.Schema( + { + vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS), + vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), + vol.Optional("electric_current"): vol.In(ElectricCurrentConverter.VALID_UNITS), + vol.Optional("voltage"): vol.In(ElectricPotentialConverter.VALID_UNITS), + vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), + vol.Optional("information"): vol.In(InformationConverter.VALID_UNITS), + vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS), + vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), + vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), + vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), + vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), + vol.Optional("unitless"): vol.In(UnitlessRatioConverter.VALID_UNITS), + vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), + } +) + @callback def async_setup(hass: HomeAssistant) -> None: @@ -93,18 +116,7 @@ def _ws_get_statistic_during_period( vol.Optional("types"): vol.All( [vol.Any("max", "mean", "min", "change")], vol.Coerce(set) ), - vol.Optional("units"): vol.Schema( - { - vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), - vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), - vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS), - vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), - vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), - vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), - vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), - vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), - } - ), + vol.Optional("units"): UNIT_SCHEMA, **PERIOD_SCHEMA.schema, } ) @@ -211,18 +223,7 @@ async def ws_handle_get_statistics_during_period( vol.Optional("end_time"): str, vol.Optional("statistic_ids"): [str], vol.Required("period"): vol.Any("5minute", "hour", "day", "week", "month"), - vol.Optional("units"): vol.Schema( - { - vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), - vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), - vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS), - vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), - vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), - vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), - vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), - vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), - } - ), + vol.Optional("units"): UNIT_SCHEMA, vol.Optional("types"): vol.All( [vol.Any("last_reset", "max", "mean", "min", "state", "sum")], vol.Coerce(set), From bfcae4e07be9a3f9693f5104deea95d75bf90ef6 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 30 Jan 2023 07:57:14 -0500 Subject: [PATCH 098/187] Add Reauth config flow to honeywell (#86170) --- .../components/honeywell/__init__.py | 13 +- homeassistant/components/honeywell/climate.py | 1 - .../components/honeywell/config_flow.py | 58 +++++++ .../components/honeywell/test_config_flow.py | 145 +++++++++++++++++- 4 files changed, 204 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index 3316d2852e7..c0ebcd9b1cf 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -7,7 +7,7 @@ import AIOSomecomfort from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -57,15 +57,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await client.login() await client.discover() - except AIOSomecomfort.AuthError as ex: - raise ConfigEntryNotReady( - "Failed to initialize the Honeywell client: " - "Check your configuration (username, password), " - ) from ex + except AIOSomecomfort.device.AuthError as ex: + raise ConfigEntryAuthFailed("Incorrect Password") from ex except ( - AIOSomecomfort.ConnectionError, - AIOSomecomfort.ConnectionTimeout, + AIOSomecomfort.device.ConnectionError, + AIOSomecomfort.device.ConnectionTimeout, asyncio.TimeoutError, ) as ex: raise ConfigEntryNotReady( diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 0267eb32e47..96fc30f57e0 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -345,7 +345,6 @@ class HoneywellUSThermostat(ClimateEntity): await self._device.set_setpoint_heat(self._heat_away_temp) except AIOSomecomfort.SomeComfortError: - _LOGGER.error( "Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f", mode, diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 9f630d90fbe..0f9d5663b3e 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -2,6 +2,8 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping +from typing import Any import AIOSomecomfort import voluptuous as vol @@ -20,11 +22,67 @@ from .const import ( DOMAIN, ) +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a honeywell config flow.""" VERSION = 1 + entry: config_entries.ConfigEntry | None + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle re-authentication with Honeywell.""" + + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm re-authentication with Honeywell.""" + errors: dict[str, str] = {} + + if user_input: + assert self.entry is not None + password = user_input[CONF_PASSWORD] + data = { + CONF_USERNAME: self.entry.data[CONF_USERNAME], + CONF_PASSWORD: password, + } + + try: + await self.is_valid( + username=data[CONF_USERNAME], password=data[CONF_PASSWORD] + ) + + except AIOSomecomfort.AuthError: + errors["base"] = "invalid_auth" + + except ( + AIOSomecomfort.ConnectionError, + AIOSomecomfort.ConnectionTimeout, + asyncio.TimeoutError, + ): + errors["base"] = "cannot_connect" + + else: + + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_PASSWORD: password, + }, + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=REAUTH_SCHEMA, + errors=errors, + ) async def async_step_user(self, user_input=None) -> FlowResult: """Create config entry. Show the setup form to the user.""" diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index 46ab48572f8..2d76962b028 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -1,7 +1,9 @@ """Tests for honeywell config flow.""" +import asyncio from unittest.mock import MagicMock, patch import AIOSomecomfort +import pytest from homeassistant import data_entry_flow from homeassistant.components.honeywell.const import ( @@ -9,8 +11,10 @@ from homeassistant.components.honeywell.const import ( CONF_HEAT_AWAY_TEMPERATURE, DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigEntryState +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -35,8 +39,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on connection fail.""" - client.login.side_effect = AIOSomecomfort.ConnectionError - + client.login.side_effect = AIOSomecomfort.device.ConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG ) @@ -45,7 +48,7 @@ async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: async def test_auth_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on login fail.""" - client.login.side_effect = AIOSomecomfort.AuthError + client.login.side_effect = AIOSomecomfort.device.AuthError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG @@ -116,3 +119,137 @@ async def test_create_option_entry( CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2, } + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a successful reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + ) + + await hass.async_block_till_done() + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert mock_entry.data == { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "new-password", + } + + +async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> None: + """Test an authorization error reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + client.login.side_effect = AIOSomecomfort.device.AuthError + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +@pytest.mark.parametrize( + "error", + [ + AIOSomecomfort.device.ConnectionError, + AIOSomecomfort.device.ConnectionTimeout, + asyncio.TimeoutError, + ], +) +async def test_reauth_flow_connnection_error( + hass: HomeAssistant, client: MagicMock, error +) -> None: + """Test a connection error reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + client.login.side_effect = error + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} From 063bbe91d152c2dc901f96e4aa616a15c785fd8a Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 2 Feb 2023 16:45:49 -0500 Subject: [PATCH 099/187] Bump AIOSomecomfort to 0.0.6 (#87203) * Bump to 0.0.5 * Bump aiosomecomfort to 0.0.6 * lower case aiosomecomfort * Fix other bad imports.... --------- Co-authored-by: Paulus Schoutsen --- .../components/honeywell/__init__.py | 14 ++++++------ homeassistant/components/honeywell/climate.py | 22 +++++++++---------- .../components/honeywell/config_flow.py | 16 +++++++------- .../components/honeywell/manifest.json | 2 +- homeassistant/components/honeywell/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/honeywell/conftest.py | 14 ++++++------ .../components/honeywell/test_config_flow.py | 12 +++++----- tests/components/honeywell/test_init.py | 4 ++-- tests/components/honeywell/test_sensor.py | 4 ++-- 11 files changed, 47 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index c0ebcd9b1cf..93c29446a53 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -2,7 +2,7 @@ import asyncio from dataclasses import dataclass -import AIOSomecomfort +import aiosomecomfort from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -50,19 +50,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b username = config_entry.data[CONF_USERNAME] password = config_entry.data[CONF_PASSWORD] - client = AIOSomecomfort.AIOSomeComfort( + client = aiosomecomfort.AIOSomeComfort( username, password, session=async_get_clientsession(hass) ) try: await client.login() await client.discover() - except AIOSomecomfort.device.AuthError as ex: + except aiosomecomfort.device.AuthError as ex: raise ConfigEntryAuthFailed("Incorrect Password") from ex except ( - AIOSomecomfort.device.ConnectionError, - AIOSomecomfort.device.ConnectionTimeout, + aiosomecomfort.device.ConnectionError, + aiosomecomfort.device.ConnectionTimeout, asyncio.TimeoutError, ) as ex: raise ConfigEntryNotReady( @@ -114,5 +114,5 @@ class HoneywellData: """Shared data for Honeywell.""" entry_id: str - client: AIOSomecomfort.AIOSomeComfort - devices: dict[str, AIOSomecomfort.device.Device] + client: aiosomecomfort.AIOSomeComfort + devices: dict[str, aiosomecomfort.device.Device] diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 96fc30f57e0..1296dfbfdb3 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -4,7 +4,7 @@ from __future__ import annotations import datetime from typing import Any -import AIOSomecomfort +import aiosomecomfort from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, @@ -100,7 +100,7 @@ class HoneywellUSThermostat(ClimateEntity): def __init__( self, data: HoneywellData, - device: AIOSomecomfort.device.Device, + device: aiosomecomfort.device.Device, cool_away_temp: int | None, heat_away_temp: int | None, ) -> None: @@ -295,7 +295,7 @@ class HoneywellUSThermostat(ClimateEntity): if mode == "heat": await self._device.set_setpoint_heat(temperature) - except AIOSomecomfort.SomeComfortError as err: + except aiosomecomfort.SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) async def async_set_temperature(self, **kwargs: Any) -> None: @@ -308,7 +308,7 @@ class HoneywellUSThermostat(ClimateEntity): if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): await self._device.set_setpoint_heat(temperature) - except AIOSomecomfort.SomeComfortError as err: + except aiosomecomfort.SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) async def async_set_fan_mode(self, fan_mode: str) -> None: @@ -330,7 +330,7 @@ class HoneywellUSThermostat(ClimateEntity): try: # Get current mode mode = self._device.system_mode - except AIOSomecomfort.SomeComfortError: + except aiosomecomfort.SomeComfortError: _LOGGER.error("Can not get system mode") return try: @@ -344,7 +344,7 @@ class HoneywellUSThermostat(ClimateEntity): await self._device.set_hold_heat(True) await self._device.set_setpoint_heat(self._heat_away_temp) - except AIOSomecomfort.SomeComfortError: + except aiosomecomfort.SomeComfortError: _LOGGER.error( "Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f", mode, @@ -357,7 +357,7 @@ class HoneywellUSThermostat(ClimateEntity): try: # Get current mode mode = self._device.system_mode - except AIOSomecomfort.SomeComfortError: + except aiosomecomfort.SomeComfortError: _LOGGER.error("Can not get system mode") return # Check that we got a valid mode back @@ -369,7 +369,7 @@ class HoneywellUSThermostat(ClimateEntity): if mode in HEATING_MODES: await self._device.set_hold_heat(True) - except AIOSomecomfort.SomeComfortError: + except aiosomecomfort.SomeComfortError: _LOGGER.error("Couldn't set permanent hold") else: _LOGGER.error("Invalid system mode returned: %s", mode) @@ -381,7 +381,7 @@ class HoneywellUSThermostat(ClimateEntity): # Disabling all hold modes await self._device.set_hold_cool(False) await self._device.set_hold_heat(False) - except AIOSomecomfort.SomeComfortError: + except aiosomecomfort.SomeComfortError: _LOGGER.error("Can not stop hold mode") async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -410,13 +410,13 @@ class HoneywellUSThermostat(ClimateEntity): try: await self._device.refresh() except ( - AIOSomecomfort.SomeComfortError, + aiosomecomfort.SomeComfortError, OSError, ): try: await self._data.client.login() - except AIOSomecomfort.SomeComfortError: + except aiosomecomfort.SomeComfortError: self._attr_available = False await self.hass.async_create_task( self.hass.config_entries.async_reload(self._data.entry_id) diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 0f9d5663b3e..76d208f72f3 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Mapping from typing import Any -import AIOSomecomfort +import aiosomecomfort import voluptuous as vol from homeassistant import config_entries @@ -56,12 +56,12 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): username=data[CONF_USERNAME], password=data[CONF_PASSWORD] ) - except AIOSomecomfort.AuthError: + except aiosomecomfort.AuthError: errors["base"] = "invalid_auth" except ( - AIOSomecomfort.ConnectionError, - AIOSomecomfort.ConnectionTimeout, + aiosomecomfort.ConnectionError, + aiosomecomfort.ConnectionTimeout, asyncio.TimeoutError, ): errors["base"] = "cannot_connect" @@ -90,11 +90,11 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: await self.is_valid(**user_input) - except AIOSomecomfort.AuthError: + except aiosomecomfort.AuthError: errors["base"] = "invalid_auth" except ( - AIOSomecomfort.ConnectionError, - AIOSomecomfort.ConnectionTimeout, + aiosomecomfort.ConnectionError, + aiosomecomfort.ConnectionTimeout, asyncio.TimeoutError, ): errors["base"] = "cannot_connect" @@ -115,7 +115,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def is_valid(self, **kwargs) -> bool: """Check if login credentials are valid.""" - client = AIOSomecomfort.AIOSomeComfort( + client = aiosomecomfort.AIOSomeComfort( kwargs[CONF_USERNAME], kwargs[CONF_PASSWORD], session=async_get_clientsession(self.hass), diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 7eb07711b09..974123825fa 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -3,7 +3,7 @@ "name": "Honeywell Total Connect Comfort (US)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/honeywell", - "requirements": ["aiosomecomfort==0.0.3"], + "requirements": ["aiosomecomfort==0.0.6"], "codeowners": ["@rdfurman", "@mkmer"], "iot_class": "cloud_polling", "loggers": ["somecomfort"] diff --git a/homeassistant/components/honeywell/sensor.py b/homeassistant/components/honeywell/sensor.py index 59f00472700..9e85ba7727a 100644 --- a/homeassistant/components/honeywell/sensor.py +++ b/homeassistant/components/honeywell/sensor.py @@ -5,7 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any -from AIOSomecomfort.device import Device +from aiosomecomfort.device import Device from homeassistant.components.sensor import ( SensorDeviceClass, diff --git a/requirements_all.txt b/requirements_all.txt index f8abf9d975b..7f51ac35817 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aioskybell==22.7.0 aioslimproto==2.1.1 # homeassistant.components.honeywell -aiosomecomfort==0.0.3 +aiosomecomfort==0.0.6 # homeassistant.components.steamist aiosteamist==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18071e51a33..341b688b098 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,7 +254,7 @@ aioskybell==22.7.0 aioslimproto==2.1.1 # homeassistant.components.honeywell -aiosomecomfort==0.0.3 +aiosomecomfort==0.0.6 # homeassistant.components.steamist aiosteamist==0.3.2 diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py index bead64c71d1..95e1758ec22 100644 --- a/tests/components/honeywell/conftest.py +++ b/tests/components/honeywell/conftest.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, create_autospec, patch -import AIOSomecomfort +import aiosomecomfort import pytest from homeassistant.components.honeywell.const import DOMAIN @@ -30,7 +30,7 @@ def config_entry(config_data): @pytest.fixture def device(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) + mock_device = create_autospec(aiosomecomfort.device.Device, instance=True) mock_device.deviceid = 1234567 mock_device._data = { "canControlHumidification": False, @@ -48,7 +48,7 @@ def device(): @pytest.fixture def device_with_outdoor_sensor(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) + mock_device = create_autospec(aiosomecomfort.device.Device, instance=True) mock_device.deviceid = 1234567 mock_device._data = { "canControlHumidification": False, @@ -67,7 +67,7 @@ def device_with_outdoor_sensor(): @pytest.fixture def another_device(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) + mock_device = create_autospec(aiosomecomfort.device.Device, instance=True) mock_device.deviceid = 7654321 mock_device._data = { "canControlHumidification": False, @@ -85,7 +85,7 @@ def another_device(): @pytest.fixture def location(device): """Mock a somecomfort.Location.""" - mock_location = create_autospec(AIOSomecomfort.location.Location, instance=True) + mock_location = create_autospec(aiosomecomfort.location.Location, instance=True) mock_location.locationid.return_value = "location1" mock_location.devices_by_id = {device.deviceid: device} return mock_location @@ -94,13 +94,13 @@ def location(device): @pytest.fixture(autouse=True) def client(location): """Mock a somecomfort.SomeComfort client.""" - client_mock = create_autospec(AIOSomecomfort.AIOSomeComfort, instance=True) + client_mock = create_autospec(aiosomecomfort.AIOSomeComfort, instance=True) client_mock.locations_by_id = {location.locationid: location} client_mock.login = AsyncMock(return_value=True) client_mock.discover = AsyncMock() with patch( - "homeassistant.components.honeywell.AIOSomecomfort.AIOSomeComfort" + "homeassistant.components.honeywell.aiosomecomfort.AIOSomeComfort" ) as sc_class_mock: sc_class_mock.return_value = client_mock yield client_mock diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index 2d76962b028..ff970a0e8c5 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -2,7 +2,7 @@ import asyncio from unittest.mock import MagicMock, patch -import AIOSomecomfort +import aiosomecomfort import pytest from homeassistant import data_entry_flow @@ -39,7 +39,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on connection fail.""" - client.login.side_effect = AIOSomecomfort.device.ConnectionError + client.login.side_effect = aiosomecomfort.device.ConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG ) @@ -48,7 +48,7 @@ async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: async def test_auth_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on login fail.""" - client.login.side_effect = AIOSomecomfort.device.AuthError + client.login.side_effect = aiosomecomfort.device.AuthError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG @@ -194,7 +194,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - client.login.side_effect = AIOSomecomfort.device.AuthError + client.login.side_effect = aiosomecomfort.device.AuthError with patch( "homeassistant.components.honeywell.async_setup_entry", return_value=True, @@ -212,8 +212,8 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> @pytest.mark.parametrize( "error", [ - AIOSomecomfort.device.ConnectionError, - AIOSomecomfort.device.ConnectionTimeout, + aiosomecomfort.device.ConnectionError, + aiosomecomfort.device.ConnectionTimeout, asyncio.TimeoutError, ], ) diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 4ecd2a3172d..855d503401e 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import create_autospec, patch -import AIOSomecomfort +import aiosomecomfort from homeassistant.components.honeywell.const import ( CONF_COOL_AWAY_TEMPERATURE, @@ -46,7 +46,7 @@ async def test_setup_multiple_thermostats_with_same_deviceid( hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client ) -> None: """Test Honeywell TCC API returning duplicate device IDs.""" - mock_location2 = create_autospec(AIOSomecomfort.Location, instance=True) + mock_location2 = create_autospec(aiosomecomfort.Location, instance=True) mock_location2.locationid.return_value = "location2" mock_location2.devices_by_id = {device.deviceid: device} client.locations_by_id["location2"] = mock_location2 diff --git a/tests/components/honeywell/test_sensor.py b/tests/components/honeywell/test_sensor.py index 7ed047262bf..c5367764d3d 100644 --- a/tests/components/honeywell/test_sensor.py +++ b/tests/components/honeywell/test_sensor.py @@ -1,6 +1,6 @@ """Test honeywell sensor.""" -from AIOSomecomfort.device import Device -from AIOSomecomfort.location import Location +from aiosomecomfort.device import Device +from aiosomecomfort.location import Location import pytest from homeassistant.core import HomeAssistant From 75796e1f0f3ea6d65da141779121dcc1deaea08a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 2 Feb 2023 22:11:01 +0100 Subject: [PATCH 100/187] Update frontend to 20230202.0 (#87208) --- 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 b33668cde4c..ba4185fa838 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==20230201.0"], + "requirements": ["home-assistant-frontend==20230202.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9101375adb3..8dc60525be1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 hassil==0.2.6 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230201.0 +home-assistant-frontend==20230202.0 home-assistant-intents==2023.1.31 httpx==0.23.3 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index 7f51ac35817..c4c62b9c8f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -907,7 +907,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230201.0 +home-assistant-frontend==20230202.0 # homeassistant.components.conversation home-assistant-intents==2023.1.31 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 341b688b098..4db8dac349b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230201.0 +home-assistant-frontend==20230202.0 # homeassistant.components.conversation home-assistant-intents==2023.1.31 From 1d8f5b2e163ef4ec0935dc35bd86a77bcc90c352 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 2 Feb 2023 22:10:51 +0100 Subject: [PATCH 101/187] Bump py-synologydsm-api to 2.1.1 (#87211) bump py-synologydsm-api to 2.1.1 --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 6d0012156db..14f45fbf637 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==2.0.2"], + "requirements": ["py-synologydsm-api==2.1.1"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index c4c62b9c8f8..ac62e334ec8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1442,7 +1442,7 @@ py-schluter==0.1.7 py-sucks==0.9.8 # homeassistant.components.synology_dsm -py-synologydsm-api==2.0.2 +py-synologydsm-api==2.1.1 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4db8dac349b..52727f66ee4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1051,7 +1051,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==2.0.2 +py-synologydsm-api==2.1.1 # homeassistant.components.seventeentrack py17track==2021.12.2 From ed8a0ef0eae239dfe465305f5964bf8e94c7b600 Mon Sep 17 00:00:00 2001 From: Karlie Meads <68717336+karliemeads@users.noreply.github.com> Date: Thu, 2 Feb 2023 16:35:02 -0500 Subject: [PATCH 102/187] Fix disabled condition within an automation action (#87213) fixes undefined --- homeassistant/helpers/script.py | 2 +- tests/helpers/test_script.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 34a0d4de2d4..02fa9dc7806 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -757,7 +757,7 @@ class _ScriptRun: with trace_path(condition_path): for idx, cond in enumerate(conditions): with trace_path(str(idx)): - if not cond(hass, variables): + if cond(hass, variables) is False: return False except exceptions.ConditionError as ex: _LOGGER.warning("Error in '%s[%s]' evaluation: %s", name, idx, ex) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b44e7b7c458..86172c6c7fd 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -2915,6 +2915,45 @@ async def test_if( assert_action_trace(expected_trace) +async def test_if_disabled( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test if action with a disabled condition.""" + sequence = cv.SCRIPT_SCHEMA( + { + "if": { + "alias": "if condition", + "condition": "template", + "value_template": "{{ var == 1 }}", + "enabled": "false", + }, + "then": { + "alias": "if then", + "event": "test_event", + "event_data": {"if": "then"}, + }, + "else": { + "alias": "if else", + "event": "test_event", + "event_data": {"if": "else"}, + }, + } + ) + + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + expected_trace = { + "0": [{"result": {"choice": "then"}}], + "0/if": [{"result": {"result": True}}], + "0/if/condition/0": [{"result": {"result": None}}], + "0/then/0": [{"result": {"event": "test_event", "event_data": {"if": "then"}}}], + } + assert_action_trace(expected_trace) + + async def test_if_condition_validation( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 372afc5c28483b6336f300728149d8a046e09316 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Feb 2023 16:48:09 -0500 Subject: [PATCH 103/187] Bumped version to 2023.2.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 ee5d60a32be..070f9a37862 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 = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 5c014aabd4b..affc4b6446c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0" +version = "2023.2.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 5aaddf72e6591e0f39b655608eba0370cba88eea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Feb 2023 19:08:18 -0600 Subject: [PATCH 104/187] Add missing mopeka translations (#87421) fixes #87163 --- .../components/mopeka/translations/af.json | 7 ++++++ .../components/mopeka/translations/bg.json | 21 ++++++++++++++++++ .../components/mopeka/translations/ca.json | 22 +++++++++++++++++++ .../components/mopeka/translations/de.json | 22 +++++++++++++++++++ .../components/mopeka/translations/el.json | 7 ++++++ .../components/mopeka/translations/en.json | 22 +++++++++++++++++++ .../components/mopeka/translations/es.json | 22 +++++++++++++++++++ .../components/mopeka/translations/et.json | 22 +++++++++++++++++++ .../components/mopeka/translations/fr.json | 21 ++++++++++++++++++ .../components/mopeka/translations/hu.json | 22 +++++++++++++++++++ .../components/mopeka/translations/id.json | 22 +++++++++++++++++++ .../components/mopeka/translations/it.json | 22 +++++++++++++++++++ .../components/mopeka/translations/lv.json | 20 +++++++++++++++++ .../components/mopeka/translations/nl.json | 22 +++++++++++++++++++ .../components/mopeka/translations/no.json | 22 +++++++++++++++++++ .../components/mopeka/translations/pl.json | 22 +++++++++++++++++++ .../components/mopeka/translations/pt-BR.json | 22 +++++++++++++++++++ .../components/mopeka/translations/ru.json | 22 +++++++++++++++++++ .../components/mopeka/translations/sk.json | 22 +++++++++++++++++++ .../mopeka/translations/zh-Hant.json | 22 +++++++++++++++++++ 20 files changed, 406 insertions(+) create mode 100644 homeassistant/components/mopeka/translations/af.json create mode 100644 homeassistant/components/mopeka/translations/bg.json create mode 100644 homeassistant/components/mopeka/translations/ca.json create mode 100644 homeassistant/components/mopeka/translations/de.json create mode 100644 homeassistant/components/mopeka/translations/el.json create mode 100644 homeassistant/components/mopeka/translations/en.json create mode 100644 homeassistant/components/mopeka/translations/es.json create mode 100644 homeassistant/components/mopeka/translations/et.json create mode 100644 homeassistant/components/mopeka/translations/fr.json create mode 100644 homeassistant/components/mopeka/translations/hu.json create mode 100644 homeassistant/components/mopeka/translations/id.json create mode 100644 homeassistant/components/mopeka/translations/it.json create mode 100644 homeassistant/components/mopeka/translations/lv.json create mode 100644 homeassistant/components/mopeka/translations/nl.json create mode 100644 homeassistant/components/mopeka/translations/no.json create mode 100644 homeassistant/components/mopeka/translations/pl.json create mode 100644 homeassistant/components/mopeka/translations/pt-BR.json create mode 100644 homeassistant/components/mopeka/translations/ru.json create mode 100644 homeassistant/components/mopeka/translations/sk.json create mode 100644 homeassistant/components/mopeka/translations/zh-Hant.json diff --git a/homeassistant/components/mopeka/translations/af.json b/homeassistant/components/mopeka/translations/af.json new file mode 100644 index 00000000000..30d19019659 --- /dev/null +++ b/homeassistant/components/mopeka/translations/af.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e konfigurovan\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/bg.json b/homeassistant/components/mopeka/translations/bg.json new file mode 100644 index 00000000000..e899c3bcddb --- /dev/null +++ b/homeassistant/components/mopeka/translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043a\u043e\u0435\u0442\u043e \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/ca.json b/homeassistant/components/mopeka/translations/ca.json new file mode 100644 index 00000000000..c121ff7408c --- /dev/null +++ b/homeassistant/components/mopeka/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "not_supported": "Dispositiu no compatible" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/de.json b/homeassistant/components/mopeka/translations/de.json new file mode 100644 index 00000000000..4c5720ec6fb --- /dev/null +++ b/homeassistant/components/mopeka/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "not_supported": "Ger\u00e4t nicht unterst\u00fctzt" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/el.json b/homeassistant/components/mopeka/translations/el.json new file mode 100644 index 00000000000..d3346614257 --- /dev/null +++ b/homeassistant/components/mopeka/translations/el.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/en.json b/homeassistant/components/mopeka/translations/en.json new file mode 100644 index 00000000000..afe859ca766 --- /dev/null +++ b/homeassistant/components/mopeka/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network", + "not_supported": "Device not supported" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to set up {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to set up" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/es.json b/homeassistant/components/mopeka/translations/es.json new file mode 100644 index 00000000000..ae0ab01acdf --- /dev/null +++ b/homeassistant/components/mopeka/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "not_supported": "Dispositivo no compatible" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/et.json b/homeassistant/components/mopeka/translations/et.json new file mode 100644 index 00000000000..8f424097aa5 --- /dev/null +++ b/homeassistant/components/mopeka/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "not_supported": "Seadet ei toetata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name} ?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/fr.json b/homeassistant/components/mopeka/translations/fr.json new file mode 100644 index 00000000000..2cb8c3c6ad0 --- /dev/null +++ b/homeassistant/components/mopeka/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "not_supported": "Appareil non pris en charge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/hu.json b/homeassistant/components/mopeka/translations/hu.json new file mode 100644 index 00000000000..4668ffea416 --- /dev/null +++ b/homeassistant/components/mopeka/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/id.json b/homeassistant/components/mopeka/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/mopeka/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_supported": "Perangkat tidak didukung" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/it.json b/homeassistant/components/mopeka/translations/it.json new file mode 100644 index 00000000000..97113c57103 --- /dev/null +++ b/homeassistant/components/mopeka/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "not_supported": "Dispositivo non supportato" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Scegli un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/lv.json b/homeassistant/components/mopeka/translations/lv.json new file mode 100644 index 00000000000..cb2a17efe72 --- /dev/null +++ b/homeassistant/components/mopeka/translations/lv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "no_devices_found": "T\u012bkl\u0101 nav atrasta neviena ier\u012bce", + "not_supported": "Ier\u012bce netiek atbalst\u012bta" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vai v\u0113laties iestat\u012bt {name}?" + }, + "user": { + "data": { + "address": "Ier\u012bce" + }, + "description": "Izv\u0113lieties ier\u012bci, kuru iestat\u012bt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/nl.json b/homeassistant/components/mopeka/translations/nl.json new file mode 100644 index 00000000000..93f866be7d9 --- /dev/null +++ b/homeassistant/components/mopeka/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "Configuratie wordt al uitgevoerd", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_supported": "Apparaat wordt niet ondersteund" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wil je {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/no.json b/homeassistant/components/mopeka/translations/no.json new file mode 100644 index 00000000000..38ab3d096f2 --- /dev/null +++ b/homeassistant/components/mopeka/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "not_supported": "Enheten st\u00f8ttes ikke" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du sette opp {name} ?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/pl.json b/homeassistant/components/mopeka/translations/pl.json new file mode 100644 index 00000000000..4715905a2e9 --- /dev/null +++ b/homeassistant/components/mopeka/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/pt-BR.json b/homeassistant/components/mopeka/translations/pt-BR.json new file mode 100644 index 00000000000..5b654163201 --- /dev/null +++ b/homeassistant/components/mopeka/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/ru.json b/homeassistant/components/mopeka/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/mopeka/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/sk.json b/homeassistant/components/mopeka/translations/sk.json new file mode 100644 index 00000000000..8273d877c92 --- /dev/null +++ b/homeassistant/components/mopeka/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/translations/zh-Hant.json b/homeassistant/components/mopeka/translations/zh-Hant.json new file mode 100644 index 00000000000..64ae1f19094 --- /dev/null +++ b/homeassistant/components/mopeka/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file From e12425c229d2a9dd3f7fc2b37eaafeb9bb3ca55f Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 3 Feb 2023 12:10:47 +1000 Subject: [PATCH 105/187] Fix call values in Aussie Broadband (#87229) Fixed calls values Added lamda for International, Voicemail, and Other calls. --- homeassistant/components/aussie_broadband/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/aussie_broadband/sensor.py b/homeassistant/components/aussie_broadband/sensor.py index 896930649eb..1ed146b6237 100644 --- a/homeassistant/components/aussie_broadband/sensor.py +++ b/homeassistant/components/aussie_broadband/sensor.py @@ -78,6 +78,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone-plus", + value=lambda x: x.get("calls"), ), SensorValueEntityDescription( key="sms", @@ -101,6 +102,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", + value=lambda x: x.get("calls"), ), SensorValueEntityDescription( key="other", @@ -108,6 +110,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", + value=lambda x: x.get("calls"), ), # Generic sensors SensorValueEntityDescription( From 012ba551540536fc4ff54ac57eeddd2ca747dcda Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 2 Feb 2023 22:42:36 -0600 Subject: [PATCH 106/187] Handle failed Sonos subscriptions better (#87240) Catch unsubscribe failure separately from ZGS poll --- homeassistant/components/sonos/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index f8047ba371f..1133b5b4d9d 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -11,6 +11,7 @@ import socket from typing import TYPE_CHECKING, Any, cast from urllib.parse import urlparse +from aiohttp import ClientError from requests.exceptions import Timeout from soco import events_asyncio, zonegroupstate import soco.config as soco_config @@ -229,6 +230,10 @@ class SonosDiscoveryManager: ) try: await sub.unsubscribe() + except (ClientError, OSError, Timeout) as ex: + _LOGGER.debug("Unsubscription from %s failed: %s", ip_address, ex) + + try: await self.hass.async_add_executor_job(soco.zone_group_state.poll, soco) except (OSError, SoCoException, Timeout) as ex: _LOGGER.warning( From 6543a1169ba17689bf93642bf70a8ab120d0a3ee Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 3 Feb 2023 09:15:54 +0100 Subject: [PATCH 107/187] Filesize timestamp remove state class (#87247) fixes undefined --- homeassistant/components/filesize/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index fb5b498e106..2152243b8dd 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -56,7 +56,6 @@ SENSOR_TYPES = ( icon=ICON, name="Last Updated", device_class=SensorDeviceClass.TIMESTAMP, - state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), ) From e78dd34a451b9d6a8ddde22db7574bf4d8b863e7 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 3 Feb 2023 15:00:22 +0100 Subject: [PATCH 108/187] Bump reolink-aio to 0.3.4 (#87272) --- 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 ab944314054..72ac70ef180 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.3.2"], + "requirements": ["reolink-aio==0.3.4"], "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index ac62e334ec8..46eb4ac1b0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2227,7 +2227,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.2 +reolink-aio==0.3.4 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 52727f66ee4..8f6d1f15a5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1572,7 +1572,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.2 +reolink-aio==0.3.4 # homeassistant.components.python_script restrictedpython==6.0 From 9769a0f0ec906254322e51e3beb1a54623f95dcf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Feb 2023 15:01:25 +0100 Subject: [PATCH 109/187] Fix volume state class in renault (#87280) --- homeassistant/components/renault/sensor.py | 2 +- tests/components/renault/const.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 52538d1e873..d75dc55aa21 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -313,7 +313,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( icon="mdi:fuel", name="Fuel quantity", native_unit_of_measurement=UnitOfVolume.LITERS, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, requires_fuel=True, value_lambda=_get_rounded_value, ), diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index b15b00b825f..ee4f1683aad 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -671,7 +671,7 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_fuel_quantity", ATTR_ICON: "mdi:fuel", ATTR_STATE: "3", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_quantity", ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS, }, @@ -796,7 +796,7 @@ MOCK_VEHICLES = { ATTR_ENTITY_ID: "sensor.reg_number_fuel_quantity", ATTR_ICON: "mdi:fuel", ATTR_STATE: "3", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_quantity", ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS, }, From 4c0ea397c8422c738d195ad5c3015785e66fc774 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Feb 2023 15:01:51 +0100 Subject: [PATCH 110/187] Bump sfrbox-api to 0.0.6 (#87281) --- homeassistant/components/sfr_box/manifest.json | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sfr_box/manifest.json b/homeassistant/components/sfr_box/manifest.json index 78901006d9a..eb3c9cb1b68 100644 --- a/homeassistant/components/sfr_box/manifest.json +++ b/homeassistant/components/sfr_box/manifest.json @@ -1,10 +1,10 @@ { "domain": "sfr_box", "name": "SFR Box", + "codeowners": ["@epenet"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sfr_box", - "requirements": ["sfrbox-api==0.0.5"], - "codeowners": ["@epenet"], + "integration_type": "device", "iot_class": "local_polling", - "integration_type": "device" + "requirements": ["sfrbox-api==0.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 46eb4ac1b0b..891679dcf30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2321,7 +2321,7 @@ sensorpush-ble==1.5.2 sentry-sdk==1.13.0 # homeassistant.components.sfr_box -sfrbox-api==0.0.5 +sfrbox-api==0.0.6 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f6d1f15a5c..e84d59293fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1636,7 +1636,7 @@ sensorpush-ble==1.5.2 sentry-sdk==1.13.0 # homeassistant.components.sfr_box -sfrbox-api==0.0.5 +sfrbox-api==0.0.6 # homeassistant.components.sharkiq sharkiq==0.0.1 From 54687d6b56089c76371dcbf3240ff0aebc0bec99 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Feb 2023 11:49:24 +0100 Subject: [PATCH 111/187] Extend state class sensor warnings with expected values (#87294) --- homeassistant/components/sensor/__init__.py | 3 +++ homeassistant/components/sensor/const.py | 2 +- tests/components/sensor/test_init.py | 8 +++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 35ffc1c3d2a..5108a167552 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -503,12 +503,15 @@ class SensorEntity(Entity): _LOGGER.warning( "Entity %s (%s) is using state class '%s' which " "is impossible considering device class ('%s') it is using; " + "expected %s%s; " "Please update your configuration if your entity is manually " "configured, otherwise %s", self.entity_id, type(self), state_class, device_class, + "None or one of " if classes else "None", + ", ".join(f"'{value.value}'" for value in classes), report_issue, ) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index c8402a28ffe..2167e1a1ba5 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -501,7 +501,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.WIND_SPEED: set(UnitOfSpeed), } -DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass | None]] = { +DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = { SensorDeviceClass.APPARENT_POWER: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.ATMOSPHERIC_PRESSURE: {SensorStateClass.MEASUREMENT}, diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 89501cf37df..b43c63f015c 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -10,6 +10,7 @@ from pytest import approx from homeassistant.components.number import NumberDeviceClass from homeassistant.components.sensor import ( + DEVICE_CLASS_STATE_CLASSES, DEVICE_CLASS_UNITS, SensorDeviceClass, SensorStateClass, @@ -1637,7 +1638,12 @@ async def test_device_classes_with_invalid_state_class( assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() + classes = DEVICE_CLASS_STATE_CLASSES.get(device_class, set()) + one_of = ", ".join(f"'{value.value}'" for value in classes) + expected = f"None or one of {one_of}" if classes else "None" + assert ( "is using state class 'INVALID!' which is impossible considering device " - f"class ('{device_class}') it is using" + f"class ('{device_class}') it is using; " + f"expected {expected}" ) in caplog.text From a5378ec9a81b5ced636db22d6fdc677d83169fa5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Feb 2023 15:59:34 +0100 Subject: [PATCH 112/187] Fix incorrect description in sensor group config flow (#87298) --- homeassistant/components/group/strings.json | 3 +-- homeassistant/components/group/translations/en.json | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index dcc97803adc..75a2423d932 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -68,7 +68,6 @@ }, "sensor": { "title": "[%key:component::group::config::step::user::title%]", - "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.", "data": { "ignore_non_numeric": "Ignore non-numeric", "entities": "Members", @@ -134,7 +133,7 @@ } }, "sensor": { - "description": "[%key:component::group::config::step::sensor::description%]", + "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.", "data": { "ignore_non_numeric": "[%key:component::group::config::step::sensor::data::ignore_non_numeric%]", "entities": "[%key:component::group::config::step::sensor::data::entities%]", diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index 97e7e23238b..2a1d93566bb 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -63,7 +63,6 @@ "type": "Type", "unit_of_measurement": "Unit of Measurement" }, - "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.", "title": "Add Group" }, "switch": { From 681de9a5168616bcad7228310eac99595368ac13 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Feb 2023 16:38:05 +0100 Subject: [PATCH 113/187] Don't override icon in sensor group when device class is set (#87304) --- homeassistant/components/group/sensor.py | 11 ++++++++++- tests/components/group/test_sensor.py | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 52ce90b2535..088678b6c1a 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -244,7 +244,6 @@ class SensorGroup(GroupEntity, SensorEntity): _attr_available = False _attr_should_poll = False - _attr_icon = "mdi:calculator" def __init__( self, @@ -352,6 +351,16 @@ class SensorGroup(GroupEntity, SensorEntity): return self._attr_device_class return self.calc_device_class + @property + def icon(self) -> str | None: + """Return the icon. + + Only override the icon if the device class is not set. + """ + if not self.device_class: + return "mdi:calculator" + return None + @property def state_class(self) -> SensorStateClass | str | None: """Return state class.""" diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py index 265ee90534a..87dbbccab08 100644 --- a/tests/components/group/test_sensor.py +++ b/tests/components/group/test_sensor.py @@ -25,6 +25,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, STATE_UNAVAILABLE, @@ -100,6 +101,7 @@ async def test_sensors( for key, value in attributes.items(): assert state.attributes.get(key) == value assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLUME + assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "L" From 1df7fcea09ac3fa97f5dab7213114b6ab420cf8b Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Sat, 4 Feb 2023 01:22:33 +0300 Subject: [PATCH 114/187] Fix Bravia TV refreshing zero volume level (#87318) fixes undefined --- homeassistant/components/braviatv/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 6923bacc1ac..9b89c667b3c 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -171,7 +171,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): async def async_update_volume(self) -> None: """Update volume information.""" volume_info = await self.client.get_volume_info() - if volume_level := volume_info.get("volume"): + if (volume_level := volume_info.get("volume")) is not None: self.volume_level = volume_level / 100 self.volume_muted = volume_info.get("mute", False) self.volume_target = volume_info.get("target") From 6d2a2b1c9122bf3e3e1310a7076c01ddd93ebdaf Mon Sep 17 00:00:00 2001 From: Koen van Zuijlen <8818390+kvanzuijlen@users.noreply.github.com> Date: Fri, 3 Feb 2023 18:46:11 +0100 Subject: [PATCH 115/187] Fixed parser for zeversolar hardware version M10 (#87319) fixes undefined --- homeassistant/components/zeversolar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zeversolar/manifest.json b/homeassistant/components/zeversolar/manifest.json index 0d67022920d..62682709b04 100644 --- a/homeassistant/components/zeversolar/manifest.json +++ b/homeassistant/components/zeversolar/manifest.json @@ -3,7 +3,7 @@ "name": "Zeversolar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zeversolar", - "requirements": ["zeversolar==0.2.0"], + "requirements": ["zeversolar==0.3.0"], "codeowners": ["@kvanzuijlen"], "iot_class": "local_polling", "integration_type": "device" diff --git a/requirements_all.txt b/requirements_all.txt index 891679dcf30..f0b7436b990 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2690,7 +2690,7 @@ zengge==0.2 zeroconf==0.47.1 # homeassistant.components.zeversolar -zeversolar==0.2.0 +zeversolar==0.3.0 # homeassistant.components.zha zha-quirks==0.0.92 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e84d59293fe..319a8c12c1f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1903,7 +1903,7 @@ zamg==0.2.2 zeroconf==0.47.1 # homeassistant.components.zeversolar -zeversolar==0.2.0 +zeversolar==0.3.0 # homeassistant.components.zha zha-quirks==0.0.92 From fe7c7001ad713245dee9db0655d75e7f9bee434b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 3 Feb 2023 20:04:51 +0100 Subject: [PATCH 116/187] Fix code format issue in Yale Smart Alarm (#87323) fixes undefined --- homeassistant/components/yale_smart_alarm/lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index 807ecdecada..fde08d08fbd 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -45,7 +45,7 @@ class YaleDoorlock(YaleEntity, LockEntity): ) -> None: """Initialize the Yale Lock Device.""" super().__init__(coordinator, data) - self._attr_code_format = f"^\\d{code_format}$" + self._attr_code_format = rf"^\d{{{code_format}}}$" self.lock_name: str = data["name"] async def async_unlock(self, **kwargs: Any) -> None: From c791a7c7fb3dde9310e97767801f56cd10a1e23b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 3 Feb 2023 18:39:30 +0100 Subject: [PATCH 117/187] Bump py-synologydsm-api to 2.1.2 (#87324) fixes undefined --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 14f45fbf637..a4b340bd3fa 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==2.1.1"], + "requirements": ["py-synologydsm-api==2.1.2"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index f0b7436b990..18835b95640 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1442,7 +1442,7 @@ py-schluter==0.1.7 py-sucks==0.9.8 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.1 +py-synologydsm-api==2.1.2 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 319a8c12c1f..815b047cb22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1051,7 +1051,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.1 +py-synologydsm-api==2.1.2 # homeassistant.components.seventeentrack py17track==2021.12.2 From 2d8589078972bc07ba64f2292ad873f8faa0e3d5 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 4 Feb 2023 05:54:38 -0500 Subject: [PATCH 118/187] Bump oralb-ble to 0.17.2 (#87355) --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 602c674b329..08b257c1869 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.17.1"], + "requirements": ["oralb-ble==0.17.2"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco", "@Lash-L"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 18835b95640..9e08434319d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1299,7 +1299,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.17.1 +oralb-ble==0.17.2 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 815b047cb22..a485802015b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -947,7 +947,7 @@ openai==0.26.2 openerz-api==0.2.0 # homeassistant.components.oralb -oralb-ble==0.17.1 +oralb-ble==0.17.2 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 30f63ae0ead6037c5f86a71058e2cbc4a7e32449 Mon Sep 17 00:00:00 2001 From: Matthew Donoughe Date: Sat, 4 Feb 2023 04:17:16 -0500 Subject: [PATCH 119/187] Update pylutron-caseta to 0.18.1 (#87361) update pylutron-caseta to 0.18.1 --- homeassistant/components/lutron_caseta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 388ef7c1ee0..59485628505 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.18.0"], + "requirements": ["pylutron-caseta==0.18.1"], "config_flow": true, "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 9e08434319d..114e11b6454 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1753,7 +1753,7 @@ pylitejet==0.5.0 pylitterbot==2023.1.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.18.0 +pylutron-caseta==0.18.1 # homeassistant.components.lutron pylutron==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a485802015b..e440c70f5ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1260,7 +1260,7 @@ pylitejet==0.5.0 pylitterbot==2023.1.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.18.0 +pylutron-caseta==0.18.1 # homeassistant.components.mailgun pymailgunner==1.4 From 2d5ab5a0dc884d8d9e17465808b452419697829a Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 4 Feb 2023 11:38:22 +0000 Subject: [PATCH 120/187] Fix exception when trying to poll a HomeKit device over Thread with no active encryption context (#87379) Bump aiohomekit==2.4.5 --- 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 9edcba5bf72..d5dc221ad46 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.4"], + "requirements": ["aiohomekit==2.4.5"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth_adapters", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 114e11b6454..c7dab503ae2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -174,7 +174,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.4 +aiohomekit==2.4.5 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e440c70f5ab..6de0ac31420 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,7 +158,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.4 +aiohomekit==2.4.5 # homeassistant.components.emulated_hue # homeassistant.components.http From 23e04ba8915bb95ff2f849e27abb2043a2143b88 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sat, 4 Feb 2023 20:12:34 +0200 Subject: [PATCH 121/187] Fix Ruuvi Gateway data being ignored when system is not using UTC time (#87384) --- homeassistant/components/ruuvi_gateway/bluetooth.py | 11 ++++++----- homeassistant/components/ruuvi_gateway/const.py | 7 ------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ruuvi_gateway/bluetooth.py b/homeassistant/components/ruuvi_gateway/bluetooth.py index f5748b8b4e9..4dd973155a9 100644 --- a/homeassistant/components/ruuvi_gateway/bluetooth.py +++ b/homeassistant/components/ruuvi_gateway/bluetooth.py @@ -2,12 +2,13 @@ from __future__ import annotations from collections.abc import Callable -import datetime import logging +import time from home_assistant_bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, BaseHaRemoteScanner, async_get_advertisement_callback, async_register_scanner, @@ -15,7 +16,6 @@ from homeassistant.components.bluetooth import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from .const import OLD_ADVERTISEMENT_CUTOFF from .coordinator import RuuviGatewayUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -46,10 +46,11 @@ class RuuviGatewayScanner(BaseHaRemoteScanner): @callback def _async_handle_new_data(self) -> None: - now = datetime.datetime.now() + now = time.time() for tag_data in self.coordinator.data: - if now - tag_data.datetime > OLD_ADVERTISEMENT_CUTOFF: - # Don't process data that is older than 10 minutes + data_age_seconds = now - tag_data.timestamp # Both are Unix time + if data_age_seconds > FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS: + # Don't process stale data at all continue anno = tag_data.parse_announcement() self._async_on_advertisement( diff --git a/homeassistant/components/ruuvi_gateway/const.py b/homeassistant/components/ruuvi_gateway/const.py index 609bad9a226..80bebda105b 100644 --- a/homeassistant/components/ruuvi_gateway/const.py +++ b/homeassistant/components/ruuvi_gateway/const.py @@ -1,12 +1,5 @@ """Constants for the Ruuvi Gateway integration.""" from datetime import timedelta -from homeassistant.components.bluetooth import ( - FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, -) - DOMAIN = "ruuvi_gateway" SCAN_INTERVAL = timedelta(seconds=5) -OLD_ADVERTISEMENT_CUTOFF = timedelta( - seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS -) From 93229196d8ea4bc8f9a0654ed6fe07a5455b143f Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Sun, 5 Feb 2023 02:06:42 +0100 Subject: [PATCH 122/187] Fix state class in Enphase Envoy (#87397) * Change total_increasing to total * As suggested in PR: only Last Seven Days TOTAL --- homeassistant/components/enphase_envoy/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index 16aae3f9a64..cd3235f1be5 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -33,7 +33,7 @@ SENSORS = ( key="seven_days_production", name="Last Seven Days Energy Production", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( @@ -61,7 +61,7 @@ SENSORS = ( key="seven_days_consumption", name="Last Seven Days Energy Consumption", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( From 323ab97ff5e437ca27213a8bc99b040fd9253ce7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Feb 2023 16:29:03 -0600 Subject: [PATCH 123/187] Ignore invalid zeroconf names from devices with broken firmwares (#87414) --- homeassistant/components/zeroconf/__init__.py | 15 +++++++- tests/components/zeroconf/test_init.py | 38 ++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 6b54aa18961..088e46aa1cb 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -15,7 +15,12 @@ import sys from typing import Any, Final, cast import voluptuous as vol -from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange +from zeroconf import ( + BadTypeInNameException, + InterfaceChoice, + IPVersion, + ServiceStateChange, +) from zeroconf.asyncio import AsyncServiceInfo from homeassistant import config_entries @@ -399,7 +404,13 @@ class ZeroconfDiscovery: self, zeroconf: HaZeroconf, service_type: str, name: str ) -> None: """Process a zeroconf update.""" - async_service_info = AsyncServiceInfo(service_type, name) + try: + async_service_info = AsyncServiceInfo(service_type, name) + except BadTypeInNameException as ex: + # Some devices broadcast a name that is not a valid DNS name + # This is a bug in the device firmware and we should ignore it + _LOGGER.debug("Bad name in zeroconf record: %s: %s", name, ex) + return await async_service_info.async_request(zeroconf, 3000) info = info_from_service(async_service_info) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 5e499c93fff..fd383fd653c 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -3,7 +3,12 @@ from ipaddress import ip_address from typing import Any from unittest.mock import call, patch -from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange +from zeroconf import ( + BadTypeInNameException, + InterfaceChoice, + IPVersion, + ServiceStateChange, +) from zeroconf.asyncio import AsyncServiceInfo from homeassistant.components import zeroconf @@ -556,6 +561,37 @@ async def test_homekit_match_partial_space(hass, mock_async_zeroconf): } +async def test_device_with_invalid_name(hass, mock_async_zeroconf, caplog): + """Test we ignore devices with an invalid name.""" + with patch.dict( + zc_gen.ZEROCONF, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), patch.dict( + zc_gen.HOMEKIT, + {"LIFX": "lifx"}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, + "HaAsyncServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=BadTypeInNameException, + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 0 + assert "Bad name in zeroconf record" in caplog.text + + async def test_homekit_match_partial_dash(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( From 1f08fc72bb8036376164f61f3a81f9920606f0c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Feb 2023 14:34:34 -0600 Subject: [PATCH 124/187] Disable mopeka accelerometer sensors by default (#87420) * Disable mopeka accelerometer sensors by default These generate a significant amount of noise and are only useful when placing the sensor. Disable them by default. This not a breaking change because existing preferences are preserved. * adjust tests --- homeassistant/components/mopeka/sensor.py | 2 ++ tests/components/mopeka/test_sensor.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mopeka/sensor.py b/homeassistant/components/mopeka/sensor.py index 26965cd9a14..f4dba535b75 100644 --- a/homeassistant/components/mopeka/sensor.py +++ b/homeassistant/components/mopeka/sensor.py @@ -76,10 +76,12 @@ SENSOR_DESCRIPTIONS = { "accelerometer_x": SensorEntityDescription( key="accelerometer_x", entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, ), "accelerometer_y": SensorEntityDescription( key="accelerometer_y", entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, ), } diff --git a/tests/components/mopeka/test_sensor.py b/tests/components/mopeka/test_sensor.py index 27704ec38ed..04761ffc8d2 100644 --- a/tests/components/mopeka/test_sensor.py +++ b/tests/components/mopeka/test_sensor.py @@ -31,7 +31,7 @@ async def test_sensors_bad_signal(hass): assert len(hass.states.async_all("sensor")) == 0 inject_bluetooth_service_info(hass, PRO_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all("sensor")) == 6 + assert len(hass.states.async_all("sensor")) == 4 temp_sensor = hass.states.get("sensor.pro_plus_eeff_temperature") temp_sensor_attrs = temp_sensor.attributes @@ -65,7 +65,7 @@ async def test_sensors_good_signal(hass): assert len(hass.states.async_all("sensor")) == 0 inject_bluetooth_service_info(hass, PRO_GOOD_SIGNAL_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all("sensor")) == 6 + assert len(hass.states.async_all("sensor")) == 4 temp_sensor = hass.states.get("sensor.pro_plus_eeff_temperature") temp_sensor_attrs = temp_sensor.attributes From f4f1b0dfc508d8458feab23a87aec411b005f770 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Feb 2023 19:05:40 -0600 Subject: [PATCH 125/187] Bump aiohomekit to 2.4.6 (#87427) fixes #86083 changelog: https://github.com/Jc2k/aiohomekit/compare/2.4.5...2.4.6 --- 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 d5dc221ad46..f0a8cb74efe 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.5"], + "requirements": ["aiohomekit==2.4.6"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth_adapters", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index c7dab503ae2..73861751456 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -174,7 +174,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.5 +aiohomekit==2.4.6 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6de0ac31420..c4dc297934b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,7 +158,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.4.5 +aiohomekit==2.4.6 # homeassistant.components.emulated_hue # homeassistant.components.http From 3253eeca7aac398543cf07026e58e9adf7b83350 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Feb 2023 21:08:23 -0500 Subject: [PATCH 126/187] Bumped version to 2023.2.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 070f9a37862..168bf80aa11 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 = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index affc4b6446c..825c2bc356f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.1" +version = "2023.2.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d82e409d8e18b346567721988436b7132e8543ac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Feb 2023 18:35:24 +0100 Subject: [PATCH 127/187] Update black to 23.1.0 (#87188) --- .pre-commit-config.yaml | 2 +- homeassistant/bootstrap.py | 1 - homeassistant/components/adax/__init__.py | 1 - .../components/aladdin_connect/config_flow.py | 1 - homeassistant/components/alert/__init__.py | 1 - homeassistant/components/alexa/auth.py | 1 - homeassistant/components/alexa/intent.py | 1 - homeassistant/components/asuswrt/router.py | 1 - .../components/axis/binary_sensor.py | 2 -- .../components/bayesian/binary_sensor.py | 1 - .../components/bluesound/media_player.py | 1 - homeassistant/components/bluetooth/models.py | 1 - homeassistant/components/bosch_shc/switch.py | 5 ---- homeassistant/components/bthome/sensor.py | 10 ++++++-- homeassistant/components/buienradar/sensor.py | 1 - .../components/control4/config_flow.py | 1 - homeassistant/components/darksky/weather.py | 1 + .../components/deconz/binary_sensor.py | 1 - .../components/deconz/config_flow.py | 1 - homeassistant/components/deconz/sensor.py | 1 - homeassistant/components/deconz/services.py | 2 -- homeassistant/components/delijn/sensor.py | 2 +- homeassistant/components/derivative/sensor.py | 2 +- .../device_sun_light_trigger/__init__.py | 1 - homeassistant/components/dnsip/config_flow.py | 1 - .../components/doods/image_processing.py | 1 - homeassistant/components/ebusd/__init__.py | 1 - homeassistant/components/ecovacs/vacuum.py | 1 - homeassistant/components/elv/switch.py | 2 +- homeassistant/components/emoncms/sensor.py | 1 - .../components/emulated_hue/hue_api.py | 6 ++--- homeassistant/components/escea/config_flow.py | 1 - homeassistant/components/ezviz/camera.py | 3 --- homeassistant/components/ezviz/config_flow.py | 1 - .../components/faa_delays/config_flow.py | 1 - homeassistant/components/filter/sensor.py | 3 --- homeassistant/components/fints/sensor.py | 1 - homeassistant/components/firmata/__init__.py | 2 +- .../components/fleetgo/device_tracker.py | 1 - homeassistant/components/flipr/__init__.py | 2 +- homeassistant/components/flo/device.py | 2 +- homeassistant/components/flume/sensor.py | 1 - homeassistant/components/fritz/switch.py | 2 -- homeassistant/components/glances/sensor.py | 1 - .../components/google_assistant/helpers.py | 1 - .../components/harmony/config_flow.py | 1 - .../components/homematicip_cloud/climate.py | 7 +++++- homeassistant/components/honeywell/climate.py | 1 - .../components/honeywell/config_flow.py | 1 - homeassistant/components/hue/migration.py | 2 -- .../hvv_departures/binary_sensor.py | 1 - .../components/hvv_departures/config_flow.py | 3 --- homeassistant/components/hyperion/__init__.py | 2 +- homeassistant/components/ifttt/__init__.py | 1 - .../components/intellifire/config_flow.py | 1 - .../components/intellifire/coordinator.py | 1 - .../components/isy994/binary_sensor.py | 2 +- homeassistant/components/isy994/helpers.py | 1 - homeassistant/components/izone/climate.py | 5 ++-- homeassistant/components/izone/config_flow.py | 1 - .../components/juicenet/config_flow.py | 1 - .../components/kitchen_sink/repairs.py | 2 +- .../landisgyr_heat_meter/__init__.py | 1 - homeassistant/components/lifx/manager.py | 4 ---- homeassistant/components/london_air/sensor.py | 1 - homeassistant/components/lovelace/__init__.py | 1 - homeassistant/components/lupusec/switch.py | 1 - .../components/lutron/binary_sensor.py | 2 +- homeassistant/components/lutron/cover.py | 2 +- homeassistant/components/lutron/light.py | 2 +- homeassistant/components/lutron/switch.py | 2 +- .../components/lutron_caseta/__init__.py | 1 - .../components/lutron_caseta/button.py | 1 - .../components/mediaroom/media_player.py | 1 - .../components/melnor/config_flow.py | 2 -- homeassistant/components/met/weather.py | 2 +- .../components/meteo_france/__init__.py | 1 - .../components/mikrotik/device_tracker.py | 2 -- .../components/minecraft_server/helpers.py | 2 +- homeassistant/components/modbus/__init__.py | 5 +++- .../components/mold_indicator/sensor.py | 1 - .../components/mqtt/binary_sensor.py | 1 - homeassistant/components/mqtt/client.py | 1 - homeassistant/components/mqtt/discovery.py | 2 +- homeassistant/components/mqtt/subscription.py | 6 ++++- homeassistant/components/nam/config_flow.py | 1 - homeassistant/components/nest/config_flow.py | 1 - .../components/nfandroidtv/config_flow.py | 1 - .../components/nibe_heatpump/__init__.py | 1 - homeassistant/components/nina/__init__.py | 1 - homeassistant/components/nina/config_flow.py | 2 -- .../components/numato/binary_sensor.py | 1 - homeassistant/components/onvif/config_flow.py | 1 - .../components/open_meteo/weather.py | 1 - .../components/opencv/image_processing.py | 2 +- .../components/openhome/media_player.py | 12 ++++++---- .../weather_update_coordinator.py | 1 - .../components/philips_js/config_flow.py | 1 - .../components/philips_js/media_player.py | 1 - homeassistant/components/plex/__init__.py | 1 - .../components/poolsense/__init__.py | 2 +- .../components/progettihwsw/config_flow.py | 1 - homeassistant/components/ps4/media_player.py | 1 - .../components/pushbullet/config_flow.py | 1 - .../components/pushover/config_flow.py | 1 - homeassistant/components/pushover/notify.py | 1 - homeassistant/components/qvr_pro/camera.py | 1 - homeassistant/components/rachio/switch.py | 2 +- .../components/rainbird/config_flow.py | 2 +- homeassistant/components/recorder/pool.py | 1 - homeassistant/components/recorder/purge.py | 1 - .../components/repairs/issue_handler.py | 2 +- .../components/rest_command/__init__.py | 1 - .../components/rfxtrx/binary_sensor.py | 1 - .../components/rss_feed_template/__init__.py | 2 +- homeassistant/components/sabnzbd/__init__.py | 1 - .../components/sabnzbd/config_flow.py | 1 - homeassistant/components/script/__init__.py | 1 - .../components/sensibo/config_flow.py | 1 - homeassistant/components/shelly/climate.py | 1 - .../components/simplepush/config_flow.py | 1 - .../components/smartthings/__init__.py | 1 + homeassistant/components/snmp/sensor.py | 1 - homeassistant/components/snmp/switch.py | 1 - homeassistant/components/sonos/speaker.py | 2 +- homeassistant/components/spc/__init__.py | 1 - homeassistant/components/sql/config_flow.py | 1 - .../components/srp_energy/__init__.py | 2 +- .../components/srp_energy/config_flow.py | 1 - homeassistant/components/srp_energy/sensor.py | 2 +- homeassistant/components/supla/__init__.py | 1 - .../components/surepetcare/binary_sensor.py | 1 - .../components/surepetcare/sensor.py | 1 - .../swiss_hydrological_data/sensor.py | 1 - homeassistant/components/switchbee/climate.py | 1 - homeassistant/components/switchbee/light.py | 1 - homeassistant/components/switchbee/switch.py | 1 - .../components/system_log/__init__.py | 2 -- homeassistant/components/tasmota/discovery.py | 4 ++-- .../components/tellduslive/config_flow.py | 1 - homeassistant/components/template/cover.py | 1 - homeassistant/components/template/fan.py | 1 - homeassistant/components/template/switch.py | 1 - .../components/tomato/device_tracker.py | 2 -- homeassistant/components/toon/coordinator.py | 1 - .../components/traccar/device_tracker.py | 6 ++++- .../components/trafikverket_train/sensor.py | 1 - .../components/transmission/config_flow.py | 1 - homeassistant/components/tuya/number.py | 1 - homeassistant/components/twinkly/light.py | 2 -- .../components/uk_transport/sensor.py | 2 +- homeassistant/components/unifi/config_flow.py | 2 -- homeassistant/components/unifi/controller.py | 3 --- .../components/unifi/device_tracker.py | 1 - homeassistant/components/unifi/services.py | 2 -- .../components/unifi_direct/device_tracker.py | 1 - .../components/unifiprotect/button.py | 1 - .../components/unifiprotect/sensor.py | 1 - .../components/w800rf32/binary_sensor.py | 2 -- homeassistant/components/whirlpool/sensor.py | 1 - homeassistant/components/ws66i/__init__.py | 1 - homeassistant/components/zamg/coordinator.py | 2 +- homeassistant/components/zamg/sensor.py | 2 +- homeassistant/components/zha/core/device.py | 6 ++--- homeassistant/components/zha/core/helpers.py | 1 - homeassistant/helpers/entity_platform.py | 6 ++--- homeassistant/helpers/network.py | 1 - homeassistant/helpers/service.py | 1 - requirements_test_pre_commit.txt | 2 +- script/translations/migrate.py | 2 -- tests/components/abode/test_config_flow.py | 1 - .../accuweather/test_config_flow.py | 4 ---- tests/components/accuweather/test_init.py | 2 -- tests/components/adax/test_config_flow.py | 5 +++- .../components/airthings/test_config_flow.py | 5 +++- .../components/aladdin_connect/test_cover.py | 2 -- tests/components/aladdin_connect/test_init.py | 5 ---- tests/components/alexa/test_smart_home.py | 10 ++++++-- tests/components/analytics/test_analytics.py | 2 -- .../components/androidtv/test_config_flow.py | 1 - .../aurora_abb_powerone/test_config_flow.py | 11 ++++++--- .../aussie_broadband/test_config_flow.py | 2 -- tests/components/aws/test_init.py | 1 - tests/components/axis/conftest.py | 1 - tests/components/backup/test_http.py | 1 - tests/components/backup/test_websocket.py | 1 - tests/components/blink/test_config_flow.py | 4 +++- .../components/bluetooth/test_base_scanner.py | 5 +++- .../components/bluetooth/test_diagnostics.py | 1 - tests/components/bluetooth/test_wrappers.py | 4 ++-- .../test_device_tracker.py | 4 ---- tests/components/brother/test_config_flow.py | 3 --- tests/components/canary/test_sensor.py | 4 ++-- tests/components/cloudflare/test_init.py | 1 - .../components/co2signal/test_config_flow.py | 15 +++++++++--- tests/components/coinbase/test_config_flow.py | 1 - tests/components/coinbase/test_diagnostics.py | 1 - .../deconz/test_alarm_control_panel.py | 2 -- tests/components/dhcp/test_init.py | 4 +++- .../components/dlna_dmr/test_media_player.py | 2 +- tests/components/ecobee/test_config_flow.py | 1 - tests/components/elkm1/__init__.py | 5 +++- tests/components/file/test_notify.py | 1 - tests/components/filesize/test_config_flow.py | 8 +++++-- .../fireservicerota/test_config_flow.py | 1 - tests/components/firmata/test_config_flow.py | 1 - .../freedompro/test_binary_sensor.py | 2 -- .../components/freedompro/test_config_flow.py | 3 --- tests/components/freedompro/test_sensor.py | 1 - tests/components/fritz/conftest.py | 1 - tests/components/fritz/test_config_flow.py | 13 ---------- tests/components/fronius/test_config_flow.py | 5 +++- tests/components/generic/test_camera.py | 2 -- tests/components/greeneye_monitor/conftest.py | 2 +- tests/components/hddtemp/test_sensor.py | 1 - tests/components/history_stats/test_sensor.py | 4 ---- .../components/honeywell/test_config_flow.py | 1 - tests/components/hue/test_config_flow.py | 2 +- tests/components/hue/test_init.py | 1 - .../components/huisbaasje/test_config_flow.py | 5 +++- tests/components/huisbaasje/test_sensor.py | 2 -- .../hvv_departures/test_config_flow.py | 5 ---- tests/components/insteon/test_config_flow.py | 17 +++++++++---- .../intellifire/test_config_flow.py | 2 -- tests/components/ipma/test_config_flow.py | 2 -- tests/components/ipma/test_init.py | 1 - tests/components/iss/test_config_flow.py | 1 - .../lacrosse_view/test_config_flow.py | 10 ++++++-- .../launch_library/test_config_flow.py | 1 - tests/components/led_ble/test_config_flow.py | 16 +++++++++---- .../litterrobot/test_config_flow.py | 2 -- tests/components/local_file/test_camera.py | 1 - tests/components/melnor/conftest.py | 1 - tests/components/melnor/test_config_flow.py | 5 ---- tests/components/melnor/test_number.py | 1 - tests/components/melnor/test_switch.py | 1 - .../minecraft_server/test_config_flow.py | 15 +++++++++--- tests/components/modbus/test_init.py | 2 +- .../moehlenhoff_alpha2/test_config_flow.py | 1 - tests/components/mqtt/test_common.py | 12 +++++----- tests/components/mqtt/test_discovery.py | 2 -- .../mqtt_json/test_device_tracker.py | 1 - tests/components/mutesync/test_config_flow.py | 5 +++- tests/components/nam/test_config_flow.py | 1 - tests/components/netatmo/test_init.py | 1 - tests/components/nexia/test_config_flow.py | 4 +++- tests/components/nina/test_binary_sensor.py | 2 -- tests/components/nina/test_config_flow.py | 5 ---- tests/components/nina/test_init.py | 1 - tests/components/nzbget/test_sensor.py | 2 +- tests/components/oncue/__init__.py | 9 ++++--- tests/components/plaato/test_config_flow.py | 2 -- tests/components/plant/test_init.py | 1 - .../prosegur/test_alarm_control_panel.py | 2 -- tests/components/prosegur/test_init.py | 1 - .../components/radiotherm/test_config_flow.py | 1 - tests/components/recorder/test_migrate.py | 5 +++- .../components/repairs/test_websocket_api.py | 2 +- tests/components/roon/test_config_flow.py | 5 ---- .../components/samsungtv/test_config_flow.py | 4 ---- .../components/samsungtv/test_media_player.py | 3 --- tests/components/scrape/test_config_flow.py | 5 +++- tests/components/script/test_blueprint.py | 1 - tests/components/senseme/__init__.py | 1 - tests/components/shelly/test_config_flow.py | 1 - tests/components/shelly/test_init.py | 2 -- tests/components/sleepiq/test_config_flow.py | 1 - tests/components/sma/test_config_flow.py | 1 - tests/components/smappee/test_config_flow.py | 5 +++- .../smartthings/test_config_flow.py | 1 - tests/components/sonarr/test_sensor.py | 2 +- tests/components/sql/test_sensor.py | 4 +++- .../components/squeezebox/test_config_flow.py | 10 ++++++-- tests/components/srp_energy/__init__.py | 1 - .../components/srp_energy/test_config_flow.py | 1 - tests/components/subaru/conftest.py | 5 +++- tests/components/subaru/test_config_flow.py | 9 +++++-- tests/components/subaru/test_init.py | 5 +++- .../components/switchbee/test_config_flow.py | 1 - .../synology_dsm/test_config_flow.py | 4 ---- .../tankerkoenig/test_config_flow.py | 1 - tests/components/tibber/test_statistics.py | 2 +- .../totalconnect/test_config_flow.py | 12 ++++++---- tests/components/upnp/conftest.py | 2 ++ tests/components/uptimerobot/common.py | 1 - .../uptimerobot/test_config_flow.py | 4 ---- .../uptimerobot/test_diagnostics.py | 2 -- tests/components/uptimerobot/test_init.py | 2 -- tests/components/uptimerobot/test_switch.py | 2 -- tests/components/vera/test_sensor.py | 2 +- tests/components/version/common.py | 2 -- tests/components/vulcan/test_config_flow.py | 6 ----- tests/components/vultr/test_binary_sensor.py | 1 - tests/components/vultr/test_sensor.py | 2 -- tests/components/wake_on_lan/test_switch.py | 7 ------ tests/components/wallbox/test_number.py | 1 - .../components/whirlpool/test_config_flow.py | 1 - tests/components/wolflink/test_config_flow.py | 1 - tests/components/ws66i/test_config_flow.py | 1 - tests/components/yeelight/test_config_flow.py | 1 - tests/components/zeroconf/test_init.py | 24 +++++++++++++++---- tests/components/zha/test_channels.py | 2 +- tests/components/zwave_me/test_config_flow.py | 1 - .../zwave_me/test_remove_stale_devices.py | 5 +++- tests/conftest.py | 1 - tests/helpers/test_config_entry_flow.py | 2 -- tests/helpers/test_device_registry.py | 2 +- tests/helpers/test_entity.py | 1 - tests/helpers/test_translation.py | 6 ++++- tests/scripts/test_check_config.py | 1 - tests/test_config_entries.py | 1 - tests/test_requirements.py | 5 ---- 312 files changed, 299 insertions(+), 478 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7684e4d4d7..4fa5a1a716f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - --remove-all-unused-imports stages: [manual] - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black args: diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e821d0de10e..1cc850f31e4 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -406,7 +406,6 @@ def async_enable_logging( if (err_path_exists and os.access(err_log_path, os.W_OK)) or ( not err_path_exists and os.access(err_dir, os.W_OK) ): - err_handler: ( logging.handlers.RotatingFileHandler | logging.handlers.TimedRotatingFileHandler diff --git a/homeassistant/components/adax/__init__.py b/homeassistant/components/adax/__init__.py index 438f1fc74ad..511fb746216 100644 --- a/homeassistant/components/adax/__init__.py +++ b/homeassistant/components/adax/__init__.py @@ -24,7 +24,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # convert title and unique_id to string if config_entry.version == 1: if isinstance(config_entry.unique_id, int): - hass.config_entries.async_update_entry( config_entry, unique_id=str(config_entry.unique_id), diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index eb201182b68..89d3b0faf14 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -88,7 +88,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" else: - self.hass.config_entries.async_update_entry( self.entry, data={ diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 4f8f1dade93..120b24dcc44 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -270,7 +270,6 @@ class Alert(Entity): await self._send_notification_message(message) async def _send_notification_message(self, message: Any) -> None: - if not self._notifiers: return diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 1ce20d154bd..2dbda64568f 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -103,7 +103,6 @@ class Auth: return dt.utcnow() < preemptive_expire_time async def _async_request_new_token(self, lwa_params): - try: session = aiohttp_client.async_get_clientsession(self.hass) async with async_timeout.timeout(10): diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index ef145a9ceb8..c18ffd316b1 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -193,7 +193,6 @@ def resolve_slot_synonyms(key, request): and "resolutionsPerAuthority" in request["resolutions"] and len(request["resolutions"]["resolutionsPerAuthority"]) >= 1 ): - # Extract all of the possible values from each authority with a # successful match possible_values = [] diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index ffdec02bd3e..4291c21d0ed 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -274,7 +274,6 @@ class AsusWrtRouter: entity_reg, self._entry.entry_id ) for entry in track_entries: - if entry.domain != TRACKER_DOMAIN: continue device_mac = format_mac(entry.unique_id) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 729d69ed45b..067014cc81f 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -112,7 +112,6 @@ class AxisBinarySensor(AxisEventEntity, BinarySensorEntity): self._attr_name = self.device.api.vapix.ports[event.id].name elif event.group == EventGroup.MOTION: - for event_topic, event_data in ( (EventTopic.FENCE_GUARD, self.device.api.vapix.fence_guard), (EventTopic.LOITERING_GUARD, self.device.api.vapix.loitering_guard), @@ -120,7 +119,6 @@ class AxisBinarySensor(AxisEventEntity, BinarySensorEntity): (EventTopic.OBJECT_ANALYTICS, self.device.api.vapix.object_analytics), (EventTopic.MOTION_DETECTION_4, self.device.api.vapix.vmd4), ): - if ( event.topic_base == event_topic and event_data diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 4cd68c7a9b4..5266e310428 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -399,7 +399,6 @@ class BayesianBinarySensor(BinarySensorEntity): observations_by_entity: dict[str, list[Observation]] = {} for observation in self._observations: - if (key := observation.entity_id) is None: continue observations_by_entity.setdefault(key, []).append(observation) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 7c23e76385a..69e115470ad 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -396,7 +396,6 @@ class BluesoundPlayer(MediaPlayerEntity): _LOGGER.debug("Calling URL: %s", url) try: - async with async_timeout.timeout(125): response = await self._polling_session.get( url, headers={CONNECTION: KEEP_ALIVE} diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 58067a467ce..40ac86de607 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -12,7 +12,6 @@ from home_assistant_bluetooth import BluetoothServiceInfoBleak from homeassistant.util.dt import monotonic_time_coarse if TYPE_CHECKING: - from .manager import BluetoothManager diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index 1bc430c8d4a..dc9159622d9 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -95,7 +95,6 @@ async def async_setup_entry( session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] for switch in session.device_helper.smart_plugs: - entities.append( SHCSwitch( device=switch, @@ -113,7 +112,6 @@ async def async_setup_entry( ) for switch in session.device_helper.light_switches: - entities.append( SHCSwitch( device=switch, @@ -124,7 +122,6 @@ async def async_setup_entry( ) for switch in session.device_helper.smart_plugs_compact: - entities.append( SHCSwitch( device=switch, @@ -135,7 +132,6 @@ async def async_setup_entry( ) for switch in session.device_helper.camera_eyes: - entities.append( SHCSwitch( device=switch, @@ -146,7 +142,6 @@ async def async_setup_entry( ) for switch in session.device_helper.camera_360: - entities.append( SHCSwitch( device=switch, diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 8cc8b10a67c..2d4fb07d579 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -231,7 +231,10 @@ SENSOR_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, ), # UV index (-) - (BTHomeSensorDeviceClass.UV_INDEX, None,): SensorEntityDescription( + ( + BTHomeSensorDeviceClass.UV_INDEX, + None, + ): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.UV_INDEX}", state_class=SensorStateClass.MEASUREMENT, ), @@ -256,7 +259,10 @@ SENSOR_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, ), # Volume (L) - (BTHomeSensorDeviceClass.VOLUME, Units.VOLUME_LITERS,): SensorEntityDescription( + ( + BTHomeSensorDeviceClass.VOLUME, + Units.VOLUME_LITERS, + ): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.VOLUME}_{Units.VOLUME_LITERS}", device_class=SensorDeviceClass.VOLUME, native_unit_of_measurement=UnitOfVolume.LITERS, diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 1ba51e476d8..ae762efefe7 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -735,7 +735,6 @@ class BrSensor(SensorEntity): or sensor_type.endswith("_4d") or sensor_type.endswith("_5d") ): - # update forecasting sensors: fcday = 0 if sensor_type.endswith("_2d"): diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py index 05fd8a2b7c8..06dc62d114b 100644 --- a/homeassistant/components/control4/config_flow.py +++ b/homeassistant/components/control4/config_flow.py @@ -96,7 +96,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: - hub = Control4Validator( user_input[CONF_HOST], user_input[CONF_USERNAME], diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 530f1788dd8..25672908670 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -188,6 +188,7 @@ class DarkSkyWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" + # Per conversation with Joshua Reyes of Dark Sky, to get the total # forecasted precipitation, you have to multiple the intensity by # the hours for the forecast interval diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index ef78e8f1419..0b646bedbd8 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -271,7 +271,6 @@ class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity): attr[ATTR_TEMPERATURE] = self._device.internal_temperature if isinstance(self._device, Presence): - if self._device.dark is not None: attr[ATTR_DARK] = self._device.dark diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index d94b40e8525..80f0d6f5dd7 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -87,7 +87,6 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): If no bridge is found allow user to manually input configuration. """ if user_input is not None: - if CONF_MANUAL_INPUT == user_input[CONF_HOST]: return await self.async_step_manual_input() diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index ee27beaa8e2..03c9f9c70f5 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -372,7 +372,6 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): attr[ATTR_DAYLIGHT] = self._device.daylight elif isinstance(self._device, LightLevel): - if self._device.dark is not None: attr[ATTR_DARK] = self._device.dark diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index e4399e53524..5cea4ca3b15 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -186,10 +186,8 @@ async def async_remove_orphaned_entries_service(gateway: DeconzGateway) -> None: devices_to_be_removed.remove(event.device_id) for entry in entity_entries: - # Don't remove available entities if entry.unique_id in gateway.entities[entry.domain]: - # Don't remove devices with available entities if entry.device_id in devices_to_be_removed: devices_to_be_removed.remove(entry.device_id) diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index f0a263d0d0f..2cf85b91abd 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -121,6 +121,6 @@ class DeLijnPublicTransportSensor(SensorEntity): self._attr_extra_state_attributes["next_passages"] = self.line.passages self._attr_available = True - except (KeyError) as error: + except KeyError as error: _LOGGER.error("Invalid data received from De Lijn: %s", error) self._attr_available = False diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 85d964a84cb..adf91eb706b 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -245,7 +245,7 @@ class DerivativeSensor(RestoreEntity, SensorEntity): derivative = new_derivative else: derivative = Decimal(0) - for (start, end, value) in self._state_list: + for start, end, value in self._state_list: weight = calculate_weight(start, end, new_state.last_updated) derivative = derivative + (value * Decimal(weight)) diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index a78b3432016..ea9205ebdec 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -214,7 +214,6 @@ async def activate_automation( # noqa: C901 elif start_point and start_point < now < get_astral_event_next( hass, SUN_EVENT_SUNSET ): - # Check for every light if it would be on if someone was home # when the fading in started and turn it on if so for index, light_id in enumerate(light_ids): diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py index eb48095539a..6a1437d5159 100644 --- a/homeassistant/components/dnsip/config_flow.py +++ b/homeassistant/components/dnsip/config_flow.py @@ -93,7 +93,6 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input: - hostname = user_input[CONF_HOSTNAME] name = DEFAULT_NAME if hostname == DEFAULT_HOSTNAME else hostname resolver = user_input.get(CONF_RESOLVER, DEFAULT_RESOLVER) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 677aafc0b52..8240bb117ea 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -255,7 +255,6 @@ class Doods(ImageProcessingEntity): ) for label, values in matches.items(): - # Draw custom label regions/areas if label in self._label_areas and self._label_areas[label] != [0, 0, 1, 1]: box_label = f"{label.capitalize()} Detection Area" diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index 14158b6c379..ab83759bb2d 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -66,7 +66,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: server_address = (conf.get(CONF_HOST), conf.get(CONF_PORT)) try: - ebusdpy.init(server_address) hass.data[DOMAIN] = EbusdData(server_address, circuit) diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index ca1a153b5ae..f1bf7deb502 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -160,7 +160,6 @@ class EcovacsVacuum(VacuumEntity): def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" if self.is_on: - self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed)) def send_command( diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index d7e35f3e04c..b998e2dd737 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -87,7 +87,7 @@ class SmartPlugSwitch(SwitchEntity): self._state = self._pca.get_state(self._device_id) self._available = True - except (OSError) as ex: + except OSError as ex: if self._available: _LOGGER.warning("Could not read state for %s: %s", self.name, ex) self._available = False diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 4a427615aaf..f26ed72f44c 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -106,7 +106,6 @@ def setup_platform( sensors = [] for elem in data.data: - if exclude_feeds is not None and int(elem["id"]) in exclude_feeds: continue diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 272645909a5..a81544151ea 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -379,7 +379,7 @@ class HueOneLightChangeView(HomeAssistantView): else: parsed[STATE_ON] = entity.state != STATE_OFF - for (key, attr) in ( + for key, attr in ( (HUE_API_STATE_BRI, STATE_BRIGHTNESS), (HUE_API_STATE_HUE, STATE_HUE), (HUE_API_STATE_SAT, STATE_SATURATION), @@ -587,7 +587,7 @@ class HueOneLightChangeView(HomeAssistantView): ) ] - for (key, val) in ( + for key, val in ( (STATE_BRIGHTNESS, HUE_API_STATE_BRI), (STATE_HUE, HUE_API_STATE_HUE), (STATE_SATURATION, HUE_API_STATE_SAT), @@ -705,7 +705,7 @@ def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]: data[STATE_SATURATION] = 0 # Clamp brightness, hue, saturation, and color temp to valid values - for (key, v_min, v_max) in ( + for key, v_min, v_max in ( (STATE_BRIGHTNESS, HUE_API_STATE_BRI_MIN, HUE_API_STATE_BRI_MAX), (STATE_HUE, HUE_API_STATE_HUE_MIN, HUE_API_STATE_HUE_MAX), (STATE_SATURATION, HUE_API_STATE_SAT_MIN, HUE_API_STATE_SAT_MAX), diff --git a/homeassistant/components/escea/config_flow.py b/homeassistant/components/escea/config_flow.py index 5d0dfea1157..2a6e19343d9 100644 --- a/homeassistant/components/escea/config_flow.py +++ b/homeassistant/components/escea/config_flow.py @@ -21,7 +21,6 @@ _LOGGER = logging.getLogger(__name__) async def _async_has_devices(hass: HomeAssistant) -> bool: - controller_ready = asyncio.Event() @callback diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 9fb7c1f8b5a..65b5df100dd 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -66,7 +66,6 @@ async def async_setup_entry( camera_entities = [] for camera, value in coordinator.data.items(): - camera_rtsp_entry = [ item for item in hass.config_entries.async_entries(DOMAIN) @@ -81,7 +80,6 @@ async def async_setup_entry( ) if camera_rtsp_entry: - ffmpeg_arguments = camera_rtsp_entry[0].options[CONF_FFMPEG_ARGUMENTS] camera_username = camera_rtsp_entry[0].data[CONF_USERNAME] camera_password = camera_rtsp_entry[0].data[CONF_PASSWORD] @@ -96,7 +94,6 @@ async def async_setup_entry( ) else: - discovery_flow.async_create_flow( hass, DOMAIN, diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index 61b66280ae8..4c8b1418fa5 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -182,7 +182,6 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - if user_input[CONF_URL] == CONF_CUSTOMIZE: self.context["data"] = { CONF_USERNAME: user_input[CONF_USERNAME], diff --git a/homeassistant/components/faa_delays/config_flow.py b/homeassistant/components/faa_delays/config_flow.py index 1abd1f00fa5..023fe4d6a5b 100644 --- a/homeassistant/components/faa_delays/config_flow.py +++ b/homeassistant/components/faa_delays/config_flow.py @@ -25,7 +25,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_ID]) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index e4166494ce4..6f290ccb293 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -507,7 +507,6 @@ class RangeFilter(Filter, SensorEntity): new_state_value = cast(float, new_state.state) if self._upper_bound is not None and new_state_value > self._upper_bound: - self._stats_internal["erasures_up"] += 1 _LOGGER.debug( @@ -519,7 +518,6 @@ class RangeFilter(Filter, SensorEntity): new_state.state = self._upper_bound elif self._lower_bound is not None and new_state_value < self._lower_bound: - self._stats_internal["erasures_low"] += 1 _LOGGER.debug( @@ -564,7 +562,6 @@ class OutlierFilter(Filter, SensorEntity): len(self.states) == self.states.maxlen and abs(new_state_value - median) > self._radius ): - self._stats_internal["erasures"] += 1 _LOGGER.debug( diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index dc6b40982b7..6ef0467f7b6 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -208,7 +208,6 @@ class FinTsClient: holdings_accounts = [] for account in self.client.get_sepa_accounts(): - if self.is_balance_account(account): balance_accounts.append(account) diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index a58cd0591d1..78b5592a54e 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -213,7 +213,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> _LOGGER.debug("Closing Firmata board %s", config_entry.data[CONF_NAME]) unload_entries = [] - for (conf, platform) in CONF_PLATFORM_MAP.items(): + for conf, platform in CONF_PLATFORM_MAP.items(): if conf in config_entry.data: unload_entries.append( hass.config_entries.async_forward_entry_unload(config_entry, platform) diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index e80acae8627..c901ccce3e7 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -86,7 +86,6 @@ class FleetGoDeviceScanner: for device in devices: if not self._include or device.license_plate in self._include: - if device.active or device.current_address is None: device.get_map_details() diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 3f9f70e294c..f02911e30d5 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -74,7 +74,7 @@ class FliprDataUpdateCoordinator(DataUpdateCoordinator): data = await self.hass.async_add_executor_job( self.client.get_pool_measure_latest, self.flipr_id ) - except (FliprError) as error: + except FliprError as error: raise UpdateFailed(error) from error return data diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index d6e05c17136..1b28a2552a2 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -43,7 +43,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): await self.send_presence_ping() await self._update_device() await self._update_consumption_data() - except (RequestError) as error: + except RequestError as error: raise UpdateFailed(error) from error @property diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index 4b5fe5bc8e9..b656f5e9715 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -97,7 +97,6 @@ async def async_setup_entry( ] flume_entity_list = [] for device in flume_devices: - device_id = device[KEY_DEVICE_ID] device_timezone = device[KEY_DEVICE_LOCATION][KEY_DEVICE_LOCATION_TIMEZONE] device_location_name = device[KEY_DEVICE_LOCATION][KEY_DEVICE_LOCATION_NAME] diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index a83b39ebbb1..6072aee89a7 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -102,7 +102,6 @@ async def _async_port_entities_list( _LOGGER.debug("IP source for %s is %s", avm_wrapper.host, local_ip) for i in range(port_forwards_count): - portmap = await avm_wrapper.async_get_port_mapping( avm_wrapper.device_conn_type, i ) @@ -406,7 +405,6 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch, SwitchEntity): self._attributes[attr] = self.port_mapping[key] async def _async_switch_on_off_executor(self, turn_on: bool) -> bool: - if self.port_mapping is None: return False diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index ee959943b82..e0eaf3bb38a 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -262,7 +262,6 @@ async def async_setup_entry( if entity_id := ent_reg.async_get_entity_id( Platform.SENSOR, DOMAIN, old_unique_id ): - ent_reg.async_update_entity( entity_id, new_unique_id=f"{config_entry.entry_id}-{new_key}" ) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 2a011679d09..196fa580ea8 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -289,7 +289,6 @@ class AbstractConfig(ABC): return for user_agent_id, _ in self._store.agent_user_ids.items(): - if (webhook_id := self.get_local_webhook_id(user_agent_id)) is None: setup_successful = False break diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 675acf600cb..f74a19425ab 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -64,7 +64,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: - try: validated = await validate_input(user_input) except CannotConnect: diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 737ffaf8c19..6e2ca202fd3 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -316,7 +316,12 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): @property def _first_radiator_thermostat( self, - ) -> AsyncHeatingThermostat | AsyncHeatingThermostatCompact | AsyncHeatingThermostatEvo | None: + ) -> ( + AsyncHeatingThermostat + | AsyncHeatingThermostatCompact + | AsyncHeatingThermostatEvo + | None + ): """Return the first radiator thermostat from the hmip heating group.""" for device in self._device.devices: if isinstance( diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 1296dfbfdb3..3ea86d7197b 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -334,7 +334,6 @@ class HoneywellUSThermostat(ClimateEntity): _LOGGER.error("Can not get system mode") return try: - # Set permanent hold # and Set temperature if mode in COOLING_MODES: diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 76d208f72f3..8b24fc912f1 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -67,7 +67,6 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" else: - self.hass.config_entries.async_update_entry( self.entry, data={ diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 221888b0854..035da145cc0 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -92,7 +92,6 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N # initialize bridge connection just for the migration async with HueBridgeV2(host, api_key) as api: - sensor_class_mapping = { SensorDeviceClass.BATTERY.value: ResourceTypes.DEVICE_POWER, BinarySensorDeviceClass.MOTION.value: ResourceTypes.MOTION, @@ -130,7 +129,6 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N # loop through all entities for device and find match for ent in async_entries_for_device(ent_reg, hass_dev_id, True): - if ent.entity_id.startswith("light"): # migrate light # should always return one lightid here diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py index a99dea57447..36b1b5f927f 100644 --- a/homeassistant/components/hvv_departures/binary_sensor.py +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -48,7 +48,6 @@ async def async_setup_entry( for partial_station in station_information.get("partialStations", []): for elevator in partial_station.get("elevators", []): - state = elevator.get("state") != "READY" available = elevator.get("state") != "UNKNOWN" label = elevator.get("label") diff --git a/homeassistant/components/hvv_departures/config_flow.py b/homeassistant/components/hvv_departures/config_flow.py index 5f07593eeb5..4cbd0a83321 100644 --- a/homeassistant/components/hvv_departures/config_flow.py +++ b/homeassistant/components/hvv_departures/config_flow.py @@ -81,7 +81,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_station(self, user_input=None): """Handle the step where the user inputs his/her station.""" if user_input is not None: - errors = {} check_name = await self.hub.gti.checkName( @@ -145,7 +144,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Manage the options.""" errors = {} if not self.departure_filters: - departure_list = {} hub: GTIHub = self.hass.data[DOMAIN][self.config_entry.entry_id] @@ -172,7 +170,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): } if user_input is not None and not errors: - options = { CONF_FILTER: [ self.departure_filters[x] for x in user_input[CONF_FILTER] diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index c3cd473c3f4..6c2842c190e 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -259,7 +259,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for device_entry in dr.async_entries_for_config_entry( device_registry, entry.entry_id ): - for (kind, key) in device_entry.identifiers: + for kind, key in device_entry.identifiers: if kind == DOMAIN and key in known_devices: break else: diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 40b44f00a73..0f8b7a0fa74 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -77,7 +77,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: target_keys[target] = api_keys[target] try: - for target, key in target_keys.items(): res = pyfttt.send_event(key, event, value1, value2, value3) if res.status_code != HTTPStatus.OK: diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index d2b019cb381..91676724a8b 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -119,7 +119,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) if user_input is not None: - control_schema = vol.Schema( { vol.Required( diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index 356ddedf16d..b6753adef76 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -33,7 +33,6 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData self._api = api async def _async_update_data(self) -> IntellifirePollData: - if not self._api.is_polling_in_background: LOGGER.info("Starting Intellifire Background Polling Loop") await self._api.start_background_polling() diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 521bfb41a80..90c2d6b3818 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -95,7 +95,7 @@ async def async_setup_entry( entities_by_address[node.address] = entity # Handle some special child node cases for Insteon Devices - for (node, device_class, device_type, device_info) in child_nodes: + for node, device_class, device_type, device_info in child_nodes: subnode_id = int(node.address.split(" ")[-1], 16) # Handle Insteon Thermostats if device_type is not None and device_type.startswith(TYPE_CATEGORY_CLIMATE): diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index b190638fb35..53ad87c9ddc 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -109,7 +109,6 @@ def _check_for_insteon_type( device_type.startswith(t) for t in set(NODE_FILTERS[platform][FILTER_INSTEON_TYPE]) ): - # Hacky special-cases for certain devices with different platforms # included as subnodes. Note that special-cases are not necessary # on ISY 5.x firmware as it uses the superior NodeDefs method diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 59b57f888a7..306615543cd 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -173,6 +173,7 @@ class ControllerDevice(ClimateEntity): async def async_added_to_hass(self) -> None: """Call on adding to hass.""" + # Register for connect/disconnect/update events @callback def controller_disconnected(ctrl: Controller, ex: Exception) -> None: @@ -290,7 +291,7 @@ class ControllerDevice(ClimateEntity): return HVACMode.OFF if (mode := self._controller.mode) == Controller.Mode.FREE_AIR: return HVACMode.FAN_ONLY - for (key, value) in self._state_to_pizone.items(): + for key, value in self._state_to_pizone.items(): if value == mode: return key assert False, "Should be unreachable" @@ -527,7 +528,7 @@ class ZoneDevice(ClimateEntity): def hvac_mode(self) -> HVACMode | None: """Return current operation ie. heat, cool, idle.""" mode = self._zone.mode - for (key, value) in self._state_to_pizone.items(): + for key, value in self._state_to_pizone.items(): if value == mode: return key return None diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py index bc4fb8ceddc..af5205feb07 100644 --- a/homeassistant/components/izone/config_flow.py +++ b/homeassistant/components/izone/config_flow.py @@ -17,7 +17,6 @@ _LOGGER = logging.getLogger(__name__) async def _async_has_devices(hass: HomeAssistant) -> bool: - controller_ready = asyncio.Event() @callback diff --git a/homeassistant/components/juicenet/config_flow.py b/homeassistant/components/juicenet/config_flow.py index 93c46cd4834..35c1853b974 100644 --- a/homeassistant/components/juicenet/config_flow.py +++ b/homeassistant/components/juicenet/config_flow.py @@ -46,7 +46,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_ACCESS_TOKEN]) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/kitchen_sink/repairs.py b/homeassistant/components/kitchen_sink/repairs.py index 41db200dd72..51b474dcf0f 100644 --- a/homeassistant/components/kitchen_sink/repairs.py +++ b/homeassistant/components/kitchen_sink/repairs.py @@ -17,7 +17,7 @@ class DemoFixFlow(RepairsFlow): ) -> data_entry_flow.FlowResult: """Handle the first step of a fix flow.""" - return await (self.async_step_confirm()) + return await self.async_step_confirm() async def async_step_confirm( self, user_input: dict[str, str] | None = None diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 6ef17cf24da..eae5e91196c 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -62,7 +62,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # Removing domain name and config entry id from entity unique id's, replacing it with device number if config_entry.version == 1: - config_entry.version = 2 device_number = config_entry.data["device_number"] diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py index 1eb80c0c5a2..9e72ded620e 100644 --- a/homeassistant/components/lifx/manager.py +++ b/homeassistant/components/lifx/manager.py @@ -296,7 +296,6 @@ class LIFXManager: ) elif service == SERVICE_EFFECT_MORPH: - theme_name = kwargs.get(ATTR_THEME, "exciting") palette = kwargs.get(ATTR_PALETTE, None) @@ -336,7 +335,6 @@ class LIFXManager: ) elif service == SERVICE_EFFECT_PULSE: - effect = aiolifx_effects.EffectPulse( power_on=kwargs.get(ATTR_POWER_ON), period=kwargs.get(ATTR_PERIOD), @@ -347,7 +345,6 @@ class LIFXManager: await self.effects_conductor.start(effect, bulbs) elif service == SERVICE_EFFECT_COLORLOOP: - brightness = None saturation_max = None saturation_min = None @@ -378,7 +375,6 @@ class LIFXManager: await self.effects_conductor.start(effect, bulbs) elif service == SERVICE_EFFECT_STOP: - await self.effects_conductor.stop(bulbs) for coordinator in coordinators: diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index 45f585da39d..6538c06d0ba 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -218,7 +218,6 @@ def parse_api_response(response): for authority in AUTHORITIES: for entry in response["HourlyAirQualityIndex"]["LocalAuthority"]: if entry["@LocalAuthorityName"] == authority: - if isinstance(entry["Site"], dict): entry_sites_data = [entry["Site"]] else: diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 485579087a1..f880f83d766 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -157,7 +157,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return if change_type == collection.CHANGE_ADDED: - existing = hass.data[DOMAIN]["dashboards"].get(url_path) if existing: diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index 546f96fd0a6..b4294216003 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -31,7 +31,6 @@ def setup_platform( devices = [] for device in data.lupusec.get_devices(generic_type=CONST.TYPE_SWITCH): - devices.append(LupusecSwitch(data, device)) add_entities(devices) diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py index 4fc432bf66d..9f9851fb484 100644 --- a/homeassistant/components/lutron/binary_sensor.py +++ b/homeassistant/components/lutron/binary_sensor.py @@ -24,7 +24,7 @@ def setup_platform( if discovery_info is None: return devs = [] - for (area_name, device) in hass.data[LUTRON_DEVICES]["binary_sensor"]: + for area_name, device in hass.data[LUTRON_DEVICES]["binary_sensor"]: dev = LutronOccupancySensor(area_name, device, hass.data[LUTRON_CONTROLLER]) devs.append(dev) diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py index 65a1c737d55..57fd8ac9d5b 100644 --- a/homeassistant/components/lutron/cover.py +++ b/homeassistant/components/lutron/cover.py @@ -26,7 +26,7 @@ def setup_platform( ) -> None: """Set up the Lutron shades.""" devs = [] - for (area_name, device) in hass.data[LUTRON_DEVICES]["cover"]: + for area_name, device in hass.data[LUTRON_DEVICES]["cover"]: dev = LutronCover(area_name, device, hass.data[LUTRON_CONTROLLER]) devs.append(dev) diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index 15024122338..6bd556d36d1 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -19,7 +19,7 @@ def setup_platform( ) -> None: """Set up the Lutron lights.""" devs = [] - for (area_name, device) in hass.data[LUTRON_DEVICES]["light"]: + for area_name, device in hass.data[LUTRON_DEVICES]["light"]: dev = LutronLight(area_name, device, hass.data[LUTRON_CONTROLLER]) devs.append(dev) diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py index 8595f809035..f8ca93beb2e 100644 --- a/homeassistant/components/lutron/switch.py +++ b/homeassistant/components/lutron/switch.py @@ -21,7 +21,7 @@ def setup_platform( devs = [] # Add Lutron Switches - for (area_name, device) in hass.data[LUTRON_DEVICES]["switch"]: + for area_name, device in hass.data[LUTRON_DEVICES]["switch"]: dev = LutronSwitch(area_name, device, hass.data[LUTRON_CONTROLLER]) devs.append(dev) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index b9418373b38..e1d71353426 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -256,7 +256,6 @@ def _async_setup_keypads( leap_to_keypad_button_names: dict[int, dict[int, str]] = {} for bridge_button in bridge_buttons.values(): - parent_device = cast(str, bridge_button["parent_device"]) bridge_keypad = bridge_devices[parent_device] keypad_lutron_device_id = cast(int, bridge_keypad["device_id"]) diff --git a/homeassistant/components/lutron_caseta/button.py b/homeassistant/components/lutron_caseta/button.py index ee737673082..b0e6d49de65 100644 --- a/homeassistant/components/lutron_caseta/button.py +++ b/homeassistant/components/lutron_caseta/button.py @@ -29,7 +29,6 @@ async def async_setup_entry( entities: list[LutronCasetaButton] = [] for device in button_devices.values(): - parent_keypad = keypads[device["parent_device"]] parent_device_info = parent_keypad["device_info"] diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 09e137c96d1..4dc6d677abb 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -93,7 +93,6 @@ async def async_setup_platform( async_add_entities([new_stb]) if not config[CONF_OPTIMISTIC]: - already_installed = hass.data.get(DISCOVERY_MEDIAROOM) if not already_installed: hass.data[DISCOVERY_MEDIAROOM] = await install_mediaroom_protocol( diff --git a/homeassistant/components/melnor/config_flow.py b/homeassistant/components/melnor/config_flow.py index 7e9aed24e8a..7aad59acf7b 100644 --- a/homeassistant/components/melnor/config_flow.py +++ b/homeassistant/components/melnor/config_flow.py @@ -80,12 +80,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for discovery_info in async_discovered_service_info( self.hass, connectable=True ): - if discovery_info.manufacturer_id == MANUFACTURER_ID and any( manufacturer_data.startswith(MANUFACTURER_DATA_START) for manufacturer_data in discovery_info.manufacturer_data.values() ): - address = discovery_info.address if ( address not in current_addresses diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index ba8ccadfca5..f507cf8cf32 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -94,7 +94,7 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): self._hourly = hourly @property - def track_home(self) -> (Any | bool): + def track_home(self) -> Any | bool: """Return if we are tracking home.""" return self._config.get(CONF_TRACK_HOME, False) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 6c9af12a322..3b82399f217 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -172,7 +172,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT]: - department = hass.data[DOMAIN][entry.entry_id][ COORDINATOR_FORECAST ].data.position.get("dept") diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 2c3e480c297..71d94a27fec 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -35,12 +35,10 @@ async def async_setup_entry( # Restore clients that is not a part of active clients list. for entity in registry.entities.values(): - if ( entity.config_entry_id == config_entry.entry_id and entity.domain == DEVICE_TRACKER ): - if ( entity.unique_id in coordinator.api.devices or entity.unique_id not in coordinator.api.all_devices diff --git a/homeassistant/components/minecraft_server/helpers.py b/homeassistant/components/minecraft_server/helpers.py index 7153c170a6a..d4a49d96f83 100644 --- a/homeassistant/components/minecraft_server/helpers.py +++ b/homeassistant/components/minecraft_server/helpers.py @@ -22,7 +22,7 @@ async def async_check_srv_record( srv_records = await aiodns.DNSResolver().query( host=f"{SRV_RECORD_PREFIX}.{host}", qtype="SRV" ) - except (aiodns.error.DNSError): + except aiodns.error.DNSError: # 'host' is not a SRV record. pass else: diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index a62d8537a33..24555704ac0 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -249,7 +249,10 @@ CLIMATE_SCHEMA = vol.All( COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend( { - vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING,): vol.In( + vol.Optional( + CONF_INPUT_TYPE, + default=CALL_TYPE_REGISTER_HOLDING, + ): vol.In( [ CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_COIL, diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index 95cb24fe7ed..ee3ab9817ea 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -315,7 +315,6 @@ class MoldIndicator(SensorEntity): def _calc_moldindicator(self): """Calculate the humidity at the (cold) calibration point.""" if None in (self._dewpoint, self._calib_factor) or self._calib_factor == 0: - _LOGGER.debug( "Invalid inputs - dewpoint: %s, calibration-factor: %s", self._dewpoint, diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 135151f179c..d09e31f65f9 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -200,7 +200,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): """Handle a new received MQTT state message.""" # auto-expire enabled? if self._expire_after: - # When expire_after is set, and we receive a message, assume device is # not expired since it has to be to receive the message self._expired = False diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 755bf3636df..28d60a5fdb9 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -666,7 +666,6 @@ class MQTT: subscriptions = self._matching_subscriptions(msg.topic) for subscription in subscriptions: - payload: SubscribePayloadType = msg.payload if subscription.encoding is not None: try: diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 3accb7c8ade..90f04b084b0 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -279,7 +279,7 @@ async def async_start( # noqa: C901 mqtt_data.last_discovery = time.time() mqtt_integrations = await async_get_mqtt(hass) - for (integration, topics) in mqtt_integrations.items(): + for integration, topics in mqtt_integrations.items(): async def async_integration_message_received( integration: str, msg: ReceiveMessage diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index e3fd5e50093..29beafdbe84 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -64,7 +64,11 @@ class EntitySubscription: if other is None: return True - return (self.topic, self.qos, self.encoding,) != ( + return ( + self.topic, + self.qos, + self.encoding, + ) != ( other.topic, other.qos, other.encoding, diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 20021f1e6d4..6cf9dd55451 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -135,7 +135,6 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - return self.async_create_entry( title=self.host, data={**user_input, CONF_HOST: self.host}, diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 6c1768d9855..3b777aa6f22 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -428,7 +428,6 @@ class NestFlowHandler( _LOGGER.error("Error creating subscription: %s", err) errors[CONF_CLOUD_PROJECT_ID] = "subscriber_error" if not errors: - try: device_manager = await subscriber.async_get_device_manager() except ApiException as err: diff --git a/homeassistant/components/nfandroidtv/config_flow.py b/homeassistant/components/nfandroidtv/config_flow.py index 88eebe1b4d4..d4491cee48e 100644 --- a/homeassistant/components/nfandroidtv/config_flow.py +++ b/homeassistant/components/nfandroidtv/config_flow.py @@ -26,7 +26,6 @@ class NFAndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - self._async_abort_entries_match( {CONF_HOST: user_input[CONF_HOST], CONF_NAME: user_input[CONF_NAME]} ) diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index a7b3c0968cc..57c5f680e01 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -265,7 +265,6 @@ class Coordinator(ContextCoordinator[dict[int, Coil], int]): self.task = None async def _async_update_data_internal(self) -> dict[int, Coil]: - result: dict[int, Coil] = {} def _get_coils() -> Iterable[Coil]: diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index 0555f175cfb..88d61a427b5 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -99,7 +99,6 @@ class NINADataUpdateCoordinator( all_filtered_warnings: dict[str, list[Any]] = {} for region_id, raw_warnings in warnings.items(): - filtered_warnings: list[Any] = [] processed_details: list[tuple[str, str]] = [] diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py index bdaf164fadb..f1579bc05ec 100644 --- a/homeassistant/components/nina/config_flow.py +++ b/homeassistant/components/nina/config_flow.py @@ -126,7 +126,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_REGIONS] += group_input if user_input[CONF_REGIONS]: - return self.async_create_entry( title="NINA", data=prepare_user_input(user_input, self._all_region_codes_sorted), @@ -203,7 +202,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): user_input[CONF_REGIONS] += group_input if user_input[CONF_REGIONS]: - user_input = prepare_user_input( user_input, self._all_region_codes_sorted ) diff --git a/homeassistant/components/numato/binary_sensor.py b/homeassistant/components/numato/binary_sensor.py index cefec42ba3a..98e71b9fc2d 100644 --- a/homeassistant/components/numato/binary_sensor.py +++ b/homeassistant/components/numato/binary_sensor.py @@ -52,7 +52,6 @@ def setup_platform( ports = platform[CONF_PORTS] for port, port_name in ports.items(): try: - api.setup_input(device_id, port) api.edge_detect(device_id, port, partial(read_gpio, device_id)) diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index a0dd77df53a..ac0293b262e 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -108,7 +108,6 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): If no device is found allow user to manually input configuration. """ if user_input: - if CONF_MANUAL_INPUT == user_input[CONF_HOST]: return await self.async_step_configure() diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index 614aed6eefb..2d06b20a30a 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -94,7 +94,6 @@ class OpenMeteoWeatherEntity( forecasts: list[Forecast] = [] daily = self.coordinator.data.daily for index, time in enumerate(self.coordinator.data.daily.time): - forecast = Forecast( datetime=time.isoformat(), ) diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py index 463b5e0eed1..bef163252fa 100644 --- a/homeassistant/components/opencv/image_processing.py +++ b/homeassistant/components/opencv/image_processing.py @@ -189,7 +189,7 @@ class OpenCVImageProcessor(ImageProcessingEntity): ) regions = [] # pylint: disable=invalid-name - for (x, y, w, h) in detections: + for x, y, w, h in detections: regions.append((int(x), int(y), int(w), int(h))) total_matches += 1 diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 707be76be3b..33f42ea949d 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -79,10 +79,12 @@ async def async_setup_platform( ) -def catch_request_errors() -> Callable[ - [Callable[Concatenate[_OpenhomeDeviceT, _P], Awaitable[_R]]], - Callable[Concatenate[_OpenhomeDeviceT, _P], Coroutine[Any, Any, _R | None]], -]: +def catch_request_errors() -> ( + Callable[ + [Callable[Concatenate[_OpenhomeDeviceT, _P], Awaitable[_R]]], + Callable[Concatenate[_OpenhomeDeviceT, _P], Coroutine[Any, Any, _R | None]], + ] +): """Catch asyncio.TimeoutError, aiohttp.ClientError, UpnpError errors.""" def call_wrapper( @@ -265,7 +267,7 @@ class OpenhomeDevice(MediaPlayerEntity): await self._device.invoke_pin(pin) else: _LOGGER.error("Pins service not supported") - except (UpnpError): + except UpnpError: _LOGGER.error("Error invoking pin %s", pin) @property diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index cedc33c1a8e..90e2c426d27 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -251,7 +251,6 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _get_condition(self, weather_code, timestamp=None): """Get weather condition from weather data.""" if weather_code == WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT: - if timestamp: timestamp = dt.utc_from_timestamp(timestamp) diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 39f1c78b070..dab8d4fbe24 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -49,7 +49,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._pair_state: Any = None async def _async_create_current(self) -> FlowResult: - system = self._current[CONF_SYSTEM] return self.async_create_entry( title=f"{system['name']} ({system['serialnumber']})", diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index f88f02128e2..89cb29f0a07 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -437,7 +437,6 @@ class PhilipsTVMediaPlayer( @callback def _update_from_coordinator(self): - if self._tv.on: if self._tv.powerstate in ("Standby", "StandbyKeep"): self._attr_state = MediaPlayerState.OFF diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 0012a5987e7..b215bc0d821 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -178,7 +178,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def plex_websocket_callback(msgtype, data, error): """Handle callbacks from plexwebsocket library.""" if msgtype == SIGNAL_CONNECTION_STATE: - if data == STATE_CONNECTED: _LOGGER.debug("Websocket to %s successful", entry.data[CONF_SERVER]) hass.async_create_task(plex_server.async_update_platforms()) diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index a3e9a93da37..3652883b0f7 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -93,7 +93,7 @@ class PoolSenseDataUpdateCoordinator(DataUpdateCoordinator): async with async_timeout.timeout(10): try: data = await self.poolsense.get_poolsense_data() - except (PoolSenseError) as error: + except PoolSenseError as error: _LOGGER.error("PoolSense query did not complete") raise UpdateFailed(error) from error diff --git a/homeassistant/components/progettihwsw/config_flow.py b/homeassistant/components/progettihwsw/config_flow.py index 89d5916a3fd..5b996fd5eb4 100644 --- a/homeassistant/components/progettihwsw/config_flow.py +++ b/homeassistant/components/progettihwsw/config_flow.py @@ -42,7 +42,6 @@ class ProgettiHWSWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Manage relay modes step.""" errors = {} if user_input is not None: - whole_data = user_input whole_data.update(self.s1_in) diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 50c11781fa1..5df92fd795a 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -411,7 +411,6 @@ class PS4Device(MediaPlayerEntity): == game.lower().encode(encoding="utf-8") or source == title_id ): - _LOGGER.debug( "Starting PS4 game %s (%s) using source %s", game, title_id, source ) diff --git a/homeassistant/components/pushbullet/config_flow.py b/homeassistant/components/pushbullet/config_flow.py index bfa12a911b6..e6259fa8cee 100644 --- a/homeassistant/components/pushbullet/config_flow.py +++ b/homeassistant/components/pushbullet/config_flow.py @@ -36,7 +36,6 @@ class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - self._async_abort_entries_match({CONF_NAME: user_input[CONF_NAME]}) try: diff --git a/homeassistant/components/pushover/config_flow.py b/homeassistant/components/pushover/config_flow.py index 5119d91a174..32f91031351 100644 --- a/homeassistant/components/pushover/config_flow.py +++ b/homeassistant/components/pushover/config_flow.py @@ -89,7 +89,6 @@ class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - self._async_abort_entries_match( { CONF_USER_KEY: user_input[CONF_USER_KEY], diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 9d6ae4b080d..2d79a0bf65a 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -52,7 +52,6 @@ async def async_get_service( ) -> PushoverNotificationService | None: """Get the Pushover notification service.""" if discovery_info is None: - async_create_issue( hass, DOMAIN, diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py index 6ab918f26fe..f3caac864d9 100644 --- a/homeassistant/components/qvr_pro/camera.py +++ b/homeassistant/components/qvr_pro/camera.py @@ -30,7 +30,6 @@ def setup_platform( entities = [] for channel in hass.data[DOMAIN]["channels"]: - stream_source = get_stream_source(channel["guid"], client) entities.append( QVRProCamera(**channel, stream_source=stream_source, client=client) diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 5e91d339d0d..bc27c0b2203 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -122,7 +122,7 @@ async def async_setup_entry( entity.entity_id: entity.zone_id for entity in zone_entities } - for (count, data) in enumerate(entity_id): + for count, data in enumerate(entity_id): if data in entity_to_zone_id: # Time can be passed as a list per zone, # or one time for all zones diff --git a/homeassistant/components/rainbird/config_flow.py b/homeassistant/components/rainbird/config_flow.py index 057fc6fe396..3ade67c79f7 100644 --- a/homeassistant/components/rainbird/config_flow.py +++ b/homeassistant/components/rainbird/config_flow.py @@ -138,7 +138,7 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_SERIAL_NUMBER: serial_number, } names: dict[str, str] = {} - for (zone, zone_config) in config.get(CONF_ZONES, {}).items(): + for zone, zone_config in config.get(CONF_ZONES, {}).items(): if name := zone_config.get(CONF_FRIENDLY_NAME): names[str(zone)] = name if names: diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index d839f183125..8f39bdb9b7f 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -121,7 +121,6 @@ class MutexPool(StaticPool): # type: ignore[misc] MutexPool.pool_lock.release() def _do_get(self) -> Any: - if DEBUG_MUTEX_POOL_TRACE: trace = traceback.extract_stack() trace_msg = "".join(traceback.format_list(trace[:-1])) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 01b10ea966a..4fb52dabb21 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -383,7 +383,6 @@ def _select_unused_event_data_ids( def _purge_unused_data_ids( instance: Recorder, session: Session, data_ids_batch: set[int], using_sqlite: bool ) -> None: - if unused_data_ids_set := _select_unused_event_data_ids( session, data_ids_batch, using_sqlite ): diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 1dda2fb87a6..54b1f249ca6 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -27,7 +27,7 @@ class ConfirmRepairFlow(RepairsFlow): self, user_input: dict[str, str] | None = None ) -> data_entry_flow.FlowResult: """Handle the first step of a fix flow.""" - return await (self.async_step_confirm()) + return await self.async_step_confirm() async def async_step_confirm( self, user_input: dict[str, str] | None = None diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index c26f9711e1f..1007ee1d2de 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -125,7 +125,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: headers=headers, timeout=timeout, ) as response: - if response.status < HTTPStatus.BAD_REQUEST: _LOGGER.debug( "Success. Url: %s. Status code: %d. Payload: %s", diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 03ad7ce071b..b729138f73e 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -104,7 +104,6 @@ async def async_setup_entry( device_id: DeviceTuple, entity_info: dict[str, Any], ) -> list[Entity]: - return [ RfxtrxBinarySensor( event.device, diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py index 27a67fd6640..7b7cbb7e94f 100644 --- a/homeassistant/components/rss_feed_template/__init__.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -43,7 +43,7 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the RSS feed template component.""" - for (feeduri, feedconfig) in config[DOMAIN].items(): + for feeduri, feedconfig in config[DOMAIN].items(): url = f"/api/rss_template/{feeduri}" requires_auth: bool = feedconfig["requires_api_password"] diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index fb3b4e46533..18f1ce9c451 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -225,7 +225,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (SERVICE_RESUME, async_resume_queue, SERVICE_BASE_SCHEMA), (SERVICE_SET_SPEED, async_set_queue_speed, SERVICE_SPEED_SCHEMA), ): - if hass.services.has_service(DOMAIN, service): continue diff --git a/homeassistant/components/sabnzbd/config_flow.py b/homeassistant/components/sabnzbd/config_flow.py index 073f4a08f76..d433b562183 100644 --- a/homeassistant/components/sabnzbd/config_flow.py +++ b/homeassistant/components/sabnzbd/config_flow.py @@ -52,7 +52,6 @@ class SABnzbdConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - errors = await self._async_validate_input(user_input) if not errors: diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 45786ae984a..b23f3d916cc 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -268,7 +268,6 @@ async def _create_script_entities( entities: list[ScriptEntity] = [] for script_config in script_configs: - entity = ScriptEntity( hass, script_config.key, diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index f3b413071e8..b7d4bca890e 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -82,7 +82,6 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input: - api_key = user_input[CONF_API_KEY] try: username = await async_validate_api(self.hass, api_key) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 5102ed9e4c3..6cd4c19c638 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -91,7 +91,6 @@ def async_restore_climate_entities( entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) for entry in entries: - if entry.domain != CLIMATE_DOMAIN: continue diff --git a/homeassistant/components/simplepush/config_flow.py b/homeassistant/components/simplepush/config_flow.py index 01ca508a5c4..702be4391e4 100644 --- a/homeassistant/components/simplepush/config_flow.py +++ b/homeassistant/components/simplepush/config_flow.py @@ -45,7 +45,6 @@ class SimplePushFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initiated by the user.""" errors: dict[str, str] | None = None if user_input is not None: - await self.async_set_unique_id(user_input[CONF_DEVICE_KEY]) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 60cc2cea0d4..4f0c3aae2aa 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -318,6 +318,7 @@ class DeviceBroker: def connect(self): """Connect handlers/listeners for device/lifecycle events.""" + # Setup interval to regenerate the refresh token on a periodic basis. # Tokens expire in 30 days and once expired, cannot be recovered. async def regenerate_refresh_token(now): diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 7fd192304a8..c20e5fe6e36 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -120,7 +120,6 @@ async def async_setup_platform( return if version == "3": - if not authkey: authproto = "none" if not privkey: diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index bd5078c4349..4699aaefd77 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -210,7 +210,6 @@ class SnmpSwitch(SwitchEntity): self._payload_off = payload_off if version == "3": - if not authkey: authproto = "none" if not privkey: diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index b1dfac7beed..09a6cfdf89f 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -180,7 +180,7 @@ class SonosSpeaker: (f"{SONOS_VANISHED}-{self.soco.uid}", self.async_vanished), ) - for (signal, target) in dispatch_pairs: + for signal, target in dispatch_pairs: entry.async_on_unload( async_dispatcher_connect( self.hass, diff --git a/homeassistant/components/spc/__init__.py b/homeassistant/components/spc/__init__.py index f3c829f5d0c..49dc7b13fca 100644 --- a/homeassistant/components/spc/__init__.py +++ b/homeassistant/components/spc/__init__.py @@ -41,7 +41,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the SPC component.""" async def async_upate_callback(spc_object): - if isinstance(spc_object, Area): async_dispatcher_send(hass, SIGNAL_UPDATE_ALARM.format(spc_object.id)) elif isinstance(spc_object, Zone): diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index cc8bbe672ba..f33aad68c3e 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -90,7 +90,6 @@ class SQLConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) if user_input is not None: - db_url = user_input.get(CONF_DB_URL, db_url_default) query = user_input[CONF_QUERY] column = user_input[CONF_COLUMN_NAME] diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index bed6780e523..b98b8a39dfa 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data.get(CONF_PASSWORD), ) hass.data[SRP_ENERGY_DOMAIN] = srp_energy_client - except (Exception) as ex: + except Exception as ex: _LOGGER.error("Unable to connect to Srp Energy: %s", str(ex)) raise ConfigEntryNotReady from ex diff --git a/homeassistant/components/srp_energy/config_flow.py b/homeassistant/components/srp_energy/config_flow.py index f3df156f746..2d5505b7631 100644 --- a/homeassistant/components/srp_energy/config_flow.py +++ b/homeassistant/components/srp_energy/config_flow.py @@ -34,7 +34,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=SRP_ENERGY_DOMAIN): if user_input is not None: try: - srp_client = SrpEnergyClient( user_input[CONF_ID], user_input[CONF_USERNAME], diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index c4c7ec728e0..1aaf5175e53 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -61,7 +61,7 @@ async def async_setup_entry( for _, _, _, kwh, _ in hourly_usage: previous_daily_usage += float(kwh) return previous_daily_usage - except (TimeoutError) as timeout_err: + except TimeoutError as timeout_err: raise UpdateFailed("Timeout communicating with API") from timeout_err except (ConnectError, HTTPError, Timeout, ValueError, TypeError) as err: raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 674855c6205..02818ccf8d6 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -63,7 +63,6 @@ async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: session = async_get_clientsession(hass) for server_conf in server_confs: - server_address = server_conf[CONF_SERVER] server = SuplaAPI(server_address, server_conf[CONF_ACCESS_TOKEN], session) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index f4565ea3757..9af6c307d4c 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -31,7 +31,6 @@ async def async_setup_entry( coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id] for surepy_entity in coordinator.data.values(): - # connectivity if surepy_entity.type in [ EntityType.CAT_FLAP, diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index 534f2396751..b6b2b75504e 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -29,7 +29,6 @@ async def async_setup_entry( coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id] for surepy_entity in coordinator.data.values(): - if surepy_entity.type in [ EntityType.CAT_FLAP, EntityType.PET_FLAP, diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index dfa8b8fba04..d51ce18ada4 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -125,7 +125,6 @@ class SwissHydrologicalDataSensor(SensorEntity): attrs = {} if not self._data: - return attrs attrs[ATTR_WATER_BODY_TYPE] = self._data["water-body-type"] diff --git a/homeassistant/components/switchbee/climate.py b/homeassistant/components/switchbee/climate.py index 3b42287e89f..bf780e76eb0 100644 --- a/homeassistant/components/switchbee/climate.py +++ b/homeassistant/components/switchbee/climate.py @@ -115,7 +115,6 @@ class SwitchBeeClimateEntity(SwitchBeeDeviceEntity[SwitchBeeThermostat], Climate super()._handle_coordinator_update() def _update_attrs_from_coordinator(self) -> None: - coordinator_device = self._get_coordinator_device() self._attr_hvac_mode: HVACMode = ( diff --git a/homeassistant/components/switchbee/light.py b/homeassistant/components/switchbee/light.py index 0ff14a45e2f..7227815d976 100644 --- a/homeassistant/components/switchbee/light.py +++ b/homeassistant/components/switchbee/light.py @@ -71,7 +71,6 @@ class SwitchBeeLightEntity(SwitchBeeDeviceEntity[SwitchBeeDimmer], LightEntity): super()._handle_coordinator_update() def _update_attrs_from_coordinator(self) -> None: - coordinator_device = self._get_coordinator_device() brightness = coordinator_device.brightness diff --git a/homeassistant/components/switchbee/switch.py b/homeassistant/components/switchbee/switch.py index f8f6e30e368..d48a3e2e02a 100644 --- a/homeassistant/components/switchbee/switch.py +++ b/homeassistant/components/switchbee/switch.py @@ -79,7 +79,6 @@ class SwitchBeeSwitchEntity(SwitchBeeDeviceEntity[_DeviceTypeT], SwitchEntity): coordinator_device = self._get_coordinator_device() if coordinator_device.state == -1: - self._check_if_became_offline() return diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 9f00009b322..cd46b4fd957 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -61,7 +61,6 @@ SERVICE_WRITE_SCHEMA = vol.Schema( def _figure_out_source( record: logging.LogRecord, call_stack: list[tuple[str, int]], paths_re: re.Pattern ) -> tuple[str, int]: - # If a stack trace exists, extract file names from the entire call stack. # The other case is when a regular "log" is made (without an attached # exception). In that case, just use the file where the log was made from. @@ -82,7 +81,6 @@ def _figure_out_source( # Iterate through the stack call (in reverse) and find the last call from # a file in Home Assistant. Try to figure out where error happened. for pathname in reversed(stack): - # Try to match with a file within Home Assistant if match := paths_re.match(pathname[0]): return (cast(str, match.group(1)), pathname[1]) diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index 4ab3f780ad3..b490b4c724c 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -290,7 +290,7 @@ async def async_start( # noqa: C901 for platform in PLATFORMS: tasmota_entities = tasmota_get_entities_for_platform(payload, platform) - for (tasmota_entity_config, discovery_hash) in tasmota_entities: + for tasmota_entity_config, discovery_hash in tasmota_entities: _discover_entity(tasmota_entity_config, discovery_hash, platform) async def async_sensors_discovered( @@ -316,7 +316,7 @@ async def async_start( # noqa: C901 ) if entry.domain == sensor.DOMAIN and entry.platform == DOMAIN } - for (tasmota_sensor_config, discovery_hash) in sensors: + for tasmota_sensor_config, discovery_hash in sensors: if tasmota_sensor_config: orphaned_entities.discard(tasmota_sensor_config.unique_id) _discover_entity(tasmota_sensor_config, discovery_hash, platform) diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index 18ae324a572..18bbe59a25f 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -42,7 +42,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._scan_interval = SCAN_INTERVAL def _get_auth_url(self): - self._session = Session( public_key=PUBLIC_KEY, private_key=NOT_SO_PRIVATE_KEY, diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 4c234e21875..e36ca75ed01 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -105,7 +105,6 @@ async def _async_create_entities(hass, config): covers = [] for object_id, entity_config in config[CONF_COVERS].items(): - entity_config = rewrite_common_legacy_to_modern_conf(entity_config) unique_id = entity_config.get(CONF_UNIQUE_ID) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 3d3c17551ca..88309810ad2 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -93,7 +93,6 @@ async def _async_create_entities(hass, config): fans = [] for object_id, entity_config in config[CONF_FANS].items(): - entity_config = rewrite_common_legacy_to_modern_conf(entity_config) unique_id = entity_config.get(CONF_UNIQUE_ID) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 9f282cb9b11..b21e02e4074 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -134,7 +134,6 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template is None: - # restore state after startup await super().async_added_to_hass() if state := await self.async_get_last_state(): diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index c99a44120e5..e10bc3b81d6 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -108,9 +108,7 @@ class TomatoDeviceScanner(DeviceScanner): # Calling and parsing the Tomato api here. We only need the # wldev and dhcpd_lease values. if response.status_code == HTTPStatus.OK: - for param, value in self.parse_api_pattern.findall(response.text): - if param in ("wldev", "dhcpd_lease"): self.last_results[param] = json.loads(value.replace("'", '"')) return True diff --git a/homeassistant/components/toon/coordinator.py b/homeassistant/components/toon/coordinator.py index 5819ff12743..58165935215 100644 --- a/homeassistant/components/toon/coordinator.py +++ b/homeassistant/components/toon/coordinator.py @@ -54,7 +54,6 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator[Status]): self.hass.config_entries.async_update_entry(self.entry, data=data) if cloud.async_active_subscription(self.hass): - if CONF_CLOUDHOOK_URL not in self.entry.data: try: webhook_url = await cloud.async_create_cloudhook( diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 601229ed2e0..bbc089de0c9 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -251,7 +251,11 @@ class TraccarScanner: """Update info from Traccar.""" _LOGGER.debug("Updating device data") try: - (self._devices, self._positions, self._geofences,) = await asyncio.gather( + ( + self._devices, + self._positions, + self._geofences, + ) = await asyncio.gather( self._api.get_devices(), self._api.get_positions(), self._api.get_geofences(), diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 8ecf0a99c31..678a0af2294 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -138,7 +138,6 @@ class TrainSensor(SensorEntity): self._from_station, self._to_station, when ) else: - _state = await self._train_api.async_get_next_train_stop( self._from_station, self._to_station, when ) diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index a21fe9b8837..b7784fbe4a9 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -62,7 +62,6 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - for entry in self._async_current_entries(): if ( entry.data[CONF_HOST] == user_input[CONF_HOST] diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 8a65c06b70e..f7b051f52c2 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -372,7 +372,6 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): and not self.device_class.startswith(DOMAIN) and description.native_unit_of_measurement is None ): - # We cannot have a device class, if the UOM isn't set or the # device class cannot be found in the validation mapping. if ( diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 0145439493b..1a04f6c712e 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -196,7 +196,6 @@ class TwinklyLight(LightEntity): ATTR_RGBW_COLOR in kwargs and kwargs[ATTR_RGBW_COLOR] != self._attr_rgbw_color ): - await self._client.interview() if LightEntityFeature.EFFECT & self.supported_features: # Static color only supports rgb @@ -223,7 +222,6 @@ class TwinklyLight(LightEntity): self._attr_rgbw_color = kwargs[ATTR_RGBW_COLOR] if ATTR_RGB_COLOR in kwargs and kwargs[ATTR_RGB_COLOR] != self._attr_rgb_color: - await self._client.interview() if LightEntityFeature.EFFECT & self.supported_features: await self._client.set_static_colour(kwargs[ATTR_RGB_COLOR]) diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index cd1983f9479..d98f9d5011c 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -173,7 +173,7 @@ class UkTransportLiveBusTimeSensor(UkTransportSensor): if self._data != {}: self._next_buses = [] - for (route, departures) in self._data["departures"].items(): + for route, departures in self._data["departures"].items(): for departure in departures: if self._destination_re.search(departure["direction"]): self._next_buses.append( diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 64ea7b39778..d4e885b295e 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -86,7 +86,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): errors = {} if user_input is not None: - self.config = { CONF_HOST: user_input[CONF_HOST], CONF_USERNAME: user_input[CONF_USERNAME], @@ -148,7 +147,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): ) -> FlowResult: """Select site to control.""" if user_input is not None: - unique_id = user_input[CONF_SITE_ID] self.config[CONF_SITE_ID] = self.site_ids[unique_id] diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index d26780ab019..2721e254de8 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -228,7 +228,6 @@ class UniFiController: def async_unifi_signalling_callback(self, signal, data): """Handle messages back from UniFi library.""" if signal == WebsocketSignal.CONNECTION_STATE: - if data == WebsocketState.DISCONNECTED and self.available: LOGGER.warning("Lost connection to UniFi Network") @@ -244,14 +243,12 @@ class UniFiController: LOGGER.info("Connected to UniFi Network") elif signal == WebsocketSignal.DATA and data: - if DATA_EVENT in data: clients_connected = set() devices_connected = set() wireless_clients_connected = False for event in data[DATA_EVENT]: - if event.key in CLIENT_CONNECTED: clients_connected.add(event.mac) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index a7678445c8c..c845b6d5d38 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -272,7 +272,6 @@ class UniFiClientTracker(UniFiClientBase, ScannerEntity): self.client.last_updated == SOURCE_EVENT and not self._only_listen_to_data_source ): - if (self.is_wired and self.client.event.key in WIRED_CONNECTION) or ( not self.is_wired and self.client.event.key in WIRELESS_CONNECTION ): diff --git a/homeassistant/components/unifi/services.py b/homeassistant/components/unifi/services.py index 4574a27016d..cba5f0b9d0e 100644 --- a/homeassistant/components/unifi/services.py +++ b/homeassistant/components/unifi/services.py @@ -89,14 +89,12 @@ async def async_remove_clients(hass, data) -> None: - Neither IP, hostname nor name is configured. """ for controller in hass.data[UNIFI_DOMAIN].values(): - if not controller.available: continue clients_to_remove = [] for client in controller.api.clients_all.values(): - if ( client.last_seen and client.first_seen diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index d0ac37f312c..42f83dad5d5 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -102,7 +102,6 @@ class UnifiDeviceScanner(DeviceScanner): self.connected = False def _get_update(self): - try: if not self.connected: self._connect() diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 29007571927..8c620402e77 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -98,7 +98,6 @@ CHIME_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( def _async_remove_adopt_button( hass: HomeAssistant, device: ProtectAdoptableDeviceModel ) -> None: - entity_registry = er.async_get(hass) if entity_id := entity_registry.async_get_entity_id( Platform.BUTTON, DOMAIN, f"{device.mac}_adopt" diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 4be8b489de9..1af4d82ff98 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -104,7 +104,6 @@ def _get_nvr_memory(obj: NVR) -> float | None: def _get_alarm_sound(obj: Sensor) -> str: - alarm_type = OBJECT_TYPE_NONE if ( obj.is_alarm_detected diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index ef8e02803f6..04007469992 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -54,7 +54,6 @@ async def async_setup_platform( # device_id --> "c1 or a3" X10 device. entity (type dictionary) # --> name, device_class etc for device_id, entity in config[CONF_DEVICES].items(): - _LOGGER.debug( "Add %s w800rf32.binary_sensor (class %s)", entity[CONF_NAME], @@ -128,7 +127,6 @@ class W800rf32BinarySensor(BinarySensorEntity): self.update_state(is_on) if self.is_on and self._off_delay is not None and self._delay_listener is None: - self._delay_listener = evt.async_track_point_in_time( self.hass, self._off_delay_listener, dt_util.utcnow() + self._off_delay ) diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index 972760f5af6..b026dd0ce32 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -295,6 +295,5 @@ class WasherDryerTimeClass(RestoreSensor): if isinstance(self._attr_native_value, datetime) and abs( new_timestamp - self._attr_native_value ) > timedelta(seconds=60): - self._attr_native_value = new_timestamp self._async_write_ha_state() diff --git a/homeassistant/components/ws66i/__init__.py b/homeassistant/components/ws66i/__init__.py index cffedc2f684..64f049aa9ac 100644 --- a/homeassistant/components/ws66i/__init__.py +++ b/homeassistant/components/ws66i/__init__.py @@ -41,7 +41,6 @@ def _find_zones(hass: HomeAssistant, ws66i: WS66i) -> list[int]: # Zones 21,31 - 26,36 are the daisy-chained amps zone_list = [] for amp_num in range(1, 4): - if amp_num > 1: # Don't add entities that aren't present status = ws66i.zone_status(amp_num * 10 + 1) diff --git a/homeassistant/components/zamg/coordinator.py b/homeassistant/components/zamg/coordinator.py index 4b25f4f7fbc..f785524e866 100644 --- a/homeassistant/components/zamg/coordinator.py +++ b/homeassistant/components/zamg/coordinator.py @@ -44,7 +44,7 @@ class ZamgDataUpdateCoordinator(DataUpdateCoordinator[ZamgDevice]): device = await self.zamg.update() except ZamgNoDataError as error: raise UpdateFailed("No response from API") from error - except (ZamgError) as error: + except ZamgError as error: raise UpdateFailed(f"Invalid response from API: {error}") from error self.data = device self.data["last_update"] = self.zamg.last_update diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 348052bc5f4..21e0dd8de1e 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -214,7 +214,7 @@ class ZamgSensor(CoordinatorEntity, SensorEntity): return self.coordinator.data[self.station_id][ self.entity_description.para_name ]["data"] - except (KeyError): + except KeyError: return None @property diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index c0959cb40c0..dd8b3b9c2a7 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -725,10 +725,8 @@ class ZHADevice(LogMixin): response = await getattr(cluster, commands[command].name)(*args) else: assert params is not None - response = await ( - getattr(cluster, commands[command].name)( - **convert_to_zcl_values(params, commands[command].schema) - ) + response = await getattr(cluster, commands[command].name)( + **convert_to_zcl_values(params, commands[command].schema) ) self.debug( "Issued cluster command: %s %s %s %s %s %s %s %s", diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 431ab8620fc..9f472a1a939 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -338,7 +338,6 @@ def retryable_req( def decorator(func): @functools.wraps(func) async def wrapper(channel, *args, **kwargs): - exceptions = (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) try_count, errors = 1, [] for delay in itertools.chain(delays, [None]): diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 18dd9aea344..f003d755033 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -210,9 +210,9 @@ class EntityPlatform: return @callback - def async_create_setup_task() -> Coroutine[ - Any, Any, None - ] | asyncio.Future[None]: + def async_create_setup_task() -> ( + Coroutine[Any, Any, None] | asyncio.Future[None] + ): """Get task to set up platform.""" if getattr(platform, "async_setup_platform", None): return platform.async_setup_platform( # type: ignore[union-attr] diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index a6060226d7b..12accf2725a 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -140,7 +140,6 @@ def get_url( # Try finding an URL in the order specified for url_type in order: - if allow_internal and url_type == TYPE_URL_INTERNAL: with suppress(NoURLAvailableError): return _get_internal_url( diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index ef8bee1fc7e..3c3da10db7c 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -613,7 +613,6 @@ async def entity_service_call( # noqa: C901 for platform in platforms: platform_entities = [] for entity in platform.entities.values(): - if entity.entity_id not in all_referenced: continue diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index a852f1b3161..569dc663707 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -2,7 +2,7 @@ autoflake==2.0.0 bandit==1.7.4 -black==22.12.0 +black==23.1.0 codespell==2.2.2 flake8-comprehensions==3.10.1 flake8-docstrings==1.6.0 diff --git a/script/translations/migrate.py b/script/translations/migrate.py index d3efdc28d13..f5bd60c30b4 100644 --- a/script/translations/migrate.py +++ b/script/translations/migrate.py @@ -263,7 +263,6 @@ def find_frontend_states(): from_key_base = f"state::{domain}" if domain in STATES_WITH_DEV_CLASS: - domain_to_write = dict(states) for device_class, dev_class_states in domain_to_write.items(): @@ -338,7 +337,6 @@ def apply_data_references(to_migrate): for step_data in steps.values(): step_data = step_data.get("data", {}) for key, value in step_data.items(): - if key in to_migrate and value != to_migrate[key]: if key.split("_")[0].lower() in value.lower(): step_data[key] = to_migrate[key] diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index c16fea4ef20..5982a02c59d 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -99,7 +99,6 @@ async def test_step_user(hass: HomeAssistant) -> None: conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} with patch("homeassistant.components.abode.config_flow.Abode"): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf ) diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index 4abde4d7620..b0b0def1d9a 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -53,7 +53,6 @@ async def test_invalid_api_key(hass): "homeassistant.components.accuweather.AccuWeather._async_get_data", side_effect=InvalidApiKeyError("Invalid API key"), ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -69,7 +68,6 @@ async def test_api_error(hass): "homeassistant.components.accuweather.AccuWeather._async_get_data", side_effect=ApiError("Invalid response from AccuWeather API"), ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -87,7 +85,6 @@ async def test_requests_exceeded_error(hass): "The allowed number of requests has been exceeded" ), ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -127,7 +124,6 @@ async def test_create_entry(hass): ), patch( "homeassistant.components.accuweather.async_setup_entry", return_value=True ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, diff --git a/tests/components/accuweather/test_init.py b/tests/components/accuweather/test_init.py index ee91fa4d219..6210aba2cec 100644 --- a/tests/components/accuweather/test_init.py +++ b/tests/components/accuweather/test_init.py @@ -75,7 +75,6 @@ async def test_update_interval(hass): "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", return_value=current, ) as mock_current: - assert mock_current.call_count == 0 async_fire_time_changed(hass, future) @@ -101,7 +100,6 @@ async def test_update_interval_forecast(hass): "homeassistant.components.accuweather.AccuWeather.async_get_forecast", return_value=forecast, ) as mock_forecast: - assert mock_current.call_count == 0 assert mock_forecast.call_count == 0 diff --git a/tests/components/adax/test_config_flow.py b/tests/components/adax/test_config_flow.py index 0c12c486754..ce6bf3ac9ae 100644 --- a/tests/components/adax/test_config_flow.py +++ b/tests/components/adax/test_config_flow.py @@ -41,7 +41,10 @@ async def test_form(hass: HomeAssistant) -> None: ) assert result2["type"] == FlowResultType.FORM - with patch("adax.get_adax_token", return_value="test_token",), patch( + with patch( + "adax.get_adax_token", + return_value="test_token", + ), patch( "homeassistant.components.adax.async_setup_entry", return_value=True, ) as mock_setup_entry: diff --git a/tests/components/airthings/test_config_flow.py b/tests/components/airthings/test_config_flow.py index 6e3a1579f70..3a0c852535e 100644 --- a/tests/components/airthings/test_config_flow.py +++ b/tests/components/airthings/test_config_flow.py @@ -25,7 +25,10 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch("airthings.get_token", return_value="test_token",), patch( + with patch( + "airthings.get_token", + return_value="test_token", + ), patch( "homeassistant.components.airthings.async_setup_entry", return_value=True, ) as mock_setup_entry: diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index 2192017c26f..f78bf36775f 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -129,7 +129,6 @@ async def test_cover_operation( "homeassistant.components.aladdin_connect.AladdinConnectClient", return_value=mock_aladdinconnect_api, ): - await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, @@ -183,7 +182,6 @@ async def test_cover_operation( "homeassistant.components.aladdin_connect.AladdinConnectClient", return_value=mock_aladdinconnect_api, ): - await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index 9084754d569..4d5e2678725 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -49,7 +49,6 @@ async def test_setup_login_error( "homeassistant.components.aladdin_connect.cover.AladdinConnectClient", return_value=mock_aladdinconnect_api, ): - assert await hass.config_entries.async_setup(config_entry.entry_id) is False @@ -68,7 +67,6 @@ async def test_setup_connection_error( "homeassistant.components.aladdin_connect.AladdinConnectClient", return_value=mock_aladdinconnect_api, ): - assert await hass.config_entries.async_setup(config_entry.entry_id) is False @@ -84,7 +82,6 @@ async def test_setup_component_no_error(hass: HomeAssistant) -> None: "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", return_value=True, ): - assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -107,7 +104,6 @@ async def test_entry_password_fail( "homeassistant.components.aladdin_connect.AladdinConnectClient", return_value=mock_aladdinconnect_api, ): - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state is ConfigEntryState.SETUP_ERROR @@ -128,7 +124,6 @@ async def test_load_and_unload( "homeassistant.components.aladdin_connect.AladdinConnectClient", return_value=mock_aladdinconnect_api, ): - await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index a84d7342490..7ca10c5a23c 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1522,7 +1522,10 @@ async def test_media_player_speaker(hass): ) assert call.data["is_volume_muted"] - call, _, = await assert_request_calls_service( + ( + call, + _, + ) = await assert_request_calls_service( "Alexa.Speaker", "SetMute", "media_player#test_speaker", @@ -1571,7 +1574,10 @@ async def test_media_player_step_speaker(hass): ) assert call.data["is_volume_muted"] - call, _, = await assert_request_calls_service( + ( + call, + _, + ) = await assert_request_calls_service( "Alexa.StepSpeaker", "SetMute", "media_player#test_step_speaker", diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index bd16ce96ffe..f4f09fc160f 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -400,7 +400,6 @@ async def test_dev_url_error(hass, aioclient_mock, caplog): with patch( "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION_DEV ): - await analytics.send_analytics() payload = aioclient_mock.mock_calls[0] @@ -420,7 +419,6 @@ async def test_nightly_endpoint(hass, aioclient_mock): with patch( "homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION_NIGHTLY ): - await analytics.send_analytics() payload = aioclient_mock.mock_calls[0] diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index 03101733019..856762a3d3e 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -125,7 +125,6 @@ async def test_user_adbkey(hass): CONNECT_METHOD, return_value=(MockConfigDevice(), None), ), PATCH_ISFILE, PATCH_ACCESS, PATCH_SETUP_ENTRY as mock_setup_entry: - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True}, diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index 73b4b67bc04..88d024356d8 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -33,7 +33,10 @@ async def test_form(hass): assert result["type"] == "form" assert result["errors"] == {} - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( + with patch( + "aurorapy.client.AuroraSerialClient.connect", + return_value=None, + ), patch( "aurorapy.client.AuroraSerialClient.serial_number", return_value="9876543", ), patch( @@ -52,7 +55,6 @@ async def test_form(hass): "homeassistant.components.aurora_abb_powerone.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, @@ -140,7 +142,10 @@ async def test_form_invalid_com_ports(hass): "aurorapy.client.AuroraSerialClient.connect", side_effect=AuroraError("...Some other message!!!123..."), return_value=None, - ), patch("serial.Serial.isOpen", return_value=True,), patch( + ), patch( + "serial.Serial.isOpen", + return_value=True, + ), patch( "aurorapy.client.AuroraSerialClient.close", ) as mock_clientclose: result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/aussie_broadband/test_config_flow.py b/tests/components/aussie_broadband/test_config_flow.py index ed98924e19d..9b4be6d9463 100644 --- a/tests/components/aussie_broadband/test_config_flow.py +++ b/tests/components/aussie_broadband/test_config_flow.py @@ -187,7 +187,6 @@ async def test_reauth(hass: HomeAssistant) -> None: with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( "aussiebb.asyncio.AussieBB.login", side_effect=AuthenticationException() ), patch("aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]]): - result6 = await hass.config_entries.flow.async_configure( result5["flow_id"], { @@ -202,7 +201,6 @@ async def test_reauth(hass: HomeAssistant) -> None: with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( "aussiebb.asyncio.AussieBB.login", return_value=True ), patch("aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]]): - result7 = await hass.config_entries.flow.async_configure( result6["flow_id"], { diff --git a/tests/components/aws/test_init.py b/tests/components/aws/test_init.py index 3b1dcaea1e5..7875f366fcc 100644 --- a/tests/components/aws/test_init.py +++ b/tests/components/aws/test_init.py @@ -85,7 +85,6 @@ async def test_profile_credential(hass): with async_patch( "homeassistant.components.aws.AioSession", return_value=mock_session ): - await async_setup_component( hass, "aws", diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index 38f20df7ff0..b86c63d2eab 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -205,7 +205,6 @@ async def setup_config_entry_fixture(hass, config_entry, setup_default_vapix_req def mock_axis_rtspclient(): """No real RTSP communication allowed.""" with patch("axis.stream_manager.RTSPClient") as rtsp_client_mock: - rtsp_client_mock.return_value.session.state = State.STOPPED async def start_stream(): diff --git a/tests/components/backup/test_http.py b/tests/components/backup/test_http.py index 708abec057b..f3c459e2efd 100644 --- a/tests/components/backup/test_http.py +++ b/tests/components/backup/test_http.py @@ -27,7 +27,6 @@ async def test_downloading_backup( "homeassistant.components.backup.http.FileResponse", return_value=web.Response(text=""), ): - resp = await client.get("/api/backup/download/abc123") assert resp.status == 200 diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 4179eb026c5..2e8ac8f2f56 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -24,7 +24,6 @@ async def test_info( "homeassistant.components.backup.websocket.BackupManager.get_backups", return_value={TEST_BACKUP.slug: TEST_BACKUP}, ): - await client.send_json({"id": 1, "type": "backup/info"}) msg = await client.receive_json() diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 9d5d92f8dd2..22834f48314 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -149,7 +149,9 @@ async def test_form_2fa_invalid_key(hass): assert result2["type"] == "form" assert result2["step_id"] == "2fa" - with patch("homeassistant.components.blink.config_flow.Auth.startup",), patch( + with patch( + "homeassistant.components.blink.config_flow.Auth.startup", + ), patch( "homeassistant.components.blink.config_flow.Auth.check_key_required", return_value=False, ), patch( diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index 5c6bbccd443..b0fc9cfa40e 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -361,7 +361,10 @@ async def test_restore_history_remote_adapter( if address != "E3:A5:63:3E:5E:23": timestamps[address] = now - with patch("bluetooth_adapters.systems.linux.LinuxAdapters.history", {},), patch( + with patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.history", + {}, + ), patch( "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index f4c9beafd4a..17807bf8d3a 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -450,7 +450,6 @@ async def test_diagnostics_remote_adapter( "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects", return_value={}, ): - entry1 = MockConfigEntry( domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" ) diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index b2be6bf7280..1237fd89407 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -180,10 +180,10 @@ def _generate_scanners_with_fake_devices(hass): hass, "00:00:00:00:00:02", "hci1", new_info_callback, None, True ) - for (device, adv_data) in hci0_device_advs.values(): + for device, adv_data in hci0_device_advs.values(): scanner_hci0.inject_advertisement(device, adv_data) - for (device, adv_data) in hci1_device_advs.values(): + for device, adv_data in hci1_device_advs.values(): scanner_hci1.inject_advertisement(device, adv_data) cancel_hci0 = manager.async_register_scanner(scanner_hci0, True, 2) diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 04f8c534cf2..4bacd9a4479 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -78,7 +78,6 @@ async def test_preserve_new_tracked_device_name( ) as mock_async_discovered_service_info, patch.object( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfoBleak( name=name, address=address, @@ -147,7 +146,6 @@ async def test_tracking_battery_times_out( ) as mock_async_discovered_service_info, patch.object( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfoBleak( name=name, address=address, @@ -213,7 +211,6 @@ async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_ ) as mock_async_discovered_service_info, patch.object( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfoBleak( name=name, address=address, @@ -281,7 +278,6 @@ async def test_tracking_battery_successful( ) as mock_async_discovered_service_info, patch.object( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfoBleak( name=name, address=address, diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 5a145edf030..832e874a466 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -118,7 +118,6 @@ async def test_unsupported_model_error(hass: HomeAssistant) -> None: with patch("brother.Brother.initialize"), patch( "brother.Brother._get_data", side_effect=UnsupportedModel("error") ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) @@ -149,7 +148,6 @@ async def test_zeroconf_snmp_error(hass: HomeAssistant) -> None: with patch("brother.Brother.initialize"), patch( "brother.Brother._get_data", side_effect=SnmpError("error") ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -259,7 +257,6 @@ async def test_zeroconf_confirm_create_entry(hass: HomeAssistant) -> None: "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 0a895dbe297..7de66613255 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -73,7 +73,7 @@ async def test_sensors_pro(hass, canary) -> None: ), } - for (sensor_id, data) in sensors.items(): + for sensor_id, data in sensors.items(): entity_entry = registry.async_get(f"sensor.{sensor_id}") assert entity_entry assert entity_entry.original_device_class == data[3] @@ -192,7 +192,7 @@ async def test_sensors_flex(hass, canary) -> None: ), } - for (sensor_id, data) in sensors.items(): + for sensor_id, data in sensors.items(): entity_entry = registry.async_get(f"sensor.{sensor_id}") assert entity_entry assert entity_entry.original_device_class == data[3] diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index c476b3ef376..773d65f27d8 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -99,7 +99,6 @@ async def test_integration_services(hass, cfupdate): True, ), ): - await hass.services.async_call( DOMAIN, SERVICE_UPDATE_RECORDS, diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index d85279fc30c..08d87d5e987 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -20,7 +20,10 @@ async def test_form_home(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( + with patch( + "CO2Signal.get_latest", + return_value=VALID_PAYLOAD, + ), patch( "homeassistant.components.co2signal.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -59,7 +62,10 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: ) assert result2["type"] == FlowResultType.FORM - with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( + with patch( + "CO2Signal.get_latest", + return_value=VALID_PAYLOAD, + ), patch( "homeassistant.components.co2signal.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -100,7 +106,10 @@ async def test_form_country(hass: HomeAssistant) -> None: ) assert result2["type"] == FlowResultType.FORM - with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( + with patch( + "CO2Signal.get_latest", + return_value=VALID_PAYLOAD, + ), patch( "homeassistant.components.co2signal.async_setup_entry", return_value=True, ) as mock_setup_entry: diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index 80d394c38ef..2ffb714975a 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -196,7 +196,6 @@ async def test_option_form(hass): ), patch( "homeassistant.components.coinbase.update_listener" ) as mock_update_listener: - config_entry = await init_mock_coinbase(hass) await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) diff --git a/tests/components/coinbase/test_diagnostics.py b/tests/components/coinbase/test_diagnostics.py index f997b02f853..726f0ef4cab 100644 --- a/tests/components/coinbase/test_diagnostics.py +++ b/tests/components/coinbase/test_diagnostics.py @@ -27,7 +27,6 @@ async def test_entry_diagnostics(hass, hass_client: ClientSession): "coinbase.wallet.client.Client.get_exchange_rates", return_value=mock_get_exchange_rates(), ): - config_entry = await init_mock_coinbase(hass) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index 213ce3b2e08..afa63579572 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -169,7 +169,6 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): AncillaryControlPanel.ARMING_NIGHT, AncillaryControlPanel.ARMING_STAY, }: - event_changed_sensor = { "t": "event", "e": "changed", @@ -188,7 +187,6 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): AncillaryControlPanel.ENTRY_DELAY, AncillaryControlPanel.EXIT_DELAY, }: - event_changed_sensor = { "t": "event", "e": "changed", diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index ffd58ec5ea8..1eb97f3e6a6 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -146,7 +146,9 @@ async def _async_get_handle_dhcp_packet(hass, integration_matchers): async_handle_dhcp_packet = _async_handle_dhcp_packet return MagicMock() - with patch("homeassistant.components.dhcp._verify_l2socket_setup",), patch( + with patch( + "homeassistant.components.dhcp._verify_l2socket_setup", + ), patch( "scapy.arch.common.compile_filter" ), patch("scapy.sendrecv.AsyncSniffer", _mock_sniffer): await dhcp_watcher.async_start() diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 889cc92c969..35beed727e6 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -434,7 +434,7 @@ async def test_available_device( assert device.name == "device_name" # Check entity state gets updated when device changes state - for (dev_state, ent_state) in [ + for dev_state, ent_state in [ (None, MediaPlayerState.ON), (TransportState.STOPPED, MediaPlayerState.IDLE), (TransportState.PLAYING, MediaPlayerState.PLAYING), diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index ca94d05f743..95da581aafb 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -175,7 +175,6 @@ async def test_import_flow_triggered_with_ecobee_conf_and_invalid_data(hass): ), patch.object( flow, "async_step_user", return_value=mock_coro() ) as mock_async_step_user: - await flow.async_step_import(import_data=None) mock_async_step_user.assert_called_once_with( diff --git a/tests/components/elkm1/__init__.py b/tests/components/elkm1/__init__.py index b818570ec6d..cb34f78360a 100644 --- a/tests/components/elkm1/__init__.py +++ b/tests/components/elkm1/__init__.py @@ -50,7 +50,10 @@ def _patch_elk(elk=None): @contextmanager def _patcher(): - with patch("homeassistant.components.elkm1.config_flow.Elk", new=_elk,), patch( + with patch( + "homeassistant.components.elkm1.config_flow.Elk", + new=_elk, + ), patch( "homeassistant.components.elkm1.config_flow.Elk", new=_elk, ): diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index 5c91460237e..ba007a9987a 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -51,7 +51,6 @@ async def test_notify_file(hass: HomeAssistant, timestamp: bool): with patch("homeassistant.components.file.notify.open", m_open, create=True), patch( "homeassistant.components.file.notify.os.stat" ) as mock_st, patch("homeassistant.util.dt.utcnow", return_value=dt_util.utcnow()): - mock_st.return_value.st_size = 0 title = ( f"{ATTR_TITLE_DEFAULT} notifications " diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py index 2bdc1ed82a0..7010a3e9a4d 100644 --- a/tests/components/filesize/test_config_flow.py +++ b/tests/components/filesize/test_config_flow.py @@ -73,7 +73,9 @@ async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: await async_create_file(hass, TEST_FILE) - with patch("homeassistant.components.filesize.config_flow.pathlib.Path",), patch( + with patch( + "homeassistant.components.filesize.config_flow.pathlib.Path", + ), patch( "homeassistant.components.filesize.async_setup_entry", return_value=True, ): @@ -87,7 +89,9 @@ async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "not_allowed"} hass.config.allowlist_external_dirs = {TEST_DIR} - with patch("homeassistant.components.filesize.config_flow.pathlib.Path",), patch( + with patch( + "homeassistant.components.filesize.config_flow.pathlib.Path", + ), patch( "homeassistant.components.filesize.async_setup_entry", return_value=True, ): diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index 35467ffc449..a26f8030513 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -81,7 +81,6 @@ async def test_step_user(hass): "homeassistant.components.fireservicerota.async_setup_entry", return_value=True, ) as mock_setup_entry: - mock_fireservicerota = mock_fsr.return_value mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO diff --git a/tests/components/firmata/test_config_flow.py b/tests/components/firmata/test_config_flow.py index 22f8b326781..d4bf159c5c9 100644 --- a/tests/components/firmata/test_config_flow.py +++ b/tests/components/firmata/test_config_flow.py @@ -70,7 +70,6 @@ async def test_import(hass: HomeAssistant) -> None: ) as mock_setup, patch( "homeassistant.components.firmata.async_setup_entry", return_value=True ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, diff --git a/tests/components/freedompro/test_binary_sensor.py b/tests/components/freedompro/test_binary_sensor.py index f43857038ef..f366d89a9a9 100644 --- a/tests/components/freedompro/test_binary_sensor.py +++ b/tests/components/freedompro/test_binary_sensor.py @@ -71,7 +71,6 @@ async def test_binary_sensor_get_state( "homeassistant.components.freedompro.get_states", return_value=[], ): - async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() @@ -98,7 +97,6 @@ async def test_binary_sensor_get_state( "homeassistant.components.freedompro.get_states", return_value=states_response, ): - async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() diff --git a/tests/components/freedompro/test_config_flow.py b/tests/components/freedompro/test_config_flow.py index d80bd22736c..c06e37a0333 100644 --- a/tests/components/freedompro/test_config_flow.py +++ b/tests/components/freedompro/test_config_flow.py @@ -32,7 +32,6 @@ async def test_invalid_auth(hass): "code": -201, }, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -51,7 +50,6 @@ async def test_connection_error(hass): "code": -200, }, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -70,7 +68,6 @@ async def test_create_entry(hass): "devices": DEVICES, }, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, diff --git a/tests/components/freedompro/test_sensor.py b/tests/components/freedompro/test_sensor.py index 0405c5393ce..47a29fa8db9 100644 --- a/tests/components/freedompro/test_sensor.py +++ b/tests/components/freedompro/test_sensor.py @@ -60,7 +60,6 @@ async def test_sensor_get_state( "homeassistant.components.freedompro.get_states", return_value=states_response, ): - async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index edbde883f56..18a6d36fe9f 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -57,7 +57,6 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods service = service + "1" if kwargs: - if (index := kwargs.get("NewIndex")) is None: index = next(iter(kwargs.values())) diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 1eeb779da1f..80a272e9477 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -49,7 +49,6 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): "homeassistant.components.fritz.config_flow.socket.gethostbyname", return_value=MOCK_IPS["fritz.box"], ): - mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST mock_request_post.return_value.status_code = 200 @@ -100,7 +99,6 @@ async def test_user_already_configured( "homeassistant.components.fritz.config_flow.socket.gethostbyname", return_value=MOCK_IPS["fritz.box"], ): - mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST mock_request_post.return_value.status_code = 200 @@ -133,7 +131,6 @@ async def test_exception_security(hass: HomeAssistant, mock_get_source_ip): "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=FritzSecurityError, ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) @@ -156,7 +153,6 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip): "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=FritzConnectionException, ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) @@ -179,7 +175,6 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip): "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=OSError, ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) @@ -210,7 +205,6 @@ async def test_reauth_successful( ) as mock_request_get, patch( "requests.post" ) as mock_request_post: - mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST mock_request_post.return_value.status_code = 200 @@ -251,7 +245,6 @@ async def test_reauth_not_successful( "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=FritzConnectionException, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, @@ -293,7 +286,6 @@ async def test_ssdp_already_configured( "homeassistant.components.fritz.config_flow.socket.gethostbyname", return_value=MOCK_IPS["fritz.box"], ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) @@ -320,7 +312,6 @@ async def test_ssdp_already_configured_host( "homeassistant.components.fritz.config_flow.socket.gethostbyname", return_value=MOCK_IPS["fritz.box"], ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) @@ -347,7 +338,6 @@ async def test_ssdp_already_configured_host_uuid( "homeassistant.components.fritz.config_flow.socket.gethostbyname", return_value=MOCK_IPS["fritz.box"], ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) @@ -363,7 +353,6 @@ async def test_ssdp_already_in_progress_host( "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=fc_class_mock, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) @@ -395,7 +384,6 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): ) as mock_request_get, patch( "requests.post" ) as mock_request_post: - mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST mock_request_post.return_value.status_code = 200 @@ -429,7 +417,6 @@ async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip): "homeassistant.components.fritz.config_flow.FritzConnection", side_effect=FritzConnectionException, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index 69f1dffa64b..9c586352052 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -302,7 +302,10 @@ async def test_dhcp_invalid(hass, aioclient_mock): """Test starting a flow from discovery.""" with patch( "homeassistant.components.fronius.config_flow.DHCP_REQUEST_DELAY", 0 - ), patch("pyfronius.Fronius.current_logger_info", side_effect=FroniusError,), patch( + ), patch( + "pyfronius.Fronius.current_logger_info", + side_effect=FroniusError, + ), patch( "pyfronius.Fronius.inverter_info", side_effect=FroniusError, ): diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 04c7fcca5b5..f41f4c4f592 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -486,7 +486,6 @@ async def test_no_still_image_url(hass, hass_client): "homeassistant.components.generic.camera.GenericCamera.stream_source", return_value=None, ) as mock_stream_source: - # First test when there is no stream_source should fail resp = await client.get("/api/camera_proxy/camera.config_test") await hass.async_block_till_done() @@ -494,7 +493,6 @@ async def test_no_still_image_url(hass, hass_client): assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR with patch("homeassistant.components.camera.create_stream") as mock_create_stream: - # Now test when creating the stream succeeds mock_stream = Mock() mock_stream.async_get_image = AsyncMock() diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index eabac97d1cc..70b337430c5 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -27,7 +27,7 @@ def assert_sensor_state( assert state actual_state = state.state assert actual_state == expected_state - for (key, value) in attributes.items(): + for key, value in attributes.items(): assert key in state.attributes assert state.attributes[key] == value diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index c95c6af50f4..710f97cdc59 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -161,7 +161,6 @@ async def test_hddtemp_multiple_disks(hass, telnetmock): "sensor.hd_temperature_dev_sdb1", "sensor.hd_temperature_dev_sdc1", ]: - state = hass.states.get(sensor) reference = REFERENCE[state.attributes.get("device")] diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 3cb32608159..96d54fcde39 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -630,7 +630,6 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin "homeassistant.components.recorder.history.state_changes_during_period", _fake_states, ): - with freeze_time(start_time): await async_setup_component( hass, @@ -730,7 +729,6 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin "homeassistant.components.recorder.history.state_changes_during_period", _fake_states, ): - with freeze_time(start_time): await async_setup_component( hass, @@ -829,7 +827,6 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul "homeassistant.components.recorder.history.state_changes_during_period", _fake_states, ): - with freeze_time(start_time): await async_setup_component( hass, @@ -967,7 +964,6 @@ async def test_does_not_work_into_the_future(recorder_mock, hass): "homeassistant.components.recorder.history.state_changes_during_period", _fake_states, ): - with freeze_time(start_time): await async_setup_component( hass, diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index ff970a0e8c5..a416f030a05 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -134,7 +134,6 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: "homeassistant.components.honeywell.async_setup_entry", return_value=True, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={ diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index aad574bd1db..4be703c808f 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -34,7 +34,7 @@ def create_mock_api_discovery(aioclient_mock, bridges): URL_NUPNP, json=[{"internalipaddress": host, "id": id} for (host, id) in bridges], ) - for (host, bridge_id) in bridges: + for host, bridge_id in bridges: aioclient_mock.get( f"http://{host}/api/config", json={"bridgeid": bridge_id}, diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 3bce9f9ec83..74ece83c3df 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -151,7 +151,6 @@ async def test_security_vuln_check(hass): ) ), ): - assert await async_setup_component(hass, "hue", {}) await hass.async_block_till_done() diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index e270079de8c..ab77729e86d 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -206,7 +206,10 @@ async def test_form_entry_exists(hass): with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( "energyflip.EnergyFlip.customer_overview", return_value=None - ), patch("energyflip.EnergyFlip.get_user_id", return_value="test-id",), patch( + ), patch( + "energyflip.EnergyFlip.get_user_id", + return_value="test-id", + ), patch( "homeassistant.components.huisbaasje.async_setup_entry", return_value=True, ): diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index bd792f3230f..16b6b87fbb1 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -36,7 +36,6 @@ async def test_setup_entry(hass: HomeAssistant): "energyflip.EnergyFlip.current_measurements", return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements: - hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( version=1, @@ -364,7 +363,6 @@ async def test_setup_entry_absent_measurement(hass: HomeAssistant): "energyflip.EnergyFlip.current_measurements", return_value=MOCK_LIMITED_CURRENT_MEASUREMENTS, ) as mock_current_measurements: - hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( version=1, diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 126e61cf629..82995473b27 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -42,7 +42,6 @@ async def test_user_flow(hass): "homeassistant.components.hvv_departures.async_setup_entry", return_value=True, ): - # step: user result_user = await hass.config_entries.flow.async_init( @@ -103,7 +102,6 @@ async def test_user_flow_no_results(hass): "homeassistant.components.hvv_departures.async_setup_entry", return_value=True, ): - # step: user result_user = await hass.config_entries.flow.async_init( @@ -139,7 +137,6 @@ async def test_user_flow_invalid_auth(hass): "Authentication failed!", ), ): - # step: user result_user = await hass.config_entries.flow.async_init( DOMAIN, @@ -162,7 +159,6 @@ async def test_user_flow_cannot_connect(hass): "homeassistant.components.hvv_departures.hub.GTI.init", side_effect=CannotConnect(), ): - # step: user result_user = await hass.config_entries.flow.async_init( DOMAIN, @@ -188,7 +184,6 @@ async def test_user_flow_station(hass): "homeassistant.components.hvv_departures.hub.GTI.checkName", return_value={"returnCode": "OK", "results": []}, ): - # step: user result_user = await hass.config_entries.flow.async_init( diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 99a505cc630..c18b4e48946 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -84,9 +84,10 @@ async def _init_form(hass, modem_type): async def _device_form(hass, flow_id, connection, user_input): """Test the PLM, Hub v1 or Hub v2 form.""" - with patch(PATCH_CONNECTION, new=connection,), patch( - PATCH_ASYNC_SETUP, return_value=True - ) as mock_setup, patch( + with patch( + PATCH_CONNECTION, + new=connection, + ), patch(PATCH_ASYNC_SETUP, return_value=True) as mock_setup, patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ) as mock_setup_entry: @@ -200,7 +201,10 @@ async def test_failed_connection_hub(hass: HomeAssistant): async def _import_config(hass, config): """Run the import step.""" - with patch(PATCH_CONNECTION, new=mock_successful_connection,), patch( + with patch( + PATCH_CONNECTION, + new=mock_successful_connection, + ), patch( PATCH_ASYNC_SETUP, return_value=True ), patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): return await hass.config_entries.flow.async_init( @@ -287,7 +291,10 @@ async def test_import_existing(hass: HomeAssistant): async def test_import_failed_connection(hass: HomeAssistant): """Test a failed connection on import.""" - with patch(PATCH_CONNECTION, new=mock_failed_connection,), patch( + with patch( + PATCH_CONNECTION, + new=mock_failed_connection, + ), patch( PATCH_ASYNC_SETUP, return_value=True ), patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 9e5d5627a94..93aef0944aa 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -144,7 +144,6 @@ async def test_manual_entry( "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -190,7 +189,6 @@ async def test_multi_discovery_cannot_connect( "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], ): - mock_intellifire_config_flow.poll.side_effect = ConnectionError result = await hass.config_entries.flow.async_init( diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index e254ba402fb..c2a590b5b31 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -89,7 +89,6 @@ async def test_flow_entry_created_from_user_input(): "async_entries", return_value=[], ) as config_entries: - result = await flow.async_step_user(user_input=test_data) assert result["type"] == "create_entry" @@ -116,7 +115,6 @@ async def test_flow_entry_config_entry_already_exists(): ) as config_form, patch.object( flow.hass.config_entries, "async_entries", return_value={"home": test_data} ) as config_entries: - await flow.async_step_user(user_input=test_data) assert len(config_form.mock_calls) == 1 diff --git a/tests/components/ipma/test_init.py b/tests/components/ipma/test_init.py index 8dd808b1b1b..f80e1b0743d 100644 --- a/tests/components/ipma/test_init.py +++ b/tests/components/ipma/test_init.py @@ -19,7 +19,6 @@ async def test_async_setup_raises_entry_not_ready(hass): with patch( "pyipma.location.Location.get", side_effect=IPMAException("API unavailable") ): - config_entry = MockConfigEntry( domain=DOMAIN, title="Home", diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py index a806bea3056..b73dc6681cf 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -21,7 +21,6 @@ async def test_create_entry(hass: HomeAssistant): assert result.get("step_id") == SOURCE_USER with patch("homeassistant.components.iss.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py index 8325cec9209..67c9bf5752c 100644 --- a/tests/components/lacrosse_view/test_config_flow.py +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -19,7 +19,10 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch("lacrosse_view.LaCrosse.login", return_value=True,), patch( + with patch( + "lacrosse_view.LaCrosse.login", + return_value=True, + ), patch( "lacrosse_view.LaCrosse.get_locations", return_value=[Location(id=1, name="Test")], ), patch( @@ -189,7 +192,10 @@ async def test_already_configured_device(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch("lacrosse_view.LaCrosse.login", return_value=True,), patch( + with patch( + "lacrosse_view.LaCrosse.login", + return_value=True, + ), patch( "lacrosse_view.LaCrosse.get_locations", return_value=[Location(id=1, name="Test")], ): diff --git a/tests/components/launch_library/test_config_flow.py b/tests/components/launch_library/test_config_flow.py index a9dd794d05e..a33b957b329 100644 --- a/tests/components/launch_library/test_config_flow.py +++ b/tests/components/launch_library/test_config_flow.py @@ -21,7 +21,6 @@ async def test_create_entry(hass): with patch( "homeassistant.components.launch_library.async_setup_entry", return_value=True ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, diff --git a/tests/components/led_ble/test_config_flow.py b/tests/components/led_ble/test_config_flow.py index 6767302af50..3948a599260 100644 --- a/tests/components/led_ble/test_config_flow.py +++ b/tests/components/led_ble/test_config_flow.py @@ -31,7 +31,9 @@ async def test_user_step_success(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch("homeassistant.components.led_ble.config_flow.LEDBLE.update",), patch( + with patch( + "homeassistant.components.led_ble.config_flow.LEDBLE.update", + ), patch( "homeassistant.components.led_ble.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -115,7 +117,9 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} - with patch("homeassistant.components.led_ble.config_flow.LEDBLE.update",), patch( + with patch( + "homeassistant.components.led_ble.config_flow.LEDBLE.update", + ), patch( "homeassistant.components.led_ble.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -165,7 +169,9 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} - with patch("homeassistant.components.led_ble.config_flow.LEDBLE.update",), patch( + with patch( + "homeassistant.components.led_ble.config_flow.LEDBLE.update", + ), patch( "homeassistant.components.led_ble.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -197,7 +203,9 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch("homeassistant.components.led_ble.config_flow.LEDBLE.update",), patch( + with patch( + "homeassistant.components.led_ble.config_flow.LEDBLE.update", + ), patch( "homeassistant.components.led_ble.async_setup_entry", return_value=True, ) as mock_setup_entry: diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py index ee5b718fa60..05a114e7188 100644 --- a/tests/components/litterrobot/test_config_flow.py +++ b/tests/components/litterrobot/test_config_flow.py @@ -140,7 +140,6 @@ async def test_step_reauth(hass: HomeAssistant, mock_account: Account) -> None: "homeassistant.components.litterrobot.async_setup_entry", return_value=True, ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, @@ -190,7 +189,6 @@ async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> "homeassistant.components.litterrobot.async_setup_entry", return_value=True, ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, diff --git a/tests/components/local_file/test_camera.py b/tests/components/local_file/test_camera.py index 8432420244e..56d79a0c06c 100644 --- a/tests/components/local_file/test_camera.py +++ b/tests/components/local_file/test_camera.py @@ -145,7 +145,6 @@ async def test_update_file_path(hass): "homeassistant.components.local_file.camera.mimetypes.guess_type", mock.Mock(return_value=(None, None)), ): - camera_1 = {"platform": "local_file", "file_path": "mock/path.jpg"} camera_2 = { "platform": "local_file", diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index 2f3c765cfa8..e030e198787 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -118,7 +118,6 @@ def mock_melnor_device(): """Return a mocked Melnor device.""" with patch("melnor_bluetooth.device.Device") as mock: - device = mock.return_value device.connect = AsyncMock(return_value=True) diff --git a/tests/components/melnor/test_config_flow.py b/tests/components/melnor/test_config_flow.py index 364531a314a..9fda8dcae7c 100644 --- a/tests/components/melnor/test_config_flow.py +++ b/tests/components/melnor/test_config_flow.py @@ -22,7 +22,6 @@ async def test_user_step_no_devices(hass): with patch_async_setup_entry() as mock_setup_entry, patch_async_discovered_service_info( [] ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, @@ -38,7 +37,6 @@ async def test_user_step_discovered_devices(hass): """Test we properly handle device picking.""" with patch_async_setup_entry() as mock_setup_entry, patch_async_discovered_service_info(): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, @@ -68,7 +66,6 @@ async def test_user_step_with_existing_device(hass): with patch_async_setup_entry() as mock_setup_entry, patch_async_discovered_service_info( [FAKE_SERVICE_INFO_1, FAKE_SERVICE_INFO_2] ): - # Create the config flow result = await hass.config_entries.flow.async_init( DOMAIN, @@ -105,7 +102,6 @@ async def test_bluetooth_discovered(hass): """Test we short circuit to config entry creation.""" with patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, @@ -123,7 +119,6 @@ async def test_bluetooth_confirm(hass): """Test we short circuit to config entry creation.""" with patch_async_setup_entry() as mock_setup_entry: - # Create the config flow result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/melnor/test_number.py b/tests/components/melnor/test_number.py index 9cddbfceb77..1366e428fc1 100644 --- a/tests/components/melnor/test_number.py +++ b/tests/components/melnor/test_number.py @@ -16,7 +16,6 @@ async def test_manual_watering_minutes(hass): entry = mock_config_entry(hass) with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback(): - device = device_patch.return_value assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/melnor/test_switch.py b/tests/components/melnor/test_switch.py index 9539b00d9c6..822892dcf32 100644 --- a/tests/components/melnor/test_switch.py +++ b/tests/components/melnor/test_switch.py @@ -33,7 +33,6 @@ async def test_manual_watering_switch_on_off(hass): entry = mock_config_entry(hass) with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback(): - device = device_patch.return_value assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 7d6b8227396..25c0cec441b 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -99,7 +99,10 @@ async def test_invalid_ip(hass: HomeAssistant) -> None: async def test_same_host(hass: HomeAssistant) -> None: """Test abort in case of same host name.""" - with patch("aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError,), patch( + with patch( + "aiodns.DNSResolver.query", + side_effect=aiodns.error.DNSError, + ), patch( "mcstatus.server.MinecraftServer.status", return_value=PingResponse(STATUS_RESPONSE_RAW), ): @@ -166,7 +169,10 @@ async def test_connection_failed(hass: HomeAssistant) -> None: async def test_connection_succeeded_with_srv_record(hass: HomeAssistant) -> None: """Test config entry in case of a successful connection with a SRV record.""" - with patch("aiodns.DNSResolver.query", return_value=SRV_RECORDS,), patch( + with patch( + "aiodns.DNSResolver.query", + return_value=SRV_RECORDS, + ), patch( "mcstatus.server.MinecraftServer.status", return_value=PingResponse(STATUS_RESPONSE_RAW), ): @@ -182,7 +188,10 @@ async def test_connection_succeeded_with_srv_record(hass: HomeAssistant) -> None async def test_connection_succeeded_with_host(hass: HomeAssistant) -> None: """Test config entry in case of a successful connection with a host name.""" - with patch("aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError,), patch( + with patch( + "aiodns.DNSResolver.query", + side_effect=aiodns.error.DNSError, + ), patch( "mcstatus.server.MinecraftServer.status", return_value=PingResponse(STATUS_RESPONSE_RAW), ): diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 2a212535916..7297ef2dee5 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -135,7 +135,7 @@ async def test_number_validator(): try: number_validator("x15.1") - except (vol.Invalid): + except vol.Invalid: return pytest.fail("Number_validator not throwing exception") diff --git a/tests/components/moehlenhoff_alpha2/test_config_flow.py b/tests/components/moehlenhoff_alpha2/test_config_flow.py index a1f98454015..4842d648828 100644 --- a/tests/components/moehlenhoff_alpha2/test_config_flow.py +++ b/tests/components/moehlenhoff_alpha2/test_config_flow.py @@ -60,7 +60,6 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None: assert config_entry.data["host"] == MOCK_BASE_HOST with patch("moehlenhoff_alpha2.Alpha2Base.update_data", mock_update_data): - result = await hass.config_entries.flow.async_init( DOMAIN, data={"host": MOCK_BASE_HOST}, diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index f0a22987aa3..654fb2009ca 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -728,14 +728,14 @@ async def help_test_discovery_update( assert state.name == "Beer" if state_data1: - for (mqtt_messages, expected_state, attributes) in state_data1: - for (topic, data) in mqtt_messages: + for mqtt_messages, expected_state, attributes in state_data1: + for topic, data in mqtt_messages: async_fire_mqtt_message(hass, topic, data) state = hass.states.get(f"{domain}.beer") if expected_state: assert state.state == expected_state if attributes: - for (attr, value) in attributes: + for attr, value in attributes: assert state.attributes.get(attr) == value async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", discovery_data2) @@ -746,14 +746,14 @@ async def help_test_discovery_update( assert state.name == "Milk" if state_data2: - for (mqtt_messages, expected_state, attributes) in state_data2: - for (topic, data) in mqtt_messages: + for mqtt_messages, expected_state, attributes in state_data2: + for topic, data in mqtt_messages: async_fire_mqtt_message(hass, topic, data) state = hass.states.get(f"{domain}.beer") if expected_state: assert state.state == expected_state if attributes: - for (attr, value) in attributes: + for attr, value in attributes: assert state.attributes.get(attr) == value state = hass.states.get(f"{domain}.milk") diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 251c0af24a6..c716cb4dae7 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -105,7 +105,6 @@ async def test_invalid_json(hass, mqtt_mock_entry_no_yaml_config, caplog): with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: - mock_dispatcher_send = AsyncMock(return_value=None) async_fire_mqtt_message( @@ -122,7 +121,6 @@ async def test_only_valid_components(hass, mqtt_mock_entry_no_yaml_config, caplo with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: - invalid_component = "timer" mock_dispatcher_send = AsyncMock(return_value=None) diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index b0cba664250..03719c39c59 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -46,7 +46,6 @@ async def test_ensure_device_tracker_platform_validation(hass): autospec=True, side_effect=mock_setup_scanner, ) as mock_sp: - dev_id = "paulus" topic = "location/paulus" assert await async_setup_component( diff --git a/tests/components/mutesync/test_config_flow.py b/tests/components/mutesync/test_config_flow.py index 934eb190742..5550ed899c0 100644 --- a/tests/components/mutesync/test_config_flow.py +++ b/tests/components/mutesync/test_config_flow.py @@ -19,7 +19,10 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch("mutesync.authenticate", return_value="bla",), patch( + with patch( + "mutesync.authenticate", + return_value="bla", + ), patch( "homeassistant.components.mutesync.async_setup_entry", return_value=True, ) as mock_setup_entry: diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index aef0fa41fe5..2531263e58a 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -219,7 +219,6 @@ async def test_form_errors(hass, error): "homeassistant.components.nam.NettigoAirMonitor.initialize", side_effect=exc, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 65cc991ec67..137c26e1c4f 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -304,7 +304,6 @@ async def test_setup_component_with_delay(hass, config_entry): ) as mock_post_api_request, patch( "homeassistant.components.netatmo.data_handler.PLATFORMS", ["light"] ): - assert await async_setup_component( hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} ) diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index 5bfd7fb582c..cdf81ea09d3 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -53,7 +53,9 @@ async def test_form_invalid_auth(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.nexia.config_flow.NexiaHome.login",), patch( + with patch( + "homeassistant.components.nexia.config_flow.NexiaHome.login", + ), patch( "homeassistant.components.nexia.config_flow.NexiaHome.get_name", return_value=None, ): diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index 855b733ef46..6238496ed09 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -46,7 +46,6 @@ async def test_sensors(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", wraps=mocked_request_function, ): - conf_entry: MockConfigEntry = MockConfigEntry( domain=DOMAIN, title="NINA", data=ENTRY_DATA ) @@ -155,7 +154,6 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", wraps=mocked_request_function, ): - conf_entry: MockConfigEntry = MockConfigEntry( domain=DOMAIN, title="NINA", data=ENTRY_DATA_NO_CORONA ) diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index bfea7781780..e09bdfc739b 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -54,7 +54,6 @@ async def test_show_set_form(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", wraps=mocked_request_function, ): - result: dict[str, Any] = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -69,7 +68,6 @@ async def test_step_user_connection_error(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", side_effect=ApiError("Could not connect to Api"), ): - result: dict[str, Any] = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) @@ -84,7 +82,6 @@ async def test_step_user_unexpected_exception(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", side_effect=Exception("DUMMY"), ): - result: dict[str, Any] = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) @@ -101,7 +98,6 @@ async def test_step_user(hass: HomeAssistant) -> None: "homeassistant.components.nina.async_setup_entry", return_value=True, ): - result: dict[str, Any] = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) ) @@ -116,7 +112,6 @@ async def test_step_user_no_selection(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", wraps=mocked_request_function, ): - result: dict[str, Any] = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={} ) diff --git a/tests/components/nina/test_init.py b/tests/components/nina/test_init.py index 66e8edb2806..d75e35473ca 100644 --- a/tests/components/nina/test_init.py +++ b/tests/components/nina/test_init.py @@ -27,7 +27,6 @@ async def init_integration(hass) -> MockConfigEntry: "pynina.baseApi.BaseAPI._makeRequest", wraps=mocked_request_function, ): - entry: MockConfigEntry = MockConfigEntry( domain=DOMAIN, title="NINA", data=ENTRY_DATA ) diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index 524133cf957..5e82879209a 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -73,7 +73,7 @@ async def test_sensors(hass, nzbget_api) -> None: ), } - for (sensor_id, data) in sensors.items(): + for sensor_id, data in sensors.items(): entity_entry = registry.async_get(f"sensor.nzbgettest_{sensor_id}") assert entity_entry assert entity_entry.original_device_class == data[3] diff --git a/tests/components/oncue/__init__.py b/tests/components/oncue/__init__.py index 2ddaf1987f8..26b08460a93 100644 --- a/tests/components/oncue/__init__.py +++ b/tests/components/oncue/__init__.py @@ -801,7 +801,9 @@ MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE = { def _patch_login_and_data(): @contextmanager def _patcher(): - with patch("homeassistant.components.oncue.Oncue.async_login",), patch( + with patch( + "homeassistant.components.oncue.Oncue.async_login", + ), patch( "homeassistant.components.oncue.Oncue.async_fetch_all", return_value=MOCK_ASYNC_FETCH_ALL, ): @@ -813,7 +815,9 @@ def _patch_login_and_data(): def _patch_login_and_data_offline_device(): @contextmanager def _patcher(): - with patch("homeassistant.components.oncue.Oncue.async_login",), patch( + with patch( + "homeassistant.components.oncue.Oncue.async_login", + ), patch( "homeassistant.components.oncue.Oncue.async_fetch_all", return_value=MOCK_ASYNC_FETCH_ALL_OFFLINE_DEVICE, ): @@ -837,7 +841,6 @@ def _patch_login_and_data_unavailable(): def _patch_login_and_data_unavailable_device(): @contextmanager def _patcher(): - with patch("homeassistant.components.oncue.Oncue.async_login"), patch( "homeassistant.components.oncue.Oncue.async_fetch_all", return_value=MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE, diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index f3abd5a6905..14fa1dd8069 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -294,7 +294,6 @@ async def test_options(hass): with patch( "homeassistant.components.plaato.async_setup_entry", return_value=True ) as mock_setup_entry: - await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -329,7 +328,6 @@ async def test_options_webhook(hass, webhook_id): with patch( "homeassistant.components.plaato.async_setup_entry", return_value=True ) as mock_setup_entry: - await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index d143bbef00d..15ad409b4cc 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -152,7 +152,6 @@ async def test_load_from_db(recorder_mock, hass): """ plant_name = "wise_plant" for value in [20, 30, 10]: - hass.states.async_set( BRIGHTNESS_ENTITY, value, {ATTR_UNIT_OF_MEASUREMENT: "Lux"} ) diff --git a/tests/components/prosegur/test_alarm_control_panel.py b/tests/components/prosegur/test_alarm_control_panel.py index 8a50319047e..5caaabdb3eb 100644 --- a/tests/components/prosegur/test_alarm_control_panel.py +++ b/tests/components/prosegur/test_alarm_control_panel.py @@ -73,7 +73,6 @@ async def test_connection_error(hass, mock_auth): install.status = Status.ARMED with patch("pyprosegur.installation.Installation.retrieve", return_value=install): - await setup_platform(hass) await hass.async_block_till_done() @@ -81,7 +80,6 @@ async def test_connection_error(hass, mock_auth): with patch( "pyprosegur.installation.Installation.retrieve", side_effect=ConnectionError ): - await entity_component.async_update_entity(hass, PROSEGUR_ALARM_ENTITY) state = hass.states.get(PROSEGUR_ALARM_ENTITY) diff --git a/tests/components/prosegur/test_init.py b/tests/components/prosegur/test_init.py index 2079d7a2b3c..da0a2d9fba2 100644 --- a/tests/components/prosegur/test_init.py +++ b/tests/components/prosegur/test_init.py @@ -64,7 +64,6 @@ async def test_unload_entry(hass, aioclient_mock): "homeassistant.components.prosegur.config_flow.Installation.retrieve", return_value=install, ): - assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/radiotherm/test_config_flow.py b/tests/components/radiotherm/test_config_flow.py index 862b7b30032..cc8abef93e2 100644 --- a/tests/components/radiotherm/test_config_flow.py +++ b/tests/components/radiotherm/test_config_flow.py @@ -106,7 +106,6 @@ async def test_import(hass): "homeassistant.components.radiotherm.async_setup_entry", return_value=True, ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index d04d436f72c..9cf7c1d7c09 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -195,7 +195,10 @@ async def test_events_during_migration_are_queued(hass): assert recorder.util.async_migration_in_progress(hass) is False - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True,), patch( + with patch( + "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", + True, + ), patch( "homeassistant.components.recorder.core.create_engine", new=create_engine_test, ): diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 206cedbe6a5..d35591b660a 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -92,7 +92,7 @@ class MockFixFlow(RepairsFlow): assert self.issue_id in EXPECTED_DATA assert self.data == EXPECTED_DATA[self.issue_id] - return await (self.async_step_custom_step()) + return await self.async_step_custom_step() async def async_step_custom_step( self, user_input: dict[str, str] | None = None diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 32810d22789..5698d1de31c 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -81,7 +81,6 @@ async def test_successful_discovery_and_auth(hass): "homeassistant.components.roon.async_setup_entry", return_value=True, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -120,7 +119,6 @@ async def test_unsuccessful_discovery_user_form_and_auth(hass): "homeassistant.components.roon.async_setup_entry", return_value=True, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -165,7 +163,6 @@ async def test_duplicate_config(hass): "homeassistant.components.roon.config_flow.RoonDiscovery", return_value=RoonDiscoveryFailedMock(), ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -207,7 +204,6 @@ async def test_successful_discovery_no_auth(hass): "homeassistant.components.roon.async_setup_entry", return_value=True, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -239,7 +235,6 @@ async def test_unexpected_exception(hass): "homeassistant.components.roon.async_setup_entry", return_value=True, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 6679f1d786e..943f91eec1e 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -822,7 +822,6 @@ async def test_ssdp_not_successful(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, ): - # confirm to add the entry result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -851,7 +850,6 @@ async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, ): - # confirm to add the entry result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -874,7 +872,6 @@ async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, ): - # confirm to add the entry result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -897,7 +894,6 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, ): - # entry was added result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index aec072ab448..b9fa2202afa 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -292,7 +292,6 @@ async def test_update_off(hass: HomeAssistant, mock_now: datetime) -> None: "homeassistant.components.samsungtv.bridge.Remote", side_effect=[OSError("Boom"), DEFAULT_MOCK], ): - next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) @@ -526,7 +525,6 @@ async def test_update_unhandled_response( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[exceptions.UnhandledResponse("Boom"), DEFAULT_MOCK], ): - next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) @@ -547,7 +545,6 @@ async def test_connection_closed_during_update_can_recover( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[exceptions.ConnectionClosed(), DEFAULT_MOCK], ): - next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py index f6df8a80f21..e12a7c15a0c 100644 --- a/tests/components/scrape/test_config_flow.py +++ b/tests/components/scrape/test_config_flow.py @@ -131,7 +131,10 @@ async def test_flow_fails(hass: HomeAssistant, get_data: MockRestData) -> None: assert result2["errors"] == {"base": "resource_error"} - with patch("homeassistant.components.rest.RestData", return_value=get_data,), patch( + with patch( + "homeassistant.components.rest.RestData", + return_value=get_data, + ), patch( "homeassistant.components.scrape.async_setup_entry", return_value=True, ): diff --git a/tests/components/script/test_blueprint.py b/tests/components/script/test_blueprint.py index 10e73a80939..fb056aead20 100644 --- a/tests/components/script/test_blueprint.py +++ b/tests/components/script/test_blueprint.py @@ -76,7 +76,6 @@ async def test_confirmable_notification(hass: HomeAssistant) -> None: with patch( "homeassistant.components.mobile_app.device_action.async_call_action_from_config" ) as mock_call_action: - # Trigger script await hass.services.async_call(script.DOMAIN, "confirm", context=context) diff --git a/tests/components/senseme/__init__.py b/tests/components/senseme/__init__.py index 8c9a7669889..43d586328bb 100644 --- a/tests/components/senseme/__init__.py +++ b/tests/components/senseme/__init__.py @@ -119,7 +119,6 @@ def _patch_discovery(device=None, no_device=None): @contextmanager def _patcher(): - with patch.object(config_flow, "DISCOVER_TIMEOUT", 0), patch( "homeassistant.components.senseme.discovery.SensemeDiscovery", return_value=mock_senseme_discovery, diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 7a8ffc87e52..10b0471aa41 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -433,7 +433,6 @@ async def test_user_setup_ignored_device(hass): "homeassistant.components.shelly.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 707e8d5cfb1..57b756402ec 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -184,7 +184,6 @@ async def test_entry_unload_not_connected(hass, mock_rpc_device, monkeypatch): with patch( "homeassistant.components.shelly.coordinator.async_stop_scanner" ) as mock_stop_scanner: - entry = await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} ) @@ -211,7 +210,6 @@ async def test_entry_unload_not_connected_but_we_think_we_are( "homeassistant.components.shelly.coordinator.async_stop_scanner", side_effect=DeviceConnectionError, ) as mock_stop_scanner: - entry = await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} ) diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index 3e944ec69f8..44ea613ed3a 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -84,7 +84,6 @@ async def test_success(hass: HomeAssistant) -> None: "homeassistant.components.sleepiq.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], SLEEPIQ_CONFIG ) diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index eeaa0d75f07..3fbcd2c0ed0 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -128,7 +128,6 @@ async def test_form_already_configured(hass, mock_config_entry): ), patch( "pysma.SMA.close_session", return_value=True ), _patch_async_setup_entry() as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index b29207afbca..06c90b0c54d 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -538,7 +538,10 @@ async def test_full_zeroconf_flow_next_generation(hass): """Test the full zeroconf flow.""" with patch( "pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=True - ), patch("pysmappee.mqtt.SmappeeLocalMqtt.start", return_value=None,), patch( + ), patch( + "pysmappee.mqtt.SmappeeLocalMqtt.start", + return_value=None, + ), patch( "pysmappee.mqtt.SmappeeLocalMqtt.is_config_ready", return_value=None, ): diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 93b646c44dc..5c277b38c9c 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -363,7 +363,6 @@ async def test_entry_created_with_cloudhook( "async_create_cloudhook", AsyncMock(return_value="http://cloud.test"), ) as mock_create_cloudhook: - await smartapp.setup_smartapp_endpoint(hass) # Webhook confirmation shown diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 9240e99d3e0..c00236c54e6 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -42,7 +42,7 @@ async def test_sensors( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - for (unique, oid) in sensors.items(): + for unique, oid in sensors.items(): entity = registry.async_get(f"sensor.{oid}") assert entity assert entity.unique_id == f"{mock_config_entry.entry_id}_{unique}" diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index fdcf8fe1a5b..d586ccbe3b1 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -202,7 +202,9 @@ async def test_invalid_url_on_update( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - with patch("homeassistant.components.recorder",), patch( + with patch( + "homeassistant.components.recorder", + ), patch( "homeassistant.components.sql.sensor.sqlalchemy.engine.cursor.CursorResult", side_effect=SQLAlchemyError( "sqlite://homeassistant:hunter2@homeassistant.local" diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index a36abcc77aa..afe46ffdef5 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -37,7 +37,10 @@ async def patch_async_query_unauthorized(self, *args): async def test_user_form(hass): """Test user-initiated flow, including discovery and the edit step.""" - with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch( + with patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ), patch( "homeassistant.components.squeezebox.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( @@ -191,7 +194,10 @@ async def test_discovery_no_uuid(hass): async def test_dhcp_discovery(hass): """Test we can process discovery from dhcp.""" - with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch( + with patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ), patch( "homeassistant.components.squeezebox.config_flow.async_discover", mock_discover ): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/srp_energy/__init__.py b/tests/components/srp_energy/__init__.py index c3966e940b0..682a43c4429 100644 --- a/tests/components/srp_energy/__init__.py +++ b/tests/components/srp_energy/__init__.py @@ -47,7 +47,6 @@ async def init_integration( ), patch("srpenergy.client.SrpEnergyClient.usage", return_value=usage), patch( "homeassistant.components.srp_energy.SrpEnergyClient.usage", return_value=usage ): - config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index 6eac5298ae4..21d808fef5d 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -24,7 +24,6 @@ async def test_form(hass): "homeassistant.components.srp_energy.async_setup_entry", return_value=True, ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=ENTRY_CONFIG, diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py index 20d70a7d496..94b803fea01 100644 --- a/tests/components/subaru/conftest.py +++ b/tests/components/subaru/conftest.py @@ -105,7 +105,10 @@ async def setup_subaru_config_entry( MOCK_API_CONNECT, return_value=connect_effect is None, side_effect=connect_effect, - ), patch(MOCK_API_GET_VEHICLES, return_value=vehicle_list,), patch( + ), patch( + MOCK_API_GET_VEHICLES, + return_value=vehicle_list, + ), patch( MOCK_API_VIN_TO_NAME, return_value=vehicle_data[VEHICLE_NAME], ), patch( diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 0a48c736ef7..7b290a4df1b 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -257,7 +257,9 @@ async def test_pin_form_init(pin_form): async def test_pin_form_bad_pin_format(hass, pin_form): """Test we handle invalid pin.""" - with patch(MOCK_API_TEST_PIN,) as mock_test_pin, patch( + with patch( + MOCK_API_TEST_PIN, + ) as mock_test_pin, patch( MOCK_API_UPDATE_SAVED_PIN, return_value=True, ) as mock_update_saved_pin: @@ -272,7 +274,10 @@ async def test_pin_form_bad_pin_format(hass, pin_form): async def test_pin_form_success(hass, pin_form): """Test successful PIN entry.""" - with patch(MOCK_API_TEST_PIN, return_value=True,) as mock_test_pin, patch( + with patch( + MOCK_API_TEST_PIN, + return_value=True, + ) as mock_test_pin, patch( MOCK_API_UPDATE_SAVED_PIN, return_value=True, ) as mock_update_saved_pin, patch( diff --git a/tests/components/subaru/test_init.py b/tests/components/subaru/test_init.py index 46a8b2e103b..1e3b3d6743e 100644 --- a/tests/components/subaru/test_init.py +++ b/tests/components/subaru/test_init.py @@ -122,7 +122,10 @@ async def test_update_skip_unsubscribed(hass, subaru_config_entry): async def test_update_disabled(hass, ev_entry): """Test update function disable option.""" - with patch(MOCK_API_FETCH, side_effect=SubaruException("403 Error"),), patch( + with patch( + MOCK_API_FETCH, + side_effect=SubaruException("403 Error"), + ), patch( MOCK_API_UPDATE, ) as mock_update: await hass.services.async_call( diff --git a/tests/components/switchbee/test_config_flow.py b/tests/components/switchbee/test_config_flow.py index 1745d6c5d40..80bdb18a874 100644 --- a/tests/components/switchbee/test_config_flow.py +++ b/tests/components/switchbee/test_config_flow.py @@ -35,7 +35,6 @@ async def test_form(hass): ), patch( "switchbee.api.polling.CentralUnitPolling._login", return_value=None ): - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 402dcd2f602..2a682dacb82 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -65,7 +65,6 @@ from tests.common import MockConfigEntry def mock_controller_service(): """Mock a successful service.""" with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: - dsm.login = AsyncMock(return_value=True) dsm.update = AsyncMock(return_value=True) @@ -109,7 +108,6 @@ def mock_controller_service_2sa(): def mock_controller_service_vdsm(): """Mock a successful service.""" with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: - dsm.login = AsyncMock(return_value=True) dsm.update = AsyncMock(return_value=True) @@ -131,7 +129,6 @@ def mock_controller_service_vdsm(): def mock_controller_service_failed(): """Mock a failed service.""" with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: - dsm.login = AsyncMock(return_value=True) dsm.update = AsyncMock(return_value=True) @@ -333,7 +330,6 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): "homeassistant.config_entries.ConfigEntries.async_reload", return_value=True, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={ diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index 266f9b67376..0f814f494f7 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -146,7 +146,6 @@ async def test_exception_security(hass: HomeAssistant): "homeassistant.components.tankerkoenig.config_flow.getNearbyStations", side_effect=customException, ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) diff --git a/tests/components/tibber/test_statistics.py b/tests/components/tibber/test_statistics.py index 661297f9a37..002f8b7d15a 100644 --- a/tests/components/tibber/test_statistics.py +++ b/tests/components/tibber/test_statistics.py @@ -22,7 +22,7 @@ async def test_async_setup_entry(recorder_mock, hass): await coordinator._async_update_data() await async_wait_recording_done(hass) - for (statistic_id, data, key) in ( + for statistic_id, data, key in ( ("tibber:energy_consumption_home_id", CONSUMPTION_DATA_1, "consumption"), ("tibber:energy_totalcost_home_id", CONSUMPTION_DATA_1, "totalCost"), ("tibber:energy_production_home_id", PRODUCTION_DATA_1, "production"), diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 5e5124db71c..eb8e1af0d9c 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -54,10 +54,12 @@ async def test_user_show_locations(hass): RESPONSE_SUCCESS, ] - with patch(TOTALCONNECT_REQUEST, side_effect=responses,) as mock_request, patch( + with patch( + TOTALCONNECT_REQUEST, + side_effect=responses, + ) as mock_request, patch( "homeassistant.components.totalconnect.async_setup_entry", return_value=True ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -178,13 +180,15 @@ async def test_no_locations(hass): RESPONSE_DISARMED, ] - with patch(TOTALCONNECT_REQUEST, side_effect=responses,) as mock_request, patch( + with patch( + TOTALCONNECT_REQUEST, + side_effect=responses, + ) as mock_request, patch( "homeassistant.components.totalconnect.async_setup_entry", return_value=True ), patch( "homeassistant.components.totalconnect.TotalConnectClient.get_number_locations", return_value=0, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index f26fb39e42a..79bd6daba17 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -141,6 +141,7 @@ async def silent_ssdp_scanner(hass): @pytest.fixture async def ssdp_instant_discovery(): """Instance discovery.""" + # Set up device discovery callback. async def register_callback(hass, callback, match_dict): """Immediately do callback.""" @@ -160,6 +161,7 @@ async def ssdp_instant_discovery(): @pytest.fixture async def ssdp_no_discovery(): """No discovery.""" + # Set up device discovery callback. async def register_callback(hass, callback, match_dict): """Don't do callback.""" diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py index 2003f411358..6a82d75a9f8 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -110,7 +110,6 @@ async def setup_uptimerobot_integration(hass: HomeAssistant) -> MockConfigEntry: "pyuptimerobot.UptimeRobot.async_get_monitors", return_value=mock_uptimerobot_api_response(data=[MOCK_UPTIMEROBOT_MONITOR]), ): - assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index b058ceecbdc..804687e79d0 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -181,7 +181,6 @@ async def test_reauthentication( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, ): - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, @@ -220,7 +219,6 @@ async def test_reauthentication_failure( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, ): - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, @@ -262,7 +260,6 @@ async def test_reauthentication_failure_no_existing_entry( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, ): - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, @@ -304,7 +301,6 @@ async def test_reauthentication_failure_account_not_matching( "homeassistant.components.uptimerobot.async_setup_entry", return_value=True, ): - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, diff --git a/tests/components/uptimerobot/test_diagnostics.py b/tests/components/uptimerobot/test_diagnostics.py index a0780f64e90..ae7371f8e90 100644 --- a/tests/components/uptimerobot/test_diagnostics.py +++ b/tests/components/uptimerobot/test_diagnostics.py @@ -33,7 +33,6 @@ async def test_entry_diagnostics( data=MOCK_UPTIMEROBOT_ACCOUNT, ), ): - result = await get_diagnostics_for_config_entry( hass, hass_client, @@ -68,7 +67,6 @@ async def test_entry_diagnostics_exception( "pyuptimerobot.UptimeRobot.async_get_account_details", side_effect=UptimeRobotException("Test exception"), ): - result = await get_diagnostics_for_config_entry( hass, hass_client, diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index 00d9b1c6a85..3133125172d 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -38,7 +38,6 @@ async def test_reauthentication_trigger_in_setup( "pyuptimerobot.UptimeRobot.async_get_monitors", side_effect=UptimeRobotAuthenticationException, ): - await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -107,7 +106,6 @@ async def test_reauthentication_trigger_after_setup( "pyuptimerobot.UptimeRobot.async_get_monitors", side_effect=UptimeRobotAuthenticationException, ): - async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() diff --git a/tests/components/uptimerobot/test_switch.py b/tests/components/uptimerobot/test_switch.py index 82ea06ce836..4560909d3e4 100644 --- a/tests/components/uptimerobot/test_switch.py +++ b/tests/components/uptimerobot/test_switch.py @@ -53,7 +53,6 @@ async def test_switch_off(hass: HomeAssistant) -> None: "pyuptimerobot.UptimeRobot.async_edit_monitor", return_value=mock_uptimerobot_api_response(), ): - assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -81,7 +80,6 @@ async def test_switch_on(hass: HomeAssistant) -> None: "pyuptimerobot.UptimeRobot.async_edit_monitor", return_value=mock_uptimerobot_api_response(), ): - assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index b6641402c83..9ddf05a5b0a 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -40,7 +40,7 @@ async def run_sensor_test( ) update_callback = component_data.controller_data[0].update_callback - for (initial_value, state_value) in assert_states: + for initial_value, state_value in assert_states: setattr(vera_device, class_property, initial_value) update_callback(vera_device) await hass.async_block_till_done() diff --git a/tests/components/version/common.py b/tests/components/version/common.py index b210a8600b8..a6b554d780d 100644 --- a/tests/components/version/common.py +++ b/tests/components/version/common.py @@ -47,7 +47,6 @@ async def mock_get_version_update( return_value=(version, data), side_effect=side_effect, ): - async_fire_time_changed(hass, dt.utcnow() + UPDATE_COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() @@ -69,7 +68,6 @@ async def setup_version_integration( "pyhaversion.HaVersion.get_version", return_value=(MOCK_VERSION, MOCK_VERSION_DATA), ): - assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/vulcan/test_config_flow.py b/tests/components/vulcan/test_config_flow.py index c45ab430c2e..3e6588c1caf 100644 --- a/tests/components/vulcan/test_config_flow.py +++ b/tests/components/vulcan/test_config_flow.py @@ -209,7 +209,6 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) "homeassistant.components.vulcan.config_flow.Account.register", side_effect=InvalidTokenException, ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, @@ -223,7 +222,6 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) "homeassistant.components.vulcan.config_flow.Account.register", side_effect=ExpiredTokenException, ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, @@ -237,7 +235,6 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) "homeassistant.components.vulcan.config_flow.Account.register", side_effect=InvalidPINException, ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, @@ -251,7 +248,6 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) "homeassistant.components.vulcan.config_flow.Account.register", side_effect=InvalidSymbolException, ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, @@ -265,7 +261,6 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) "homeassistant.components.vulcan.config_flow.Account.register", side_effect=ClientConnectionError, ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, @@ -279,7 +274,6 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) "homeassistant.components.vulcan.config_flow.Account.register", side_effect=Exception, ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, diff --git a/tests/components/vultr/test_binary_sensor.py b/tests/components/vultr/test_binary_sensor.py index 80cd198e371..899b36e59b1 100644 --- a/tests/components/vultr/test_binary_sensor.py +++ b/tests/components/vultr/test_binary_sensor.py @@ -41,7 +41,6 @@ def test_binary_sensor(hass: HomeAssistant): assert len(hass_devices) == 3 for device in hass_devices: - # Test pre data retrieval if device.subscription == "555555": assert device.name == "Vultr {}" diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 95f91270fa9..ad63e02c4fe 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -51,7 +51,6 @@ def test_sensor(hass: HomeAssistant): tested = 0 for device in hass_devices: - # Test pre update if device.subscription == "576965": assert vultr.DEFAULT_NAME == device.name @@ -74,7 +73,6 @@ def test_sensor(hass: HomeAssistant): tested += 1 elif device.unit_of_measurement == "US$": # Test Pending Charges - if device.subscription == "576965": # Default 'Vultr {} {}' assert device.name == "Vultr my new server Pending Charges" assert device.icon == "mdi:currency-usd" diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index b73f5223576..8a7fe185662 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -39,7 +39,6 @@ async def test_valid_hostname( assert state.state == STATE_OFF with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, @@ -87,7 +86,6 @@ async def test_broadcast_config_ip_and_port( assert state.state == STATE_OFF with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, @@ -125,7 +123,6 @@ async def test_broadcast_config_ip( assert state.state == STATE_OFF with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, @@ -155,7 +152,6 @@ async def test_broadcast_config_port( assert state.state == STATE_OFF with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, @@ -190,7 +186,6 @@ async def test_off_script( assert state.state == STATE_OFF with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, @@ -203,7 +198,6 @@ async def test_off_script( assert len(calls) == 0 with patch.object(subprocess, "call", return_value=2): - await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_OFF, @@ -237,7 +231,6 @@ async def test_no_hostname_state( assert state.state == STATE_OFF with patch.object(subprocess, "call", return_value=0): - await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py index c16c85d3696..eb11c4fc0cc 100644 --- a/tests/components/wallbox/test_number.py +++ b/tests/components/wallbox/test_number.py @@ -65,7 +65,6 @@ async def test_wallbox_number_class_connection_error(hass: HomeAssistant) -> Non ) with pytest.raises(ConnectionError): - await hass.services.async_call( "number", SERVICE_SET_VALUE, diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index ad6620dc057..d408ac1d15e 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -198,7 +198,6 @@ async def test_no_appliances_flow(hass: HomeAssistant, region) -> None: "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", return_value=True, ): - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG_INPUT | {"region": region[0]}, diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index 9a6105a4b8b..e09d6821362 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -124,7 +124,6 @@ async def test_already_configured_error(hass): "homeassistant.components.wolflink.config_flow.WolfClient.fetch_system_list", return_value=[DEVICE], ), patch("homeassistant.components.wolflink.async_setup_entry", return_value=True): - MockConfigEntry( domain=DOMAIN, unique_id=CONFIG[DEVICE_ID], data=CONFIG ).add_to_hass(hass) diff --git a/tests/components/ws66i/test_config_flow.py b/tests/components/ws66i/test_config_flow.py index da5a16882e8..ec09419e7f8 100644 --- a/tests/components/ws66i/test_config_flow.py +++ b/tests/components/ws66i/test_config_flow.py @@ -36,7 +36,6 @@ async def test_form(hass): "homeassistant.components.ws66i.async_setup_entry", return_value=True, ) as mock_setup_entry: - ws66i_instance = mock_ws66i.return_value result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 391c969d893..44fa1263a6c 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -844,7 +844,6 @@ async def test_discovered_during_onboarding(hass, source, data): ) as mock_async_setup_entry, patch( "homeassistant.components.onboarding.async_is_onboarded", return_value=False ) as mock_is_onboarded: - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index fd383fd653c..ae461a5ca0f 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -162,7 +162,11 @@ async def test_setup(hass, mock_async_zeroconf): ], "_Volumio._tcp.local.": [{"domain": "volumio"}], } - with patch.dict(zc_gen.ZEROCONF, mock_zc, clear=True,), patch.object( + with patch.dict( + zc_gen.ZEROCONF, + mock_zc, + clear=True, + ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock @@ -535,7 +539,11 @@ async def test_homekit_match_partial_space(hass, mock_async_zeroconf): zc_gen.ZEROCONF, {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, clear=True, - ), patch.dict(zc_gen.HOMEKIT, {"LIFX": "lifx"}, clear=True,), patch.object( + ), patch.dict( + zc_gen.HOMEKIT, + {"LIFX": "lifx"}, + clear=True, + ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, @@ -656,7 +664,11 @@ async def test_homekit_match_full(hass, mock_async_zeroconf): zc_gen.ZEROCONF, {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, clear=True, - ), patch.dict(zc_gen.HOMEKIT, {"BSB002": "hue"}, clear=True,), patch.object( + ), patch.dict( + zc_gen.HOMEKIT, + {"BSB002": "hue"}, + clear=True, + ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, @@ -779,7 +791,11 @@ async def test_homekit_controller_still_discovered_unpaired_for_cloud( zc_gen.ZEROCONF, {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, clear=True, - ), patch.dict(zc_gen.HOMEKIT, {"Rachio": "rachio"}, clear=True,), patch.object( + ), patch.dict( + zc_gen.HOMEKIT, + {"Rachio": "rachio"}, + clear=True, + ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 0ab905692c2..b264213d5a0 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -321,7 +321,7 @@ async def test_out_channel_config( def test_channel_registry(): """Test ZIGBEE Channel Registry.""" - for (cluster_id, channel) in registries.ZIGBEE_CHANNEL_REGISTRY.items(): + for cluster_id, channel in registries.ZIGBEE_CHANNEL_REGISTRY.items(): assert isinstance(cluster_id, int) assert 0 <= cluster_id <= 0xFFFF assert issubclass(channel, base_channels.ZigbeeChannel) diff --git a/tests/components/zwave_me/test_config_flow.py b/tests/components/zwave_me/test_config_flow.py index 57ead9a9e60..e3be334c3d8 100644 --- a/tests/components/zwave_me/test_config_flow.py +++ b/tests/components/zwave_me/test_config_flow.py @@ -170,7 +170,6 @@ async def test_duplicate_zeroconf(hass: HomeAssistant): "homeassistant.components.zwave_me.helpers.get_uuid", return_value="test_uuid", ): - result: FlowResult = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, diff --git a/tests/components/zwave_me/test_remove_stale_devices.py b/tests/components/zwave_me/test_remove_stale_devices.py index 484c38b9f33..1ecb106d942 100644 --- a/tests/components/zwave_me/test_remove_stale_devices.py +++ b/tests/components/zwave_me/test_remove_stale_devices.py @@ -55,7 +55,10 @@ async def test_remove_stale_devices( connections={("mac", "12:34:56:AB:CD:EF")}, identifiers={("zwave_me", f"{config_entry.unique_id}-{identifier}")}, ) - with patch("zwave_me_ws.ZWaveMe.get_connection", mock_connection,), patch( + with patch( + "zwave_me_ws.ZWaveMe.get_connection", + mock_connection, + ), patch( "homeassistant.components.zwave_me.async_setup_platforms", ): await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/conftest.py b/tests/conftest.py index e131cf6cdc3..2f475cc2051 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -351,7 +351,6 @@ def aiohttp_client( server_kwargs: dict[str, Any] | None = None, **kwargs: Any, ) -> TestClient: - if isinstance(__param, Callable) and not isinstance( # type: ignore[arg-type] __param, (Application, BaseTestServer) ): diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index a4a3e2b27e7..bc545a647cf 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -370,7 +370,6 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): "hass_nabucasa.cloudhooks.Cloudhooks.async_delete", return_value={"cloudhook_url": "https://example.com"}, ) as mock_delete: - result = await hass.config_entries.async_remove(result["result"].entry_id) assert len(mock_delete.mock_calls) == 1 @@ -413,7 +412,6 @@ async def test_webhook_create_cloudhook_aborts_not_connected(hass, webhook_flow_ "hass_nabucasa.iot_base.BaseIoT.connected", new_callable=PropertyMock(return_value=False), ): - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == data_entry_flow.FlowResultType.ABORT diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 2c9a7956874..9d5c95ccaa3 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -825,7 +825,7 @@ async def test_loading_saving_data(hass, registry, area_registry): assert orig_light4 == new_light4 # Ensure enums converted - for (old, new) in ( + for old, new in ( (orig_via, new_via), (orig_light, new_light), (orig_light4, new_light4), diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 19f5ea242b6..ac337b11b50 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -484,7 +484,6 @@ async def test_async_parallel_updates_with_two(hass): await test_lock.acquire() try: - ent_1.async_schedule_update_ha_state(True) ent_2.async_schedule_update_ha_state(True) ent_3.async_schedule_update_ha_state(True) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index d993233ac5d..7bf50462164 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -42,7 +42,11 @@ async def test_component_translation_path(hass, enable_custom_integrations): ) assert await async_setup_component(hass, "test_package", {"test_package"}) - (int_test, int_test_embedded, int_test_package,) = await asyncio.gather( + ( + int_test, + int_test_embedded, + int_test_package, + ) = await asyncio.gather( async_get_integration(hass, "test"), async_get_integration(hass, "test_embedded"), async_get_integration(hass, "test_package"), diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index fc35b28f7c9..89093a78f6f 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -107,7 +107,6 @@ def test_secrets(mock_is_file, event_loop): } with patch_yaml_files(files): - res = check_config.check(get_test_config_dir(), True) assert res["except"] == {} diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 087ccaaae28..25501a774fe 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3555,7 +3555,6 @@ async def test_initializing_flows_canceled_on_shutdown(hass: HomeAssistant, mana with patch.dict( config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler} ): - task = asyncio.create_task( manager.flow.async_init("test", context={"source": "reauth"}) ) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 6dadeba3797..f2362b0a3be 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -138,7 +138,6 @@ async def test_get_integration_with_requirements(hass): ) as mock_is_installed, patch( "homeassistant.util.package.install_package", return_value=True ) as mock_inst: - integration = await async_get_integration_with_requirements( hass, "test_component" ) @@ -193,7 +192,6 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes(ha ) as mock_is_installed, patch( "homeassistant.util.package.install_package", side_effect=_mock_install_package ) as mock_inst: - integration = await async_get_integration_with_requirements( hass, "test_component" ) @@ -224,7 +222,6 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes(ha ) as mock_is_installed, patch( "homeassistant.util.package.install_package", side_effect=_mock_install_package ) as mock_inst: - integration = await async_get_integration_with_requirements( hass, "test_component" ) @@ -243,7 +240,6 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes(ha ) as mock_is_installed, patch( "homeassistant.util.package.install_package", side_effect=_mock_install_package ) as mock_inst: - integration = await async_get_integration_with_requirements( hass, "test_component" ) @@ -274,7 +270,6 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes(ha ) as mock_is_installed, patch( "homeassistant.util.package.install_package", return_value=True ) as mock_inst: - integration = await async_get_integration_with_requirements( hass, "test_component" ) From 1df82f39c14db5eb382748e4da292adf598f5de7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Feb 2023 16:58:34 -0600 Subject: [PATCH 128/187] Speed up purge time with newer MariaDB versions (#87409) * Speed up purge time with newer MariaDB versions * fix * document * document * document * rename * self review * Update homeassistant/components/recorder/util.py * fixes --- homeassistant/components/recorder/core.py | 8 +- homeassistant/components/recorder/models.py | 28 ++++++ homeassistant/components/recorder/purge.py | 76 ++++++++-------- homeassistant/components/recorder/queries.py | 4 +- .../recorder/system_health/__init__.py | 4 +- homeassistant/components/recorder/util.py | 88 +++++++++++-------- tests/components/recorder/test_util.py | 48 ++++++++-- 7 files changed, 169 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index ddca32e6970..497ae364b64 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -14,7 +14,6 @@ import time from typing import Any, TypeVar, cast import async_timeout -from awesomeversion import AwesomeVersion from lru import LRU # pylint: disable=no-name-in-module from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select from sqlalchemy.engine import Engine @@ -67,6 +66,7 @@ from .db_schema import ( ) from .executor import DBInterruptibleThreadPoolExecutor from .models import ( + DatabaseEngine, StatisticData, StatisticMetaData, UnsupportedDialect, @@ -173,7 +173,7 @@ class Recorder(threading.Thread): self.db_url = uri self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait - self.engine_version: AwesomeVersion | None = None + self.database_engine: DatabaseEngine | None = None # Database connection is ready, but non-live migration may be in progress db_connected: asyncio.Future[bool] = hass.data[DOMAIN].db_connected self.async_db_connected: asyncio.Future[bool] = db_connected @@ -1125,13 +1125,13 @@ class Recorder(threading.Thread): ) -> None: """Dbapi specific connection settings.""" assert self.engine is not None - if version := setup_connection_for_dialect( + if database_engine := setup_connection_for_dialect( self, self.engine.dialect.name, dbapi_connection, not self._completed_first_database_setup, ): - self.engine_version = version + self.database_engine = database_engine self._completed_first_database_setup = True if self.db_url == SQLITE_URL_PREFIX or ":memory:" in self.db_url: diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 3bbd9f173a3..b071a686761 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,10 +1,12 @@ """Models for Recorder.""" from __future__ import annotations +from dataclasses import dataclass from datetime import datetime, timedelta import logging from typing import Any, Literal, TypedDict, overload +from awesomeversion import AwesomeVersion from sqlalchemy.engine.row import Row from homeassistant.const import ( @@ -17,6 +19,8 @@ from homeassistant.core import Context, State from homeassistant.helpers.json import json_loads import homeassistant.util.dt as dt_util +from .const import SupportedDialect + # pylint: disable=invalid-name _LOGGER = logging.getLogger(__name__) @@ -443,3 +447,27 @@ class StatisticPeriod(TypedDict, total=False): calendar: CalendarStatisticPeriod fixed_period: FixedStatisticPeriod rolling_window: RollingWindowStatisticPeriod + + +@dataclass +class DatabaseEngine: + """Properties of the database engine.""" + + dialect: SupportedDialect + optimizer: DatabaseOptimizer + version: AwesomeVersion | None + + +@dataclass +class DatabaseOptimizer: + """Properties of the database optimizer for the configured database engine.""" + + # Some MariaDB versions have a bug that causes a slow query when using + # a range in a select statement with an IN clause. + # + # https://jira.mariadb.org/browse/MDEV-25020 + # + # Historically, we have applied this logic to PostgreSQL as well, but + # it may not be necessary. We should revisit this in the future + # when we have more data. + slow_range_in_select: bool diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 4fb52dabb21..fa380e5a7e2 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -14,13 +14,14 @@ from sqlalchemy.sql.expression import distinct from homeassistant.const import EVENT_STATE_CHANGED import homeassistant.util.dt as dt_util -from .const import MAX_ROWS_TO_PURGE, SupportedDialect +from .const import MAX_ROWS_TO_PURGE from .db_schema import Events, StateAttributes, States +from .models import DatabaseEngine from .queries import ( attributes_ids_exist_in_states, - attributes_ids_exist_in_states_sqlite, + attributes_ids_exist_in_states_with_fast_in_distinct, data_ids_exist_in_events, - data_ids_exist_in_events_sqlite, + data_ids_exist_in_events_with_fast_in_distinct, delete_event_data_rows, delete_event_rows, delete_recorder_runs_rows, @@ -83,8 +84,6 @@ def purge_old_data( "Purging states and events before target %s", purge_before.isoformat(sep=" ", timespec="seconds"), ) - using_sqlite = instance.dialect_name == SupportedDialect.SQLITE - with session_scope(session=instance.get_session()) as session: # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record has_more_to_purge = False @@ -93,9 +92,7 @@ def purge_old_data( "Purge running in legacy format as there are states with event_id" " remaining" ) - has_more_to_purge |= _purge_legacy_format( - instance, session, purge_before, using_sqlite - ) + has_more_to_purge |= _purge_legacy_format(instance, session, purge_before) else: _LOGGER.debug( "Purge running in new format as there are NO states with event_id" @@ -103,10 +100,10 @@ def purge_old_data( ) # Once we are done purging legacy rows, we use the new method has_more_to_purge |= _purge_states_and_attributes_ids( - instance, session, states_batch_size, purge_before, using_sqlite + instance, session, states_batch_size, purge_before ) has_more_to_purge |= _purge_events_and_data_ids( - instance, session, events_batch_size, purge_before, using_sqlite + instance, session, events_batch_size, purge_before ) statistics_runs = _select_statistics_runs_to_purge(session, purge_before) @@ -140,7 +137,7 @@ def _purging_legacy_format(session: Session) -> bool: def _purge_legacy_format( - instance: Recorder, session: Session, purge_before: datetime, using_sqlite: bool + instance: Recorder, session: Session, purge_before: datetime ) -> bool: """Purge rows that are still linked by the event_ids.""" ( @@ -153,10 +150,10 @@ def _purge_legacy_format( ) if state_ids: _purge_state_ids(instance, session, state_ids) - _purge_unused_attributes_ids(instance, session, attributes_ids, using_sqlite) + _purge_unused_attributes_ids(instance, session, attributes_ids) if event_ids: _purge_event_ids(session, event_ids) - _purge_unused_data_ids(instance, session, data_ids, using_sqlite) + _purge_unused_data_ids(instance, session, data_ids) return bool(event_ids or state_ids or attributes_ids or data_ids) @@ -165,12 +162,13 @@ def _purge_states_and_attributes_ids( session: Session, states_batch_size: int, purge_before: datetime, - using_sqlite: bool, ) -> bool: """Purge states and linked attributes id in a batch. Returns true if there are more states to purge. """ + database_engine = instance.database_engine + assert database_engine is not None has_remaining_state_ids_to_purge = True # There are more states relative to attributes_ids so # we purge enough state_ids to try to generate a full @@ -187,7 +185,7 @@ def _purge_states_and_attributes_ids( _purge_state_ids(instance, session, state_ids) attributes_ids_batch = attributes_ids_batch | attributes_ids - _purge_unused_attributes_ids(instance, session, attributes_ids_batch, using_sqlite) + _purge_unused_attributes_ids(instance, session, attributes_ids_batch) _LOGGER.debug( "After purging states and attributes_ids remaining=%s", has_remaining_state_ids_to_purge, @@ -200,7 +198,6 @@ def _purge_events_and_data_ids( session: Session, events_batch_size: int, purge_before: datetime, - using_sqlite: bool, ) -> bool: """Purge states and linked attributes id in a batch. @@ -220,7 +217,7 @@ def _purge_events_and_data_ids( _purge_event_ids(session, event_ids) data_ids_batch = data_ids_batch | data_ids - _purge_unused_data_ids(instance, session, data_ids_batch, using_sqlite) + _purge_unused_data_ids(instance, session, data_ids_batch) _LOGGER.debug( "After purging event and data_ids remaining=%s", has_remaining_event_ids_to_purge, @@ -267,13 +264,13 @@ def _select_event_data_ids_to_purge( def _select_unused_attributes_ids( - session: Session, attributes_ids: set[int], using_sqlite: bool + session: Session, attributes_ids: set[int], database_engine: DatabaseEngine ) -> set[int]: """Return a set of attributes ids that are not used by any states in the db.""" if not attributes_ids: return set() - if using_sqlite: + if not database_engine.optimizer.slow_range_in_select: # # SQLite has a superior query optimizer for the distinct query below as it uses # the covering index without having to examine the rows directly for both of the @@ -290,7 +287,7 @@ def _select_unused_attributes_ids( seen_ids = { state[0] for state in session.execute( - attributes_ids_exist_in_states_sqlite(attributes_ids) + attributes_ids_exist_in_states_with_fast_in_distinct(attributes_ids) ).all() } else: @@ -340,16 +337,18 @@ def _purge_unused_attributes_ids( instance: Recorder, session: Session, attributes_ids_batch: set[int], - using_sqlite: bool, ) -> None: + """Purge unused attributes ids.""" + database_engine = instance.database_engine + assert database_engine is not None if unused_attribute_ids_set := _select_unused_attributes_ids( - session, attributes_ids_batch, using_sqlite + session, attributes_ids_batch, database_engine ): _purge_batch_attributes_ids(instance, session, unused_attribute_ids_set) def _select_unused_event_data_ids( - session: Session, data_ids: set[int], using_sqlite: bool + session: Session, data_ids: set[int], database_engine: DatabaseEngine ) -> set[int]: """Return a set of event data ids that are not used by any events in the db.""" if not data_ids: @@ -357,11 +356,11 @@ def _select_unused_event_data_ids( # See _select_unused_attributes_ids for why this function # branches for non-sqlite databases. - if using_sqlite: + if not database_engine.optimizer.slow_range_in_select: seen_ids = { state[0] for state in session.execute( - data_ids_exist_in_events_sqlite(data_ids) + data_ids_exist_in_events_with_fast_in_distinct(data_ids) ).all() } else: @@ -381,10 +380,12 @@ def _select_unused_event_data_ids( def _purge_unused_data_ids( - instance: Recorder, session: Session, data_ids_batch: set[int], using_sqlite: bool + instance: Recorder, session: Session, data_ids_batch: set[int] ) -> None: + database_engine = instance.database_engine + assert database_engine is not None if unused_data_ids_set := _select_unused_event_data_ids( - session, data_ids_batch, using_sqlite + session, data_ids_batch, database_engine ): _purge_batch_data_ids(instance, session, unused_data_ids_set) @@ -582,7 +583,8 @@ def _purge_old_recorder_runs( def _purge_filtered_data(instance: Recorder, session: Session) -> bool: """Remove filtered states and events that shouldn't be in the database.""" _LOGGER.debug("Cleanup filtered data") - using_sqlite = instance.dialect_name == SupportedDialect.SQLITE + database_engine = instance.database_engine + assert database_engine is not None # Check if excluded entity_ids are in database excluded_entity_ids: list[str] = [ @@ -591,7 +593,7 @@ def _purge_filtered_data(instance: Recorder, session: Session) -> bool: if not instance.entity_filter(entity_id) ] if len(excluded_entity_ids) > 0: - _purge_filtered_states(instance, session, excluded_entity_ids, using_sqlite) + _purge_filtered_states(instance, session, excluded_entity_ids, database_engine) return False # Check if excluded event_types are in database @@ -611,7 +613,7 @@ def _purge_filtered_states( instance: Recorder, session: Session, excluded_entity_ids: list[str], - using_sqlite: bool, + database_engine: DatabaseEngine, ) -> None: """Remove filtered states and linked events.""" state_ids: list[int] @@ -632,7 +634,7 @@ def _purge_filtered_states( _purge_state_ids(instance, session, set(state_ids)) _purge_event_ids(session, event_ids) unused_attribute_ids_set = _select_unused_attributes_ids( - session, {id_ for id_ in attributes_ids if id_ is not None}, using_sqlite + session, {id_ for id_ in attributes_ids if id_ is not None}, database_engine ) _purge_batch_attributes_ids(instance, session, unused_attribute_ids_set) @@ -641,7 +643,8 @@ def _purge_filtered_events( instance: Recorder, session: Session, excluded_event_types: list[str] ) -> None: """Remove filtered events and linked states.""" - using_sqlite = instance.dialect_name == SupportedDialect.SQLITE + database_engine = instance.database_engine + assert database_engine is not None event_ids, data_ids = zip( *( session.query(Events.event_id, Events.data_id) @@ -660,7 +663,7 @@ def _purge_filtered_events( _purge_state_ids(instance, session, state_ids) _purge_event_ids(session, event_ids) if unused_data_ids_set := _select_unused_event_data_ids( - session, set(data_ids), using_sqlite + session, set(data_ids), database_engine ): _purge_batch_data_ids(instance, session, unused_data_ids_set) if EVENT_STATE_CHANGED in excluded_event_types: @@ -671,7 +674,8 @@ def _purge_filtered_events( @retryable_database_job("purge") def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) -> bool: """Purge states and events of specified entities.""" - using_sqlite = instance.dialect_name == SupportedDialect.SQLITE + database_engine = instance.database_engine + assert database_engine is not None with session_scope(session=instance.get_session()) as session: selected_entity_ids: list[str] = [ entity_id @@ -682,7 +686,9 @@ def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) if len(selected_entity_ids) > 0: # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states # or events record. - _purge_filtered_states(instance, session, selected_entity_ids, using_sqlite) + _purge_filtered_states( + instance, session, selected_entity_ids, database_engine + ) _LOGGER.debug("Purging entity data hasn't fully completed yet") return False diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index 0591fda4713..29bac70eef6 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -45,7 +45,7 @@ def _state_attrs_exist(attr: int | None) -> Select: return select(func.min(States.attributes_id)).where(States.attributes_id == attr) -def attributes_ids_exist_in_states_sqlite( +def attributes_ids_exist_in_states_with_fast_in_distinct( attributes_ids: Iterable[int], ) -> StatementLambdaElement: """Find attributes ids that exist in the states table.""" @@ -268,7 +268,7 @@ def attributes_ids_exist_in_states( ) -def data_ids_exist_in_events_sqlite( +def data_ids_exist_in_events_with_fast_in_distinct( data_ids: Iterable[int], ) -> StatementLambdaElement: """Find data ids that exist in the events table.""" diff --git a/homeassistant/components/recorder/system_health/__init__.py b/homeassistant/components/recorder/system_health/__init__.py index b79f526db2b..da463d38610 100644 --- a/homeassistant/components/recorder/system_health/__init__.py +++ b/homeassistant/components/recorder/system_health/__init__.py @@ -49,8 +49,8 @@ def _async_get_db_engine_info(instance: Recorder) -> dict[str, Any]: db_engine_info: dict[str, Any] = {} if dialect_name := instance.dialect_name: db_engine_info["database_engine"] = dialect_name.value - if engine_version := instance.engine_version: - db_engine_info["database_version"] = str(engine_version) + if database_engine := instance.database_engine: + db_engine_info["database_version"] = str(database_engine.version) return db_engine_info diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 0469a71009a..dde32bf05db 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -36,7 +36,13 @@ from .db_schema import ( TABLES_TO_CHECK, RecorderRuns, ) -from .models import StatisticPeriod, UnsupportedDialect, process_timestamp +from .models import ( + DatabaseEngine, + DatabaseOptimizer, + StatisticPeriod, + UnsupportedDialect, + process_timestamp, +) if TYPE_CHECKING: from . import Recorder @@ -51,44 +57,33 @@ QUERY_RETRY_WAIT = 0.1 SQLITE3_POSTFIXES = ["", "-wal", "-shm"] DEFAULT_YIELD_STATES_ROWS = 32768 + # Our minimum versions for each database # # Older MariaDB suffers https://jira.mariadb.org/browse/MDEV-25020 # which is fixed in 10.5.17, 10.6.9, 10.7.5, 10.8.4 # -MIN_VERSION_MARIA_DB = AwesomeVersion( - "10.3.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -RECOMMENDED_MIN_VERSION_MARIA_DB = AwesomeVersion( - "10.5.17", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MARIA_DB_106 = AwesomeVersion( - "10.6.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -RECOMMENDED_MIN_VERSION_MARIA_DB_106 = AwesomeVersion( - "10.6.9", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MARIA_DB_107 = AwesomeVersion( - "10.7.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -RECOMMENDED_MIN_VERSION_MARIA_DB_107 = AwesomeVersion( - "10.7.5", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MARIA_DB_108 = AwesomeVersion( - "10.8.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -RECOMMENDED_MIN_VERSION_MARIA_DB_108 = AwesomeVersion( - "10.8.4", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MIN_VERSION_MYSQL = AwesomeVersion( - "8.0.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MIN_VERSION_PGSQL = AwesomeVersion( - "12.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) -MIN_VERSION_SQLITE = AwesomeVersion( - "3.31.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER -) +def _simple_version(version: str) -> AwesomeVersion: + """Return a simple version.""" + return AwesomeVersion(version, ensure_strategy=AwesomeVersionStrategy.SIMPLEVER) + + +MIN_VERSION_MARIA_DB = _simple_version("10.3.0") +RECOMMENDED_MIN_VERSION_MARIA_DB = _simple_version("10.5.17") +MARIADB_WITH_FIXED_IN_QUERIES_105 = _simple_version("10.5.17") +MARIA_DB_106 = _simple_version("10.6.0") +MARIADB_WITH_FIXED_IN_QUERIES_106 = _simple_version("10.6.9") +RECOMMENDED_MIN_VERSION_MARIA_DB_106 = _simple_version("10.6.9") +MARIA_DB_107 = _simple_version("10.7.0") +RECOMMENDED_MIN_VERSION_MARIA_DB_107 = _simple_version("10.7.5") +MARIADB_WITH_FIXED_IN_QUERIES_107 = _simple_version("10.7.5") +MARIA_DB_108 = _simple_version("10.8.0") +RECOMMENDED_MIN_VERSION_MARIA_DB_108 = _simple_version("10.8.4") +MARIADB_WITH_FIXED_IN_QUERIES_108 = _simple_version("10.8.4") +MIN_VERSION_MYSQL = _simple_version("8.0.0") +MIN_VERSION_PGSQL = _simple_version("12.0") +MIN_VERSION_SQLITE = _simple_version("3.31.0") + # This is the maximum time after the recorder ends the session # before we no longer consider startup to be a "restart" and we @@ -467,10 +462,12 @@ def setup_connection_for_dialect( dialect_name: str, dbapi_connection: Any, first_connection: bool, -) -> AwesomeVersion | None: +) -> DatabaseEngine | None: """Execute statements needed for dialect connection.""" version: AwesomeVersion | None = None + slow_range_in_select = True if dialect_name == SupportedDialect.SQLITE: + slow_range_in_select = False if first_connection: old_isolation = dbapi_connection.isolation_level dbapi_connection.isolation_level = None @@ -536,7 +533,19 @@ def setup_connection_for_dialect( version or version_string, "MySQL", MIN_VERSION_MYSQL ) + slow_range_in_select = bool( + not version + or version < MARIADB_WITH_FIXED_IN_QUERIES_105 + or MARIA_DB_106 <= version < MARIADB_WITH_FIXED_IN_QUERIES_106 + or MARIA_DB_107 <= version < MARIADB_WITH_FIXED_IN_QUERIES_107 + or MARIA_DB_108 <= version < MARIADB_WITH_FIXED_IN_QUERIES_108 + ) elif dialect_name == SupportedDialect.POSTGRESQL: + # Historically we have marked PostgreSQL as having slow range in select + # but this may not be true for all versions. We should investigate + # this further when we have more data and remove this if possible + # in the future so we can use the simpler purge SQL query for + # _select_unused_attributes_ids and _select_unused_events_ids if first_connection: # server_version_num was added in 2006 result = query_on_connection(dbapi_connection, "SHOW server_version") @@ -550,7 +559,14 @@ def setup_connection_for_dialect( else: _fail_unsupported_dialect(dialect_name) - return version + if not first_connection: + return None + + return DatabaseEngine( + dialect=SupportedDialect(dialect_name), + version=version, + optimizer=DatabaseOptimizer(slow_range_in_select=slow_range_in_select), + ) def end_incomplete_runs(session: Session, start_time: datetime) -> None: diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 3f5ba6d40ef..932b23b1152 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -231,7 +231,12 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) + assert ( + util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, True + ) + is not None + ) assert len(execute_args) == 5 assert execute_args[0] == "PRAGMA journal_mode=WAL" @@ -241,7 +246,12 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version): assert execute_args[4] == "PRAGMA foreign_keys=ON" execute_args = [] - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, False) + assert ( + util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, False + ) + is None + ) assert len(execute_args) == 3 assert execute_args[0] == "PRAGMA cache_size = -16384" @@ -276,7 +286,12 @@ def test_setup_connection_for_dialect_sqlite_zero_commit_interval( dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) + assert ( + util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, True + ) + is not None + ) assert len(execute_args) == 5 assert execute_args[0] == "PRAGMA journal_mode=WAL" @@ -286,7 +301,12 @@ def test_setup_connection_for_dialect_sqlite_zero_commit_interval( assert execute_args[4] == "PRAGMA foreign_keys=ON" execute_args = [] - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, False) + assert ( + util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, False + ) + is None + ) assert len(execute_args) == 3 assert execute_args[0] == "PRAGMA cache_size = -16384" @@ -444,11 +464,13 @@ def test_supported_pgsql(caplog, pgsql_version): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect( + database_engine = util.setup_connection_for_dialect( instance_mock, "postgresql", dbapi_connection, True ) assert "minimum supported version" not in caplog.text + assert database_engine is not None + assert database_engine.optimizer.slow_range_in_select is True @pytest.mark.parametrize( @@ -525,9 +547,13 @@ def test_supported_sqlite(caplog, sqlite_version): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) + database_engine = util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, True + ) assert "minimum supported version" not in caplog.text + assert database_engine is not None + assert database_engine.optimizer.slow_range_in_select is False @pytest.mark.parametrize( @@ -599,7 +625,7 @@ async def test_issue_for_mariadb_with_MDEV_25020( dbapi_connection = MagicMock(cursor=_make_cursor_mock) - await hass.async_add_executor_job( + database_engine = await hass.async_add_executor_job( util.setup_connection_for_dialect, instance_mock, "mysql", @@ -613,6 +639,9 @@ async def test_issue_for_mariadb_with_MDEV_25020( assert issue is not None assert issue.translation_placeholders == {"min_version": min_version} + assert database_engine is not None + assert database_engine.optimizer.slow_range_in_select is True + @pytest.mark.parametrize( "mysql_version", @@ -649,7 +678,7 @@ async def test_no_issue_for_mariadb_with_MDEV_25020(hass, caplog, mysql_version) dbapi_connection = MagicMock(cursor=_make_cursor_mock) - await hass.async_add_executor_job( + database_engine = await hass.async_add_executor_job( util.setup_connection_for_dialect, instance_mock, "mysql", @@ -662,6 +691,9 @@ async def test_no_issue_for_mariadb_with_MDEV_25020(hass, caplog, mysql_version) issue = registry.async_get_issue(DOMAIN, "maria_db_range_index_regression") assert issue is None + assert database_engine is not None + assert database_engine.optimizer.slow_range_in_select is False + def test_basic_sanity_check(hass_recorder, recorder_db_url): """Test the basic sanity checks with a missing table.""" From 5b0c7321b5de4287991ac623887385bfbed038cc Mon Sep 17 00:00:00 2001 From: Gregory Haynes Date: Sun, 5 Feb 2023 17:34:37 +0000 Subject: [PATCH 129/187] Add missing name field to emulated_hue config (#87456) Co-authored-by: J. Nick Koston --- homeassistant/components/emulated_hue/hue_api.py | 1 + tests/components/emulated_hue/test_hue_api.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index a81544151ea..b855d0c738d 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -831,6 +831,7 @@ def create_hue_success_response( def create_config_model(config: Config, request: web.Request) -> dict[str, Any]: """Create a config resource.""" return { + "name": "HASS BRIDGE", "mac": "00:00:00:00:00:00", "swversion": "01003542", "apiversion": "1.17.0", diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 58303ce54a6..cfadccc39c4 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -465,8 +465,9 @@ async def test_discover_full_state(hue_client): # Make sure array is correct size assert len(result_json) == 2 - assert len(config_json) == 6 + assert len(config_json) == 7 assert len(lights_json) >= 1 + assert "name" in config_json # Make sure the config wrapper added to the config is there assert "mac" in config_json @@ -505,7 +506,8 @@ async def test_discover_config(hue_client): config_json = await result.json() # Make sure array is correct size - assert len(config_json) == 6 + assert len(config_json) == 7 + assert "name" in config_json # Make sure the config wrapper added to the config is there assert "mac" in config_json From 4a7aee4bde666c799e2c9438488e29fe85cb9978 Mon Sep 17 00:00:00 2001 From: majuss Date: Mon, 6 Feb 2023 13:30:35 +0100 Subject: [PATCH 130/187] Bump lupupy to 0.2.7 (#87469) --- homeassistant/components/lupusec/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index f1403e1b2d7..3e47daa0271 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -2,7 +2,7 @@ "domain": "lupusec", "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", - "requirements": ["lupupy==0.2.5"], + "requirements": ["lupupy==0.2.7"], "codeowners": ["@majuss"], "iot_class": "local_polling", "loggers": ["lupupy"] diff --git a/requirements_all.txt b/requirements_all.txt index 73861751456..43f76ad1646 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1081,7 +1081,7 @@ london-tube-status==0.5 luftdaten==0.7.4 # homeassistant.components.lupusec -lupupy==0.2.5 +lupupy==0.2.7 # homeassistant.components.lw12wifi lw12==0.9.2 From b2ccf2e87e66f9f128879fbbd11a791e6dae2ad7 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 5 Feb 2023 20:06:17 +0100 Subject: [PATCH 131/187] Bump py-synologydsm-api to 2.1.4 (#87471) fixes undefined --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index a4b340bd3fa..a6664055bc9 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==2.1.2"], + "requirements": ["py-synologydsm-api==2.1.4"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 43f76ad1646..0a326a79465 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1442,7 +1442,7 @@ py-schluter==0.1.7 py-sucks==0.9.8 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.2 +py-synologydsm-api==2.1.4 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4dc297934b..146b2e0e1f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1051,7 +1051,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==2.1.2 +py-synologydsm-api==2.1.4 # homeassistant.components.seventeentrack py17track==2021.12.2 From 423d8f0bca14c4cddb3c6e948e303c6f02394bc7 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 5 Feb 2023 13:28:28 -0500 Subject: [PATCH 132/187] Disable uptime sensor by default in Unifi (#87484) Disable Uptime sensor by default in Unifi --- homeassistant/components/unifi/sensor.py | 1 + tests/components/unifi/test_sensor.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index a88b750fdf7..aec123ca273 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -154,6 +154,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, has_entity_name=True, + entity_registry_enabled_default=False, allowed_fn=lambda controller, _: controller.option_allow_uptime_sensors, api_handler_fn=lambda api: api.clients, available_fn=lambda controller, obj_id: controller.available, diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 3aa4114e829..d3823c2fd9e 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -193,6 +193,7 @@ async def test_uptime_sensors( hass, aioclient_mock, mock_unifi_websocket, + entity_registry_enabled_by_default, initial_uptime, event_uptime, new_uptime, @@ -263,7 +264,9 @@ async def test_uptime_sensors( assert hass.states.get("sensor.client1_uptime") is None -async def test_remove_sensors(hass, aioclient_mock, mock_unifi_websocket): +async def test_remove_sensors( + hass, aioclient_mock, mock_unifi_websocket, entity_registry_enabled_by_default +): """Verify removing of clients work as expected.""" wired_client = { "hostname": "Wired client", From e30ea3e344fbef2337420574e94fb006a58f3555 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 6 Feb 2023 17:11:21 +0100 Subject: [PATCH 133/187] Add the correct loggers to velbus manifest.json (#87488) --- homeassistant/components/velbus/manifest.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 9384947cb82..9052e73ba69 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -1,13 +1,19 @@ { "domain": "velbus", "name": "Velbus", - "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2022.12.0"], - "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], + "config_flow": true, "dependencies": ["usb"], + "documentation": "https://www.home-assistant.io/integrations/velbus", "integration_type": "hub", "iot_class": "local_push", + "loggers": [ + "velbus-parser", + "velbus-module", + "velbus-packet", + "velbus-protocol" + ], + "requirements": ["velbus-aio==2022.12.0"], "usb": [ { "vid": "10CF", @@ -25,6 +31,5 @@ "vid": "10CF", "pid": "0518" } - ], - "loggers": ["velbusaio"] + ] } From 8a257df59f17cf749740d6db9a688a5163a853e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Feb 2023 21:12:41 -0600 Subject: [PATCH 134/187] Fix recorder run history during schema migration and startup (#87492) Fix recorder run history during schema migration RunHistory.get and RunHistory.current can be called before RunHistory.start. We need to return a RecorderRuns object with the recording_start time that will be used when start it called to ensure history queries still work as expected. fixes #87112 --- homeassistant/components/recorder/run_history.py | 9 +++++++-- tests/components/recorder/test_run_history.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/run_history.py b/homeassistant/components/recorder/run_history.py index fb87d9a1fa2..02b2df066bd 100644 --- a/homeassistant/components/recorder/run_history.py +++ b/homeassistant/components/recorder/run_history.py @@ -64,8 +64,13 @@ class RunHistory: @property def current(self) -> RecorderRuns: """Get the current run.""" - assert self._current_run_info is not None - return self._current_run_info + # If start has not been called yet because the recorder is + # still starting up we want history to use the current time + # as the created time to ensure we can still return results + # and we do not try to pull data from the previous run. + return self._current_run_info or RecorderRuns( + start=self.recording_start, created=dt_util.utcnow() + ) def get(self, start: datetime) -> RecorderRuns | None: """Return the recorder run that started before or at start. diff --git a/tests/components/recorder/test_run_history.py b/tests/components/recorder/test_run_history.py index 7504404f779..3b5bd7dda6b 100644 --- a/tests/components/recorder/test_run_history.py +++ b/tests/components/recorder/test_run_history.py @@ -45,3 +45,14 @@ async def test_run_history(recorder_mock, hass): process_timestamp(instance.run_history.get(now).start) == instance.run_history.recording_start ) + + +async def test_run_history_during_schema_migration(recorder_mock, hass): + """Test the run history during schema migration.""" + instance = recorder.get_instance(hass) + run_history = instance.run_history + assert run_history.current.start == run_history.recording_start + with instance.get_session() as session: + run_history.start(session) + assert run_history.current.start == run_history.recording_start + assert run_history.current.created >= run_history.recording_start From 1a72b64024af52ed602b02b9a3887089cbaf4448 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Mon, 6 Feb 2023 00:31:21 +0100 Subject: [PATCH 135/187] Bump xiaomi-ble to 0.16.1 (#87496) Co-authored-by: J. Nick Koston --- homeassistant/components/xiaomi_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/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 1f36ac10d10..f3904003411 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -14,7 +14,7 @@ } ], "dependencies": ["bluetooth_adapters"], - "requirements": ["xiaomi-ble==0.15.0"], + "requirements": ["xiaomi-ble==0.16.1"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 0a326a79465..9738e087786 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2637,7 +2637,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.15.0 +xiaomi-ble==0.16.1 # homeassistant.components.knx xknx==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 146b2e0e1f8..ad76811f4c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1862,7 +1862,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.15.0 +xiaomi-ble==0.16.1 # homeassistant.components.knx xknx==2.3.0 From 08c23dd688c9220e8cff08a5d332eaa3da2a96e6 Mon Sep 17 00:00:00 2001 From: snapcase Date: Mon, 6 Feb 2023 11:12:54 -0500 Subject: [PATCH 136/187] Bump jaraco.abode to 3.3.0 (#87498) Fixes https://github.com/home-assistant/core/issues/86765 fixes undefined --- homeassistant/components/abode/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 6045f8797b4..771dfc581a9 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -3,7 +3,7 @@ "name": "Abode", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", - "requirements": ["jaraco.abode==3.2.1"], + "requirements": ["jaraco.abode==3.3.0"], "codeowners": ["@shred86"], "homekit": { "models": ["Abode", "Iota"] diff --git a/requirements_all.txt b/requirements_all.txt index 9738e087786..3c5fe37a04d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -997,7 +997,7 @@ ismartgate==4.0.4 janus==1.0.0 # homeassistant.components.abode -jaraco.abode==3.2.1 +jaraco.abode==3.3.0 # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad76811f4c6..772f4a10c07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -753,7 +753,7 @@ ismartgate==4.0.4 janus==1.0.0 # homeassistant.components.abode -jaraco.abode==3.2.1 +jaraco.abode==3.3.0 # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 From f59eb6c277ece964052202d82de51e70df03d5bc Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 6 Feb 2023 00:34:52 +0100 Subject: [PATCH 137/187] Bump bimmer_connected to 0.12.1 (#87506) Co-authored-by: rikroe fixes undefined --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index c03bdf6a26f..c5fbb192846 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.12.0"], + "requirements": ["bimmer_connected==0.12.1"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 3c5fe37a04d..96c78fd861f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -425,7 +425,7 @@ beautifulsoup4==4.11.1 bellows==0.34.7 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.12.0 +bimmer_connected==0.12.1 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 772f4a10c07..f21105746fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -355,7 +355,7 @@ beautifulsoup4==4.11.1 bellows==0.34.7 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.12.0 +bimmer_connected==0.12.1 # homeassistant.components.bluetooth bleak-retry-connector==2.13.0 From 7fd3f404de22a7e1b5578eac77297a483fc01a26 Mon Sep 17 00:00:00 2001 From: Ben Corrado Date: Mon, 6 Feb 2023 10:00:06 -0800 Subject: [PATCH 138/187] Add LD2410BLE support for new firmware version (#87507) * Updated local_name to reflect the naming scheme in HLK firmware 2.01.23020209 * Adding generated bluetooth file. --- homeassistant/components/ld2410_ble/manifest.json | 9 ++++++++- homeassistant/generated/bluetooth.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index df7ddff7018..14af223baf0 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -6,7 +6,14 @@ "requirements": ["bluetooth-data-tools==0.3.1", "ld2410-ble==0.1.1"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@930913"], - "bluetooth": [{ "local_name": "HLK-LD2410B_*" }], + "bluetooth": [ + { + "local_name": "HLK-LD2410B_*" + }, + { + "local_name": "HLK-LD2410_*" + } + ], "integration_type": "device", "iot_class": "local_push" } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index efc05ff2831..ef79445ff32 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -225,6 +225,10 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "ld2410_ble", "local_name": "HLK-LD2410B_*", }, + { + "domain": "ld2410_ble", + "local_name": "HLK-LD2410_*", + }, { "domain": "led_ble", "local_name": "LEDnet*", From bc01df6b52d0a6a48c23eedc7950e11bfb05ad4a Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Mon, 6 Feb 2023 11:05:28 -0500 Subject: [PATCH 139/187] Bump env_canada to 0.5.28 (#87509) Co-authored-by: J. Nick Koston fixes undefined --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index b7d3644d481..f0e27c30f7d 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.5.27"], + "requirements": ["env_canada==0.5.28"], "codeowners": ["@gwww", "@michaeldavie"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 96c78fd861f..fab562589bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env_canada==0.5.27 +env_canada==0.5.28 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f21105746fe..4e87c19e698 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -511,7 +511,7 @@ energyzero==0.3.1 enocean==0.50 # homeassistant.components.environment_canada -env_canada==0.5.27 +env_canada==0.5.28 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 From 590bdc1f496eb804c62ab26f285fae2af6cd3af6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Feb 2023 07:50:44 -0600 Subject: [PATCH 140/187] Optimize history.get_last_state_changes query (#87554) fixes undefined --- homeassistant/components/recorder/history.py | 48 ++++++++++--------- tests/components/recorder/test_history.py | 2 +- .../recorder/test_history_db_schema_30.py | 2 +- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index e4f5a39f1cc..1d3716abc47 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -519,48 +519,52 @@ def state_changes_during_period( def _get_last_state_changes_stmt( - schema_version: int, number_of_states: int, entity_id: str | None + schema_version: int, number_of_states: int, entity_id: str ) -> StatementLambdaElement: stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, False, include_last_changed=False ) if schema_version >= 31: - stmt += lambda q: q.filter( - (States.last_changed_ts == States.last_updated_ts) - | States.last_changed_ts.is_(None) + stmt += lambda q: q.where( + States.state_id + == ( + select(States.state_id) + .filter(States.entity_id == entity_id) + .order_by(States.last_updated_ts.desc()) + .limit(number_of_states) + .subquery() + ).c.state_id ) else: - stmt += lambda q: q.filter( - (States.last_changed == States.last_updated) | States.last_changed.is_(None) + stmt += lambda q: q.where( + States.state_id + == ( + select(States.state_id) + .filter(States.entity_id == entity_id) + .order_by(States.last_updated.desc()) + .limit(number_of_states) + .subquery() + ).c.state_id ) - if entity_id: - stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - if schema_version >= 31: - stmt += lambda q: q.order_by( - States.entity_id, States.last_updated_ts.desc() - ).limit(number_of_states) - else: - stmt += lambda q: q.order_by( - States.entity_id, States.last_updated.desc() - ).limit(number_of_states) + + stmt += lambda q: q.order_by(States.state_id.desc()) return stmt def get_last_state_changes( - hass: HomeAssistant, number_of_states: int, entity_id: str | None + hass: HomeAssistant, number_of_states: int, entity_id: str ) -> MutableMapping[str, list[State]]: """Return the last number_of_states.""" - start_time = dt_util.utcnow() - entity_id = entity_id.lower() if entity_id is not None else None - entity_ids = [entity_id] if entity_id is not None else None + entity_id_lower = entity_id.lower() + entity_ids = [entity_id_lower] with session_scope(hass=hass) as session: stmt = _get_last_state_changes_stmt( - _schema_version(hass), number_of_states, entity_id + _schema_version(hass), number_of_states, entity_id_lower ) states = list(execute_stmt_lambda_element(session, stmt)) return cast( @@ -569,7 +573,7 @@ def get_last_state_changes( hass, session, reversed(states), - start_time, + dt_util.utcnow(), entity_ids, include_start_time_state=False, ), diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index c35f0075844..fab2b7fbacd 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -324,7 +324,7 @@ def test_get_last_state_changes(hass_recorder): start = dt_util.utcnow() - timedelta(minutes=2) point = start + timedelta(minutes=1) - point2 = point + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) with patch( "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start diff --git a/tests/components/recorder/test_history_db_schema_30.py b/tests/components/recorder/test_history_db_schema_30.py index 5e944ce454a..f1104145b18 100644 --- a/tests/components/recorder/test_history_db_schema_30.py +++ b/tests/components/recorder/test_history_db_schema_30.py @@ -249,7 +249,7 @@ def test_get_last_state_changes(hass_recorder): start = dt_util.utcnow() - timedelta(minutes=2) point = start + timedelta(minutes=1) - point2 = point + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) with patch( "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start From 5bc49b1757563a6831040dcbf897df04710cdd55 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Feb 2023 23:57:08 -0500 Subject: [PATCH 141/187] OpenAI: Ignore devices without a name (#87558) Ignore devices without a name --- homeassistant/components/openai_conversation/const.py | 2 +- tests/components/openai_conversation/test_init.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index 378548173b0..b5644915d91 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -9,7 +9,7 @@ An overview of the areas and the devices in this smart home: {%- for area in areas %} {%- set area_info = namespace(printed=false) %} {%- for device in area_devices(area.name) -%} - {%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") %} + {%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") and device_attr(device, "name") %} {%- if not area_info.printed %} {{ area.name }}: diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index 551d493df8e..b64f3322895 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -67,6 +67,13 @@ async def test_default_prompt(hass, mock_init_component): device_reg.async_update_device( device.id, disabled_by=device_registry.DeviceEntryDisabler.USER ) + device = device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "9876-no-name")}, + manufacturer="Test Manufacturer NoName", + model="Test Model NoName", + suggested_area="Test Area 2", + ) with patch("openai.Completion.create") as mock_create: result = await conversation.async_converse(hass, "hello", None, Context()) From 9657567280f254bfd9f444c0a5f0c7812b8875ca Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 6 Feb 2023 14:28:53 -0500 Subject: [PATCH 142/187] Bump oralb-ble to 0.17.4 (#87570) closes undefined --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 08b257c1869..e2f1749eb76 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.17.2"], + "requirements": ["oralb-ble==0.17.4"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco", "@Lash-L"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index fab562589bf..616ef1fd78e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1299,7 +1299,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.17.2 +oralb-ble==0.17.4 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e87c19e698..341b5a0d269 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -947,7 +947,7 @@ openai==0.26.2 openerz-api==0.2.0 # homeassistant.components.oralb -oralb-ble==0.17.2 +oralb-ble==0.17.4 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From d06e640889bd61f7292f1e3fd5de9dce961be8a2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 6 Feb 2023 22:41:52 +0100 Subject: [PATCH 143/187] Fix matter remove config entry device (#87571) --- homeassistant/components/matter/__init__.py | 20 ++--- .../components/matter/diagnostics.py | 21 +---- homeassistant/components/matter/helpers.py | 42 ++++++++- tests/components/matter/test_helpers.py | 50 ++++++++++- tests/components/matter/test_init.py | 85 ++++++++++++++++++- 5 files changed, 180 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index a0e4dcf7483..6a11e5ded44 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -32,7 +32,7 @@ from .addon import get_addon_manager from .api import async_register_api from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN, LOGGER from .device_platform import DEVICE_PLATFORM -from .helpers import MatterEntryData, get_matter +from .helpers import MatterEntryData, get_matter, get_node_from_device_entry CONNECT_TIMEOUT = 10 LISTEN_READY_TIMEOUT = 30 @@ -192,23 +192,13 @@ async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove a config entry from a device.""" - unique_id = None + node = await get_node_from_device_entry(hass, device_entry) - for ident in device_entry.identifiers: - if ident[0] == DOMAIN: - unique_id = ident[1] - break - - if not unique_id: + if node is None: return True - matter_entry_data: MatterEntryData = hass.data[DOMAIN][config_entry.entry_id] - matter_client = matter_entry_data.adapter.matter_client - - for node in await matter_client.get_nodes(): - if node.unique_id == unique_id: - await matter_client.remove_node(node.node_id) - break + matter = get_matter(hass) + await matter.matter_client.remove_node(node.node_id) return True diff --git a/homeassistant/components/matter/diagnostics.py b/homeassistant/components/matter/diagnostics.py index 77234ab48b5..571523f7f0c 100644 --- a/homeassistant/components/matter/diagnostics.py +++ b/homeassistant/components/matter/diagnostics.py @@ -11,8 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .const import DOMAIN, ID_TYPE_DEVICE_ID -from .helpers import get_device_id, get_matter +from .helpers import get_matter, get_node_from_device_entry ATTRIBUTES_TO_REDACT = {"chip.clusters.Objects.BasicInformation.Attributes.Location"} @@ -53,28 +52,14 @@ async def async_get_device_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a device.""" matter = get_matter(hass) - device_id_type_prefix = f"{ID_TYPE_DEVICE_ID}_" - device_id_full = next( - identifier[1] - for identifier in device.identifiers - if identifier[0] == DOMAIN and identifier[1].startswith(device_id_type_prefix) - ) - device_id = device_id_full.lstrip(device_id_type_prefix) - server_diagnostics = await matter.matter_client.get_diagnostics() - - node = next( - node - for node in await matter.matter_client.get_nodes() - for node_device in node.node_devices - if get_device_id(server_diagnostics.info, node_device) == device_id - ) + node = await get_node_from_device_entry(hass, device) return { "server_info": remove_serialization_type( dataclass_to_dict(server_diagnostics.info) ), "node": redact_matter_attributes( - remove_serialization_type(dataclass_to_dict(node)) + remove_serialization_type(dataclass_to_dict(node) if node else {}) ), } diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 5abf81ee608..ef42f9354ca 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -6,8 +6,9 @@ from dataclasses import dataclass from typing import TYPE_CHECKING from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr -from .const import DOMAIN +from .const import DOMAIN, ID_TYPE_DEVICE_ID if TYPE_CHECKING: from matter_server.common.models.node import MatterNode @@ -58,3 +59,42 @@ def get_device_id( # Append nodedevice(type) to differentiate between a root node # and bridge within Home Assistant devices. return f"{operational_instance_id}-{node_device.__class__.__name__}" + + +async def get_node_from_device_entry( + hass: HomeAssistant, device: dr.DeviceEntry +) -> MatterNode | None: + """Return MatterNode from device entry.""" + matter = get_matter(hass) + device_id_type_prefix = f"{ID_TYPE_DEVICE_ID}_" + device_id_full = next( + ( + identifier[1] + for identifier in device.identifiers + if identifier[0] == DOMAIN + and identifier[1].startswith(device_id_type_prefix) + ), + None, + ) + + if device_id_full is None: + raise ValueError(f"Device {device.id} is not a Matter device") + + device_id = device_id_full.lstrip(device_id_type_prefix) + matter_client = matter.matter_client + server_info = matter_client.server_info + + if server_info is None: + raise RuntimeError("Matter server information is not available") + + node = next( + ( + node + for node in await matter_client.get_nodes() + for node_device in node.node_devices + if get_device_id(server_info, node_device) == device_id + ), + None, + ) + + return node diff --git a/tests/components/matter/test_helpers.py b/tests/components/matter/test_helpers.py index 3da0a26b7ee..8f849c85941 100644 --- a/tests/components/matter/test_helpers.py +++ b/tests/components/matter/test_helpers.py @@ -3,11 +3,20 @@ from __future__ import annotations from unittest.mock import MagicMock -from homeassistant.components.matter.helpers import get_device_id +import pytest + +from homeassistant.components.matter.const import DOMAIN +from homeassistant.components.matter.helpers import ( + get_device_id, + get_node_from_device_entry, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from .common import setup_integration_with_node_fixture +from tests.common import MockConfigEntry + async def test_get_device_id( hass: HomeAssistant, @@ -20,3 +29,42 @@ async def test_get_device_id( device_id = get_device_id(matter_client.server_info, node.node_devices[0]) assert device_id == "00000000000004D2-0000000000000005-MatterNodeDevice" + + +async def test_get_node_from_device_entry( + hass: HomeAssistant, + matter_client: MagicMock, +) -> None: + """Test get_node_from_device_entry.""" + device_registry = dr.async_get(hass) + other_domain = "other_domain" + other_config_entry = MockConfigEntry(domain=other_domain) + other_device_entry = device_registry.async_get_or_create( + config_entry_id=other_config_entry.entry_id, + identifiers={(other_domain, "1234")}, + ) + node = await setup_integration_with_node_fixture( + hass, "device_diagnostics", matter_client + ) + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + device_entry = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + )[0] + assert device_entry + node_from_device_entry = await get_node_from_device_entry(hass, device_entry) + + assert node_from_device_entry is node + + with pytest.raises(ValueError) as value_error: + await get_node_from_device_entry(hass, other_device_entry) + + assert f"Device {other_device_entry.id} is not a Matter device" in str( + value_error.value + ) + + matter_client.server_info = None + + with pytest.raises(RuntimeError) as runtime_error: + node_from_device_entry = await get_node_from_device_entry(hass, device_entry) + + assert "Matter server information is not available" in str(runtime_error.value) diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index a3febe799a5..ce2e1010f64 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio -from collections.abc import Generator +from collections.abc import Awaitable, Callable, Generator from unittest.mock import AsyncMock, MagicMock, call, patch +from aiohttp import ClientWebSocketResponse from matter_server.client.exceptions import CannotConnect, InvalidServerVersion from matter_server.common.helpers.util import dataclass_from_dict from matter_server.common.models.error import MatterError @@ -16,9 +17,14 @@ from homeassistant.components.matter.const import DOMAIN from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir +from homeassistant.helpers import ( + device_registry as dr, + entity_registry as er, + issue_registry as ir, +) +from homeassistant.setup import async_setup_component -from .common import load_and_parse_node_fixture +from .common import load_and_parse_node_fixture, setup_integration_with_node_fixture from tests.common import MockConfigEntry @@ -587,3 +593,76 @@ async def test_remove_entry( assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to uninstall the Matter Server add-on" in caplog.text + + +async def test_remove_config_entry_device( + hass: HomeAssistant, + matter_client: MagicMock, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test that a device can be removed ok.""" + assert await async_setup_component(hass, "config", {}) + await setup_integration_with_node_fixture(hass, "device_diagnostics", matter_client) + await hass.async_block_till_done() + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + device_registry = dr.async_get(hass) + device_entry = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + )[0] + entity_registry = er.async_get(hass) + entity_id = "light.m5stamp_lighting_app" + + assert device_entry + assert entity_registry.async_get(entity_id) + assert hass.states.get(entity_id) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry.entry_id, + "device_id": device_entry.id, + } + ) + response = await client.receive_json() + assert response["success"] + await hass.async_block_till_done() + + assert not device_registry.async_get(device_entry.id) + assert not entity_registry.async_get(entity_id) + assert not hass.states.get(entity_id) + + +async def test_remove_config_entry_device_no_node( + hass: HomeAssistant, + matter_client: MagicMock, + integration: MockConfigEntry, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test that a device can be removed ok without an existing node.""" + assert await async_setup_component(hass, "config", {}) + config_entry = integration + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={ + (DOMAIN, "deviceid_00000000000004D2-0000000000000005-MatterNodeDevice") + }, + ) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry.entry_id, + "device_id": device_entry.id, + } + ) + response = await client.receive_json() + assert response["success"] + await hass.async_block_till_done() + + assert not device_registry.async_get(device_entry.id) From 0d9393ca793f68e59cc5ec3ed5a27012082366dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:54 -0600 Subject: [PATCH 144/187] Fix indent on slow_range_in_select for MySQL/MariaDB (#87581) We would calculate this value and throw it away because it was only used on first_connection --- homeassistant/components/recorder/util.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index dde32bf05db..ff6f9f1e7e1 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -533,13 +533,13 @@ def setup_connection_for_dialect( version or version_string, "MySQL", MIN_VERSION_MYSQL ) - slow_range_in_select = bool( - not version - or version < MARIADB_WITH_FIXED_IN_QUERIES_105 - or MARIA_DB_106 <= version < MARIADB_WITH_FIXED_IN_QUERIES_106 - or MARIA_DB_107 <= version < MARIADB_WITH_FIXED_IN_QUERIES_107 - or MARIA_DB_108 <= version < MARIADB_WITH_FIXED_IN_QUERIES_108 - ) + slow_range_in_select = bool( + not version + or version < MARIADB_WITH_FIXED_IN_QUERIES_105 + or MARIA_DB_106 <= version < MARIADB_WITH_FIXED_IN_QUERIES_106 + or MARIA_DB_107 <= version < MARIADB_WITH_FIXED_IN_QUERIES_107 + or MARIA_DB_108 <= version < MARIADB_WITH_FIXED_IN_QUERIES_108 + ) elif dialect_name == SupportedDialect.POSTGRESQL: # Historically we have marked PostgreSQL as having slow range in select # but this may not be true for all versions. We should investigate From e0a6a6cfa6cd2f5b449f1edf450db3fa23aef3d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Feb 2023 12:38:48 -0600 Subject: [PATCH 145/187] Fix LD2410 BLE detection with passive scans (#87584) --- homeassistant/components/ld2410_ble/manifest.json | 5 +++++ homeassistant/generated/bluetooth.py | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index 14af223baf0..9d133876a7d 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -12,6 +12,11 @@ }, { "local_name": "HLK-LD2410_*" + }, + { + "manufacturer_id": 256, + "manufacturer_data_start": [7, 1], + "service_uuid": "0000af30-0000-1000-8000-00805f9b34fb" } ], "integration_type": "device", diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index ef79445ff32..031791ca106 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -229,6 +229,15 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "ld2410_ble", "local_name": "HLK-LD2410_*", }, + { + "domain": "ld2410_ble", + "manufacturer_data_start": [ + 7, + 1, + ], + "manufacturer_id": 256, + "service_uuid": "0000af30-0000-1000-8000-00805f9b34fb", + }, { "domain": "led_ble", "local_name": "LEDnet*", From be837535144b2abee4223f8013f4bee2c62e81ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:21 -0600 Subject: [PATCH 146/187] Bump inkbird-ble to 0.5.6 (#87590) changelog: https://github.com/Bluetooth-Devices/inkbird-ble/compare/v0.5.5...v0.5.6 fixes #85432 --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index e9984284c72..90bb44101af 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -10,7 +10,7 @@ { "local_name": "xBBQ*", "connectable": false }, { "local_name": "tps", "connectable": false } ], - "requirements": ["inkbird-ble==0.5.5"], + "requirements": ["inkbird-ble==0.5.6"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 616ef1fd78e..59551eeaf53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -976,7 +976,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.5 +inkbird-ble==0.5.6 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 341b5a0d269..4a590176ec8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -735,7 +735,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.5 +inkbird-ble==0.5.6 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 From ab14671dbc97c16cdb49956e1e9dabbdcfca9740 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:17 -0600 Subject: [PATCH 147/187] Bump sensorpro-ble to 0.5.3 (#87591) changelog: https://github.com/Bluetooth-Devices/sensorpro-ble/compare/v0.5.1...v0.5.3 same fix as #85432 but for sensorpro --- homeassistant/components/sensorpro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensorpro/manifest.json b/homeassistant/components/sensorpro/manifest.json index 07499609133..603c29b539c 100644 --- a/homeassistant/components/sensorpro/manifest.json +++ b/homeassistant/components/sensorpro/manifest.json @@ -15,7 +15,7 @@ "connectable": false } ], - "requirements": ["sensorpro-ble==0.5.1"], + "requirements": ["sensorpro-ble==0.5.3"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 59551eeaf53..b4ef3784405 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2312,7 +2312,7 @@ sense_energy==0.11.1 sensirion-ble==0.0.1 # homeassistant.components.sensorpro -sensorpro-ble==0.5.1 +sensorpro-ble==0.5.3 # homeassistant.components.sensorpush sensorpush-ble==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a590176ec8..1bcd3c41d9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1627,7 +1627,7 @@ sense_energy==0.11.1 sensirion-ble==0.0.1 # homeassistant.components.sensorpro -sensorpro-ble==0.5.1 +sensorpro-ble==0.5.3 # homeassistant.components.sensorpush sensorpush-ble==1.5.2 From c4470fc36dc738f12f2a18ea5bfdc3d54a7a5b11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:13 -0600 Subject: [PATCH 148/187] Bump thermopro-ble to 0.4.5 (#87592) changelog: https://github.com/Bluetooth-Devices/thermopro-ble/compare/v0.4.3..v0.4.5 same fix as #85432 but for thermopro --- homeassistant/components/thermopro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thermopro/manifest.json b/homeassistant/components/thermopro/manifest.json index e9135a44324..fc9efdf81b2 100644 --- a/homeassistant/components/thermopro/manifest.json +++ b/homeassistant/components/thermopro/manifest.json @@ -8,7 +8,7 @@ { "local_name": "TP39*", "connectable": false } ], "dependencies": ["bluetooth_adapters"], - "requirements": ["thermopro-ble==0.4.3"], + "requirements": ["thermopro-ble==0.4.5"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index b4ef3784405..a5e05b06d3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2481,7 +2481,7 @@ tesla-wall-connector==1.0.2 thermobeacon-ble==0.6.0 # homeassistant.components.thermopro -thermopro-ble==0.4.3 +thermopro-ble==0.4.5 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1bcd3c41d9d..dcb47b8ba80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1745,7 +1745,7 @@ tesla-wall-connector==1.0.2 thermobeacon-ble==0.6.0 # homeassistant.components.thermopro -thermopro-ble==0.4.3 +thermopro-ble==0.4.5 # homeassistant.components.tilt_ble tilt-ble==0.2.3 From e67d9988fd5887f3d8f23ffaaa2898c9289d9f31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:09 -0600 Subject: [PATCH 149/187] Bump bluemaestro-ble to 0.2.3 (#87594) changelog: https://github.com/Bluetooth-Devices/bluemaestro-ble/compare/v0.2.1..v0.2.3 same fix as #85432 but for bluemaestro --- homeassistant/components/bluemaestro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluemaestro/manifest.json b/homeassistant/components/bluemaestro/manifest.json index 277e02ab488..d4a9890efae 100644 --- a/homeassistant/components/bluemaestro/manifest.json +++ b/homeassistant/components/bluemaestro/manifest.json @@ -9,7 +9,7 @@ "connectable": false } ], - "requirements": ["bluemaestro-ble==0.2.1"], + "requirements": ["bluemaestro-ble==0.2.3"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index a5e05b06d3c..c7674ea4029 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ blinkstick==1.2.0 blockchain==1.4.4 # homeassistant.components.bluemaestro -bluemaestro-ble==0.2.1 +bluemaestro-ble==0.2.3 # homeassistant.components.decora # homeassistant.components.zengge diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dcb47b8ba80..742a765a8bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ blebox_uniapi==2.1.4 blinkpy==0.19.2 # homeassistant.components.bluemaestro -bluemaestro-ble==0.2.1 +bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth bluetooth-adapters==0.15.2 From a2c9f9242097944bd09bd6551bf97671b5df485f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Feb 2023 22:59:05 -0600 Subject: [PATCH 150/187] Bump sensorpush-ble to 1.5.5 (#87595) * Bump sensorpush-ble to 1.5.4 changelog: https://github.com/Bluetooth-Devices/sensorpush-ble/compare/v1.5.2..v1.5.4 same fix as #85432 but for sensorpush * bump again to fix parser with passive scans --- homeassistant/components/sensorpush/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json index 7046837e45f..7e68628c91e 100644 --- a/homeassistant/components/sensorpush/manifest.json +++ b/homeassistant/components/sensorpush/manifest.json @@ -9,7 +9,7 @@ "connectable": false } ], - "requirements": ["sensorpush-ble==1.5.2"], + "requirements": ["sensorpush-ble==1.5.5"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index c7674ea4029..c28ee421fa3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2315,7 +2315,7 @@ sensirion-ble==0.0.1 sensorpro-ble==0.5.3 # homeassistant.components.sensorpush -sensorpush-ble==1.5.2 +sensorpush-ble==1.5.5 # homeassistant.components.sentry sentry-sdk==1.13.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 742a765a8bd..25409f3a692 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1630,7 +1630,7 @@ sensirion-ble==0.0.1 sensorpro-ble==0.5.3 # homeassistant.components.sensorpush -sensorpush-ble==1.5.2 +sensorpush-ble==1.5.5 # homeassistant.components.sentry sentry-sdk==1.13.0 From 2e541e7ef64eed70a1cd9287fb7013c3f0ffd897 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 6 Feb 2023 19:57:16 -0800 Subject: [PATCH 151/187] Improve rainbird device reliability by sending requests serially (#87603) Send rainbird requests serially --- .../components/rainbird/coordinator.py | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index ddb2b70324d..14598921a61 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio from dataclasses import dataclass import datetime import logging @@ -84,27 +83,18 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): raise UpdateFailed(f"Error communicating with Device: {err}") from err async def _fetch_data(self) -> RainbirdDeviceState: - """Fetch data from the Rain Bird device.""" - (zones, states, rain, rain_delay) = await asyncio.gather( - self._fetch_zones(), - self._controller.get_zone_states(), - self._controller.get_rain_sensor_state(), - self._controller.get_rain_delay(), - ) + """Fetch data from the Rain Bird device. + + Rainbird devices can only reliably handle a single request at a time, + so the requests are sent serially. + """ + available_stations = await self._controller.get_available_stations() + states = await self._controller.get_zone_states() + rain = await self._controller.get_rain_sensor_state() + rain_delay = await self._controller.get_rain_delay() return RainbirdDeviceState( - zones=set(zones), - active_zones={zone for zone in zones if states.active(zone)}, + zones=available_stations.active_set, + active_zones=states.active_set, rain=rain, rain_delay=rain_delay, ) - - async def _fetch_zones(self) -> set[int]: - """Fetch the zones from the device, caching the results.""" - if self._zones is None: - available_stations = await self._controller.get_available_stations() - self._zones = { - zone - for zone in range(1, available_stations.stations.count + 1) - if available_stations.stations.active(zone) - } - return self._zones From 354d77d26b49f21939b848e802535f4c4c255a4a Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Tue, 7 Feb 2023 19:39:08 +0100 Subject: [PATCH 152/187] Do not return cached values for entity states in emulated_hue (#87642) * Do not return cached values for state and brightness * Move building the uncached state dict behind a lru_cache (_build_entity_state_dict) --------- Co-authored-by: J. Nick Koston --- .../components/emulated_hue/hue_api.py | 146 ++++++++++-------- tests/components/emulated_hue/test_hue_api.py | 13 +- 2 files changed, 92 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index b855d0c738d..41c25943a77 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -634,77 +634,87 @@ def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]: # Remove the now stale cached entry. config.cached_states.pop(entity.entity_id) + if cached_state is None: + return _build_entity_state_dict(entity) + + data: dict[str, Any] = cached_state + # Make sure brightness is valid + if data[STATE_BRIGHTNESS] is None: + data[STATE_BRIGHTNESS] = HUE_API_STATE_BRI_MAX if data[STATE_ON] else 0 + + # Make sure hue/saturation are valid + if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None): + data[STATE_HUE] = 0 + data[STATE_SATURATION] = 0 + + # If the light is off, set the color to off + if data[STATE_BRIGHTNESS] == 0: + data[STATE_HUE] = 0 + data[STATE_SATURATION] = 0 + + _clamp_values(data) + return data + + +@lru_cache(maxsize=512) +def _build_entity_state_dict(entity: State) -> dict[str, Any]: + """Build a state dict for an entity.""" data: dict[str, Any] = { - STATE_ON: False, + STATE_ON: entity.state != STATE_OFF, STATE_BRIGHTNESS: None, STATE_HUE: None, STATE_SATURATION: None, STATE_COLOR_TEMP: None, } - - if cached_state is None: - data[STATE_ON] = entity.state != STATE_OFF - - if data[STATE_ON]: - data[STATE_BRIGHTNESS] = hass_to_hue_brightness( - entity.attributes.get(ATTR_BRIGHTNESS, 0) - ) - hue_sat = entity.attributes.get(ATTR_HS_COLOR) - if hue_sat is not None: - hue = hue_sat[0] - sat = hue_sat[1] - # Convert hass hs values back to hue hs values - data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX) - data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX) - else: - data[STATE_HUE] = HUE_API_STATE_HUE_MIN - data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN - data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0) - + if data[STATE_ON]: + data[STATE_BRIGHTNESS] = hass_to_hue_brightness( + entity.attributes.get(ATTR_BRIGHTNESS, 0) + ) + hue_sat = entity.attributes.get(ATTR_HS_COLOR) + if hue_sat is not None: + hue = hue_sat[0] + sat = hue_sat[1] + # Convert hass hs values back to hue hs values + data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX) + data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX) else: - data[STATE_BRIGHTNESS] = 0 - data[STATE_HUE] = 0 - data[STATE_SATURATION] = 0 - data[STATE_COLOR_TEMP] = 0 + data[STATE_HUE] = HUE_API_STATE_HUE_MIN + data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN + data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0) - if entity.domain == climate.DOMAIN: - temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) - # Convert 0-100 to 0-254 - data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100) - elif entity.domain == humidifier.DOMAIN: - humidity = entity.attributes.get(ATTR_HUMIDITY, 0) - # Convert 0-100 to 0-254 - data[STATE_BRIGHTNESS] = round(humidity * HUE_API_STATE_BRI_MAX / 100) - elif entity.domain == media_player.DOMAIN: - level = entity.attributes.get( - ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0 - ) - # Convert 0.0-1.0 to 0-254 - data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX) - elif entity.domain == fan.DOMAIN: - percentage = entity.attributes.get(ATTR_PERCENTAGE) or 0 - # Convert 0-100 to 0-254 - data[STATE_BRIGHTNESS] = round(percentage * HUE_API_STATE_BRI_MAX / 100) - elif entity.domain == cover.DOMAIN: - level = entity.attributes.get(ATTR_CURRENT_POSITION, 0) - data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX) else: - data = cached_state - # Make sure brightness is valid - if data[STATE_BRIGHTNESS] is None: - data[STATE_BRIGHTNESS] = HUE_API_STATE_BRI_MAX if data[STATE_ON] else 0 + data[STATE_BRIGHTNESS] = 0 + data[STATE_HUE] = 0 + data[STATE_SATURATION] = 0 + data[STATE_COLOR_TEMP] = 0 - # Make sure hue/saturation are valid - if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None): - data[STATE_HUE] = 0 - data[STATE_SATURATION] = 0 + if entity.domain == climate.DOMAIN: + temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) + # Convert 0-100 to 0-254 + data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100) + elif entity.domain == humidifier.DOMAIN: + humidity = entity.attributes.get(ATTR_HUMIDITY, 0) + # Convert 0-100 to 0-254 + data[STATE_BRIGHTNESS] = round(humidity * HUE_API_STATE_BRI_MAX / 100) + elif entity.domain == media_player.DOMAIN: + level = entity.attributes.get( + ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0 + ) + # Convert 0.0-1.0 to 0-254 + data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX) + elif entity.domain == fan.DOMAIN: + percentage = entity.attributes.get(ATTR_PERCENTAGE) or 0 + # Convert 0-100 to 0-254 + data[STATE_BRIGHTNESS] = round(percentage * HUE_API_STATE_BRI_MAX / 100) + elif entity.domain == cover.DOMAIN: + level = entity.attributes.get(ATTR_CURRENT_POSITION, 0) + data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX) + _clamp_values(data) + return data - # If the light is off, set the color to off - if data[STATE_BRIGHTNESS] == 0: - data[STATE_HUE] = 0 - data[STATE_SATURATION] = 0 - # Clamp brightness, hue, saturation, and color temp to valid values +def _clamp_values(data: dict[str, Any]) -> None: + """Clamp brightness, hue, saturation, and color temp to valid values.""" for key, v_min, v_max in ( (STATE_BRIGHTNESS, HUE_API_STATE_BRI_MIN, HUE_API_STATE_BRI_MAX), (STATE_HUE, HUE_API_STATE_HUE_MIN, HUE_API_STATE_HUE_MAX), @@ -714,8 +724,6 @@ def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]: if data[key] is not None: data[key] = max(v_min, min(data[key], v_max)) - return data - @lru_cache(maxsize=1024) def _entity_unique_id(entity_id: str) -> str: @@ -843,10 +851,18 @@ def create_config_model(config: Config, request: web.Request) -> dict[str, Any]: def create_list_of_entities(config: Config, request: web.Request) -> dict[str, Any]: """Create a list of all entities.""" - json_response: dict[str, Any] = { - config.entity_id_to_number(state.entity_id): state_to_json(config, state) - for state in config.get_exposed_states() - } + hass: core.HomeAssistant = request.app["hass"] + + json_response: dict[str, Any] = {} + for cached_state in config.get_exposed_states(): + entity_id = cached_state.entity_id + state = hass.states.get(entity_id) + assert state is not None + + json_response[config.entity_id_to_number(entity_id)] = state_to_json( + config, state + ) + return json_response diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index cfadccc39c4..3d2d91dfa6d 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -301,6 +301,7 @@ async def test_discover_lights(hass, hue_client): await hass.async_block_till_done() result_json = await async_get_lights(hue_client) + assert "1" not in result_json.keys() devices = {val["uniqueid"] for val in result_json.values()} assert "00:2f:d2:31:ce:c5:55:cc-ee" not in devices # light.ceiling_lights @@ -308,8 +309,16 @@ async def test_discover_lights(hass, hue_client): hass.states.async_set("light.ceiling_lights", STATE_ON) await hass.async_block_till_done() result_json = await async_get_lights(hue_client) - devices = {val["uniqueid"] for val in result_json.values()} - assert "00:2f:d2:31:ce:c5:55:cc-ee" in devices # light.ceiling_lights + device = result_json["1"] # Test that light ID did not change + assert device["uniqueid"] == "00:2f:d2:31:ce:c5:55:cc-ee" # light.ceiling_lights + assert device["state"][HUE_API_STATE_ON] is True + + # Test that returned value is fresh and not cached + hass.states.async_set("light.ceiling_lights", STATE_OFF) + await hass.async_block_till_done() + result_json = await async_get_lights(hue_client) + device = result_json["1"] + assert device["state"][HUE_API_STATE_ON] is False async def test_light_without_brightness_supported(hass_hue, hue_client): From 08fb16a0c28909672f569f0224abb095e97cb794 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Feb 2023 13:40:53 -0500 Subject: [PATCH 153/187] Bumped version to 2023.2.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 168bf80aa11..63d691f6ac7 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 = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 825c2bc356f..6253e204117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.2" +version = "2023.2.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From aacd8e044d9c6e8da4e64e2aabc16262a4ac2cf5 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 29 Jan 2023 06:11:56 -0800 Subject: [PATCH 154/187] Bump pyrainbird to 2.0.0 (#86851) --- homeassistant/components/rainbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 50eb11c3fe9..dcecaac81ab 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -3,7 +3,7 @@ "name": "Rain Bird", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainbird", - "requirements": ["pyrainbird==1.1.0"], + "requirements": ["pyrainbird==2.0.0"], "codeowners": ["@konikvranik", "@allenporter"], "iot_class": "local_polling", "loggers": ["pyrainbird"] diff --git a/requirements_all.txt b/requirements_all.txt index c28ee421fa3..9f2558e5aa7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1896,7 +1896,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==1.1.0 +pyrainbird==2.0.0 # homeassistant.components.recswitch pyrecswitch==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25409f3a692..c3b2ad9d2bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1367,7 +1367,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.rainbird -pyrainbird==1.1.0 +pyrainbird==2.0.0 # homeassistant.components.risco pyrisco==0.5.7 From 8e8a170121d8279354489125f169e6161274d71d Mon Sep 17 00:00:00 2001 From: shbatm Date: Tue, 7 Feb 2023 13:03:27 -0600 Subject: [PATCH 155/187] Bump PyISY to 3.1.13, check portal for network buttons (#87650) --- homeassistant/components/isy994/__init__.py | 8 +++++--- homeassistant/components/isy994/const.py | 1 - homeassistant/components/isy994/manifest.json | 2 +- homeassistant/components/isy994/services.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index c9e4f6ed16e..3612e87f8e6 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -7,6 +7,7 @@ from urllib.parse import urlparse from aiohttp import CookieJar import async_timeout from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError +from pyisy.constants import CONFIG_NETWORKING, CONFIG_PORTAL import voluptuous as vol from homeassistant import config_entries @@ -43,7 +44,6 @@ from .const import ( ISY_CONF_FIRMWARE, ISY_CONF_MODEL, ISY_CONF_NAME, - ISY_CONF_NETWORKING, MANUFACTURER, PLATFORMS, SCHEME_HTTP, @@ -220,9 +220,11 @@ async def async_setup_entry( numbers = isy_data.variables[Platform.NUMBER] for vtype, _, vid in isy.variables.children: numbers.append(isy.variables[vtype][vid]) - if isy.conf[ISY_CONF_NETWORKING]: + if ( + isy.conf[CONFIG_NETWORKING] or isy.conf[CONFIG_PORTAL] + ) and isy.networking.nobjs: isy_data.devices[CONF_NETWORK] = _create_service_device_info( - isy, name=ISY_CONF_NETWORKING, unique_id=CONF_NETWORK + isy, name=CONFIG_NETWORKING, unique_id=CONF_NETWORK ) for resource in isy.networking.nobjs: isy_data.net_resources.append(resource) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 95e68c5fa11..37ae1a82b91 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -118,7 +118,6 @@ SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] # (they can turn off, and report their state) ISY_GROUP_PLATFORM = Platform.SWITCH -ISY_CONF_NETWORKING = "Networking Module" ISY_CONF_UUID = "uuid" ISY_CONF_NAME = "name" ISY_CONF_MODEL = "model" diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 8fa77cd126c..b24167e0c78 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.1.11"], + "requirements": ["pyisy==3.1.13"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 759ebfbde0e..05e0425c3f5 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -23,7 +23,7 @@ import homeassistant.helpers.entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import entity_service_call -from .const import _LOGGER, CONF_NETWORK, DOMAIN, ISY_CONF_NAME, ISY_CONF_NETWORKING +from .const import _LOGGER, CONF_NETWORK, DOMAIN, ISY_CONF_NAME from .util import _async_cleanup_registry_entries # Common Services for All Platforms: @@ -233,7 +233,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy = isy_data.root if isy_name and isy_name != isy.conf[ISY_CONF_NAME]: continue - if isy.networking is None or not isy.conf[ISY_CONF_NETWORKING]: + if isy.networking is None: continue command = None if address: diff --git a/requirements_all.txt b/requirements_all.txt index 9f2558e5aa7..2c7b26cd923 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1702,7 +1702,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.11 +pyisy==3.1.13 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3b2ad9d2bc..91445d7ad5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1221,7 +1221,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.1.11 +pyisy==3.1.13 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 640f5f41cc1f71fff3af39bd2781d94ba9311e11 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 8 Feb 2023 22:38:44 +0100 Subject: [PATCH 156/187] Netgear ssdp discovery abort if no serial (#87532) fixes undefined --- .../components/netgear/config_flow.py | 3 +++ homeassistant/components/netgear/strings.json | 4 +++- tests/components/netgear/test_config_flow.py | 22 ++++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 9024d0510dd..024c2d9d6d7 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -135,6 +135,9 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info) + if ssdp.ATTR_UPNP_SERIAL not in discovery_info.upnp: + return self.async_abort(reason="no_serial") + await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) self._abort_if_unique_id_configured(updates=updated_data) diff --git a/homeassistant/components/netgear/strings.json b/homeassistant/components/netgear/strings.json index 3585d1e613b..d58c4878f65 100644 --- a/homeassistant/components/netgear/strings.json +++ b/homeassistant/components/netgear/strings.json @@ -14,7 +14,9 @@ "config": "Connection or login error: please check your configuration" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "not_ipv4_address": "No IPv4 address in ssdp discovery information", + "no_serial": "No serial number in ssdp discovery information" } }, "options": { diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index 69dc57b1d2c..28ff93d2404 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -235,7 +236,26 @@ async def test_ssdp_already_configured(hass): assert result["reason"] == "already_configured" -async def test_ssdp_ipv6(hass): +async def test_ssdp_no_serial(hass: HomeAssistant) -> None: + """Test ssdp abort when the ssdp info does not include a serial number.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=SSDP_URL, + upnp={ + ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20", + ssdp.ATTR_UPNP_PRESENTATION_URL: URL, + }, + ), + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "no_serial" + + +async def test_ssdp_ipv6(hass: HomeAssistant) -> None: """Test ssdp abort when using a ipv6 address.""" MockConfigEntry( domain=DOMAIN, From 0a69f825d27e94206a1fbeb643d50f44be49a757 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 8 Feb 2023 12:23:27 +0100 Subject: [PATCH 157/187] Reolink unregistered webhook on unexpected error (#87538) --- homeassistant/components/reolink/__init__.py | 3 ++ homeassistant/components/reolink/host.py | 43 ++++++++++---------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index f15ce90427c..aa9e623aa6a 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -54,6 +54,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b raise ConfigEntryNotReady( f"Error while trying to setup {host.api.host}:{host.api.port}: {str(err)}" ) from err + except Exception: # pylint: disable=broad-except + await host.stop() + raise config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index e44623e1a1e..d552c8daef5 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -138,24 +138,27 @@ class ReolinkHost: async def disconnect(self): """Disconnect from the API, so the connection will be released.""" - await self._api.unsubscribe() - try: - await self._api.logout() - except aiohttp.ClientConnectorError as err: + await self._api.unsubscribe() + except ( + aiohttp.ClientConnectorError, + asyncio.TimeoutError, + ReolinkError, + ) as err: _LOGGER.error( - "Reolink connection error while logging out for host %s:%s: %s", + "Reolink error while unsubscribing from host %s:%s: %s", self._api.host, self._api.port, str(err), ) - except asyncio.TimeoutError: - _LOGGER.error( - "Reolink connection timeout while logging out for host %s:%s", - self._api.host, - self._api.port, - ) - except ReolinkError as err: + + try: + await self._api.logout() + except ( + aiohttp.ClientConnectorError, + asyncio.TimeoutError, + ReolinkError, + ) as err: _LOGGER.error( "Reolink error while logging out for host %s:%s: %s", self._api.host, @@ -165,13 +168,13 @@ class ReolinkHost: async def stop(self, event=None): """Disconnect the API.""" - await self.unregister_webhook() + self.unregister_webhook() await self.disconnect() async def subscribe(self) -> None: """Subscribe to motion events and register the webhook as a callback.""" if self.webhook_id is None: - await self.register_webhook() + self.register_webhook() if self._api.subscribed: _LOGGER.debug( @@ -248,7 +251,7 @@ class ReolinkHost: self._api.host, ) - async def register_webhook(self) -> None: + def register_webhook(self) -> None: """Register the webhook for motion events.""" self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}_ONVIF" event_id = self.webhook_id @@ -263,8 +266,7 @@ class ReolinkHost: try: base_url = get_url(self._hass, prefer_external=True) except NoURLAvailableError as err: - webhook.async_unregister(self._hass, event_id) - self.webhook_id = None + self.unregister_webhook() raise ReolinkWebhookException( f"Error registering URL for webhook {event_id}: " "HomeAssistant URL is not available" @@ -275,11 +277,10 @@ class ReolinkHost: _LOGGER.debug("Registered webhook: %s", event_id) - async def unregister_webhook(self): + def unregister_webhook(self): """Unregister the webhook for motion events.""" - if self.webhook_id: - _LOGGER.debug("Unregistering webhook %s", self.webhook_id) - webhook.async_unregister(self._hass, self.webhook_id) + _LOGGER.debug("Unregistering webhook %s", self.webhook_id) + webhook.async_unregister(self._hass, self.webhook_id) self.webhook_id = None async def handle_webhook( From f2697b3e9662a6185df59ad8b64c60610ba74d3e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 9 Feb 2023 21:35:44 +0100 Subject: [PATCH 158/187] Correct sensor restore entity in modbus (#87563) * Correct sensor restore entity. * Review comments. --- homeassistant/components/modbus/sensor.py | 16 ++++++++++------ tests/components/modbus/test_sensor.py | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 04b986e41ba..ca8246577fd 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -5,7 +5,11 @@ from datetime import datetime import logging from typing import Any -from homeassistant.components.sensor import CONF_STATE_CLASS, SensorEntity +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + RestoreSensor, + SensorEntity, +) from homeassistant.const import ( CONF_NAME, CONF_SENSORS, @@ -14,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -53,7 +56,7 @@ async def async_setup_platform( async_add_entities(sensors) -class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): +class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): """Modbus register sensor.""" def __init__( @@ -90,8 +93,9 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() - if state := await self.async_get_last_state(): - self._attr_native_value = state.state + state = await self.async_get_last_sensor_data() + if state: + self._attr_native_value = state.native_value async def async_update(self, now: datetime | None = None) -> None: """Update the state of the sensor.""" @@ -135,7 +139,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): class SlaveSensor( CoordinatorEntity[DataUpdateCoordinator[list[int] | None]], - RestoreEntity, + RestoreSensor, SensorEntity, ): """Modbus slave register sensor.""" diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 4a6495d5b46..1369f09005e 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -855,7 +855,7 @@ async def test_wrap_sensor(hass, mock_do_cycle, expected): @pytest.mark.parametrize( "mock_test_state", - [(State(ENTITY_ID, "117"), State(f"{ENTITY_ID}_1", "119"))], + [(State(ENTITY_ID, "unknown"), State(f"{ENTITY_ID}_1", "119"))], indirect=True, ) @pytest.mark.parametrize( From 03ba7672ead4d6ba41b020592487cd6c56ea29c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=2E=20Bal=C3=A1zs?= Date: Wed, 8 Feb 2023 22:28:47 +0100 Subject: [PATCH 159/187] Upgrading volvooncall to 0.10.2 (#87572) * Upgrading volvooncall to 0.10.2 This release fixes the incorrect remaining time of charging the battery * updating volvooncall test requirements * Updating volvooncall requirements --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/volvooncall/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index b9da3657aed..6f4724fbf00 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -2,7 +2,7 @@ "domain": "volvooncall", "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", - "requirements": ["volvooncall==0.10.1"], + "requirements": ["volvooncall==0.10.2"], "codeowners": ["@molobrakos"], "iot_class": "cloud_polling", "loggers": ["geopy", "hbmqtt", "volvooncall"], diff --git a/requirements_all.txt b/requirements_all.txt index 2c7b26cd923..e6450ee8b0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2576,7 +2576,7 @@ vilfo-api-client==0.3.2 volkszaehler==0.4.0 # homeassistant.components.volvooncall -volvooncall==0.10.1 +volvooncall==0.10.2 # homeassistant.components.verisure vsure==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91445d7ad5a..d82e4e5547e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1819,7 +1819,7 @@ venstarcolortouch==0.19 vilfo-api-client==0.3.2 # homeassistant.components.volvooncall -volvooncall==0.10.1 +volvooncall==0.10.2 # homeassistant.components.verisure vsure==1.8.1 From 151b33a4ab6e781137aee957eb2a6da6ae83f325 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 9 Feb 2023 23:42:08 +0100 Subject: [PATCH 160/187] Keep sleepy xiaomi-ble devices that don't broadcast regularly available (#87654) Co-authored-by: J. Nick Koston fixes undefined --- .../components/xiaomi_ble/__init__.py | 3 +- .../components/xiaomi_ble/binary_sensor.py | 11 ++ .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../xiaomi_ble/test_binary_sensor.py | 149 ++++++++++++++++-- 6 files changed, 149 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 372afe4b3c5..3930c50c70c 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,8 +3,7 @@ from __future__ import annotations import logging -from xiaomi_ble import SensorUpdate, XiaomiBluetoothDeviceData -from xiaomi_ble.parser import EncryptionScheme +from xiaomi_ble import EncryptionScheme, SensorUpdate, XiaomiBluetoothDeviceData from homeassistant import config_entries from homeassistant.components.bluetooth import ( diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 1de3afff53f..3d7bdfd0b48 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -1,6 +1,7 @@ """Support for Xiaomi binary sensors.""" from __future__ import annotations +from xiaomi_ble import SLEEPY_DEVICE_MODELS from xiaomi_ble.parser import ( BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass, ExtendedBinarySensorDeviceClass, @@ -19,6 +20,7 @@ from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) +from homeassistant.const import ATTR_MODEL from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info @@ -128,3 +130,12 @@ class XiaomiBluetoothSensorEntity( def is_on(self) -> bool | None: """Return the native value.""" return self.processor.entity_data.get(self.entity_key) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + if self.device_info and self.device_info[ATTR_MODEL] in SLEEPY_DEVICE_MODELS: + # These devices sleep for an indeterminate amount of time + # so there is no way to track their availability. + return True + return super().available diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index f3904003411..9fb35db3248 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -14,7 +14,7 @@ } ], "dependencies": ["bluetooth_adapters"], - "requirements": ["xiaomi-ble==0.16.1"], + "requirements": ["xiaomi-ble==0.16.3"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index e6450ee8b0b..b046166e4f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2637,7 +2637,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.16.1 +xiaomi-ble==0.16.3 # homeassistant.components.knx xknx==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d82e4e5547e..b6af545e759 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1862,7 +1862,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.16.1 +xiaomi-ble==0.16.3 # homeassistant.components.knx xknx==2.3.0 diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index 5389a2987f2..dae1569ff31 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -1,12 +1,28 @@ """Test Xiaomi binary sensors.""" +from datetime import timedelta +import time +from unittest.mock import patch + +from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, +) from homeassistant.components.xiaomi_ble.const import DOMAIN -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.util import dt as dt_util from . import make_advertisement -from tests.common import MockConfigEntry -from tests.components.bluetooth import inject_bluetooth_service_info_bleak +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.bluetooth import ( + inject_bluetooth_service_info_bleak, + patch_all_discovered_devices, +) async def test_door_problem_sensors(hass): @@ -34,19 +50,19 @@ async def test_door_problem_sensors(hass): door_sensor = hass.states.get("binary_sensor.door_lock_be98_door") door_sensor_attribtes = door_sensor.attributes - assert door_sensor.state == "off" + assert door_sensor.state == STATE_OFF assert door_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door" door_left_open = hass.states.get("binary_sensor.door_lock_be98_door_left_open") door_left_open_attribtes = door_left_open.attributes - assert door_left_open.state == "off" + assert door_left_open.state == STATE_OFF assert ( door_left_open_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door left open" ) pry_the_door = hass.states.get("binary_sensor.door_lock_be98_pry_the_door") pry_the_door_attribtes = pry_the_door.attributes - assert pry_the_door.state == "off" + assert pry_the_door.state == STATE_OFF assert pry_the_door_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Pry the door" assert await hass.config_entries.async_unload(entry.entry_id) @@ -77,12 +93,12 @@ async def test_light_motion(hass): motion_sensor = hass.states.get("binary_sensor.nightlight_9321_motion") motion_sensor_attribtes = motion_sensor.attributes - assert motion_sensor.state == "on" + assert motion_sensor.state == STATE_ON assert motion_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Motion" light_sensor = hass.states.get("binary_sensor.nightlight_9321_light") light_sensor_attribtes = light_sensor.attributes - assert light_sensor.state == "off" + assert light_sensor.state == STATE_OFF assert light_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Light" assert await hass.config_entries.async_unload(entry.entry_id) @@ -116,7 +132,7 @@ async def test_moisture(hass): sensor = hass.states.get("binary_sensor.smart_flower_pot_3e7a_moisture") sensor_attr = sensor.attributes - assert sensor.state == "on" + assert sensor.state == STATE_ON assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 3E7A Moisture" assert await hass.config_entries.async_unload(entry.entry_id) @@ -148,12 +164,12 @@ async def test_opening(hass): opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") opening_sensor_attribtes = opening_sensor.attributes - assert opening_sensor.state == "on" + + assert opening_sensor.state == STATE_ON assert ( opening_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door/Window Sensor E567 Opening" ) - assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -183,7 +199,7 @@ async def test_opening_problem_sensors(hass): opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") opening_sensor_attribtes = opening_sensor.attributes - assert opening_sensor.state == "off" + assert opening_sensor.state == STATE_OFF assert ( opening_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door/Window Sensor E567 Opening" @@ -193,7 +209,7 @@ async def test_opening_problem_sensors(hass): "binary_sensor.door_window_sensor_e567_door_left_open" ) door_left_open_attribtes = door_left_open.attributes - assert door_left_open.state == "off" + assert door_left_open.state == STATE_OFF assert ( door_left_open_attribtes[ATTR_FRIENDLY_NAME] == "Door/Window Sensor E567 Door left open" @@ -203,7 +219,7 @@ async def test_opening_problem_sensors(hass): "binary_sensor.door_window_sensor_e567_device_forcibly_removed" ) device_forcibly_removed_attribtes = device_forcibly_removed.attributes - assert device_forcibly_removed.state == "off" + assert device_forcibly_removed.state == STATE_OFF assert ( device_forcibly_removed_attribtes[ATTR_FRIENDLY_NAME] == "Door/Window Sensor E567 Device forcibly removed" @@ -238,8 +254,111 @@ async def test_smoke(hass): smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke") smoke_sensor_attribtes = smoke_sensor.attributes - assert smoke_sensor.state == "on" + assert smoke_sensor.state == STATE_ON assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_unavailable(hass): + """Test normal device goes to unavailable after 60 minutes.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:66:E5:67", + data={"bindkey": "0fdcc30fe9289254876b5ef7c11ef1f0"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "A4:C1:38:66:E5:67", + b"XY\x89\x18\x9ag\xe5f8\xc1\xa4\x9d\xd9z\xf3&\x00\x00\xc8\xa6\x0b\xd5", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + assert opening_sensor.state == STATE_ON + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + # Normal devices should go to unavailable + assert opening_sensor.state == STATE_UNAVAILABLE + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_sleepy_device(hass): + """Test sleepy device does not go to unavailable after 60 minutes.""" + start_monotonic = time.monotonic() + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:66:E5:67", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "A4:C1:38:66:E5:67", + b"@0\xd6\x03$\x19\x10\x01\x00", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + assert opening_sensor.state == STATE_ON + + # Fastforward time without BLE advertisements + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([]): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), + ) + await hass.async_block_till_done() + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + + # Sleepy devices should keep their state over time + assert opening_sensor.state == STATE_ON + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 1603a0b0372309c00ea9f0f8a81e4e931b87398d Mon Sep 17 00:00:00 2001 From: Luke Date: Tue, 7 Feb 2023 21:01:36 -0500 Subject: [PATCH 161/187] Bump oralb-ble to 0.17.5 (#87657) fixes undefined --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index e2f1749eb76..63594587974 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.17.4"], + "requirements": ["oralb-ble==0.17.5"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco", "@Lash-L"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index b046166e4f8..713883113a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1299,7 +1299,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.17.4 +oralb-ble==0.17.5 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6af545e759..b8ef8d603ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -947,7 +947,7 @@ openai==0.26.2 openerz-api==0.2.0 # homeassistant.components.oralb -oralb-ble==0.17.4 +oralb-ble==0.17.5 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From a779e43e2affc77889ab139ab76b0c2c3f07ede4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Feb 2023 19:30:16 -0600 Subject: [PATCH 162/187] Bump cryptography to 39.0.1 for CVE-2023-23931 (#87658) Bump cryptography to 39.0.1 CVE-2023-23931 --- 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 8dc60525be1..8ad9a144a1a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==1.0.3 bluetooth-data-tools==0.3.1 certifi>=2021.5.30 ciso8601==2.3.0 -cryptography==39.0.0 +cryptography==39.0.1 dbus-fast==1.84.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 diff --git a/pyproject.toml b/pyproject.toml index 6253e204117..216f5539463 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ "lru-dict==1.1.8", "PyJWT==2.5.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==39.0.0", + "cryptography==39.0.1", # pyOpenSSL 23.0.0 is required to work with cryptography 39+ "pyOpenSSL==23.0.0", "orjson==3.8.5", diff --git a/requirements.txt b/requirements.txt index c423388bbbd..7d05e1bb2e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.5.0 -cryptography==39.0.0 +cryptography==39.0.1 pyOpenSSL==23.0.0 orjson==3.8.5 pip>=21.0,<22.4 From 09970a083bea01f981b08bf630ba7011463d6397 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Feb 2023 02:35:33 -0600 Subject: [PATCH 163/187] Bump yalexs_ble to 1.12.12 to fix reconnect when services fail to resolve (#87664) * Bump yalexs_ble to 1.12.11 to fix reconnect when services fail to resolve changelog: https://github.com/bdraco/yalexs-ble/compare/v1.12.8...v1.12.11 * bump to make it work with esphome proxy as well * empty --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index cf00616b65f..a74f3f28072 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.8"], + "requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.12"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 866f5eca6bc..df9827afcb3 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.12.8"], + "requirements": ["yalexs-ble==1.12.12"], "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 713883113a6..2cb981ed6ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2657,13 +2657,13 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.12.8 +yalexs-ble==1.12.12 # homeassistant.components.august yalexs==1.2.6 # homeassistant.components.august -yalexs_ble==1.12.8 +yalexs_ble==1.12.12 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b8ef8d603ca..ea54e510aae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1879,13 +1879,13 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.12.8 +yalexs-ble==1.12.12 # homeassistant.components.august yalexs==1.2.6 # homeassistant.components.august -yalexs_ble==1.12.8 +yalexs_ble==1.12.12 # homeassistant.components.yeelight yeelight==0.7.10 From 8780db82e54912c9909492d483ac08f6095aa533 Mon Sep 17 00:00:00 2001 From: John Pettitt Date: Thu, 9 Feb 2023 01:36:05 -0800 Subject: [PATCH 164/187] Fix bad battery sense in ambient_station (#87668) --- .../components/ambient_station/binary_sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 67a54906e92..486651e2058 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -168,28 +168,28 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Leak detector battery 1", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=0, + on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_LEAK2, name="Leak detector battery 2", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=0, + on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_LEAK3, name="Leak detector battery 3", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=0, + on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_LEAK4, name="Leak detector battery 4", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=0, + on_state=1, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM1, @@ -273,7 +273,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Lightning detector battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, - on_state=0, + on_state=1, ), AmbientBinarySensorDescription( key=TYPE_LEAK1, From f8075f2dfd1c49944b138383fdeafc7353b28d70 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 8 Feb 2023 22:52:23 +0100 Subject: [PATCH 165/187] Bump reolink-aio to 0.4.0 (#87733) bump reolink-aio to 0.4.0 --- homeassistant/components/reolink/host.py | 9 ++++----- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index d552c8daef5..983ab293f69 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -68,8 +68,6 @@ class ReolinkHost: async def async_init(self) -> None: """Connect to Reolink host.""" - self._api.expire_session() - await self._api.get_host_data() if self._api.mac_address is None: @@ -301,9 +299,10 @@ class ReolinkHost: ) return - channel = await self._api.ONVIF_event_callback(data) + channels = await self._api.ONVIF_event_callback(data) - if channel is None: + if channels is None: async_dispatcher_send(hass, f"{webhook_id}_all", {}) else: - async_dispatcher_send(hass, f"{webhook_id}_{channel}", {}) + for channel in channels: + async_dispatcher_send(hass, f"{webhook_id}_{channel}", {}) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 72ac70ef180..0519960945e 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.3.4"], + "requirements": ["reolink-aio==0.4.0"], "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 2cb981ed6ba..a740b7ba1be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2227,7 +2227,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.4 +reolink-aio==0.4.0 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea54e510aae..63c75495d47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1572,7 +1572,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.4 +reolink-aio==0.4.0 # homeassistant.components.python_script restrictedpython==6.0 From ec755b34f06aca69c2fe8f39d7a23c8b5aca0c9b Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 10 Feb 2023 07:38:17 +1100 Subject: [PATCH 166/187] Bump aiolifx to 0.8.9 (#87790) Signed-off-by: Avi Miller Co-authored-by: J. Nick Koston --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index f4247f97ecf..a12809e3af7 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", "requirements": [ - "aiolifx==0.8.7", + "aiolifx==0.8.9", "aiolifx_effects==0.3.1", "aiolifx_themes==0.4.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index a740b7ba1be..6f2b7474b70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -193,7 +193,7 @@ aiokafka==0.7.2 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx==0.8.7 +aiolifx==0.8.9 # homeassistant.components.lifx aiolifx_effects==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63c75495d47..43e604d5abc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ aioimaplib==1.0.1 aiokafka==0.7.2 # homeassistant.components.lifx -aiolifx==0.8.7 +aiolifx==0.8.9 # homeassistant.components.lifx aiolifx_effects==0.3.1 From 250f032a0d5c928ca944c9faea083dccf42652c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Feb 2023 15:12:40 -0600 Subject: [PATCH 167/187] Ensure recorder still shuts down if the final commit fails (#87799) --- homeassistant/components/recorder/core.py | 17 ++++++---- .../components/recorder/run_history.py | 10 ++++-- tests/components/recorder/test_run_history.py | 34 ++++++++++++++++--- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 497ae364b64..d26d702430c 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -988,8 +988,10 @@ class Recorder(threading.Thread): def _handle_sqlite_corruption(self) -> None: """Handle the sqlite3 database being corrupt.""" - self._close_event_session() - self._close_connection() + try: + self._close_event_session() + finally: + self._close_connection() move_away_broken_database(dburl_to_path(self.db_url)) self.run_history.reset() self._setup_recorder() @@ -1212,18 +1214,21 @@ class Recorder(threading.Thread): """End the recorder session.""" if self.event_session is None: return - try: + if self.run_history.active: self.run_history.end(self.event_session) + try: self._commit_event_session_or_retry() - self.event_session.close() except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Error saving the event session during shutdown: %s", err) + self.event_session.close() self.run_history.clear() def _shutdown(self) -> None: """Save end time for current run.""" self.hass.add_job(self._async_stop_listeners) self._stop_executor() - self._end_session() - self._close_connection() + try: + self._end_session() + finally: + self._close_connection() diff --git a/homeassistant/components/recorder/run_history.py b/homeassistant/components/recorder/run_history.py index 02b2df066bd..63744db0b55 100644 --- a/homeassistant/components/recorder/run_history.py +++ b/homeassistant/components/recorder/run_history.py @@ -72,6 +72,11 @@ class RunHistory: start=self.recording_start, created=dt_util.utcnow() ) + @property + def active(self) -> bool: + """Return if a run is active.""" + return self._current_run_info is not None + def get(self, start: datetime) -> RecorderRuns | None: """Return the recorder run that started before or at start. @@ -141,6 +146,5 @@ class RunHistory: Must run in the recorder thread. """ - assert self._current_run_info is not None - assert self._current_run_info.end is not None - self._current_run_info = None + if self._current_run_info: + self._current_run_info = None diff --git a/tests/components/recorder/test_run_history.py b/tests/components/recorder/test_run_history.py index 3b5bd7dda6b..84dab78d8cc 100644 --- a/tests/components/recorder/test_run_history.py +++ b/tests/components/recorder/test_run_history.py @@ -1,12 +1,16 @@ """Test run history.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components import recorder from homeassistant.components.recorder.db_schema import RecorderRuns from homeassistant.components.recorder.models import process_timestamp +from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from tests.common import SetupRecorderInstanceT + async def test_run_history(recorder_mock, hass): """Test the run history gives the correct run.""" @@ -47,12 +51,32 @@ async def test_run_history(recorder_mock, hass): ) -async def test_run_history_during_schema_migration(recorder_mock, hass): - """Test the run history during schema migration.""" - instance = recorder.get_instance(hass) +async def test_run_history_while_recorder_is_not_yet_started( + async_setup_recorder_instance: SetupRecorderInstanceT, + hass: HomeAssistant, + recorder_db_url: str, +) -> None: + """Test the run history while recorder is not yet started. + + This usually happens during schema migration because + we do not start right away. + """ + # Prevent the run history from starting to ensure + # we can test run_history.current.start returns the expected value + with patch( + "homeassistant.components.recorder.run_history.RunHistory.start", + ): + instance = await async_setup_recorder_instance(hass) run_history = instance.run_history assert run_history.current.start == run_history.recording_start - with instance.get_session() as session: - run_history.start(session) + + def _start_run_history(): + with instance.get_session() as session: + run_history.start(session) + + # Ideally we would run run_history.start in the recorder thread + # but since we mocked it out above, we run it directly here + # via the database executor to avoid blocking the event loop. + await instance.async_add_executor_job(_start_run_history) assert run_history.current.start == run_history.recording_start assert run_history.current.created >= run_history.recording_start From da2c1c114218e1c4de23b717e88dfd758688d49f Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 12 Feb 2023 04:18:34 +0000 Subject: [PATCH 168/187] Bump pyipma to 3.0.6 (#87867) bump pyipma --- homeassistant/components/ipma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 36dca71e957..7f250f63922 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -3,7 +3,7 @@ "name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": ["pyipma==3.0.5"], + "requirements": ["pyipma==3.0.6"], "codeowners": ["@dgomes", "@abmantis"], "iot_class": "cloud_polling", "loggers": ["geopy", "pyipma"] diff --git a/requirements_all.txt b/requirements_all.txt index 6f2b7474b70..f76479ad3fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1687,7 +1687,7 @@ pyinsteon==1.2.0 pyintesishome==1.8.0 # homeassistant.components.ipma -pyipma==3.0.5 +pyipma==3.0.6 # homeassistant.components.ipp pyipp==0.12.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43e604d5abc..d25ed5540b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1209,7 +1209,7 @@ pyicloud==1.0.0 pyinsteon==1.2.0 # homeassistant.components.ipma -pyipma==3.0.5 +pyipma==3.0.6 # homeassistant.components.ipp pyipp==0.12.1 From e8081c61595cd62f0cce74097e95c9ec937eaaf0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 11 Feb 2023 16:27:22 +0100 Subject: [PATCH 169/187] Fix unbound variable in Group sensor (#87878) * group bound last * Add test --- homeassistant/components/group/sensor.py | 15 ++++++++------ tests/components/group/test_sensor.py | 25 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 088678b6c1a..6c379832ced 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -137,7 +137,7 @@ async def async_setup_entry( def calc_min( sensor_values: list[tuple[str, float, State]] -) -> tuple[dict[str, str | None], float]: +) -> tuple[dict[str, str | None], float | None]: """Calculate min value.""" val: float | None = None entity_id: str | None = None @@ -153,7 +153,7 @@ def calc_min( def calc_max( sensor_values: list[tuple[str, float, State]] -) -> tuple[dict[str, str | None], float]: +) -> tuple[dict[str, str | None], float | None]: """Calculate max value.""" val: float | None = None entity_id: str | None = None @@ -169,7 +169,7 @@ def calc_max( def calc_mean( sensor_values: list[tuple[str, float, State]] -) -> tuple[dict[str, str | None], float]: +) -> tuple[dict[str, str | None], float | None]: """Calculate mean value.""" result = (sensor_value for _, sensor_value, _ in sensor_values) @@ -179,7 +179,7 @@ def calc_mean( def calc_median( sensor_values: list[tuple[str, float, State]] -) -> tuple[dict[str, str | None], float]: +) -> tuple[dict[str, str | None], float | None]: """Calculate median value.""" result = (sensor_value for _, sensor_value, _ in sensor_values) @@ -189,10 +189,11 @@ def calc_median( def calc_last( sensor_values: list[tuple[str, float, State]] -) -> tuple[dict[str, str | None], float]: +) -> tuple[dict[str, str | None], float | None]: """Calculate last value.""" last_updated: datetime | None = None last_entity_id: str | None = None + last: float | None = None for entity_id, state_f, state in sensor_values: if last_updated is None or state.last_updated > last_updated: last_updated = state.last_updated @@ -227,7 +228,9 @@ def calc_sum( CALC_TYPES: dict[ str, - Callable[[list[tuple[str, float, State]]], tuple[dict[str, str | None], float]], + Callable[ + [list[tuple[str, float, State]]], tuple[dict[str, str | None], float | None] + ], ] = { "min": calc_min, "max": calc_max, diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py index 87dbbccab08..e9d5adb0468 100644 --- a/tests/components/group/test_sensor.py +++ b/tests/components/group/test_sensor.py @@ -369,3 +369,28 @@ async def test_sensor_calculated_properties(hass: HomeAssistant) -> None: assert state.attributes.get("device_class") is None assert state.attributes.get("state_class") is None assert state.attributes.get("unit_of_measurement") is None + + +async def test_last_sensor(hass: HomeAssistant) -> None: + """Test the last sensor.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_last", + "type": "last", + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id_last_sensor", + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_last") + assert str(float(value)) == state.state + assert entity_id == state.attributes.get("last_entity_id") From 65c8895bf703e49204de2933c6bdc4d6a2767700 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Feb 2023 09:59:51 -0600 Subject: [PATCH 170/187] Retrigger Bluetooth discovery when calling async_rediscover_address (#87884) * Retrigger Bluetooth discovery when calling async_rediscover_address * Retrigger Bluetooth discovery when calling async_rediscover_address * tweak --- homeassistant/components/bluetooth/manager.py | 25 +++++++++++++------ tests/components/bluetooth/test_init.py | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 1523f41bf1f..5d0a8edabd9 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -216,13 +216,7 @@ class BluetoothManager: if address in seen: continue seen.add(address) - for domain in self._integration_matcher.match_domains(service_info): - discovery_flow.async_create_flow( - self.hass, - domain, - {"source": config_entries.SOURCE_BLUETOOTH}, - service_info, - ) + self._async_trigger_matching_discovery(service_info) @hass_callback def async_stop(self, event: Event) -> None: @@ -649,10 +643,27 @@ class BluetoothManager: """Return the last service info for an address.""" return self._get_history_by_type(connectable).get(address) + def _async_trigger_matching_discovery( + self, service_info: BluetoothServiceInfoBleak + ) -> None: + """Trigger discovery for matching domains.""" + for domain in self._integration_matcher.match_domains(service_info): + discovery_flow.async_create_flow( + self.hass, + domain, + {"source": config_entries.SOURCE_BLUETOOTH}, + service_info, + ) + @hass_callback def async_rediscover_address(self, address: str) -> None: """Trigger discovery of devices which have already been seen.""" self._integration_matcher.async_clear_address(address) + if service_info := self._connectable_history.get(address): + self._async_trigger_matching_discovery(service_info) + return + if service_info := self._all_history.get(address): + self._async_trigger_matching_discovery(service_info) def _get_scanners_by_type(self, connectable: bool) -> list[BaseHaScanner]: """Return the scanners by type.""" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 7a0a7de8442..016da6fc135 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -980,7 +980,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): inject_advertisement(hass, switchbot_device, switchbot_adv_2) await hass.async_block_till_done() - assert len(mock_config_flow.mock_calls) == 2 + assert len(mock_config_flow.mock_calls) == 3 assert mock_config_flow.mock_calls[1][1][0] == "switchbot" From cd5042e2c2f7b219df04b036f06118675b9dfe52 Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Sun, 12 Feb 2023 00:04:59 -0800 Subject: [PATCH 171/187] Fix iaqualink exception handling after switch to httpx (#87898) Fix exception handling after switch to httpx --- homeassistant/components/iaqualink/__init__.py | 9 +++------ homeassistant/components/iaqualink/config_flow.py | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 146e2c2babc..cbdf909001a 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -7,7 +7,7 @@ from functools import wraps import logging from typing import Any, Concatenate, ParamSpec, TypeVar -import aiohttp.client_exceptions +import httpx from iaqualink.client import AqualinkClient from iaqualink.device import ( AqualinkBinarySensor, @@ -77,10 +77,7 @@ async def async_setup_entry( # noqa: C901 _LOGGER.error("Failed to login: %s", login_exception) await aqualink.close() return False - except ( - asyncio.TimeoutError, - aiohttp.client_exceptions.ClientConnectorError, - ) as aio_exception: + except (asyncio.TimeoutError, httpx.HTTPError) as aio_exception: await aqualink.close() raise ConfigEntryNotReady( f"Error while attempting login: {aio_exception}" @@ -149,7 +146,7 @@ async def async_setup_entry( # noqa: C901 try: await system.update() - except AqualinkServiceException as svc_exception: + except (AqualinkServiceException, httpx.HTTPError) as svc_exception: if prev is not None: _LOGGER.warning( "Failed to refresh system %s state: %s", diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index 3b3a99cac6e..0dfc60f2fee 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +import httpx from iaqualink.client import AqualinkClient from iaqualink.exception import ( AqualinkServiceException, @@ -42,7 +43,7 @@ class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): pass except AqualinkServiceUnauthorizedException: errors["base"] = "invalid_auth" - except AqualinkServiceException: + except (AqualinkServiceException, httpx.HTTPError): errors["base"] = "cannot_connect" else: return self.async_create_entry(title=username, data=user_input) From fb456a66c6ff07fdfe4ddeadcbbce41707b167ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Feb 2023 08:07:56 -0600 Subject: [PATCH 172/187] Bump aioesphomeapi to 13.3.1 (#87969) * Bump aioesphomeapi to 13.3.0 We probably need to include https://github.com/esphome/aioesphomeapi/pull/382 as well in another bump * bump --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 2ae066266e9..34bc5befea5 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.3"], + "requirements": ["aioesphomeapi==13.3.1", "esphome-dashboard-api==1.2.3"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index f76479ad3fb..84ece70f753 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2023.01.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.1.0 +aioesphomeapi==13.3.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d25ed5540b4..3345692e41c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aioecowitt==2023.01.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.1.0 +aioesphomeapi==13.3.1 # homeassistant.components.flo aioflo==2021.11.0 From 3b4efba6069cda55d0f3cef0f7e3ce4423eac98f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Feb 2023 09:24:50 -0500 Subject: [PATCH 173/187] Bumped version to 2023.2.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 63d691f6ac7..acad89bd43f 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 = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 216f5539463..092fdb74079 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.3" +version = "2023.2.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 7a3fdc08a44730b07b826cd6d16dd1e18fd656d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 13 Feb 2023 23:46:25 +0100 Subject: [PATCH 174/187] Update tibber lib 0.26.13 (#88018) --- 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 cb3c88532d9..770d342f52c 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.12"], + "requirements": ["pyTibber==0.26.13"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 84ece70f753..c4a6d809cb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1470,7 +1470,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.12 +pyTibber==0.26.13 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3345692e41c..61e82928c2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1073,7 +1073,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.12 +pyTibber==0.26.13 # homeassistant.components.dlink pyW215==0.7.0 From 7337cc8e893a8d68ddadab048502d1a00c8417db Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 13 Feb 2023 16:40:49 -0700 Subject: [PATCH 175/187] Bump `pyopenuv` to 2023.02.0 (#88039) --- homeassistant/components/openuv/coordinator.py | 12 +----------- homeassistant/components/openuv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/openuv/coordinator.py b/homeassistant/components/openuv/coordinator.py index 7472f213f82..5d0c4bce50a 100644 --- a/homeassistant/components/openuv/coordinator.py +++ b/homeassistant/components/openuv/coordinator.py @@ -6,7 +6,7 @@ from typing import Any, cast from pyopenuv.errors import InvalidApiKeyError, OpenUvError -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.debounce import Debouncer @@ -60,14 +60,4 @@ class OpenUvCoordinator(DataUpdateCoordinator): except OpenUvError as err: raise UpdateFailed(str(err)) from err - # 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 ef367b94dac..ae30a36022e 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==2023.01.0"], + "requirements": ["pyopenuv==2023.02.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyopenuv"], diff --git a/requirements_all.txt b/requirements_all.txt index c4a6d809cb5..d7efccf7fc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1834,7 +1834,7 @@ pyoctoprintapi==0.1.9 pyombi==0.1.10 # homeassistant.components.openuv -pyopenuv==2023.01.0 +pyopenuv==2023.02.0 # homeassistant.components.opnsense pyopnsense==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61e82928c2a..277c819277a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1320,7 +1320,7 @@ pynzbgetapi==0.2.0 pyoctoprintapi==0.1.9 # homeassistant.components.openuv -pyopenuv==2023.01.0 +pyopenuv==2023.02.0 # homeassistant.components.opnsense pyopnsense==0.2.0 From e3273a75da07c413590e6e7cd533b85081fabbbe Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 15 Feb 2023 10:01:20 -0500 Subject: [PATCH 176/187] Honeywell disable detergent level by default (#88040) * Disable fill by default * Fix tests * use TANK_FILL.get * Remove None from attribute get add reload to sensor test * Typing fix iteration error --- homeassistant/components/whirlpool/sensor.py | 6 ++-- tests/components/whirlpool/conftest.py | 23 +++++++++++++--- tests/components/whirlpool/test_sensor.py | 29 +++++++++++++++----- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index b026dd0ce32..9c26a0319f7 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -119,11 +119,12 @@ SENSORS: tuple[WhirlpoolSensorEntityDescription, ...] = ( key="DispenseLevel", name="Detergent Level", translation_key="whirlpool_tank", + entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, options=list(TANK_FILL.values()), - value_fn=lambda WasherDryer: TANK_FILL[ + value_fn=lambda WasherDryer: TANK_FILL.get( WasherDryer.get_attribute("WashCavity_OpStatusBulkDispense1Level") - ], + ), ), ) @@ -265,6 +266,7 @@ class WasherDryerTimeClass(RestoreSensor): async def async_will_remove_from_hass(self) -> None: """Close Whrilpool Appliance sockets before removing.""" + self._wd.unregister_attr_callback(self.update_from_latest_data) await self._wd.disconnect() @property diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index dd06c2d768f..1671768fdb6 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -49,6 +49,21 @@ def fixture_mock_appliances_manager_api(): yield mock_appliances_manager +@pytest.fixture(name="mock_appliances_manager_laundry_api") +def fixture_mock_appliances_manager_laundry_api(): + """Set up AppliancesManager fixture.""" + with mock.patch( + "homeassistant.components.whirlpool.AppliancesManager" + ) as mock_appliances_manager: + mock_appliances_manager.return_value.fetch_appliances = AsyncMock() + mock_appliances_manager.return_value.aircons = None + mock_appliances_manager.return_value.washer_dryers = [ + {"SAID": MOCK_SAID3, "NAME": "washer"}, + {"SAID": MOCK_SAID4, "NAME": "dryer"}, + ] + yield mock_appliances_manager + + @pytest.fixture(name="mock_backend_selector_api") def fixture_mock_backend_selector_api(): """Set up BackendSelector fixture.""" @@ -115,8 +130,6 @@ def side_effect_function(*args, **kwargs): return "0" if args[0] == "WashCavity_OpStatusBulkDispense1Level": return "3" - if args[0] == "Cavity_TimeStatusEstTimeRemaining": - return "4000" def get_sensor_mock(said): @@ -141,13 +154,13 @@ def get_sensor_mock(said): @pytest.fixture(name="mock_sensor1_api", autouse=False) -def fixture_mock_sensor1_api(mock_auth_api, mock_appliances_manager_api): +def fixture_mock_sensor1_api(mock_auth_api, mock_appliances_manager_laundry_api): """Set up sensor API fixture.""" yield get_sensor_mock(MOCK_SAID3) @pytest.fixture(name="mock_sensor2_api", autouse=False) -def fixture_mock_sensor2_api(mock_auth_api, mock_appliances_manager_api): +def fixture_mock_sensor2_api(mock_auth_api, mock_appliances_manager_laundry_api): """Set up sensor API fixture.""" yield get_sensor_mock(MOCK_SAID4) @@ -161,5 +174,7 @@ def fixture_mock_sensor_api_instances(mock_sensor1_api, mock_sensor2_api): mock_sensor_api.side_effect = [ mock_sensor1_api, mock_sensor2_api, + mock_sensor1_api, + mock_sensor2_api, ] yield mock_sensor_api diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index b8801bd4fd5..eef13c08dc9 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -17,7 +17,7 @@ async def update_sensor_state( hass: HomeAssistant, entity_id: str, mock_sensor_api_instance: MagicMock, -): +) -> None: """Simulate an update trigger from the API.""" for call in mock_sensor_api_instance.register_attr_callback.call_args_list: @@ -44,7 +44,7 @@ async def test_dryer_sensor_values( hass: HomeAssistant, mock_sensor_api_instances: MagicMock, mock_sensor2_api: MagicMock, -): +) -> None: """Test the sensor value callbacks.""" hass.state = CoreState.not_running thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) @@ -108,7 +108,7 @@ async def test_washer_sensor_values( hass: HomeAssistant, mock_sensor_api_instances: MagicMock, mock_sensor1_api: MagicMock, -): +) -> None: """Test the sensor value callbacks.""" hass.state = CoreState.not_running thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) @@ -147,6 +147,21 @@ async def test_washer_sensor_values( assert state.state == thetimestamp.isoformat() state_id = f"{entity_id.split('_')[0]}_detergent_level" + registry = entity_registry.async_get(hass) + entry = registry.async_get(state_id) + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + + update_entry = registry.async_update_entity(entry.entity_id, disabled_by=None) + await hass.async_block_till_done() + + assert update_entry != entry + assert update_entry.disabled is False + state = hass.states.get(state_id) + assert state is None + + await hass.config_entries.async_reload(entry.config_entry_id) state = hass.states.get(state_id) assert state is not None assert state.state == "50" @@ -253,7 +268,7 @@ async def test_washer_sensor_values( async def test_restore_state( hass: HomeAssistant, mock_sensor_api_instances: MagicMock, -): +) -> None: """Test sensor restore state.""" # Home assistant is not running yet hass.state = CoreState.not_running @@ -288,7 +303,7 @@ async def test_callback( hass: HomeAssistant, mock_sensor_api_instances: MagicMock, mock_sensor1_api: MagicMock, -): +) -> None: """Test callback timestamp callback function.""" hass.state = CoreState.not_running thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) @@ -314,9 +329,9 @@ async def test_callback( # restore from cache state = hass.states.get("sensor.washer_end_time") assert state.state == thetimestamp.isoformat() - callback = mock_sensor1_api.register_attr_callback.call_args_list[2][0][0] + callback = mock_sensor1_api.register_attr_callback.call_args_list[1][0][0] callback() - # await hass.async_block_till_done() + state = hass.states.get("sensor.washer_end_time") assert state.state == thetimestamp.isoformat() mock_sensor1_api.get_machine_state.return_value = MachineState.RunningMainCycle From 97dab32a4cb0c964b4991892b82c9644ef072f7b Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 13 Feb 2023 23:46:46 -0500 Subject: [PATCH 177/187] Bump AIOAladdinConnect 0.1.56 (#88041) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 3cfe7a14167..48f4ef82b81 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.55"], + "requirements": ["AIOAladdinConnect==0.1.56"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index d7efccf7fc5..63ae47dce6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.55 +AIOAladdinConnect==0.1.56 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 277c819277a..71a4498e640 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.55 +AIOAladdinConnect==0.1.56 # homeassistant.components.adax Adax-local==0.1.5 From 8024a170255b21f08664e0b5e521dd1698f30beb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Feb 2023 05:45:27 -0500 Subject: [PATCH 178/187] Handle device reg fields not being valid data in openai conversion (#88047) Handle device reg fields not being valid data --- .../components/openai_conversation/const.py | 2 +- tests/components/openai_conversation/test_init.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index b5644915d91..294bbbd6e90 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -15,7 +15,7 @@ An overview of the areas and the devices in this smart home: {{ area.name }}: {%- set area_info.printed = true %} {%- endif %} -- {{ device_attr(device, "name") }}{% if device_attr(device, "model") and device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %} +- {{ device_attr(device, "name") }}{% if device_attr(device, "model") and (device_attr(device, "model") | string) not in (device_attr(device, "name") | string) %} ({{ device_attr(device, "model") }}){% endif %} {%- endif %} {%- endfor %} {%- endfor %} diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index b64f3322895..14d2015ace2 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -67,14 +67,21 @@ async def test_default_prompt(hass, mock_init_component): device_reg.async_update_device( device.id, disabled_by=device_registry.DeviceEntryDisabler.USER ) - device = device_reg.async_get_or_create( + device_reg.async_get_or_create( config_entry_id="1234", connections={("test", "9876-no-name")}, manufacturer="Test Manufacturer NoName", model="Test Model NoName", suggested_area="Test Area 2", ) - + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "9876-integer-values")}, + name=1, + manufacturer=2, + model=3, + suggested_area="Test Area 2", + ) with patch("openai.Completion.create") as mock_create: result = await conversation.async_converse(hass, "hello", None, Context()) @@ -93,6 +100,7 @@ Test Area 2: - Test Device 2 - Test Device 3 (Test Model 3A) - Test Device 4 +- 1 (3) Answer the users questions about the world truthfully. From be5777ba59b2bac9a5e8f6a18b6206931b432274 Mon Sep 17 00:00:00 2001 From: Ryan Fleming Date: Tue, 14 Feb 2023 02:40:03 -0500 Subject: [PATCH 179/187] Bump pyoctoprintapi to 0.1.11 (#88052) Bump pyoctoprint to get camera url fixes --- homeassistant/components/octoprint/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index 9bc2f0011c0..047d7d9ce1a 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -3,7 +3,7 @@ "name": "OctoPrint", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/octoprint", - "requirements": ["pyoctoprintapi==0.1.9"], + "requirements": ["pyoctoprintapi==0.1.11"], "codeowners": ["@rfleming71"], "zeroconf": ["_octoprint._tcp.local."], "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 63ae47dce6c..59c17c20f81 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1828,7 +1828,7 @@ pynzbgetapi==0.2.0 pyobihai==1.3.2 # homeassistant.components.octoprint -pyoctoprintapi==0.1.9 +pyoctoprintapi==0.1.11 # homeassistant.components.ombi pyombi==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71a4498e640..4b236028fbb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1317,7 +1317,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.octoprint -pyoctoprintapi==0.1.9 +pyoctoprintapi==0.1.11 # homeassistant.components.openuv pyopenuv==2023.02.0 From 55fed18e3edb3f6b525a616347695e2533374d73 Mon Sep 17 00:00:00 2001 From: Gertjan Date: Wed, 15 Feb 2023 15:53:44 +0100 Subject: [PATCH 180/187] Fixed float number validation in sensor component (#88074) --- homeassistant/components/sensor/__init__.py | 2 +- tests/components/sensor/test_init.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 5108a167552..2fbd8ef211d 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -587,7 +587,7 @@ class SensorEntity(Entity): numerical_value: int | float | Decimal if not isinstance(value, (int, float, Decimal)): try: - if isinstance(value, str) and "." not in value: + if isinstance(value, str) and "." not in value and "e" not in value: numerical_value = int(value) else: numerical_value = float(value) # type:ignore[arg-type] diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index b43c63f015c..f63d10bc76a 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1548,6 +1548,7 @@ async def test_non_numeric_validation_raise( [ (13, "13"), (17.50, "17.5"), + ("1e-05", "1e-05"), (Decimal(18.50), "18.5"), ("19.70", "19.70"), (None, STATE_UNKNOWN), From 634aff0006b4a31e22aaeadfe0131aec8dfbb33e Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Wed, 15 Feb 2023 10:22:09 +0100 Subject: [PATCH 181/187] Statistics component fix device_class for incremental source sensors (#88096) * Return None device_class for incremental source sensors * Ignore linting error * Fix ignore linting error * Fix ignore linting error * Fix ignore linting error * Catch potential parsing error with enum --- homeassistant/components/statistics/sensor.py | 59 ++++++--- tests/components/statistics/test_sensor.py | 122 ++++++++++++++++-- 2 files changed, 153 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 38434c15a1c..2025eebd650 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -13,7 +13,8 @@ import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.recorder import get_instance, history -from homeassistant.components.sensor import ( +from homeassistant.components.sensor import ( # pylint: disable=hass-deprecated-import + DEVICE_CLASS_STATE_CLASSES, PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, @@ -47,6 +48,7 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util import dt as dt_util +from homeassistant.util.enum import try_parse_enum from . import DOMAIN, PLATFORMS @@ -144,7 +146,7 @@ STATS_DATETIME = { } # Statistics which retain the unit of the source entity -STAT_NUMERIC_RETAIN_UNIT = { +STATS_NUMERIC_RETAIN_UNIT = { STAT_AVERAGE_LINEAR, STAT_AVERAGE_STEP, STAT_AVERAGE_TIMELESS, @@ -166,7 +168,7 @@ STAT_NUMERIC_RETAIN_UNIT = { } # Statistics which produce percentage ratio from binary_sensor source entity -STAT_BINARY_PERCENTAGE = { +STATS_BINARY_PERCENTAGE = { STAT_AVERAGE_STEP, STAT_AVERAGE_TIMELESS, STAT_MEAN, @@ -296,15 +298,9 @@ class StatisticsSensor(SensorEntity): self.ages: deque[datetime] = deque(maxlen=self._samples_max_buffer_size) self.attributes: dict[str, StateType] = {} - self._state_characteristic_fn: Callable[[], StateType | datetime] - if self.is_binary: - self._state_characteristic_fn = getattr( - self, f"_stat_binary_{self._state_characteristic}" - ) - else: - self._state_characteristic_fn = getattr( - self, f"_stat_{self._state_characteristic}" - ) + self._state_characteristic_fn: Callable[ + [], StateType | datetime + ] = self._callable_characteristic_fn(self._state_characteristic) self._update_listener: CALLBACK_TYPE | None = None @@ -368,11 +364,11 @@ class StatisticsSensor(SensorEntity): def _derive_unit_of_measurement(self, new_state: State) -> str | None: base_unit: str | None = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) unit: str | None - if self.is_binary and self._state_characteristic in STAT_BINARY_PERCENTAGE: + if self.is_binary and self._state_characteristic in STATS_BINARY_PERCENTAGE: unit = PERCENTAGE elif not base_unit: unit = None - elif self._state_characteristic in STAT_NUMERIC_RETAIN_UNIT: + elif self._state_characteristic in STATS_NUMERIC_RETAIN_UNIT: unit = base_unit elif self._state_characteristic in STATS_NOT_A_NUMBER: unit = None @@ -393,11 +389,24 @@ class StatisticsSensor(SensorEntity): @property def device_class(self) -> SensorDeviceClass | None: """Return the class of this device.""" - if self._state_characteristic in STAT_NUMERIC_RETAIN_UNIT: - _state = self.hass.states.get(self._source_entity_id) - return None if _state is None else _state.attributes.get(ATTR_DEVICE_CLASS) if self._state_characteristic in STATS_DATETIME: return SensorDeviceClass.TIMESTAMP + if self._state_characteristic in STATS_NUMERIC_RETAIN_UNIT: + source_state = self.hass.states.get(self._source_entity_id) + if source_state is None: + return None + source_device_class = source_state.attributes.get(ATTR_DEVICE_CLASS) + if source_device_class is None: + return None + sensor_device_class = try_parse_enum(SensorDeviceClass, source_device_class) + if sensor_device_class is None: + return None + sensor_state_classes = DEVICE_CLASS_STATE_CLASSES.get( + sensor_device_class, set() + ) + if SensorStateClass.MEASUREMENT not in sensor_state_classes: + return None + return sensor_device_class return None @property @@ -472,8 +481,8 @@ class StatisticsSensor(SensorEntity): if timestamp := self._next_to_purge_timestamp(): _LOGGER.debug("%s: scheduling update at %s", self.entity_id, timestamp) if self._update_listener: - self._update_listener() - self._update_listener = None + self._update_listener() # pragma: no cover + self._update_listener = None # pragma: no cover @callback def _scheduled_update(now: datetime) -> None: @@ -563,6 +572,18 @@ class StatisticsSensor(SensorEntity): value = int(value) self._value = value + def _callable_characteristic_fn( + self, characteristic: str + ) -> Callable[[], StateType | datetime]: + """Return the function callable of one characteristic function.""" + function: Callable[[], StateType | datetime] = getattr( + self, + f"_stat_binary_{characteristic}" + if self.is_binary + else f"_stat_{characteristic}", + ) + return function + # Statistics for numeric sensor def _stat_average_linear(self) -> StateType: diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 7f68ae68973..148ae87b801 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, + UnitOfEnergy, UnitOfTemperature, ) from homeassistant.core import HomeAssistant @@ -250,6 +251,63 @@ async def test_sensor_source_with_force_update(hass: HomeAssistant): assert state_force.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) +async def test_sampling_boundaries_given(hass: HomeAssistant): + """Test if either sampling_size or max_age are given.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_boundaries_none", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + }, + { + "platform": "statistics", + "name": "test_boundaries_size", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "sampling_size": 20, + }, + { + "platform": "statistics", + "name": "test_boundaries_age", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "max_age": {"minutes": 4}, + }, + { + "platform": "statistics", + "name": "test_boundaries_both", + "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", + "sampling_size": 20, + "max_age": {"minutes": 4}, + }, + ] + }, + ) + await hass.async_block_till_done() + + hass.states.async_set( + "sensor.test_monitored", + str(VALUES_NUMERIC[0]), + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_boundaries_none") + assert state is None + state = hass.states.get("sensor.test_boundaries_size") + assert state is not None + state = hass.states.get("sensor.test_boundaries_age") + assert state is not None + state = hass.states.get("sensor.test_boundaries_both") + assert state is not None + + async def test_sampling_size_reduced(hass: HomeAssistant): """Test limited buffer size.""" assert await async_setup_component( @@ -514,9 +572,9 @@ async def test_device_class(hass: HomeAssistant): { "sensor": [ { - # Device class is carried over from source sensor for characteristics with same unit + # Device class is carried over from source sensor for characteristics which retain unit "platform": "statistics", - "name": "test_source_class", + "name": "test_retain_unit", "entity_id": "sensor.test_monitored", "state_characteristic": "mean", "sampling_size": 20, @@ -537,6 +595,14 @@ async def test_device_class(hass: HomeAssistant): "state_characteristic": "datetime_oldest", "sampling_size": 20, }, + { + # Device class is set to None for any source sensor with TOTAL state class + "platform": "statistics", + "name": "test_source_class_total", + "entity_id": "sensor.test_monitored_total", + "state_characteristic": "mean", + "sampling_size": 20, + }, ] }, ) @@ -549,11 +615,21 @@ async def test_device_class(hass: HomeAssistant): { ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + }, + ) + hass.states.async_set( + "sensor.test_monitored_total", + str(value), + { + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.WATT_HOUR, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, }, ) await hass.async_block_till_done() - state = hass.states.get("sensor.test_source_class") + state = hass.states.get("sensor.test_retain_unit") assert state is not None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE state = hass.states.get("sensor.test_none") @@ -562,6 +638,9 @@ async def test_device_class(hass: HomeAssistant): state = hass.states.get("sensor.test_timestamp") assert state is not None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + state = hass.states.get("sensor.test_source_class_total") + assert state is not None + assert state.attributes.get(ATTR_DEVICE_CLASS) is None async def test_state_class(hass: HomeAssistant): @@ -572,6 +651,15 @@ async def test_state_class(hass: HomeAssistant): { "sensor": [ { + # State class is None for datetime characteristics + "platform": "statistics", + "name": "test_nan", + "entity_id": "sensor.test_monitored", + "state_characteristic": "datetime_oldest", + "sampling_size": 20, + }, + { + # State class is MEASUREMENT for all other characteristics "platform": "statistics", "name": "test_normal", "entity_id": "sensor.test_monitored", @@ -579,10 +667,12 @@ async def test_state_class(hass: HomeAssistant): "sampling_size": 20, }, { + # State class is MEASUREMENT, even when the source sensor + # is of state class TOTAL "platform": "statistics", - "name": "test_nan", - "entity_id": "sensor.test_monitored", - "state_characteristic": "datetime_oldest", + "name": "test_total", + "entity_id": "sensor.test_monitored_total", + "state_characteristic": "count", "sampling_size": 20, }, ] @@ -596,14 +686,28 @@ async def test_state_class(hass: HomeAssistant): str(value), {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) + hass.states.async_set( + "sensor.test_monitored_total", + str(value), + { + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + }, + ) await hass.async_block_till_done() - state = hass.states.get("sensor.test_normal") - assert state is not None - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT state = hass.states.get("sensor.test_nan") assert state is not None assert state.attributes.get(ATTR_STATE_CLASS) is None + state = hass.states.get("sensor.test_normal") + assert state is not None + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + state = hass.states.get("sensor.test_monitored_total") + assert state is not None + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + state = hass.states.get("sensor.test_total") + assert state is not None + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT async def test_unitless_source_sensor(hass: HomeAssistant): From 2d293c19bf0d566405d1fc062444c1f42a2dd418 Mon Sep 17 00:00:00 2001 From: Mark Broadbent Date: Tue, 14 Feb 2023 20:26:52 +0000 Subject: [PATCH 182/187] Update orjson to resolve segmentation fault during JSON serialisation (#88119) Home Assistant uses orjson 3.8.5 that contains an issue[1] on musl libc platforms that causes a segmentation fault. This particularly affect Home Assistant container installations reported in #87283 and #87522. This updates the version to 3.8.6 that resolves the segmentation fault during json serialisation. [1] https://github.com/ijl/orjson/issues/335 --- 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 8ad9a144a1a..ac49c02cc53 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ ifaddr==0.1.7 janus==1.0.0 jinja2==3.1.2 lru-dict==1.1.8 -orjson==3.8.5 +orjson==3.8.6 paho-mqtt==1.6.1 pillow==9.4.0 pip>=21.0,<22.4 diff --git a/pyproject.toml b/pyproject.toml index 092fdb74079..786f3f90dfc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "cryptography==39.0.1", # pyOpenSSL 23.0.0 is required to work with cryptography 39+ "pyOpenSSL==23.0.0", - "orjson==3.8.5", + "orjson==3.8.6", "pip>=21.0,<22.4", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index 7d05e1bb2e7..c2404d880b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ lru-dict==1.1.8 PyJWT==2.5.0 cryptography==39.0.1 pyOpenSSL==23.0.0 -orjson==3.8.5 +orjson==3.8.6 pip>=21.0,<22.4 python-slugify==4.0.1 pyyaml==6.0 From 1ca6c263127cf92619ba0604c206d87a9084f194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Wed, 15 Feb 2023 15:27:46 +0100 Subject: [PATCH 183/187] Blebox fix thermobox reporting wrong state (#88169) fix: fixed climate hvac_mode for when device is off --- homeassistant/components/blebox/climate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index 94e79009853..e4ac8985ebd 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -22,6 +22,7 @@ from .const import DOMAIN, PRODUCT SCAN_INTERVAL = timedelta(seconds=5) BLEBOX_TO_HVACMODE = { + None: None, 0: HVACMode.OFF, 1: HVACMode.HEAT, 2: HVACMode.COOL, @@ -58,13 +59,15 @@ class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEn @property def hvac_modes(self): """Return list of supported HVAC modes.""" - return [HVACMode.OFF, self.hvac_mode] + return [HVACMode.OFF, BLEBOX_TO_HVACMODE[self._feature.mode]] @property def hvac_mode(self): """Return the desired HVAC mode.""" if self._feature.is_on is None: return None + if not self._feature.is_on: + return HVACMode.OFF if self._feature.mode is not None: return BLEBOX_TO_HVACMODE[self._feature.mode] return HVACMode.HEAT if self._feature.is_on else HVACMode.OFF From bbbc864a8c2f945502ed490fea34af05763b5436 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 15 Feb 2023 15:16:47 +0100 Subject: [PATCH 184/187] Bump reolink-aio to 0.4.2 (#88175) --- 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 0519960945e..64d5b492e3b 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.4.0"], + "requirements": ["reolink-aio==0.4.2"], "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 59c17c20f81..4fadd3ab7c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2227,7 +2227,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.4.0 +reolink-aio==0.4.2 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b236028fbb..b99a96cdb20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1572,7 +1572,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.4.0 +reolink-aio==0.4.2 # homeassistant.components.python_script restrictedpython==6.0 From 0d140426ccdba2da247ea8d46952fc23419274cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Feb 2023 11:14:27 -0500 Subject: [PATCH 185/187] Bumped version to 2023.2.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 acad89bd43f..4d7d066a890 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 = 2 -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, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index 786f3f90dfc..1103b78dc77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.4" +version = "2023.2.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 3e3936e783342eaaf81cfcbc04c35949bafd64b5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Feb 2023 12:17:03 -0500 Subject: [PATCH 186/187] backport try_parse_enum --- homeassistant/components/statistics/sensor.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 2025eebd650..60dc8f54ed7 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -5,9 +5,10 @@ from collections import deque from collections.abc import Callable import contextlib from datetime import datetime, timedelta +from enum import Enum import logging import statistics -from typing import Any, Literal, cast +from typing import Any, Literal, TypeVar, cast import voluptuous as vol @@ -48,7 +49,6 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util import dt as dt_util -from homeassistant.util.enum import try_parse_enum from . import DOMAIN, PLATFORMS @@ -769,3 +769,16 @@ class StatisticsSensor(SensorEntity): if len(self.states) > 0: return 100.0 / len(self.states) * self.states.count(True) return None + + +_EnumT = TypeVar("_EnumT", bound=Enum) + + +def try_parse_enum(cls: type[_EnumT], value: Any) -> _EnumT | None: + """Try to parse the value into an Enum. + + Return None if parsing fails. + """ + with contextlib.suppress(ValueError): + return cls(value) + return None From 684845fd8c0a36bc415b1ba0272c825fa84c70a2 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 15 Feb 2023 19:23:03 +0100 Subject: [PATCH 187/187] Bump python-matter-server to 2.1.0 (#88192) * Bump python-matter-server to 2.1.0 * Fix tests --------- Co-authored-by: Paulus Schoutsen --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/matter/conftest.py | 2 ++ tests/components/matter/fixtures/config_entry_diagnostics.json | 3 ++- .../matter/fixtures/config_entry_diagnostics_redacted.json | 3 ++- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 46fe45873b4..7b2761b5575 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==2.0.2"], + "requirements": ["python-matter-server==2.1.0"], "dependencies": ["websocket_api"], "codeowners": ["@home-assistant/matter"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 4fadd3ab7c1..125ff841354 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2072,7 +2072,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==2.0.2 +python-matter-server==2.1.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b99a96cdb20..68557b36f5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1468,7 +1468,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.matter -python-matter-server==2.0.2 +python-matter-server==2.1.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index ad66bb7f6a9..c8592c84849 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch +from matter_server.common.const import SCHEMA_VERSION from matter_server.common.models.server_information import ServerInfo import pytest @@ -45,6 +46,7 @@ async def matter_client_fixture() -> AsyncGenerator[MagicMock, None]: sdk_version="2022.11.1", wifi_credentials_set=True, thread_credentials_set=True, + min_supported_schema_version=SCHEMA_VERSION, ) yield client diff --git a/tests/components/matter/fixtures/config_entry_diagnostics.json b/tests/components/matter/fixtures/config_entry_diagnostics.json index 13a4e7d26a5..8c7b0960fc8 100644 --- a/tests/components/matter/fixtures/config_entry_diagnostics.json +++ b/tests/components/matter/fixtures/config_entry_diagnostics.json @@ -5,7 +5,8 @@ "schema_version": 1, "sdk_version": "2022.12.0", "wifi_credentials_set": true, - "thread_credentials_set": false + "thread_credentials_set": false, + "min_supported_schema_version": 1 }, "nodes": [ { diff --git a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json index 8f798a50467..bbe7cf1f2c6 100644 --- a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json +++ b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json @@ -6,7 +6,8 @@ "schema_version": 1, "sdk_version": "2022.12.0", "wifi_credentials_set": true, - "thread_credentials_set": false + "thread_credentials_set": false, + "min_supported_schema_version": 1 }, "nodes": [ {