From 05fd64fe802b82a04888244e65e012611d8eb912 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Oct 2023 17:41:53 +0200 Subject: [PATCH 001/265] Bumped version to 2023.11.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 77c5582464e4..5b8e9f43b21d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 82bb7d08e26c..8068b7e55b7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.11.0.dev0" +version = "2023.11.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 5b0e0b07b3362a3615cfc586084397f5cea696d4 Mon Sep 17 00:00:00 2001 From: Amit Finkelstein Date: Thu, 26 Oct 2023 11:46:20 +0300 Subject: [PATCH 002/265] Apple TV: Use replacement commands for deprecated ones (#102056) Co-authored-by: Robert Resch --- homeassistant/components/apple_tv/remote.py | 17 ++++++++++++- tests/components/apple_tv/test_remote.py | 28 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/components/apple_tv/test_remote.py diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index f3be69778917..bab3421c58d7 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -21,6 +21,15 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 +COMMAND_TO_ATTRIBUTE = { + "wakeup": ("power", "turn_on"), + "suspend": ("power", "turn_off"), + "turn_on": ("power", "turn_on"), + "turn_off": ("power", "turn_off"), + "volume_up": ("audio", "volume_up"), + "volume_down": ("audio", "volume_down"), + "home_hold": ("remote_control", "home"), +} async def async_setup_entry( @@ -61,7 +70,13 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity): for _ in range(num_repeats): for single_command in command: - attr_value = getattr(self.atv.remote_control, single_command, None) + attr_value = None + if attributes := COMMAND_TO_ATTRIBUTE.get(single_command): + attr_value = self.atv + for attr_name in attributes: + attr_value = getattr(attr_value, attr_name, None) + if not attr_value: + attr_value = getattr(self.atv.remote_control, single_command, None) if not attr_value: raise ValueError("Command not found. Exiting sequence") diff --git a/tests/components/apple_tv/test_remote.py b/tests/components/apple_tv/test_remote.py new file mode 100644 index 000000000000..db2a4964f6cf --- /dev/null +++ b/tests/components/apple_tv/test_remote.py @@ -0,0 +1,28 @@ +"""Test apple_tv remote.""" +from unittest.mock import AsyncMock + +import pytest + +from homeassistant.components.apple_tv.remote import AppleTVRemote +from homeassistant.components.remote import ATTR_DELAY_SECS, ATTR_NUM_REPEATS + + +@pytest.mark.parametrize( + ("command", "method"), + [ + ("up", "remote_control.up"), + ("wakeup", "power.turn_on"), + ("volume_up", "audio.volume_up"), + ("home_hold", "remote_control.home"), + ], + ids=["up", "wakeup", "volume_up", "home_hold"], +) +async def test_send_command(command: str, method: str) -> None: + """Test "send_command" method.""" + remote = AppleTVRemote("test", "test", None) + remote.atv = AsyncMock() + await remote.async_send_command( + [command], **{ATTR_NUM_REPEATS: 1, ATTR_DELAY_SECS: 0} + ) + assert len(remote.atv.method_calls) == 1 + assert str(remote.atv.method_calls[0]) == f"call.{method}()" From bbcfb5f30e46e27094ba207aeb888452aeebcf15 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 26 Oct 2023 21:34:59 +0200 Subject: [PATCH 003/265] Improve exception handling for Vodafone Station (#102761) * improve exception handling for Vodafone Station * address review comment * apply review comment * better except handling (bump library) * cleanup --- .../vodafone_station/coordinator.py | 22 +++++++++++-------- .../components/vodafone_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/vodafone_station/coordinator.py b/homeassistant/components/vodafone_station/coordinator.py index 38fc80ac3af9..a2cddcf9a656 100644 --- a/homeassistant/components/vodafone_station/coordinator.py +++ b/homeassistant/components/vodafone_station/coordinator.py @@ -95,15 +95,19 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]): """Update router data.""" _LOGGER.debug("Polling Vodafone Station host: %s", self._host) try: - logged = await self.api.login() - except exceptions.CannotConnect as err: - _LOGGER.warning("Connection error for %s", self._host) - raise UpdateFailed(f"Error fetching data: {repr(err)}") from err - except exceptions.CannotAuthenticate as err: - raise ConfigEntryAuthFailed from err - - if not logged: - raise ConfigEntryAuthFailed + try: + await self.api.login() + except exceptions.CannotAuthenticate as err: + raise ConfigEntryAuthFailed from err + except ( + exceptions.CannotConnect, + exceptions.AlreadyLogged, + exceptions.GenericLoginError, + ) as err: + raise UpdateFailed(f"Error fetching data: {repr(err)}") from err + except (ConfigEntryAuthFailed, UpdateFailed): + await self.api.close() + raise utc_point_in_time = dt_util.utcnow() data_devices = { diff --git a/homeassistant/components/vodafone_station/manifest.json b/homeassistant/components/vodafone_station/manifest.json index 628c25b987ec..2a1814c83d09 100644 --- a/homeassistant/components/vodafone_station/manifest.json +++ b/homeassistant/components/vodafone_station/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vodafone_station", "iot_class": "local_polling", "loggers": ["aiovodafone"], - "requirements": ["aiovodafone==0.4.1"] + "requirements": ["aiovodafone==0.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7d5f24f0b823..aa93dd46f264 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -375,7 +375,7 @@ aiounifi==64 aiovlc==0.1.0 # homeassistant.components.vodafone_station -aiovodafone==0.4.1 +aiovodafone==0.4.2 # homeassistant.components.waqi aiowaqi==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e46c62e8976d..dc8a40bc325e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -350,7 +350,7 @@ aiounifi==64 aiovlc==0.1.0 # homeassistant.components.vodafone_station -aiovodafone==0.4.1 +aiovodafone==0.4.2 # homeassistant.components.waqi aiowaqi==2.1.0 From 62733e830f2fd55a1d74d1534901ddebcde2e8d1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 Oct 2023 09:46:16 +0200 Subject: [PATCH 004/265] Improve validation of device automations (#102766) * Improve validation of device automations * Improve comments * Address review comment --- .../components/device_automation/helpers.py | 37 +++-- .../components/device_automation/test_init.py | 148 +++++++++++++----- 2 files changed, 133 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/device_automation/helpers.py b/homeassistant/components/device_automation/helpers.py index 83c599bc65d5..a00455293f68 100644 --- a/homeassistant/components/device_automation/helpers.py +++ b/homeassistant/components/device_automation/helpers.py @@ -5,9 +5,9 @@ from typing import cast import voluptuous as vol -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, Platform +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import ConfigType from . import DeviceAutomationType, async_get_device_automation_platform @@ -55,31 +55,42 @@ async def async_validate_device_automation_config( platform = await async_get_device_automation_platform( hass, validated_config[CONF_DOMAIN], automation_type ) + + # Make sure the referenced device and optional entity exist + device_registry = dr.async_get(hass) + if not (device := device_registry.async_get(validated_config[CONF_DEVICE_ID])): + # The device referenced by the device automation does not exist + raise InvalidDeviceAutomationConfig( + f"Unknown device '{validated_config[CONF_DEVICE_ID]}'" + ) + if entity_id := validated_config.get(CONF_ENTITY_ID): + try: + er.async_validate_entity_id(er.async_get(hass), entity_id) + except vol.Invalid as err: + raise InvalidDeviceAutomationConfig( + f"Unknown entity '{entity_id}'" + ) from err + if not hasattr(platform, DYNAMIC_VALIDATOR[automation_type]): # Pass the unvalidated config to avoid mutating the raw config twice return cast( ConfigType, getattr(platform, STATIC_VALIDATOR[automation_type])(config) ) - # Bypass checks for entity platforms + # Devices are not linked to config entries from entity platform domains, skip + # the checks below which look for a config entry matching the device automation + # domain if ( automation_type == DeviceAutomationType.ACTION and validated_config[CONF_DOMAIN] in ENTITY_PLATFORMS ): + # Pass the unvalidated config to avoid mutating the raw config twice return cast( ConfigType, await getattr(platform, DYNAMIC_VALIDATOR[automation_type])(hass, config), ) - # Only call the dynamic validator if the referenced device exists and the relevant - # config entry is loaded - registry = dr.async_get(hass) - if not (device := registry.async_get(validated_config[CONF_DEVICE_ID])): - # The device referenced by the device automation does not exist - raise InvalidDeviceAutomationConfig( - f"Unknown device '{validated_config[CONF_DEVICE_ID]}'" - ) - + # Find a config entry with the same domain as the device automation device_config_entry = None for entry_id in device.config_entries: if ( @@ -91,7 +102,7 @@ async def async_validate_device_automation_config( break if not device_config_entry: - # The config entry referenced by the device automation does not exist + # There's no config entry with the same domain as the device automation raise InvalidDeviceAutomationConfig( f"Device '{validated_config[CONF_DEVICE_ID]}' has no config entry from " f"domain '{validated_config[CONF_DOMAIN]}'" diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 3a7105684f41..457b7ccbf9b2 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -1,6 +1,7 @@ """The test for light device automation.""" from unittest.mock import AsyncMock, Mock, patch +import attr import pytest from pytest_unordered import unordered import voluptuous as vol @@ -31,6 +32,13 @@ from tests.common import ( from tests.typing import WebSocketGenerator +@attr.s(frozen=True) +class MockDeviceEntry(dr.DeviceEntry): + """Device Registry Entry with fixed UUID.""" + + id: str = attr.ib(default="very_unique") + + @pytest.fixture(autouse=True, name="stub_blueprint_populate") def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" @@ -1240,17 +1248,56 @@ async def test_automation_with_integration_without_device_trigger( ) +BAD_AUTOMATIONS = [ + ( + {"device_id": "very_unique", "domain": "light"}, + "required key not provided @ data['entity_id']", + ), + ( + {"device_id": "wrong", "domain": "light"}, + "Unknown device 'wrong'", + ), + ( + {"device_id": "wrong"}, + "required key not provided @ data{path}['domain']", + ), + ( + {"device_id": "wrong", "domain": "light"}, + "Unknown device 'wrong'", + ), + ( + {"device_id": "very_unique", "domain": "light"}, + "required key not provided @ data['entity_id']", + ), + ( + {"device_id": "very_unique", "domain": "light", "entity_id": "wrong"}, + "Unknown entity 'wrong'", + ), +] + +BAD_TRIGGERS = BAD_CONDITIONS = BAD_AUTOMATIONS + [ + ( + {"domain": "light"}, + "required key not provided @ data{path}['device_id']", + ) +] + + +@patch("homeassistant.helpers.device_registry.DeviceEntry", MockDeviceEntry) +@pytest.mark.parametrize(("action", "expected_error"), BAD_AUTOMATIONS) async def test_automation_with_bad_action( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + action: dict[str, str], + expected_error: str, ) -> None: """Test automation with bad device action.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) config_entry.state = config_entries.ConfigEntryState.LOADED config_entry.add_to_hass(hass) - device_entry = device_registry.async_get_or_create( + device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) @@ -1262,25 +1309,29 @@ async def test_automation_with_bad_action( automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event1"}, - "action": {"device_id": device_entry.id, "domain": "light"}, + "action": action, } }, ) - assert "required key not provided" in caplog.text + assert expected_error.format(path="['action'][0]") in caplog.text +@patch("homeassistant.helpers.device_registry.DeviceEntry", MockDeviceEntry) +@pytest.mark.parametrize(("condition", "expected_error"), BAD_CONDITIONS) async def test_automation_with_bad_condition_action( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + condition: dict[str, str], + expected_error: str, ) -> None: """Test automation with bad device action.""" config_entry = MockConfigEntry(domain="fake_integration", data={}) config_entry.state = config_entries.ConfigEntryState.LOADED config_entry.add_to_hass(hass) - device_entry = device_registry.async_get_or_create( + device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) @@ -1292,42 +1343,32 @@ async def test_automation_with_bad_condition_action( automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event1"}, - "action": { - "condition": "device", - "device_id": device_entry.id, - "domain": "light", - }, + "action": {"condition": "device"} | condition, } }, ) - assert "required key not provided" in caplog.text - - -async def test_automation_with_bad_condition_missing_domain( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test automation with bad device condition.""" - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "alias": "hello", - "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": {"condition": "device", "device_id": "hello.device"}, - "action": {"service": "test.automation", "entity_id": "hello.world"}, - } - }, - ) - - assert "required key not provided @ data['condition'][0]['domain']" in caplog.text + assert expected_error.format(path="['action'][0]") in caplog.text +@patch("homeassistant.helpers.device_registry.DeviceEntry", MockDeviceEntry) +@pytest.mark.parametrize(("condition", "expected_error"), BAD_CONDITIONS) async def test_automation_with_bad_condition( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + device_registry: dr.DeviceRegistry, + condition: dict[str, str], + expected_error: str, ) -> None: """Test automation with bad device condition.""" + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.add_to_hass(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert await async_setup_component( hass, automation.DOMAIN, @@ -1335,13 +1376,13 @@ async def test_automation_with_bad_condition( automation.DOMAIN: { "alias": "hello", "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": {"condition": "device", "domain": "light"}, + "condition": {"condition": "device"} | condition, "action": {"service": "test.automation", "entity_id": "hello.world"}, } }, ) - assert "required key not provided" in caplog.text + assert expected_error.format(path="['condition'][0]") in caplog.text @pytest.fixture @@ -1475,10 +1516,24 @@ async def test_automation_with_sub_condition( ) +@patch("homeassistant.helpers.device_registry.DeviceEntry", MockDeviceEntry) +@pytest.mark.parametrize(("condition", "expected_error"), BAD_CONDITIONS) async def test_automation_with_bad_sub_condition( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + device_registry: dr.DeviceRegistry, + condition: dict[str, str], + expected_error: str, ) -> None: """Test automation with bad device condition under and/or conditions.""" + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.add_to_hass(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert await async_setup_component( hass, automation.DOMAIN, @@ -1488,33 +1543,48 @@ async def test_automation_with_bad_sub_condition( "trigger": {"platform": "event", "event_type": "test_event1"}, "condition": { "condition": "and", - "conditions": [{"condition": "device", "domain": "light"}], + "conditions": [{"condition": "device"} | condition], }, "action": {"service": "test.automation", "entity_id": "hello.world"}, } }, ) - assert "required key not provided" in caplog.text + path = "['condition'][0]['conditions'][0]" + assert expected_error.format(path=path) in caplog.text +@patch("homeassistant.helpers.device_registry.DeviceEntry", MockDeviceEntry) +@pytest.mark.parametrize(("trigger", "expected_error"), BAD_TRIGGERS) async def test_automation_with_bad_trigger( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + device_registry: dr.DeviceRegistry, + trigger: dict[str, str], + expected_error: str, ) -> None: """Test automation with bad device trigger.""" + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.add_to_hass(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert await async_setup_component( hass, automation.DOMAIN, { automation.DOMAIN: { "alias": "hello", - "trigger": {"platform": "device", "domain": "light"}, + "trigger": {"platform": "device"} | trigger, "action": {"service": "test.automation", "entity_id": "hello.world"}, } }, ) - assert "required key not provided" in caplog.text + assert expected_error.format(path="") in caplog.text async def test_websocket_device_not_found( From 0a0584b0533625be58ac73e732474a11d2717199 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 25 Oct 2023 23:09:36 +0200 Subject: [PATCH 005/265] Fix velbus import (#102780) --- homeassistant/components/velbus/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 5c35303f8597..1888a1778955 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -import velbusaio +import velbusaio.controller from velbusaio.exceptions import VelbusConnectionFailed import voluptuous as vol From 5fe5013198965a86b9f99228bb2774770479b1c6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 26 Oct 2023 09:43:10 -0700 Subject: [PATCH 006/265] Change todo move API to reference previous uid (#102795) --- homeassistant/components/local_todo/todo.py | 28 +++++--- .../components/shopping_list/__init__.py | 24 ++++--- .../components/shopping_list/todo.py | 6 +- homeassistant/components/todo/__init__.py | 18 +++-- pylint/plugins/hass_enforce_type_hints.py | 2 +- tests/components/local_todo/test_todo.py | 72 ++++++++++++++----- tests/components/shopping_list/test_todo.py | 35 +++++---- tests/components/todo/test_init.py | 15 ++-- 8 files changed, 131 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/local_todo/todo.py b/homeassistant/components/local_todo/todo.py index 14d14316faf3..7e23d01ee465 100644 --- a/homeassistant/components/local_todo/todo.py +++ b/homeassistant/components/local_todo/todo.py @@ -139,20 +139,28 @@ class LocalTodoListEntity(TodoListEntity): await self._async_save() await self.async_update_ha_state(force_refresh=True) - async def async_move_todo_item(self, uid: str, pos: int) -> None: + async def async_move_todo_item( + self, uid: str, previous_uid: str | None = None + ) -> None: """Re-order an item to the To-do list.""" + if uid == previous_uid: + return todos = self._calendar.todos - found_item: Todo | None = None - for idx, itm in enumerate(todos): - if itm.uid == uid: - found_item = itm - todos.pop(idx) - break - if found_item is None: + item_idx: dict[str, int] = {itm.uid: idx for idx, itm in enumerate(todos)} + if uid not in item_idx: raise HomeAssistantError( - f"Item '{uid}' not found in todo list {self.entity_id}" + "Item '{uid}' not found in todo list {self.entity_id}" ) - todos.insert(pos, found_item) + if previous_uid and previous_uid not in item_idx: + raise HomeAssistantError( + "Item '{previous_uid}' not found in todo list {self.entity_id}" + ) + dst_idx = item_idx[previous_uid] + 1 if previous_uid else 0 + src_idx = item_idx[uid] + src_item = todos.pop(src_idx) + if dst_idx > src_idx: + dst_idx -= 1 + todos.insert(dst_idx, src_item) await self._async_save() await self.async_update_ha_state(force_refresh=True) diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index f2de59b10af6..e2f04b5d8806 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -322,17 +322,23 @@ class ShoppingData: context=context, ) - async def async_move_item(self, uid: str, pos: int) -> None: + async def async_move_item(self, uid: str, previous: str | None = None) -> None: """Re-order a shopping list item.""" - found_item: dict[str, Any] | None = None - for idx, itm in enumerate(self.items): - if cast(str, itm["id"]) == uid: - found_item = itm - self.items.pop(idx) - break - if not found_item: + if uid == previous: + return + item_idx = {cast(str, itm["id"]): idx for idx, itm in enumerate(self.items)} + if uid not in item_idx: raise NoMatchingShoppingListItem(f"Item '{uid}' not found in shopping list") - self.items.insert(pos, found_item) + if previous and previous not in item_idx: + raise NoMatchingShoppingListItem( + f"Item '{previous}' not found in shopping list" + ) + dst_idx = item_idx[previous] + 1 if previous else 0 + src_idx = item_idx[uid] + src_item = self.items.pop(src_idx) + if dst_idx > src_idx: + dst_idx -= 1 + self.items.insert(dst_idx, src_item) await self.hass.async_add_executor_job(self.save) self._async_notify() self.hass.bus.async_fire( diff --git a/homeassistant/components/shopping_list/todo.py b/homeassistant/components/shopping_list/todo.py index 53c9e6b6d74a..d89f376d6626 100644 --- a/homeassistant/components/shopping_list/todo.py +++ b/homeassistant/components/shopping_list/todo.py @@ -71,11 +71,13 @@ class ShoppingTodoListEntity(TodoListEntity): """Add an item to the To-do list.""" await self._data.async_remove_items(set(uids)) - async def async_move_todo_item(self, uid: str, pos: int) -> None: + async def async_move_todo_item( + self, uid: str, previous_uid: str | None = None + ) -> None: """Re-order an item to the To-do list.""" try: - await self._data.async_move_item(uid, pos) + await self._data.async_move_item(uid, previous_uid) except NoMatchingShoppingListItem as err: raise HomeAssistantError( f"Shopping list item '{uid}' could not be re-ordered" diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index a6660b0231a5..12eac858f754 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -152,8 +152,15 @@ class TodoListEntity(Entity): """Delete an item in the To-do list.""" raise NotImplementedError() - async def async_move_todo_item(self, uid: str, pos: int) -> None: - """Move an item in the To-do list.""" + async def async_move_todo_item( + self, uid: str, previous_uid: str | None = None + ) -> None: + """Move an item in the To-do list. + + The To-do item with the specified `uid` should be moved to the position + in the list after the specified by `previous_uid` or `None` for the first + position in the To-do list. + """ raise NotImplementedError() @@ -190,7 +197,7 @@ async def websocket_handle_todo_item_list( vol.Required("type"): "todo/item/move", vol.Required("entity_id"): cv.entity_id, vol.Required("uid"): cv.string, - vol.Optional("pos", default=0): cv.positive_int, + vol.Optional("previous_uid"): cv.string, } ) @websocket_api.async_response @@ -215,9 +222,10 @@ async def websocket_handle_todo_item_move( ) ) return - try: - await entity.async_move_todo_item(uid=msg["uid"], pos=msg["pos"]) + await entity.async_move_todo_item( + uid=msg["uid"], previous_uid=msg.get("previous_uid") + ) except HomeAssistantError as ex: connection.send_error(msg["id"], "failed", str(ex)) else: diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 845b70b72ba8..f43dd9b66720 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -2469,7 +2469,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="async_move_todo_item", arg_types={ 1: "str", - 2: "int", + 2: "str | None", }, return_type="None", ), diff --git a/tests/components/local_todo/test_todo.py b/tests/components/local_todo/test_todo.py index 6d06649a6ba1..8a7e38c9773d 100644 --- a/tests/components/local_todo/test_todo.py +++ b/tests/components/local_todo/test_todo.py @@ -59,7 +59,7 @@ async def ws_move_item( ) -> Callable[[str, str | None], Awaitable[None]]: """Fixture to move an item in the todo list.""" - async def move(uid: str, pos: int) -> None: + async def move(uid: str, previous_uid: str | None) -> None: # Fetch items using To-do platform client = await hass_ws_client() id = ws_req_id() @@ -68,8 +68,9 @@ async def ws_move_item( "type": "todo/item/move", "entity_id": TEST_ENTITY, "uid": uid, - "pos": pos, } + if previous_uid is not None: + data["previous_uid"] = previous_uid await client.send_json(data) resp = await client.receive_json() assert resp.get("id") == id @@ -237,30 +238,29 @@ async def test_update_item( @pytest.mark.parametrize( - ("src_idx", "pos", "expected_items"), + ("src_idx", "dst_idx", "expected_items"), [ # Move any item to the front of the list - (0, 0, ["item 1", "item 2", "item 3", "item 4"]), - (1, 0, ["item 2", "item 1", "item 3", "item 4"]), - (2, 0, ["item 3", "item 1", "item 2", "item 4"]), - (3, 0, ["item 4", "item 1", "item 2", "item 3"]), + (0, None, ["item 1", "item 2", "item 3", "item 4"]), + (1, None, ["item 2", "item 1", "item 3", "item 4"]), + (2, None, ["item 3", "item 1", "item 2", "item 4"]), + (3, None, ["item 4", "item 1", "item 2", "item 3"]), # Move items right (0, 1, ["item 2", "item 1", "item 3", "item 4"]), (0, 2, ["item 2", "item 3", "item 1", "item 4"]), (0, 3, ["item 2", "item 3", "item 4", "item 1"]), (1, 2, ["item 1", "item 3", "item 2", "item 4"]), (1, 3, ["item 1", "item 3", "item 4", "item 2"]), - (1, 4, ["item 1", "item 3", "item 4", "item 2"]), - (1, 5, ["item 1", "item 3", "item 4", "item 2"]), # Move items left - (2, 1, ["item 1", "item 3", "item 2", "item 4"]), - (3, 1, ["item 1", "item 4", "item 2", "item 3"]), - (3, 2, ["item 1", "item 2", "item 4", "item 3"]), + (2, 0, ["item 1", "item 3", "item 2", "item 4"]), + (3, 0, ["item 1", "item 4", "item 2", "item 3"]), + (3, 1, ["item 1", "item 2", "item 4", "item 3"]), # No-ops - (1, 1, ["item 1", "item 2", "item 3", "item 4"]), + (0, 0, ["item 1", "item 2", "item 3", "item 4"]), + (2, 1, ["item 1", "item 2", "item 3", "item 4"]), (2, 2, ["item 1", "item 2", "item 3", "item 4"]), + (3, 2, ["item 1", "item 2", "item 3", "item 4"]), (3, 3, ["item 1", "item 2", "item 3", "item 4"]), - (3, 4, ["item 1", "item 2", "item 3", "item 4"]), ], ) async def test_move_item( @@ -269,7 +269,7 @@ async def test_move_item( ws_get_items: Callable[[], Awaitable[dict[str, str]]], ws_move_item: Callable[[str, str | None], Awaitable[None]], src_idx: int, - pos: int, + dst_idx: int | None, expected_items: list[str], ) -> None: """Test moving a todo item within the list.""" @@ -289,7 +289,10 @@ async def test_move_item( assert summaries == ["item 1", "item 2", "item 3", "item 4"] # Prepare items for moving - await ws_move_item(uids[src_idx], pos) + previous_uid = None + if dst_idx is not None: + previous_uid = uids[dst_idx] + await ws_move_item(uids[src_idx], previous_uid) items = await ws_get_items() assert len(items) == 4 @@ -311,7 +314,42 @@ async def test_move_item_unknown( "type": "todo/item/move", "entity_id": TEST_ENTITY, "uid": "unknown", - "pos": 0, + "previous_uid": "item-2", + } + await client.send_json(data) + resp = await client.receive_json() + assert resp.get("id") == 1 + assert not resp.get("success") + assert resp.get("error", {}).get("code") == "failed" + assert "not found in todo list" in resp["error"]["message"] + + +async def test_move_item_previous_unknown( + hass: HomeAssistant, + setup_integration: None, + hass_ws_client: WebSocketGenerator, + ws_get_items: Callable[[], Awaitable[dict[str, str]]], +) -> None: + """Test moving a todo item that does not exist.""" + + await hass.services.async_call( + TODO_DOMAIN, + "create_item", + {"summary": "item 1"}, + target={"entity_id": TEST_ENTITY}, + blocking=True, + ) + items = await ws_get_items() + assert len(items) == 1 + + # Prepare items for moving + client = await hass_ws_client() + data = { + "id": 1, + "type": "todo/item/move", + "entity_id": TEST_ENTITY, + "uid": items[0]["uid"], + "previous_uid": "unknown", } await client.send_json(data) resp = await client.receive_json() diff --git a/tests/components/shopping_list/test_todo.py b/tests/components/shopping_list/test_todo.py index 15f1e50bdb95..ab28c6cbe6da 100644 --- a/tests/components/shopping_list/test_todo.py +++ b/tests/components/shopping_list/test_todo.py @@ -57,10 +57,10 @@ async def ws_get_items( async def ws_move_item( hass_ws_client: WebSocketGenerator, ws_req_id: Callable[[], int], -) -> Callable[[str, int | None], Awaitable[None]]: +) -> Callable[[str, str | None], Awaitable[None]]: """Fixture to move an item in the todo list.""" - async def move(uid: str, pos: int | None) -> dict[str, Any]: + async def move(uid: str, previous_uid: str | None) -> dict[str, Any]: # Fetch items using To-do platform client = await hass_ws_client() id = ws_req_id() @@ -70,8 +70,8 @@ async def ws_move_item( "entity_id": TEST_ENTITY, "uid": uid, } - if pos is not None: - data["pos"] = pos + if previous_uid is not None: + data["previous_uid"] = previous_uid await client.send_json(data) resp = await client.receive_json() assert resp.get("id") == id @@ -406,10 +406,10 @@ async def test_update_invalid_item( ("src_idx", "dst_idx", "expected_items"), [ # Move any item to the front of the list - (0, 0, ["item 1", "item 2", "item 3", "item 4"]), - (1, 0, ["item 2", "item 1", "item 3", "item 4"]), - (2, 0, ["item 3", "item 1", "item 2", "item 4"]), - (3, 0, ["item 4", "item 1", "item 2", "item 3"]), + (0, None, ["item 1", "item 2", "item 3", "item 4"]), + (1, None, ["item 2", "item 1", "item 3", "item 4"]), + (2, None, ["item 3", "item 1", "item 2", "item 4"]), + (3, None, ["item 4", "item 1", "item 2", "item 3"]), # Move items right (0, 1, ["item 2", "item 1", "item 3", "item 4"]), (0, 2, ["item 2", "item 3", "item 1", "item 4"]), @@ -417,15 +417,15 @@ async def test_update_invalid_item( (1, 2, ["item 1", "item 3", "item 2", "item 4"]), (1, 3, ["item 1", "item 3", "item 4", "item 2"]), # Move items left - (2, 1, ["item 1", "item 3", "item 2", "item 4"]), - (3, 1, ["item 1", "item 4", "item 2", "item 3"]), - (3, 2, ["item 1", "item 2", "item 4", "item 3"]), + (2, 0, ["item 1", "item 3", "item 2", "item 4"]), + (3, 0, ["item 1", "item 4", "item 2", "item 3"]), + (3, 1, ["item 1", "item 2", "item 4", "item 3"]), # No-ops (0, 0, ["item 1", "item 2", "item 3", "item 4"]), - (1, 1, ["item 1", "item 2", "item 3", "item 4"]), + (2, 1, ["item 1", "item 2", "item 3", "item 4"]), (2, 2, ["item 1", "item 2", "item 3", "item 4"]), + (3, 2, ["item 1", "item 2", "item 3", "item 4"]), (3, 3, ["item 1", "item 2", "item 3", "item 4"]), - (3, 4, ["item 1", "item 2", "item 3", "item 4"]), ], ) async def test_move_item( @@ -433,7 +433,7 @@ async def test_move_item( sl_setup: None, ws_req_id: Callable[[], int], ws_get_items: Callable[[], Awaitable[dict[str, str]]], - ws_move_item: Callable[[str, int | None], Awaitable[dict[str, Any]]], + ws_move_item: Callable[[str, str | None], Awaitable[dict[str, Any]]], src_idx: int, dst_idx: int | None, expected_items: list[str], @@ -457,7 +457,12 @@ async def test_move_item( summaries = [item["summary"] for item in items] assert summaries == ["item 1", "item 2", "item 3", "item 4"] - resp = await ws_move_item(uids[src_idx], dst_idx) + # Prepare items for moving + previous_uid: str | None = None + if dst_idx is not None: + previous_uid = uids[dst_idx] + + resp = await ws_move_item(uids[src_idx], previous_uid) assert resp.get("success") items = await ws_get_items() diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 833a4ea266bc..f4d671ad352a 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -571,7 +571,7 @@ async def test_move_todo_item_service_by_id( "type": "todo/item/move", "entity_id": "todo.entity1", "uid": "item-1", - "pos": "1", + "previous_uid": "item-2", } ) resp = await client.receive_json() @@ -581,7 +581,7 @@ async def test_move_todo_item_service_by_id( args = test_entity.async_move_todo_item.call_args assert args assert args.kwargs.get("uid") == "item-1" - assert args.kwargs.get("pos") == 1 + assert args.kwargs.get("previous_uid") == "item-2" async def test_move_todo_item_service_raises( @@ -601,7 +601,7 @@ async def test_move_todo_item_service_raises( "type": "todo/item/move", "entity_id": "todo.entity1", "uid": "item-1", - "pos": "1", + "previous_uid": "item-2", } ) resp = await client.receive_json() @@ -620,15 +620,10 @@ async def test_move_todo_item_service_raises( ), ({"entity_id": "todo.entity1"}, "invalid_format", "required key not provided"), ( - {"entity_id": "todo.entity1", "pos": "2"}, + {"entity_id": "todo.entity1", "previous_uid": "item-2"}, "invalid_format", "required key not provided", ), - ( - {"entity_id": "todo.entity1", "uid": "item-1", "pos": "-2"}, - "invalid_format", - "value must be at least 0", - ), ], ) async def test_move_todo_item_service_invalid_input( @@ -722,7 +717,7 @@ async def test_move_item_unsupported( "type": "todo/item/move", "entity_id": "todo.entity1", "uid": "item-1", - "pos": "1", + "previous_uid": "item-2", } ) resp = await client.receive_json() From 10e6a26717e5668bfa6c5ae6bcd7cf8da656db9f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 Oct 2023 05:22:38 +0200 Subject: [PATCH 007/265] Fix fan device actions (#102797) --- homeassistant/components/fan/device_action.py | 14 ++++++++++++-- tests/components/fan/test_device_action.py | 3 +++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py index 55bd862349b4..fc7f1ddce1f0 100644 --- a/homeassistant/components/fan/device_action.py +++ b/homeassistant/components/fan/device_action.py @@ -3,14 +3,24 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.device_automation import toggle_entity +from homeassistant.components.device_automation import ( + async_validate_entity_schema, + toggle_entity, +) from homeassistant.const import CONF_DOMAIN from homeassistant.core import Context, HomeAssistant from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN -ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) +_ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + + +async def async_validate_action_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + return async_validate_entity_schema(hass, config, _ACTION_SCHEMA) async def async_get_actions( diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py index 3b179bc158cd..b8756d9ace5e 100644 --- a/tests/components/fan/test_device_action.py +++ b/tests/components/fan/test_device_action.py @@ -171,6 +171,7 @@ async def test_action( hass.bus.async_fire("test_event_turn_off") await hass.async_block_till_done() assert len(turn_off_calls) == 1 + assert turn_off_calls[0].data["entity_id"] == entry.entity_id assert len(turn_on_calls) == 0 assert len(toggle_calls) == 0 @@ -178,6 +179,7 @@ async def test_action( await hass.async_block_till_done() assert len(turn_off_calls) == 1 assert len(turn_on_calls) == 1 + assert turn_on_calls[0].data["entity_id"] == entry.entity_id assert len(toggle_calls) == 0 hass.bus.async_fire("test_event_toggle") @@ -185,6 +187,7 @@ async def test_action( assert len(turn_off_calls) == 1 assert len(turn_on_calls) == 1 assert len(toggle_calls) == 1 + assert toggle_calls[0].data["entity_id"] == entry.entity_id async def test_action_legacy( From 244fccdae621bf3c12cd48542ace9474a03b02ff Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 25 Oct 2023 17:57:47 -0400 Subject: [PATCH 008/265] Move coordinator first refresh in Blink (#102805) Move coordinator first refresh --- homeassistant/components/blink/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 89438c9c7c12..c6413dd4372d 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -86,8 +86,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: blink.auth = Auth(auth_data, no_prompt=True, session=session) blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) coordinator = BlinkUpdateCoordinator(hass, blink) - await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = coordinator try: await blink.start() @@ -101,6 +99,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not blink.available: raise ConfigEntryNotReady + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) From 701a5d775826a8683d05e57b7ace71f0c0a724a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Oct 2023 15:55:28 -0500 Subject: [PATCH 009/265] Bump HAP-python 4.9.1 (#102811) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 4f6cc24edc82..17d1237e579e 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["pyhap"], "requirements": [ - "HAP-python==4.9.0", + "HAP-python==4.9.1", "fnv-hash-fast==0.5.0", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index aa93dd46f264..ab9941778cc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -26,7 +26,7 @@ CO2Signal==0.4.2 DoorBirdPy==2.1.0 # homeassistant.components.homekit -HAP-python==4.9.0 +HAP-python==4.9.1 # homeassistant.components.tasmota HATasmota==0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc8a40bc325e..f40240690ebc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -25,7 +25,7 @@ CO2Signal==0.4.2 DoorBirdPy==2.1.0 # homeassistant.components.homekit -HAP-python==4.9.0 +HAP-python==4.9.1 # homeassistant.components.tasmota HATasmota==0.7.3 From f2cef7245ac36fdd257f73a6f8268bfd058288d6 Mon Sep 17 00:00:00 2001 From: William Scanlon <6432770+w1ll1am23@users.noreply.github.com> Date: Wed, 25 Oct 2023 23:25:44 -0400 Subject: [PATCH 010/265] Bump pyeconet to 0.1.22 to handle breaking API change (#102820) --- homeassistant/components/econet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 3472ca231e98..26b04929a45b 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/econet", "iot_class": "cloud_push", "loggers": ["paho_mqtt", "pyeconet"], - "requirements": ["pyeconet==0.1.20"] + "requirements": ["pyeconet==0.1.22"] } diff --git a/requirements_all.txt b/requirements_all.txt index ab9941778cc2..5986c17e07b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1684,7 +1684,7 @@ pyebox==1.1.4 pyecoforest==0.3.0 # homeassistant.components.econet -pyeconet==0.1.20 +pyeconet==0.1.22 # homeassistant.components.edimax pyedimax==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f40240690ebc..bf76fe945ef3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1269,7 +1269,7 @@ pydroid-ipcam==2.0.0 pyecoforest==0.3.0 # homeassistant.components.econet -pyeconet==0.1.20 +pyeconet==0.1.22 # homeassistant.components.efergy pyefergy==22.1.1 From 767b7ba4d6e494fd3cef8ea21d9b5900873e70fa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 Oct 2023 01:08:31 +0200 Subject: [PATCH 011/265] Correct logic for picking bluetooth local name (#102823) * Correct logic for picking bluetooth local name * make test more robust --------- Co-authored-by: J. Nick Koston --- .../components/bluetooth/base_scanner.py | 2 +- .../components/bluetooth/test_base_scanner.py | 31 +++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index 240610e48680..8eacd3e291a3 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -330,7 +330,7 @@ class BaseHaRemoteScanner(BaseHaScanner): prev_manufacturer_data = prev_advertisement.manufacturer_data prev_name = prev_device.name - if local_name and prev_name and len(prev_name) > len(local_name): + if prev_name and (not local_name or len(prev_name) > len(local_name)): local_name = prev_name if service_uuids and service_uuids != prev_service_uuids: diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index fc870f2bfe38..31d90a6e93d8 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -42,7 +42,10 @@ from . import ( from tests.common import async_fire_time_changed, load_fixture -async def test_remote_scanner(hass: HomeAssistant, enable_bluetooth: None) -> None: +@pytest.mark.parametrize("name_2", [None, "w"]) +async def test_remote_scanner( + hass: HomeAssistant, enable_bluetooth: None, name_2: str | None +) -> None: """Test the remote scanner base class merges advertisement_data.""" manager = _get_manager() @@ -61,12 +64,25 @@ async def test_remote_scanner(hass: HomeAssistant, enable_bluetooth: None) -> No ) switchbot_device_2 = generate_ble_device( "44:44:33:11:23:45", - "w", + name_2, {}, rssi=-100, ) switchbot_device_adv_2 = generate_advertisement_data( - local_name="wohand", + local_name=name_2, + service_uuids=["00000001-0000-1000-8000-00805f9b34fb"], + service_data={"00000001-0000-1000-8000-00805f9b34fb": b"\n\xff"}, + manufacturer_data={1: b"\x01", 2: b"\x02"}, + rssi=-100, + ) + switchbot_device_3 = generate_ble_device( + "44:44:33:11:23:45", + "wohandlonger", + {}, + rssi=-100, + ) + switchbot_device_adv_3 = generate_advertisement_data( + local_name="wohandlonger", service_uuids=["00000001-0000-1000-8000-00805f9b34fb"], service_data={"00000001-0000-1000-8000-00805f9b34fb": b"\n\xff"}, manufacturer_data={1: b"\x01", 2: b"\x02"}, @@ -125,6 +141,15 @@ async def test_remote_scanner(hass: HomeAssistant, enable_bluetooth: None) -> No "00000001-0000-1000-8000-00805f9b34fb", } + # The longer name should be used + scanner.inject_advertisement(switchbot_device_3, switchbot_device_adv_3) + assert discovered_device.name == switchbot_device_3.name + + # Inject the shorter name / None again to make + # sure we always keep the longer name + scanner.inject_advertisement(switchbot_device_2, switchbot_device_adv_2) + assert discovered_device.name == switchbot_device_3.name + cancel() unsetup() From 0d7fb5b0268827107c5674d4718a3b55b26128a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 Oct 2023 05:20:50 +0200 Subject: [PATCH 012/265] Use real devices in automation blueprint tests (#102824) --- tests/components/automation/test_blueprint.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index ad35a2cfbdd0..2976886881d7 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -7,13 +7,15 @@ from unittest.mock import patch import pytest +from homeassistant import config_entries from homeassistant.components import automation from homeassistant.components.blueprint import models from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, yaml -from tests.common import async_fire_time_changed, async_mock_service +from tests.common import MockConfigEntry, async_fire_time_changed, async_mock_service BUILTIN_BLUEPRINT_FOLDER = pathlib.Path(automation.__file__).parent / "blueprints" @@ -40,8 +42,18 @@ def patch_blueprint(blueprint_path: str, data_path): yield -async def test_notify_leaving_zone(hass: HomeAssistant) -> None: +async def test_notify_leaving_zone( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: """Test notifying leaving a zone blueprint.""" + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.add_to_hass(hass) + + device = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "00:00:00:00:00:01")}, + ) def set_person_state(state, extra={}): hass.states.async_set( @@ -68,7 +80,7 @@ async def test_notify_leaving_zone(hass: HomeAssistant) -> None: "input": { "person_entity": "person.test_person", "zone_entity": "zone.school", - "notify_device": "abcdefgh", + "notify_device": device.id, }, } } @@ -89,7 +101,7 @@ async def test_notify_leaving_zone(hass: HomeAssistant) -> None: "alias": "Notify that a person has left the zone", "domain": "mobile_app", "type": "notify", - "device_id": "abcdefgh", + "device_id": device.id, } message_tpl.hass = hass assert message_tpl.async_render(variables) == "Paulus has left School" From 386c5ecc3e1e1f8c685b1c58eacb29c7982f2d91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Oct 2023 22:23:06 -0500 Subject: [PATCH 013/265] Bump bleak-retry-connector to 3.3.0 (#102825) changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v3.2.1...v3.3.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 960a86637ae2..06e7d34e68df 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.21.1", - "bleak-retry-connector==3.2.1", + "bleak-retry-connector==3.3.0", "bluetooth-adapters==0.16.1", "bluetooth-auto-recovery==1.2.3", "bluetooth-data-tools==1.13.0", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index def5f0c9afa6..3aff4601d456 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ atomicwrites-homeassistant==1.4.1 attrs==23.1.0 awesomeversion==23.8.0 bcrypt==4.0.1 -bleak-retry-connector==3.2.1 +bleak-retry-connector==3.3.0 bleak==0.21.1 bluetooth-adapters==0.16.1 bluetooth-auto-recovery==1.2.3 diff --git a/requirements_all.txt b/requirements_all.txt index 5986c17e07b2..befc5e905cb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -530,7 +530,7 @@ bimmer-connected==0.14.2 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==3.2.1 +bleak-retry-connector==3.3.0 # homeassistant.components.bluetooth bleak==0.21.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf76fe945ef3..34d7a6a4207f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -451,7 +451,7 @@ bellows==0.36.8 bimmer-connected==0.14.2 # homeassistant.components.bluetooth -bleak-retry-connector==3.2.1 +bleak-retry-connector==3.3.0 # homeassistant.components.bluetooth bleak==0.21.1 From a6f88fb1239a1345b1f1fad6367a90e796243a91 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Thu, 26 Oct 2023 02:59:48 -0700 Subject: [PATCH 014/265] Bump screenlogicpy to v0.9.4 (#102836) --- homeassistant/components/screenlogic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index e61ca04374f4..69bed1af700e 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -15,5 +15,5 @@ "documentation": "https://www.home-assistant.io/integrations/screenlogic", "iot_class": "local_push", "loggers": ["screenlogicpy"], - "requirements": ["screenlogicpy==0.9.3"] + "requirements": ["screenlogicpy==0.9.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index befc5e905cb9..63f4f31ac5e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2382,7 +2382,7 @@ satel-integra==0.3.7 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.9.3 +screenlogicpy==0.9.4 # homeassistant.components.scsgate scsgate==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34d7a6a4207f..c32f38b1cf11 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1769,7 +1769,7 @@ samsungtvws[async,encrypted]==2.6.0 scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.9.3 +screenlogicpy==0.9.4 # homeassistant.components.backup securetar==2023.3.0 From 9e140864ebc5fe91c2aa3a0286ef2b895348031b Mon Sep 17 00:00:00 2001 From: Ravaka Razafimanantsoa <3774520+SeraphicRav@users.noreply.github.com> Date: Thu, 26 Oct 2023 19:12:18 +0900 Subject: [PATCH 015/265] Address late review of switchbot cloud (#102842) For Martin's review --- homeassistant/components/switchbot_cloud/climate.py | 8 +++++--- homeassistant/components/switchbot_cloud/switch.py | 2 -- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switchbot_cloud/climate.py b/homeassistant/components/switchbot_cloud/climate.py index 8ad0e1ad43ff..803669c806d7 100644 --- a/homeassistant/components/switchbot_cloud/climate.py +++ b/homeassistant/components/switchbot_cloud/climate.py @@ -14,7 +14,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import DiscoveryInfoType from . import SwitchbotCloudData from .const import DOMAIN @@ -44,7 +43,6 @@ async def async_setup_entry( hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up SwitchBot Cloud entry.""" data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id] @@ -55,7 +53,10 @@ async def async_setup_entry( class SwitchBotCloudAirConditionner(SwitchBotCloudEntity, ClimateEntity): - """Representation of a SwitchBot air conditionner, as it is an IR device, we don't know the actual state.""" + """Representation of a SwitchBot air conditionner. + + As it is an IR device, we don't know the actual state. + """ _attr_assumed_state = True _attr_supported_features = ( @@ -116,3 +117,4 @@ class SwitchBotCloudAirConditionner(SwitchBotCloudEntity, ClimateEntity): return await self._do_send_command(temperature=temperature) self._attr_target_temperature = temperature + self.async_write_ha_state() diff --git a/homeassistant/components/switchbot_cloud/switch.py b/homeassistant/components/switchbot_cloud/switch.py index c63b1713b8de..4f2cdc22ba98 100644 --- a/homeassistant/components/switchbot_cloud/switch.py +++ b/homeassistant/components/switchbot_cloud/switch.py @@ -7,7 +7,6 @@ from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import DiscoveryInfoType from . import SwitchbotCloudData from .const import DOMAIN @@ -19,7 +18,6 @@ async def async_setup_entry( hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up SwitchBot Cloud entry.""" data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id] From 7e4da1d03be029996918854ee2caf5ce2878f272 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 26 Oct 2023 17:31:53 +0200 Subject: [PATCH 016/265] Bump aiowithings to 1.0.2 (#102852) --- homeassistant/components/withings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index a1df31ceecce..d43ae7da50c0 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_push", "loggers": ["aiowithings"], "quality_scale": "platinum", - "requirements": ["aiowithings==1.0.1"] + "requirements": ["aiowithings==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 63f4f31ac5e5..61d80f9654e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -387,7 +387,7 @@ aiowatttime==0.1.1 aiowebostv==0.3.3 # homeassistant.components.withings -aiowithings==1.0.1 +aiowithings==1.0.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c32f38b1cf11..c6dca20ecd32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -362,7 +362,7 @@ aiowatttime==0.1.1 aiowebostv==0.3.3 # homeassistant.components.withings -aiowithings==1.0.1 +aiowithings==1.0.2 # homeassistant.components.yandex_transport aioymaps==1.2.2 From a490b5e2869ea2cfe1411f46c5cf2c383b5ae095 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 27 Oct 2023 12:09:59 +0200 Subject: [PATCH 017/265] Add connections to PassiveBluetoothProcessorEntity (#102854) --- .../components/bluetooth/passive_update_processor.py | 5 ++++- .../bluetooth/test_passive_update_processor.py | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 8138587b9b50..7dd39c140393 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Generic, TypedDict, TypeVar, cast from homeassistant import config_entries from homeassistant.const import ( + ATTR_CONNECTIONS, ATTR_IDENTIFIERS, ATTR_NAME, CONF_ENTITY_CATEGORY, @@ -16,7 +17,7 @@ from homeassistant.const import ( EntityCategory, ) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_platform import async_get_current_platform from homeassistant.helpers.event import async_track_time_interval @@ -644,6 +645,8 @@ class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProce self._attr_unique_id = f"{address}-{key}" if ATTR_NAME not in self._attr_device_info: self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name + if device_id is None: + self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_BLUETOOTH, address)} self._attr_name = processor.entity_names.get(entity_key) @property diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 9e3f954a0c5c..8cc76e01d8c1 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -1208,6 +1208,7 @@ async def test_integration_with_entity_without_a_device( assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature" assert entity_one.device_info == { "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "connections": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, "name": "Generic", } assert entity_one.entity_key == PassiveBluetoothEntityKey( @@ -1396,6 +1397,7 @@ async def test_integration_multiple_entity_platforms( assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" assert sensor_entity_one.device_info == { "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "connections": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, "manufacturer": "Test Manufacturer", "model": "Test Model", "name": "Test Device", @@ -1412,6 +1414,7 @@ async def test_integration_multiple_entity_platforms( assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" assert binary_sensor_entity_one.device_info == { "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "connections": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, "manufacturer": "Test Manufacturer", "model": "Test Model", "name": "Test Device", @@ -1556,6 +1559,7 @@ async def test_integration_multiple_entity_platforms_with_reload_and_restart( assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" assert sensor_entity_one.device_info == { "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "connections": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, "manufacturer": "Test Manufacturer", "model": "Test Model", "name": "Test Device", @@ -1572,6 +1576,7 @@ async def test_integration_multiple_entity_platforms_with_reload_and_restart( assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" assert binary_sensor_entity_one.device_info == { "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "connections": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, "manufacturer": "Test Manufacturer", "model": "Test Model", "name": "Test Device", @@ -1636,6 +1641,7 @@ async def test_integration_multiple_entity_platforms_with_reload_and_restart( assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" assert sensor_entity_one.device_info == { "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "connections": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, "manufacturer": "Test Manufacturer", "model": "Test Model", "name": "Test Device", @@ -1652,6 +1658,7 @@ async def test_integration_multiple_entity_platforms_with_reload_and_restart( assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" assert binary_sensor_entity_one.device_info == { "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "connections": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, "manufacturer": "Test Manufacturer", "model": "Test Model", "name": "Test Device", @@ -1730,6 +1737,7 @@ async def test_integration_multiple_entity_platforms_with_reload_and_restart( assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" assert sensor_entity_one.device_info == { "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "connections": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, "manufacturer": "Test Manufacturer", "model": "Test Model", "name": "Test Device", @@ -1746,6 +1754,7 @@ async def test_integration_multiple_entity_platforms_with_reload_and_restart( assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" assert binary_sensor_entity_one.device_info == { "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "connections": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, "manufacturer": "Test Manufacturer", "model": "Test Model", "name": "Test Device", From 293025ab6cbf3dec5c4ce88c5a38e0bba07a1630 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 26 Oct 2023 17:26:27 +0200 Subject: [PATCH 018/265] Update frontend to 20231026.0 (#102857) --- 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 064777b4921e..31f4dc14559e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20231025.1"] + "requirements": ["home-assistant-frontend==20231026.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3aff4601d456..e99afb6330b5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.74.0 hassil==1.2.5 home-assistant-bluetooth==1.10.4 -home-assistant-frontend==20231025.1 +home-assistant-frontend==20231026.0 home-assistant-intents==2023.10.16 httpx==0.25.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 61d80f9654e6..3a85a24589a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1007,7 +1007,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20231025.1 +home-assistant-frontend==20231026.0 # homeassistant.components.conversation home-assistant-intents==2023.10.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6dca20ecd32..678a2eefcf79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -796,7 +796,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20231025.1 +home-assistant-frontend==20231026.0 # homeassistant.components.conversation home-assistant-intents==2023.10.16 From cc7a4d01e316ef8a13b94b5a1aef4e5bc38c3c8d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 27 Oct 2023 11:30:37 +0200 Subject: [PATCH 019/265] Don't return resources in safe mode (#102865) --- .../components/lovelace/websocket.py | 3 +++ tests/components/lovelace/test_resources.py | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index 423ba3117eae..c9b7cb103866 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -60,6 +60,9 @@ async def websocket_lovelace_resources( """Send Lovelace UI resources over WebSocket configuration.""" resources = hass.data[DOMAIN]["resources"] + if hass.config.safe_mode: + connection.send_result(msg["id"], []) + if not resources.loaded: await resources.async_load() resources.loaded = True diff --git a/tests/components/lovelace/test_resources.py b/tests/components/lovelace/test_resources.py index 1e2a121d6fb4..f7830f03ed65 100644 --- a/tests/components/lovelace/test_resources.py +++ b/tests/components/lovelace/test_resources.py @@ -185,3 +185,26 @@ async def test_storage_resources_import_invalid( "resources" in hass_storage[dashboard.CONFIG_STORAGE_KEY_DEFAULT]["data"]["config"] ) + + +async def test_storage_resources_safe_mode( + hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] +) -> None: + """Test defining resources in storage config.""" + + resource_config = [{**item, "id": uuid.uuid4().hex} for item in RESOURCE_EXAMPLES] + hass_storage[resources.RESOURCE_STORAGE_KEY] = { + "key": resources.RESOURCE_STORAGE_KEY, + "version": 1, + "data": {"items": resource_config}, + } + assert await async_setup_component(hass, "lovelace", {}) + + client = await hass_ws_client(hass) + hass.config.safe_mode = True + + # Fetch data + await client.send_json({"id": 5, "type": "lovelace/resources"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] From 0573981d6ff7cdada85d5b41f93b3d30c567f07d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 27 Oct 2023 10:51:45 +0200 Subject: [PATCH 020/265] Fix mqtt schema import not available for mqtt_room (#102866) --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt_room/sensor.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 1f8e5bbf2e7e..ac229cb677fb 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -47,6 +47,7 @@ from .client import ( # noqa: F401 publish, subscribe, ) +from .config import MQTT_BASE_SCHEMA, MQTT_RO_SCHEMA, MQTT_RW_SCHEMA # noqa: F401 from .config_integration import CONFIG_SCHEMA_BASE from .const import ( # noqa: F401 ATTR_PAYLOAD, diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 4eb3a3f51717..cb0e840604ed 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -47,7 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, } -).extend(mqtt.config.MQTT_RO_SCHEMA.schema) +).extend(mqtt.MQTT_RO_SCHEMA.schema) @lru_cache(maxsize=256) From 62fc9dfd6c27f6c2e1fa36b231ea988f8a6e38dc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 Oct 2023 12:25:27 +0200 Subject: [PATCH 021/265] Allow missing components in safe mode (#102888) --- homeassistant/helpers/check_config.py | 2 +- tests/helpers/test_check_config.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 3218c1e839b3..4aa4e72b0bb4 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -127,7 +127,7 @@ async def async_check_ha_config_file( # noqa: C901 try: integration = await async_get_integration_with_requirements(hass, domain) except loader.IntegrationNotFound as ex: - if not hass.config.recovery_mode: + if not hass.config.recovery_mode and not hass.config.safe_mode: result.add_error(f"Integration error: {domain} - {ex}") continue except RequirementsNotFound as ex: diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 6af031367608..a3fd02686ac7 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -125,6 +125,19 @@ async def test_component_not_found_recovery_mode(hass: HomeAssistant) -> None: assert not res.errors +async def test_component_not_found_safe_mode(hass: HomeAssistant) -> None: + """Test no errors if component not found in safe mode.""" + # Make sure they don't exist + files = {YAML_CONFIG_FILE: BASE_CONFIG + "beer:"} + hass.config.safe_mode = True + with patch("os.path.isfile", return_value=True), patch_yaml_files(files): + res = await async_check_ha_config_file(hass) + log_ha_config(res) + + assert res.keys() == {"homeassistant"} + assert not res.errors + + async def test_component_platform_not_found_2(hass: HomeAssistant) -> None: """Test errors if component or platform not found.""" # Make sure they don't exist From b5c75a2f2f429b7c91207f4677cf32b114fd49df Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 Oct 2023 13:26:26 +0200 Subject: [PATCH 022/265] Allow missing components in safe mode (#102891) --- homeassistant/helpers/check_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 4aa4e72b0bb4..a5e68cb877d8 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -216,7 +216,7 @@ async def async_check_ha_config_file( # noqa: C901 ) platform = p_integration.get_platform(domain) except loader.IntegrationNotFound as ex: - if not hass.config.recovery_mode: + if not hass.config.recovery_mode and not hass.config.safe_mode: result.add_error(f"Platform error {domain}.{p_name} - {ex}") continue except ( From 5dca3844ef1b6e422330643ac627cf3a6545d77e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 27 Oct 2023 13:55:22 +0200 Subject: [PATCH 023/265] Add redirect from shopping list to todo (#102894) --- homeassistant/components/frontend/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8201cbc5b7af..2ec991750f0f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -388,6 +388,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Can be removed in 2023 hass.http.register_redirect("/config/server_control", "/developer-tools/yaml") + # Shopping list panel was replaced by todo panel in 2023.11 + hass.http.register_redirect("/shopping-list", "/todo") + hass.http.app.router.register_resource(IndexView(repo_path, hass)) async_register_built_in_panel(hass, "profile") From 7fe1ac901fb0b3deecbc35409020f9d718114767 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 27 Oct 2023 13:28:16 +0200 Subject: [PATCH 024/265] Some textual fixes for todo (#102895) --- homeassistant/components/todo/manifest.json | 2 +- homeassistant/components/todo/services.yaml | 4 +-- homeassistant/components/todo/strings.json | 34 ++++++++++----------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/todo/manifest.json b/homeassistant/components/todo/manifest.json index 2edf3309e327..8efc93ad4e77 100644 --- a/homeassistant/components/todo/manifest.json +++ b/homeassistant/components/todo/manifest.json @@ -1,6 +1,6 @@ { "domain": "todo", - "name": "To-do", + "name": "To-do list", "codeowners": ["@home-assistant/core"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/todo", diff --git a/homeassistant/components/todo/services.yaml b/homeassistant/components/todo/services.yaml index cf5f3da2b3a2..c31a7e888085 100644 --- a/homeassistant/components/todo/services.yaml +++ b/homeassistant/components/todo/services.yaml @@ -7,7 +7,7 @@ create_item: fields: summary: required: true - example: "Submit Income Tax Return" + example: "Submit income tax return" selector: text: status: @@ -29,7 +29,7 @@ update_item: selector: text: summary: - example: "Submit Income Tax Return" + example: "Submit income tax return" selector: text: status: diff --git a/homeassistant/components/todo/strings.json b/homeassistant/components/todo/strings.json index 4a5a33e94e50..623c46375f0f 100644 --- a/homeassistant/components/todo/strings.json +++ b/homeassistant/components/todo/strings.json @@ -1,5 +1,5 @@ { - "title": "To-do List", + "title": "To-do list", "entity_component": { "_": { "name": "[%key:component::todo::title%]" @@ -7,48 +7,48 @@ }, "services": { "create_item": { - "name": "Create To-do List Item", - "description": "Add a new To-do List Item.", + "name": "Create to-do list item", + "description": "Add a new to-do list item.", "fields": { "summary": { "name": "Summary", - "description": "The short summary that represents the To-do item." + "description": "The short summary that represents the to-do item." }, "status": { "name": "Status", - "description": "A status or confirmation of the To-do item." + "description": "A status or confirmation of the to-do item." } } }, "update_item": { - "name": "Update To-do List Item", - "description": "Update an existing To-do List Item based on either its Unique Id or Summary.", + "name": "Update to-do list item", + "description": "Update an existing to-do list item based on either its unique ID or summary.", "fields": { "uid": { - "name": "To-do Item Unique Id", - "description": "Unique Identifier for the To-do List Item." + "name": "To-do item unique ID", + "description": "Unique identifier for the to-do list item." }, "summary": { "name": "Summary", - "description": "The short summary that represents the To-do item." + "description": "The short summary that represents the to-do item." }, "status": { "name": "Status", - "description": "A status or confirmation of the To-do item." + "description": "A status or confirmation of the to-do item." } } }, "delete_item": { - "name": "Delete a To-do List Item", - "description": "Delete an existing To-do List Item either by its Unique Id or Summary.", + "name": "Delete a to-do list item", + "description": "Delete an existing to-do list item either by its unique ID or summary.", "fields": { "uid": { - "name": "To-do Item Unique Ids", - "description": "Unique Identifiers for the To-do List Items." + "name": "To-do item unique IDs", + "description": "Unique identifiers for the to-do list items." }, "summary": { "name": "Summary", - "description": "The short summary that represents the To-do item." + "description": "The short summary that represents the to-do item." } } } @@ -56,7 +56,7 @@ "selector": { "status": { "options": { - "needs_action": "Needs Action", + "needs_action": "Not completed", "completed": "Completed" } } From 867aaf10ee63b5efe3bc7260724440e62e3c7007 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 27 Oct 2023 14:02:42 +0200 Subject: [PATCH 025/265] Bumped version to 2023.11.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 5b8e9f43b21d..39f957ef77c2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 8068b7e55b7a..1303d1cd7fe3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.11.0b0" +version = "2023.11.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2eb2a651978f3b74f72b60960d3942638391877d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:02 -0400 Subject: [PATCH 026/265] Use new API for Vasttrafik (#102570) --- .../components/vasttrafik/manifest.json | 2 +- homeassistant/components/vasttrafik/sensor.py | 44 +++++++++++++------ requirements_all.txt | 2 +- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/vasttrafik/manifest.json b/homeassistant/components/vasttrafik/manifest.json index aa1907a8a233..336d06e182c4 100644 --- a/homeassistant/components/vasttrafik/manifest.json +++ b/homeassistant/components/vasttrafik/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/vasttrafik", "iot_class": "cloud_polling", "loggers": ["vasttrafik"], - "requirements": ["vtjp==0.1.14"] + "requirements": ["vtjp==0.2.1"] } diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 711f66ea0330..6a0832320794 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -1,7 +1,7 @@ """Support for Västtrafik public transport.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging import vasttrafik @@ -22,6 +22,9 @@ ATTR_ACCESSIBILITY = "accessibility" ATTR_DIRECTION = "direction" ATTR_LINE = "line" ATTR_TRACK = "track" +ATTR_FROM = "from" +ATTR_TO = "to" +ATTR_DELAY = "delay" CONF_DEPARTURES = "departures" CONF_FROM = "from" @@ -32,7 +35,6 @@ CONF_SECRET = "secret" DEFAULT_DELAY = 0 - MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -101,7 +103,7 @@ class VasttrafikDepartureSensor(SensorEntity): if location.isdecimal(): station_info = {"station_name": location, "station_id": location} else: - station_id = self._planner.location_name(location)[0]["id"] + station_id = self._planner.location_name(location)[0]["gid"] station_info = {"station_name": location, "station_id": station_id} return station_info @@ -143,20 +145,36 @@ class VasttrafikDepartureSensor(SensorEntity): self._attributes = {} else: for departure in self._departureboard: - line = departure.get("sname") - if "cancelled" in departure: + service_journey = departure.get("serviceJourney", {}) + line = service_journey.get("line", {}) + + if departure.get("isCancelled"): continue - if not self._lines or line in self._lines: - if "rtTime" in departure: - self._state = departure["rtTime"] + if not self._lines or line.get("shortName") in self._lines: + if "estimatedOtherwisePlannedTime" in departure: + try: + self._state = datetime.fromisoformat( + departure["estimatedOtherwisePlannedTime"] + ).strftime("%H:%M") + except ValueError: + self._state = departure["estimatedOtherwisePlannedTime"] else: - self._state = departure["time"] + self._state = None + + stop_point = departure.get("stopPoint", {}) params = { - ATTR_ACCESSIBILITY: departure.get("accessibility"), - ATTR_DIRECTION: departure.get("direction"), - ATTR_LINE: departure.get("sname"), - ATTR_TRACK: departure.get("track"), + ATTR_ACCESSIBILITY: "wheelChair" + if line.get("isWheelchairAccessible") + else None, + ATTR_DIRECTION: service_journey.get("direction"), + ATTR_LINE: line.get("shortName"), + ATTR_TRACK: stop_point.get("platform"), + ATTR_FROM: stop_point.get("name"), + ATTR_TO: self._heading["station_name"] + if self._heading + else "ANY", + ATTR_DELAY: self._delay.seconds // 60 % 60, } self._attributes = {k: v for k, v in params.items() if v} diff --git a/requirements_all.txt b/requirements_all.txt index 3a85a24589a5..173a7f8894f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2682,7 +2682,7 @@ volvooncall==0.10.3 vsure==2.6.6 # homeassistant.components.vasttrafik -vtjp==0.1.14 +vtjp==0.2.1 # homeassistant.components.vulcan vulcan-api==2.3.0 From a60656bf29bb1db7f71ebfac50c6e94e5e2cf1d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:02 -0400 Subject: [PATCH 027/265] Improve fitbit oauth import robustness (#102833) * Improve fitbit oauth import robustness * Improve sensor tests and remove unnecessary client check * Fix oauth client id/secret config key checks * Add executor for sync call --- homeassistant/components/fitbit/sensor.py | 71 +++++++++++++-------- tests/components/fitbit/test_config_flow.py | 59 +++++++++++++++++ tests/components/fitbit/test_sensor.py | 15 ++++- 3 files changed, 118 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 45b8ea21b0ea..4885c9fa16d0 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -8,6 +8,8 @@ import logging import os from typing import Any, Final, cast +from fitbit import Fitbit +from oauthlib.oauth2.rfc6749.errors import OAuth2Error import voluptuous as vol from homeassistant.components.application_credentials import ( @@ -567,34 +569,51 @@ async def async_setup_platform( if config_file is not None: _LOGGER.debug("Importing existing fitbit.conf application credentials") - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config_file[CONF_CLIENT_ID], config_file[CONF_CLIENT_SECRET] - ), + + # Refresh the token before importing to ensure it is working and not + # expired on first initialization. + authd_client = Fitbit( + config_file[CONF_CLIENT_ID], + config_file[CONF_CLIENT_SECRET], + access_token=config_file[ATTR_ACCESS_TOKEN], + refresh_token=config_file[ATTR_REFRESH_TOKEN], + expires_at=config_file[ATTR_LAST_SAVED_AT], + refresh_cb=lambda x: None, ) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - "auth_implementation": DOMAIN, - CONF_TOKEN: { - ATTR_ACCESS_TOKEN: config_file[ATTR_ACCESS_TOKEN], - ATTR_REFRESH_TOKEN: config_file[ATTR_REFRESH_TOKEN], - "expires_at": config_file[ATTR_LAST_SAVED_AT], - }, - CONF_CLOCK_FORMAT: config[CONF_CLOCK_FORMAT], - CONF_UNIT_SYSTEM: config[CONF_UNIT_SYSTEM], - CONF_MONITORED_RESOURCES: config[CONF_MONITORED_RESOURCES], - }, - ) - translation_key = "deprecated_yaml_import" - if ( - result.get("type") == FlowResultType.ABORT - and result.get("reason") == "cannot_connect" - ): + try: + await hass.async_add_executor_job(authd_client.client.refresh_token) + except OAuth2Error as err: + _LOGGER.debug("Unable to import fitbit OAuth2 credentials: %s", err) translation_key = "deprecated_yaml_import_issue_cannot_connect" + else: + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential( + config_file[CONF_CLIENT_ID], config_file[CONF_CLIENT_SECRET] + ), + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + "auth_implementation": DOMAIN, + CONF_TOKEN: { + ATTR_ACCESS_TOKEN: config_file[ATTR_ACCESS_TOKEN], + ATTR_REFRESH_TOKEN: config_file[ATTR_REFRESH_TOKEN], + "expires_at": config_file[ATTR_LAST_SAVED_AT], + }, + CONF_CLOCK_FORMAT: config[CONF_CLOCK_FORMAT], + CONF_UNIT_SYSTEM: config[CONF_UNIT_SYSTEM], + CONF_MONITORED_RESOURCES: config[CONF_MONITORED_RESOURCES], + }, + ) + translation_key = "deprecated_yaml_import" + if ( + result.get("type") == FlowResultType.ABORT + and result.get("reason") == "cannot_connect" + ): + translation_key = "deprecated_yaml_import_issue_cannot_connect" else: translation_key = "deprecated_yaml_no_import" diff --git a/tests/components/fitbit/test_config_flow.py b/tests/components/fitbit/test_config_flow.py index e6ab39aff59b..152439ec19a9 100644 --- a/tests/components/fitbit/test_config_flow.py +++ b/tests/components/fitbit/test_config_flow.py @@ -209,9 +209,17 @@ async def test_import_fitbit_config( fitbit_config_setup: None, sensor_platform_setup: Callable[[], Awaitable[bool]], issue_registry: ir.IssueRegistry, + requests_mock: Mocker, ) -> None: """Test that platform configuration is imported successfully.""" + requests_mock.register_uri( + "POST", + OAUTH2_TOKEN, + status_code=HTTPStatus.OK, + json=SERVER_ACCESS_TOKEN, + ) + with patch( "homeassistant.components.fitbit.async_setup_entry", return_value=True ) as mock_setup: @@ -256,6 +264,12 @@ async def test_import_fitbit_config_failure_cannot_connect( ) -> None: """Test platform configuration fails to import successfully.""" + requests_mock.register_uri( + "POST", + OAUTH2_TOKEN, + status_code=HTTPStatus.OK, + json=SERVER_ACCESS_TOKEN, + ) requests_mock.register_uri( "GET", PROFILE_API_URL, status_code=HTTPStatus.INTERNAL_SERVER_ERROR ) @@ -273,6 +287,43 @@ async def test_import_fitbit_config_failure_cannot_connect( assert issue.translation_key == "deprecated_yaml_import_issue_cannot_connect" +@pytest.mark.parametrize( + "status_code", + [ + (HTTPStatus.UNAUTHORIZED), + (HTTPStatus.INTERNAL_SERVER_ERROR), + ], +) +async def test_import_fitbit_config_cannot_refresh( + hass: HomeAssistant, + fitbit_config_setup: None, + sensor_platform_setup: Callable[[], Awaitable[bool]], + issue_registry: ir.IssueRegistry, + requests_mock: Mocker, + status_code: HTTPStatus, +) -> None: + """Test platform configuration import fails when refreshing the token.""" + + requests_mock.register_uri( + "POST", + OAUTH2_TOKEN, + status_code=status_code, + json="", + ) + + with patch( + "homeassistant.components.fitbit.async_setup_entry", return_value=True + ) as mock_setup: + await sensor_platform_setup() + + assert len(mock_setup.mock_calls) == 0 + + # Verify an issue is raised that we were unable to import configuration + issue = issue_registry.issues.get((DOMAIN, "deprecated_yaml")) + assert issue + assert issue.translation_key == "deprecated_yaml_import_issue_cannot_connect" + + async def test_import_fitbit_config_already_exists( hass: HomeAssistant, config_entry: MockConfigEntry, @@ -281,9 +332,17 @@ async def test_import_fitbit_config_already_exists( fitbit_config_setup: None, sensor_platform_setup: Callable[[], Awaitable[bool]], issue_registry: ir.IssueRegistry, + requests_mock: Mocker, ) -> None: """Test that platform configuration is not imported if it already exists.""" + requests_mock.register_uri( + "POST", + OAUTH2_TOKEN, + status_code=HTTPStatus.OK, + json=SERVER_ACCESS_TOKEN, + ) + # Verify existing config entry entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 diff --git a/tests/components/fitbit/test_sensor.py b/tests/components/fitbit/test_sensor.py index b54f154d406c..5421a6521257 100644 --- a/tests/components/fitbit/test_sensor.py +++ b/tests/components/fitbit/test_sensor.py @@ -9,7 +9,7 @@ import pytest from requests_mock.mocker import Mocker from syrupy.assertion import SnapshotAssertion -from homeassistant.components.fitbit.const import DOMAIN +from homeassistant.components.fitbit.const import DOMAIN, OAUTH2_TOKEN from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -23,6 +23,7 @@ from homeassistant.util.unit_system import ( from .conftest import ( DEVICES_API_URL, PROFILE_USER_ID, + SERVER_ACCESS_TOKEN, TIMESERIES_API_URL_FORMAT, timeseries_response, ) @@ -55,6 +56,18 @@ def platforms() -> list[str]: return [Platform.SENSOR] +@pytest.fixture(autouse=True) +def mock_token_refresh(requests_mock: Mocker) -> None: + """Test that platform configuration is imported successfully.""" + + requests_mock.register_uri( + "POST", + OAUTH2_TOKEN, + status_code=HTTPStatus.OK, + json=SERVER_ACCESS_TOKEN, + ) + + @pytest.mark.parametrize( ( "monitored_resources", From 4617c16a961c2bb47eeaf62d518c11408cda1ac5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:02 -0400 Subject: [PATCH 028/265] Update aioairzone-cloud to v0.3.1 (#102899) --- homeassistant/components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airzone_cloud/util.py | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index a3c0f5e7dc09..eb9593421226 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_polling", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.3.0"] + "requirements": ["aioairzone-cloud==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 173a7f8894f1..3ece034b571f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.3.0 +aioairzone-cloud==0.3.1 # homeassistant.components.airzone aioairzone==0.6.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 678a2eefcf79..c89d160b4ce1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.3.0 +aioairzone-cloud==0.3.1 # homeassistant.components.airzone aioairzone==0.6.9 diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 412f0df13376..76349d06481d 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -101,6 +101,7 @@ GET_INSTALLATION_MOCK = { API_WS_ID: WS_ID, }, { + API_CONFIG: {}, API_DEVICE_ID: "zone1", API_NAME: "Salon", API_TYPE: API_AZ_ZONE, @@ -111,6 +112,7 @@ GET_INSTALLATION_MOCK = { API_WS_ID: WS_ID, }, { + API_CONFIG: {}, API_DEVICE_ID: "zone2", API_NAME: "Dormitorio", API_TYPE: API_AZ_ZONE, From 3d321c5ca7ceea4a2f81fcfbb1f80ee1c1471302 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:02 -0400 Subject: [PATCH 029/265] Update frontend to 20231027.0 (#102913) --- 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 31f4dc14559e..a47ef38264ee 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20231026.0"] + "requirements": ["home-assistant-frontend==20231027.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e99afb6330b5..5d68cead747e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.74.0 hassil==1.2.5 home-assistant-bluetooth==1.10.4 -home-assistant-frontend==20231026.0 +home-assistant-frontend==20231027.0 home-assistant-intents==2023.10.16 httpx==0.25.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3ece034b571f..2b78d878adf0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1007,7 +1007,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20231026.0 +home-assistant-frontend==20231027.0 # homeassistant.components.conversation home-assistant-intents==2023.10.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c89d160b4ce1..ae7c3ea23cdd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -796,7 +796,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20231026.0 +home-assistant-frontend==20231027.0 # homeassistant.components.conversation home-assistant-intents==2023.10.16 From 29c99f419f70faa36eca0116c72afff215ac4485 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:02 -0400 Subject: [PATCH 030/265] Bump velbusaio to 2023.10.2 (#102919) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 229ee8458c61..3c773e39e33b 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2023.10.1"], + "requirements": ["velbus-aio==2023.10.2"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index 2b78d878adf0..a3d43bb2a9b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2661,7 +2661,7 @@ vallox-websocket-api==3.3.0 vehicle==2.0.0 # homeassistant.components.velbus -velbus-aio==2023.10.1 +velbus-aio==2023.10.2 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae7c3ea23cdd..9a27453bf950 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1979,7 +1979,7 @@ vallox-websocket-api==3.3.0 vehicle==2.0.0 # homeassistant.components.velbus -velbus-aio==2023.10.1 +velbus-aio==2023.10.2 # homeassistant.components.venstar venstarcolortouch==0.19 From bee63ca654164150967fa392a635ae454ab10e3a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:02 -0400 Subject: [PATCH 031/265] Hide mac address from HomeWizard Energy config entry/discovery titles (#102931) --- homeassistant/components/homewizard/config_flow.py | 14 +++++++++----- tests/components/homewizard/test_config_flow.py | 14 +++++++------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 82c808a0f13b..b24b49da9659 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -62,7 +62,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): ) self._abort_if_unique_id_configured(updates=user_input) return self.async_create_entry( - title=f"{device_info.product_name} ({device_info.serial})", + title=f"{device_info.product_name}", data=user_input, ) @@ -121,14 +121,18 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): errors = {"base": ex.error_code} else: return self.async_create_entry( - title=f"{self.discovery.product_name} ({self.discovery.serial})", + title=self.discovery.product_name, data={CONF_IP_ADDRESS: self.discovery.ip}, ) self._set_confirm_only() - self.context["title_placeholders"] = { - "name": f"{self.discovery.product_name} ({self.discovery.serial})" - } + + # We won't be adding mac/serial to the title for devices + # that users generally don't have multiple of. + name = self.discovery.product_name + if self.discovery.product_type not in ["HWE-P1", "HWE-WTR"]: + name = f"{name} ({self.discovery.serial})" + self.context["title_placeholders"] = {"name": name} return self.async_show_form( step_id="discovery_confirm", diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index 7c6fb0bdb0d7..770496b5612c 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -43,7 +43,7 @@ async def test_manual_flow_works( ) assert result["type"] == "create_entry" - assert result["title"] == "P1 meter (aabbccddeeff)" + assert result["title"] == "P1 meter" assert result["data"][CONF_IP_ADDRESS] == "2.2.2.2" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -68,8 +68,8 @@ async def test_discovery_flow_works( properties={ "api_enabled": "1", "path": "/api/v1", - "product_name": "P1 meter", - "product_type": "HWE-P1", + "product_name": "Energy Socket", + "product_type": "HWE-SKT", "serial": "aabbccddeeff", }, ) @@ -109,11 +109,11 @@ async def test_discovery_flow_works( ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "P1 meter (aabbccddeeff)" + assert result["title"] == "Energy Socket" assert result["data"][CONF_IP_ADDRESS] == "192.168.43.183" assert result["result"] - assert result["result"].unique_id == "HWE-P1_aabbccddeeff" + assert result["result"].unique_id == "HWE-SKT_aabbccddeeff" async def test_discovery_flow_during_onboarding( @@ -149,7 +149,7 @@ async def test_discovery_flow_during_onboarding( ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "P1 meter (aabbccddeeff)" + assert result["title"] == "P1 meter" assert result["data"][CONF_IP_ADDRESS] == "192.168.43.183" assert result["result"] @@ -214,7 +214,7 @@ async def test_discovery_flow_during_onboarding_disabled_api( ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "P1 meter (aabbccddeeff)" + assert result["title"] == "P1 meter" assert result["data"][CONF_IP_ADDRESS] == "192.168.43.183" assert result["result"] From 1c3de76b045e23d42518eb1e7e9bbaf00580fb55 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:02 -0400 Subject: [PATCH 032/265] Move HomeWizard Energy identify button to config entity category (#102932) --- homeassistant/components/homewizard/button.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homewizard/button.py b/homeassistant/components/homewizard/button.py index 96fe1b157f86..19ffb1d60424 100644 --- a/homeassistant/components/homewizard/button.py +++ b/homeassistant/components/homewizard/button.py @@ -24,7 +24,7 @@ async def async_setup_entry( class HomeWizardIdentifyButton(HomeWizardEntity, ButtonEntity): """Representation of a identify button.""" - _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_entity_category = EntityCategory.CONFIG _attr_device_class = ButtonDeviceClass.IDENTIFY def __init__( From 974c34e2b63c9d2380f9b11799fdb926afce3990 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:02 -0400 Subject: [PATCH 033/265] Small base entity cleanup for HomeWizard Energy entities (#102933) --- homeassistant/components/homewizard/entity.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homewizard/entity.py b/homeassistant/components/homewizard/entity.py index 51dbe9fcad32..61bf20dbbc49 100644 --- a/homeassistant/components/homewizard/entity.py +++ b/homeassistant/components/homewizard/entity.py @@ -18,17 +18,13 @@ class HomeWizardEntity(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator]): """Initialize the HomeWizard entity.""" super().__init__(coordinator=coordinator) self._attr_device_info = DeviceInfo( - name=coordinator.entry.title, manufacturer="HomeWizard", sw_version=coordinator.data.device.firmware_version, model=coordinator.data.device.product_type, ) - if coordinator.data.device.serial is not None: + if (serial_number := coordinator.data.device.serial) is not None: self._attr_device_info[ATTR_CONNECTIONS] = { - (CONNECTION_NETWORK_MAC, coordinator.data.device.serial) - } - - self._attr_device_info[ATTR_IDENTIFIERS] = { - (DOMAIN, coordinator.data.device.serial) + (CONNECTION_NETWORK_MAC, serial_number) } + self._attr_device_info[ATTR_IDENTIFIERS] = {(DOMAIN, serial_number)} From f9f010643a630f73f44f4cc02c8b4f4e3cc1c853 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:02 -0400 Subject: [PATCH 034/265] Handle/extend number entity availability property in HomeWizard Energy (#102934) --- homeassistant/components/homewizard/number.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homewizard/number.py b/homeassistant/components/homewizard/number.py index d51d180edb18..07f6bb9b55ff 100644 --- a/homeassistant/components/homewizard/number.py +++ b/homeassistant/components/homewizard/number.py @@ -47,13 +47,17 @@ class HWEnergyNumberEntity(HomeWizardEntity, NumberEntity): await self.coordinator.api.state_set(brightness=int(value * (255 / 100))) await self.coordinator.async_refresh() + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.coordinator.data.state is not None + @property def native_value(self) -> float | None: """Return the current value.""" if ( - self.coordinator.data.state is None - or self.coordinator.data.state.brightness is None + not self.coordinator.data.state + or (brightness := self.coordinator.data.state.brightness) is None ): return None - brightness: float = self.coordinator.data.state.brightness return round(brightness * (100 / 255)) From 07e4e1379ad14e295a92bc1b4839a35d32c64b48 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:02 -0400 Subject: [PATCH 035/265] Improve diagnostic handling in HomeWizard Energy (#102935) --- .../components/homewizard/diagnostics.py | 33 +++++---- .../snapshots/test_diagnostics.ambr | 71 +++++++++++++++++++ .../components/homewizard/test_diagnostics.py | 70 ++---------------- 3 files changed, 97 insertions(+), 77 deletions(-) create mode 100644 tests/components/homewizard/snapshots/test_diagnostics.ambr diff --git a/homeassistant/components/homewizard/diagnostics.py b/homeassistant/components/homewizard/diagnostics.py index a8f89b67ce9e..b8103f7a4cb6 100644 --- a/homeassistant/components/homewizard/diagnostics.py +++ b/homeassistant/components/homewizard/diagnostics.py @@ -28,18 +28,23 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - meter_data = { - "device": asdict(coordinator.data.device), - "data": asdict(coordinator.data.data), - "state": asdict(coordinator.data.state) - if coordinator.data.state is not None - else None, - "system": asdict(coordinator.data.system) - if coordinator.data.system is not None - else None, - } + state: dict[str, Any] | None = None + if coordinator.data.state: + state = asdict(coordinator.data.state) - return { - "entry": async_redact_data(entry.data, TO_REDACT), - "data": async_redact_data(meter_data, TO_REDACT), - } + system: dict[str, Any] | None = None + if coordinator.data.system: + system = asdict(coordinator.data.system) + + return async_redact_data( + { + "entry": async_redact_data(entry.data, TO_REDACT), + "data": { + "device": asdict(coordinator.data.device), + "data": asdict(coordinator.data.data), + "state": state, + "system": system, + }, + }, + TO_REDACT, + ) diff --git a/tests/components/homewizard/snapshots/test_diagnostics.ambr b/tests/components/homewizard/snapshots/test_diagnostics.ambr new file mode 100644 index 000000000000..5e1025a8d318 --- /dev/null +++ b/tests/components/homewizard/snapshots/test_diagnostics.ambr @@ -0,0 +1,71 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'data': dict({ + 'data': dict({ + 'active_current_l1_a': -4, + 'active_current_l2_a': 2, + 'active_current_l3_a': 0, + 'active_frequency_hz': 50, + 'active_liter_lpm': 12.345, + 'active_power_average_w': 123.0, + 'active_power_l1_w': -123, + 'active_power_l2_w': 456, + 'active_power_l3_w': 123.456, + 'active_power_w': -123, + 'active_tariff': 2, + 'active_voltage_l1_v': 230.111, + 'active_voltage_l2_v': 230.222, + 'active_voltage_l3_v': 230.333, + 'any_power_fail_count': 4, + 'external_devices': None, + 'gas_timestamp': '2021-03-14T11:22:33', + 'gas_unique_id': '**REDACTED**', + 'long_power_fail_count': 5, + 'meter_model': 'ISKRA 2M550T-101', + 'monthly_power_peak_timestamp': '2023-01-01T08:00:10', + 'monthly_power_peak_w': 1111.0, + 'smr_version': 50, + 'total_gas_m3': 1122.333, + 'total_liter_m3': 1234.567, + 'total_power_export_kwh': 13086.777, + 'total_power_export_t1_kwh': 4321.333, + 'total_power_export_t2_kwh': 8765.444, + 'total_power_export_t3_kwh': None, + 'total_power_export_t4_kwh': None, + 'total_power_import_kwh': 13779.338, + 'total_power_import_t1_kwh': 10830.511, + 'total_power_import_t2_kwh': 2948.827, + 'total_power_import_t3_kwh': None, + 'total_power_import_t4_kwh': None, + 'unique_meter_id': '**REDACTED**', + 'voltage_sag_l1_count': 1, + 'voltage_sag_l2_count': 2, + 'voltage_sag_l3_count': 3, + 'voltage_swell_l1_count': 4, + 'voltage_swell_l2_count': 5, + 'voltage_swell_l3_count': 6, + 'wifi_ssid': '**REDACTED**', + 'wifi_strength': 100, + }), + 'device': dict({ + 'api_version': 'v1', + 'firmware_version': '2.11', + 'product_name': 'P1 Meter', + 'product_type': 'HWE-SKT', + 'serial': '**REDACTED**', + }), + 'state': dict({ + 'brightness': 255, + 'power_on': True, + 'switch_lock': False, + }), + 'system': dict({ + 'cloud_enabled': True, + }), + }), + 'entry': dict({ + 'ip_address': '**REDACTED**', + }), + }) +# --- diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index 64e8b0c6dfd4..9e9797439b33 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -1,6 +1,7 @@ """Tests for diagnostics data.""" -from homeassistant.components.diagnostics import REDACTED +from syrupy.assertion import SnapshotAssertion + from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -12,67 +13,10 @@ async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - assert await get_diagnostics_for_config_entry( - hass, hass_client, init_integration - ) == { - "entry": {"ip_address": REDACTED}, - "data": { - "device": { - "product_name": "P1 Meter", - "product_type": "HWE-SKT", - "serial": REDACTED, - "api_version": "v1", - "firmware_version": "2.11", - }, - "data": { - "wifi_ssid": REDACTED, - "wifi_strength": 100, - "smr_version": 50, - "meter_model": "ISKRA 2M550T-101", - "unique_meter_id": REDACTED, - "active_tariff": 2, - "total_power_import_kwh": 13779.338, - "total_power_import_t1_kwh": 10830.511, - "total_power_import_t2_kwh": 2948.827, - "total_power_import_t3_kwh": None, - "total_power_import_t4_kwh": None, - "total_power_export_kwh": 13086.777, - "total_power_export_t1_kwh": 4321.333, - "total_power_export_t2_kwh": 8765.444, - "total_power_export_t3_kwh": None, - "total_power_export_t4_kwh": None, - "active_power_w": -123, - "active_power_l1_w": -123, - "active_power_l2_w": 456, - "active_power_l3_w": 123.456, - "active_voltage_l1_v": 230.111, - "active_voltage_l2_v": 230.222, - "active_voltage_l3_v": 230.333, - "active_current_l1_a": -4, - "active_current_l2_a": 2, - "active_current_l3_a": 0, - "active_frequency_hz": 50, - "voltage_sag_l1_count": 1, - "voltage_sag_l2_count": 2, - "voltage_sag_l3_count": 3, - "voltage_swell_l1_count": 4, - "voltage_swell_l2_count": 5, - "voltage_swell_l3_count": 6, - "any_power_fail_count": 4, - "long_power_fail_count": 5, - "active_power_average_w": 123.0, - "monthly_power_peak_w": 1111.0, - "monthly_power_peak_timestamp": "2023-01-01T08:00:10", - "total_gas_m3": 1122.333, - "gas_timestamp": "2021-03-14T11:22:33", - "gas_unique_id": REDACTED, - "active_liter_lpm": 12.345, - "total_liter_m3": 1234.567, - "external_devices": None, - }, - "state": {"power_on": True, "switch_lock": False, "brightness": 255}, - "system": {"cloud_enabled": True}, - }, - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) From bcddf52364d08d359ba033f986c1468c6871b920 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:03 -0400 Subject: [PATCH 036/265] Update xknxproject to 3.4.0 (#102946) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index b5c98c7203ab..a233ca387053 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -12,7 +12,7 @@ "quality_scale": "platinum", "requirements": [ "xknx==2.11.2", - "xknxproject==3.3.0", + "xknxproject==3.4.0", "knx-frontend==2023.6.23.191712" ] } diff --git a/requirements_all.txt b/requirements_all.txt index a3d43bb2a9b5..b69dbf4c2580 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2740,7 +2740,7 @@ xiaomi-ble==0.21.1 xknx==2.11.2 # homeassistant.components.knx -xknxproject==3.3.0 +xknxproject==3.4.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a27453bf950..5ec473c9ef14 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2043,7 +2043,7 @@ xiaomi-ble==0.21.1 xknx==2.11.2 # homeassistant.components.knx -xknxproject==3.3.0 +xknxproject==3.4.0 # homeassistant.components.bluesound # homeassistant.components.fritz From 85d999b020de586ad07eb8a0c94e9b78ac099512 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:03 -0400 Subject: [PATCH 037/265] Add gas device class to dsmr_reader sensor (#102953) DSMR reader integration - can't configure gas meter in energy dashboard posible due to missing device_class Fixes #102367 --- homeassistant/components/dsmr_reader/definitions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 33bba375fd3f..d89e30311e92 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -141,6 +141,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( translation_key="gas_meter_usage", entity_registry_enabled_default=False, icon="mdi:fire", + device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -209,6 +210,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/currently_delivered", translation_key="current_gas_usage", + device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, state_class=SensorStateClass.MEASUREMENT, ), @@ -283,6 +285,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/day-consumption/gas", translation_key="daily_gas_usage", icon="mdi:counter", + device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, ), DSMRReaderSensorEntityDescription( @@ -460,6 +463,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/current-month/gas", translation_key="current_month_gas_usage", icon="mdi:counter", + device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, ), DSMRReaderSensorEntityDescription( @@ -538,6 +542,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( key="dsmr/current-year/gas", translation_key="current_year_gas_usage", icon="mdi:counter", + device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, ), DSMRReaderSensorEntityDescription( From 9c9f1ea685535606be244ea2313169146e2fbcce Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:03 -0400 Subject: [PATCH 038/265] Fix error message strings for Todoist configuration flow (#102968) * Fix error message strings for Todoist configuration flow * Update error code in test --- homeassistant/components/todoist/config_flow.py | 2 +- homeassistant/components/todoist/strings.json | 6 ++++-- tests/components/todoist/test_config_flow.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/todoist/config_flow.py b/homeassistant/components/todoist/config_flow.py index 6098df40ea04..b8c79210dfbf 100644 --- a/homeassistant/components/todoist/config_flow.py +++ b/homeassistant/components/todoist/config_flow.py @@ -44,7 +44,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await api.get_tasks() except HTTPError as err: if err.response.status_code == HTTPStatus.UNAUTHORIZED: - errors["base"] = "invalid_access_token" + errors["base"] = "invalid_api_key" else: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/todoist/strings.json b/homeassistant/components/todoist/strings.json index 68c2305d0736..442114eb1189 100644 --- a/homeassistant/components/todoist/strings.json +++ b/homeassistant/components/todoist/strings.json @@ -9,10 +9,12 @@ } }, "error": { - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/tests/components/todoist/test_config_flow.py b/tests/components/todoist/test_config_flow.py index 4175902da313..141f12269de0 100644 --- a/tests/components/todoist/test_config_flow.py +++ b/tests/components/todoist/test_config_flow.py @@ -69,7 +69,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) assert result2.get("type") == FlowResultType.FORM - assert result2.get("errors") == {"base": "invalid_access_token"} + assert result2.get("errors") == {"base": "invalid_api_key"} @pytest.mark.parametrize("todoist_api_status", [HTTPStatus.INTERNAL_SERVER_ERROR]) From 9c8a4bb4eb3aa9cc4058e039b56ce00fdab50f2f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:03 -0400 Subject: [PATCH 039/265] Fix proximity zone handling (#102971) * fix proximity zone * fix test --- homeassistant/components/proximity/__init__.py | 12 ++++++------ tests/components/proximity/test_init.py | 8 ++++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index a45204351619..07b5f931f791 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -70,22 +70,22 @@ def async_setup_proximity_component( ignored_zones: list[str] = config[CONF_IGNORED_ZONES] proximity_devices: list[str] = config[CONF_DEVICES] tolerance: int = config[CONF_TOLERANCE] - proximity_zone = name + proximity_zone = config[CONF_ZONE] unit_of_measurement: str = config.get( CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit ) - zone_id = f"zone.{config[CONF_ZONE]}" + zone_friendly_name = name proximity = Proximity( hass, - proximity_zone, + zone_friendly_name, DEFAULT_DIST_TO_ZONE, DEFAULT_DIR_OF_TRAVEL, DEFAULT_NEAREST, ignored_zones, proximity_devices, tolerance, - zone_id, + proximity_zone, unit_of_measurement, ) proximity.entity_id = f"{DOMAIN}.{proximity_zone}" @@ -171,7 +171,7 @@ class Proximity(Entity): devices_to_calculate = False devices_in_zone = "" - zone_state = self.hass.states.get(self.proximity_zone) + zone_state = self.hass.states.get(f"zone.{self.proximity_zone}") proximity_latitude = ( zone_state.attributes.get(ATTR_LATITUDE) if zone_state else None ) @@ -189,7 +189,7 @@ class Proximity(Entity): devices_to_calculate = True # Check the location of all devices. - if (device_state.state).lower() == (self.friendly_name).lower(): + if (device_state.state).lower() == (self.proximity_zone).lower(): device_friendly = device_state.name if devices_in_zone != "": devices_in_zone = f"{devices_in_zone}, " diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index 34f87b5c2615..0ec8765e6047 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -13,7 +13,11 @@ async def test_proximities(hass: HomeAssistant) -> None: "devices": ["device_tracker.test1", "device_tracker.test2"], "tolerance": "1", }, - "work": {"devices": ["device_tracker.test1"], "tolerance": "1"}, + "work": { + "devices": ["device_tracker.test1"], + "tolerance": "1", + "zone": "work", + }, } } @@ -42,7 +46,7 @@ async def test_proximities_setup(hass: HomeAssistant) -> None: "devices": ["device_tracker.test1", "device_tracker.test2"], "tolerance": "1", }, - "work": {"tolerance": "1"}, + "work": {"tolerance": "1", "zone": "work"}, } } From eef318f63cd74150b3e462d4af8814478126049c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Oct 2023 23:29:03 -0400 Subject: [PATCH 040/265] Bumped version to 2023.11.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 39f957ef77c2..cbc216871109 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 1303d1cd7fe3..92c11a19a12a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.11.0b1" +version = "2023.11.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 59d2bce369687246a51bde8d6033ed9ba2334a2e Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Mon, 30 Oct 2023 10:29:40 +0100 Subject: [PATCH 041/265] Enable dry mode for Tado AC's V3 (#99568) --- homeassistant/components/tado/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 9366a18b6fef..d6ae50c33c1d 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -119,7 +119,7 @@ TADO_MODES_TO_HA_CURRENT_HVAC_ACTION = { } # These modes will not allow a temp to be set -TADO_MODES_WITH_NO_TEMP_SETTING = [CONST_MODE_AUTO, CONST_MODE_DRY, CONST_MODE_FAN] +TADO_MODES_WITH_NO_TEMP_SETTING = [CONST_MODE_AUTO, CONST_MODE_FAN] # # HVAC_MODE_HEAT_COOL is mapped to CONST_MODE_AUTO # This lets tado decide on a temp From a741bc9951fc25b4b5b9fa6f29bdc35fdef6b6f9 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 30 Oct 2023 10:16:41 -0400 Subject: [PATCH 042/265] Add retry before unavailable to Honeywell (#101702) Co-authored-by: Robert Resch --- homeassistant/components/honeywell/climate.py | 13 ++++++++-- homeassistant/components/honeywell/const.py | 1 + tests/components/honeywell/test_climate.py | 26 +++++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 63d05135d5df..ab23c878c157 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -38,6 +38,7 @@ from .const import ( CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE, DOMAIN, + RETRY, ) ATTR_FAN_ACTION = "fan_action" @@ -155,6 +156,7 @@ class HoneywellUSThermostat(ClimateEntity): self._cool_away_temp = cool_away_temp self._heat_away_temp = heat_away_temp self._away = False + self._retry = 0 self._attr_unique_id = device.deviceid @@ -483,21 +485,28 @@ class HoneywellUSThermostat(ClimateEntity): try: await self._device.refresh() self._attr_available = True + self._retry = 0 + except UnauthorizedError: try: await self._data.client.login() await self._device.refresh() self._attr_available = True + self._retry = 0 except ( SomeComfortError, ClientConnectionError, asyncio.TimeoutError, ): - self._attr_available = False + self._retry += 1 + if self._retry > RETRY: + self._attr_available = False except (ClientConnectionError, asyncio.TimeoutError): - self._attr_available = False + self._retry += 1 + if self._retry > RETRY: + self._attr_available = False except UnexpectedResponse: pass diff --git a/homeassistant/components/honeywell/const.py b/homeassistant/components/honeywell/const.py index d5153a69f65a..32846563c447 100644 --- a/homeassistant/components/honeywell/const.py +++ b/homeassistant/components/honeywell/const.py @@ -10,3 +10,4 @@ DEFAULT_HEAT_AWAY_TEMPERATURE = 61 CONF_DEV_ID = "thermostat" CONF_LOC_ID = "location" _LOGGER = logging.getLogger(__name__) +RETRY = 3 diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index 7bd76cb8522f..53cb70475c98 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -28,7 +28,7 @@ from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, HVACMode, ) -from homeassistant.components.honeywell.climate import SCAN_INTERVAL +from homeassistant.components.honeywell.climate import RETRY, SCAN_INTERVAL from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -1083,6 +1083,17 @@ async def test_async_update_errors( state = hass.states.get(entity_id) assert state.state == "off" + # Due to server instability, only mark entity unavailable after RETRY update attempts + for _ in range(RETRY): + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == "off" + async_fire_time_changed( hass, utcnow() + SCAN_INTERVAL, @@ -1126,7 +1137,6 @@ async def test_async_update_errors( state = hass.states.get(entity_id) assert state.state == "off" - # "reload integration" test device.refresh.side_effect = aiosomecomfort.SomeComfortError client.login.side_effect = aiosomecomfort.AuthError async_fire_time_changed( @@ -1139,6 +1149,18 @@ async def test_async_update_errors( assert state.state == "off" device.refresh.side_effect = ClientConnectionError + + # Due to server instability, only mark entity unavailable after RETRY update attempts + for _ in range(RETRY): + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == "off" + async_fire_time_changed( hass, utcnow() + SCAN_INTERVAL, From 20409d0124243aefd86990c999c4e2a15333158f Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 30 Oct 2023 10:18:59 -0400 Subject: [PATCH 043/265] Make Hydrawise initialize data immediately (#101936) --- .../components/hydrawise/__init__.py | 26 +++----------- .../components/hydrawise/binary_sensor.py | 11 +++--- homeassistant/components/hydrawise/entity.py | 12 +++++++ homeassistant/components/hydrawise/sensor.py | 12 +++---- homeassistant/components/hydrawise/switch.py | 10 ++---- .../hydrawise/test_binary_sensor.py | 5 --- tests/components/hydrawise/test_init.py | 34 ++++++++----------- tests/components/hydrawise/test_sensor.py | 12 ++----- tests/components/hydrawise/test_switch.py | 9 +---- 9 files changed, 45 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index bc3c62cfb9f7..ddff1954eb39 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -2,7 +2,6 @@ from pydrawise import legacy -from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -13,11 +12,10 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, LOGGER, SCAN_INTERVAL +from .const import DOMAIN, SCAN_INTERVAL from .coordinator import HydrawiseDataUpdateCoordinator CONFIG_SCHEMA = vol.Schema( @@ -53,24 +51,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Hydrawise from a config entry.""" access_token = config_entry.data[CONF_API_KEY] - try: - hydrawise = await hass.async_add_executor_job( - legacy.LegacyHydrawise, access_token - ) - except (ConnectTimeout, HTTPError) as ex: - LOGGER.error("Unable to connect to Hydrawise cloud service: %s", str(ex)) - raise ConfigEntryNotReady( - f"Unable to connect to Hydrawise cloud service: {ex}" - ) from ex - - hass.data.setdefault(DOMAIN, {})[ - config_entry.entry_id - ] = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL) - if not hydrawise.controller_info or not hydrawise.controller_status: - raise ConfigEntryNotReady("Hydrawise data not loaded") - - # NOTE: We don't need to call async_config_entry_first_refresh() because - # data is fetched when the Hydrawiser object is instantiated. + hydrawise = legacy.LegacyHydrawise(access_token, load_on_init=False) + coordinator = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 30096a9bf978..1953e4136720 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -12,12 +12,12 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN, LOGGER +from .const import DOMAIN from .coordinator import HydrawiseDataUpdateCoordinator from .entity import HydrawiseEntity @@ -95,13 +95,10 @@ async def async_setup_entry( class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): """A sensor implementation for Hydrawise device.""" - @callback - def _handle_coordinator_update(self) -> None: - """Get the latest data and updates the state.""" - LOGGER.debug("Updating Hydrawise binary sensor: %s", self.name) + def _update_attrs(self) -> None: + """Update state attributes.""" if self.entity_description.key == "status": self._attr_is_on = self.coordinator.last_update_success elif self.entity_description.key == "is_watering": relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]] self._attr_is_on = relay_data["timestr"] == "Now" - super()._handle_coordinator_update() diff --git a/homeassistant/components/hydrawise/entity.py b/homeassistant/components/hydrawise/entity.py index c3f295e1c4dd..38fde3226735 100644 --- a/homeassistant/components/hydrawise/entity.py +++ b/homeassistant/components/hydrawise/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -36,3 +37,14 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]): name=data["name"], manufacturer=MANUFACTURER, ) + self._update_attrs() + + def _update_attrs(self) -> None: + """Update state attributes.""" + return # pragma: no cover + + @callback + def _handle_coordinator_update(self) -> None: + """Get the latest data and updates the state.""" + self._update_attrs() + super()._handle_coordinator_update() diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index ef98ce99bfb3..369e952c1be4 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -11,13 +11,13 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from .const import DOMAIN, LOGGER +from .const import DOMAIN from .coordinator import HydrawiseDataUpdateCoordinator from .entity import HydrawiseEntity @@ -82,10 +82,8 @@ async def async_setup_entry( class HydrawiseSensor(HydrawiseEntity, SensorEntity): """A sensor implementation for Hydrawise device.""" - @callback - def _handle_coordinator_update(self) -> None: - """Get the latest data and updates the states.""" - LOGGER.debug("Updating Hydrawise sensor: %s", self.name) + def _update_attrs(self) -> None: + """Update state attributes.""" relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]] if self.entity_description.key == "watering_time": if relay_data["timestr"] == "Now": @@ -94,8 +92,6 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): self._attr_native_value = 0 else: # _sensor_type == 'next_cycle' next_cycle = min(relay_data["time"], TWO_YEAR_SECONDS) - LOGGER.debug("New cycle time: %s", next_cycle) self._attr_native_value = dt_util.utc_from_timestamp( dt_util.as_timestamp(dt_util.now()) + next_cycle ) - super()._handle_coordinator_update() diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index d1ea0233145f..caaefd7aa265 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -13,7 +13,7 @@ from homeassistant.components.switch import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -23,7 +23,6 @@ from .const import ( CONF_WATERING_TIME, DEFAULT_WATERING_TIME, DOMAIN, - LOGGER, ) from .coordinator import HydrawiseDataUpdateCoordinator from .entity import HydrawiseEntity @@ -124,14 +123,11 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity): elif self.entity_description.key == "auto_watering": self.coordinator.api.suspend_zone(365, zone_number) - @callback - def _handle_coordinator_update(self) -> None: - """Update device state.""" + def _update_attrs(self) -> None: + """Update state attributes.""" zone_number = self.data["relay"] - LOGGER.debug("Updating Hydrawise switch: %s", self.name) timestr = self.coordinator.api.relays_by_zone_number[zone_number]["timestr"] if self.entity_description.key == "manual_watering": self._attr_is_on = timestr == "Now" elif self.entity_description.key == "auto_watering": self._attr_is_on = timestr not in {"", "Now"} - super()._handle_coordinator_update() diff --git a/tests/components/hydrawise/test_binary_sensor.py b/tests/components/hydrawise/test_binary_sensor.py index ab88c5fb750c..c60f4392f1e2 100644 --- a/tests/components/hydrawise/test_binary_sensor.py +++ b/tests/components/hydrawise/test_binary_sensor.py @@ -17,11 +17,6 @@ async def test_states( freezer: FrozenDateTimeFactory, ) -> None: """Test binary_sensor states.""" - # Make the coordinator refresh data. - freezer.tick(SCAN_INTERVAL + timedelta(seconds=30)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - connectivity = hass.states.get("binary_sensor.home_controller_connectivity") assert connectivity is not None assert connectivity.state == "on" diff --git a/tests/components/hydrawise/test_init.py b/tests/components/hydrawise/test_init.py index 87c158ec0b99..79cea94d479b 100644 --- a/tests/components/hydrawise/test_init.py +++ b/tests/components/hydrawise/test_init.py @@ -1,6 +1,6 @@ """Tests for the Hydrawise integration.""" -from unittest.mock import Mock, patch +from unittest.mock import Mock from requests.exceptions import HTTPError @@ -15,6 +15,7 @@ from tests.common import MockConfigEntry async def test_setup_import_success(hass: HomeAssistant, mock_pydrawise: Mock) -> None: """Test that setup with a YAML config triggers an import and warning.""" + mock_pydrawise.update_controller_info.return_value = True mock_pydrawise.customer_id = 12345 mock_pydrawise.status = "unknown" config = {"hydrawise": {CONF_ACCESS_TOKEN: "_access-token_"}} @@ -29,29 +30,22 @@ async def test_setup_import_success(hass: HomeAssistant, mock_pydrawise: Mock) - async def test_connect_retry( - hass: HomeAssistant, mock_config_entry: MockConfigEntry + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pydrawise: Mock ) -> None: """Test that a connection error triggers a retry.""" - with patch("pydrawise.legacy.LegacyHydrawise") as mock_api: - mock_api.side_effect = HTTPError - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - mock_api.assert_called_once() - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + mock_pydrawise.update_controller_info.side_effect = HTTPError + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_no_data( - hass: HomeAssistant, mock_config_entry: MockConfigEntry + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pydrawise: Mock ) -> None: """Test that no data from the API triggers a retry.""" - with patch("pydrawise.legacy.LegacyHydrawise") as mock_api: - mock_api.return_value.controller_info = {} - mock_api.return_value.controller_status = None - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - mock_api.assert_called_once() - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + mock_pydrawise.update_controller_info.return_value = False + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/hydrawise/test_sensor.py b/tests/components/hydrawise/test_sensor.py index b7c60f333f4d..c6d3fecab65f 100644 --- a/tests/components/hydrawise/test_sensor.py +++ b/tests/components/hydrawise/test_sensor.py @@ -1,14 +1,11 @@ """Test Hydrawise sensor.""" -from datetime import timedelta - from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.hydrawise.const import SCAN_INTERVAL from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry @pytest.mark.freeze_time("2023-10-01 00:00:00+00:00") @@ -18,11 +15,6 @@ async def test_states( freezer: FrozenDateTimeFactory, ) -> None: """Test sensor states.""" - # Make the coordinator refresh data. - freezer.tick(SCAN_INTERVAL + timedelta(seconds=30)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - watering_time1 = hass.states.get("sensor.zone_one_watering_time") assert watering_time1 is not None assert watering_time1.state == "0" @@ -33,4 +25,4 @@ async def test_states( next_cycle = hass.states.get("sensor.zone_one_next_cycle") assert next_cycle is not None - assert next_cycle.state == "2023-10-04T19:52:27+00:00" + assert next_cycle.state == "2023-10-04T19:49:57+00:00" diff --git a/tests/components/hydrawise/test_switch.py b/tests/components/hydrawise/test_switch.py index 615a336ee5fa..39d789f4cf97 100644 --- a/tests/components/hydrawise/test_switch.py +++ b/tests/components/hydrawise/test_switch.py @@ -1,16 +1,14 @@ """Test Hydrawise switch.""" -from datetime import timedelta from unittest.mock import Mock from freezegun.api import FrozenDateTimeFactory -from homeassistant.components.hydrawise.const import SCAN_INTERVAL from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry async def test_states( @@ -19,11 +17,6 @@ async def test_states( freezer: FrozenDateTimeFactory, ) -> None: """Test switch states.""" - # Make the coordinator refresh data. - freezer.tick(SCAN_INTERVAL + timedelta(seconds=30)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - watering1 = hass.states.get("switch.zone_one_manual_watering") assert watering1 is not None assert watering1.state == "off" From 12482216f614ea79035e2d16d5765d57f96ae851 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 30 Oct 2023 07:36:34 -0400 Subject: [PATCH 044/265] Fix Google Mail expired authorization (#102735) * Fix Google Mail expired authorization * add test * raise HomeAssistantError * handle in api module * uno mas --- .../components/google_mail/__init__.py | 14 +------- homeassistant/components/google_mail/api.py | 35 +++++++++++++++---- tests/components/google_mail/test_init.py | 7 +++- tests/components/google_mail/test_services.py | 15 ++++++-- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/google_mail/__init__.py b/homeassistant/components/google_mail/__init__.py index 15c4192ccf5c..96639e4a547d 100644 --- a/homeassistant/components/google_mail/__init__.py +++ b/homeassistant/components/google_mail/__init__.py @@ -1,12 +1,9 @@ """Support for Google Mail.""" from __future__ import annotations -from aiohttp.client_exceptions import ClientError, ClientResponseError - from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_NAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, @@ -35,16 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) auth = AsyncConfigEntryAuth(session) - try: - await auth.check_and_refresh_token() - except ClientResponseError as err: - if 400 <= err.status < 500: - raise ConfigEntryAuthFailed( - "OAuth session is not valid, reauth required" - ) from err - raise ConfigEntryNotReady from err - except ClientError as err: - raise ConfigEntryNotReady from err + await auth.check_and_refresh_token() hass.data[DOMAIN][entry.entry_id] = auth hass.async_create_task( diff --git a/homeassistant/components/google_mail/api.py b/homeassistant/components/google_mail/api.py index ffa33deae14b..10b2fec7467c 100644 --- a/homeassistant/components/google_mail/api.py +++ b/homeassistant/components/google_mail/api.py @@ -1,9 +1,16 @@ """API for Google Mail bound to Home Assistant OAuth.""" +from aiohttp.client_exceptions import ClientError, ClientResponseError from google.auth.exceptions import RefreshError from google.oauth2.credentials import Credentials from googleapiclient.discovery import Resource, build +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import config_entry_oauth2_flow @@ -24,14 +31,30 @@ class AsyncConfigEntryAuth: async def check_and_refresh_token(self) -> str: """Check the token.""" - await self.oauth_session.async_ensure_token_valid() + try: + await self.oauth_session.async_ensure_token_valid() + except (RefreshError, ClientResponseError, ClientError) as ex: + if ( + self.oauth_session.config_entry.state + is ConfigEntryState.SETUP_IN_PROGRESS + ): + if isinstance(ex, ClientResponseError) and 400 <= ex.status < 500: + raise ConfigEntryAuthFailed( + "OAuth session is not valid, reauth required" + ) from ex + raise ConfigEntryNotReady from ex + if ( + isinstance(ex, RefreshError) + or hasattr(ex, "status") + and ex.status == 400 + ): + self.oauth_session.config_entry.async_start_reauth( + self.oauth_session.hass + ) + raise HomeAssistantError(ex) from ex return self.access_token async def get_resource(self) -> Resource: """Get current resource.""" - try: - credentials = Credentials(await self.check_and_refresh_token()) - except RefreshError as ex: - self.oauth_session.config_entry.async_start_reauth(self.oauth_session.hass) - raise ex + credentials = Credentials(await self.check_and_refresh_token()) return build("gmail", "v1", credentials=credentials) diff --git a/tests/components/google_mail/test_init.py b/tests/components/google_mail/test_init.py index a069ae0807b1..4882fd10e80b 100644 --- a/tests/components/google_mail/test_init.py +++ b/tests/components/google_mail/test_init.py @@ -73,8 +73,13 @@ async def test_expired_token_refresh_success( http.HTTPStatus.INTERNAL_SERVER_ERROR, ConfigEntryState.SETUP_RETRY, ), + ( + time.time() - 3600, + http.HTTPStatus.BAD_REQUEST, + ConfigEntryState.SETUP_ERROR, + ), ], - ids=["failure_requires_reauth", "transient_failure"], + ids=["failure_requires_reauth", "transient_failure", "revoked_auth"], ) async def test_expired_token_refresh_failure( hass: HomeAssistant, diff --git a/tests/components/google_mail/test_services.py b/tests/components/google_mail/test_services.py index b9fefa805e6a..caa0d887dece 100644 --- a/tests/components/google_mail/test_services.py +++ b/tests/components/google_mail/test_services.py @@ -1,12 +1,14 @@ """Services tests for the Google Mail integration.""" from unittest.mock import patch +from aiohttp.client_exceptions import ClientResponseError from google.auth.exceptions import RefreshError import pytest from homeassistant import config_entries from homeassistant.components.google_mail import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .conftest import BUILD, SENSOR, TOKEN, ComponentSetup @@ -57,13 +59,22 @@ async def test_set_vacation( assert len(mock_client.mock_calls) == 5 +@pytest.mark.parametrize( + ("side_effect"), + ( + (RefreshError,), + (ClientResponseError("", (), status=400),), + ), +) async def test_reauth_trigger( - hass: HomeAssistant, setup_integration: ComponentSetup + hass: HomeAssistant, + setup_integration: ComponentSetup, + side_effect, ) -> None: """Test reauth is triggered after a refresh error during service call.""" await setup_integration() - with patch(TOKEN, side_effect=RefreshError), pytest.raises(RefreshError): + with patch(TOKEN, side_effect=side_effect), pytest.raises(HomeAssistantError): await hass.services.async_call( DOMAIN, "set_vacation", From cc3ae9e10304e217e5c48ce22f0d2860b2e8f057 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 29 Oct 2023 09:17:57 +0100 Subject: [PATCH 045/265] Correct total state_class of huisbaasje sensors (#102945) * Change all cumulative-interval sensors to TOTAL --- homeassistant/components/huisbaasje/sensor.py | 16 ++++----- tests/components/huisbaasje/test_sensor.py | 34 +++++-------------- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 6e3f5eaee333..b82b2b34a4bc 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -146,7 +146,7 @@ SENSORS_INFO = [ translation_key="energy_today", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, key=SOURCE_TYPE_ELECTRICITY, sensor_type=SENSOR_TYPE_THIS_DAY, precision=1, @@ -156,7 +156,7 @@ SENSORS_INFO = [ translation_key="energy_week", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, key=SOURCE_TYPE_ELECTRICITY, sensor_type=SENSOR_TYPE_THIS_WEEK, precision=1, @@ -166,7 +166,7 @@ SENSORS_INFO = [ translation_key="energy_month", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, key=SOURCE_TYPE_ELECTRICITY, sensor_type=SENSOR_TYPE_THIS_MONTH, precision=1, @@ -176,7 +176,7 @@ SENSORS_INFO = [ translation_key="energy_year", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, key=SOURCE_TYPE_ELECTRICITY, sensor_type=SENSOR_TYPE_THIS_YEAR, precision=1, @@ -197,7 +197,7 @@ SENSORS_INFO = [ native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, key=SOURCE_TYPE_GAS, sensor_type=SENSOR_TYPE_THIS_DAY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, icon="mdi:counter", precision=1, ), @@ -207,7 +207,7 @@ SENSORS_INFO = [ native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, key=SOURCE_TYPE_GAS, sensor_type=SENSOR_TYPE_THIS_WEEK, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, icon="mdi:counter", precision=1, ), @@ -217,7 +217,7 @@ SENSORS_INFO = [ native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, key=SOURCE_TYPE_GAS, sensor_type=SENSOR_TYPE_THIS_MONTH, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, icon="mdi:counter", precision=1, ), @@ -227,7 +227,7 @@ SENSORS_INFO = [ native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, key=SOURCE_TYPE_GAS, sensor_type=SENSOR_TYPE_THIS_YEAR, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, icon="mdi:counter", precision=1, ), diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index 484dc8bac488..3f0bdae8e538 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -222,10 +222,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: energy_today.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY ) assert energy_today.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" - assert ( - energy_today.attributes.get(ATTR_STATE_CLASS) - is SensorStateClass.TOTAL_INCREASING - ) + assert energy_today.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert ( energy_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR @@ -239,8 +236,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) assert energy_this_week.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( - energy_this_week.attributes.get(ATTR_STATE_CLASS) - is SensorStateClass.TOTAL_INCREASING + energy_this_week.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL ) assert ( energy_this_week.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -255,8 +251,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) assert energy_this_month.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( - energy_this_month.attributes.get(ATTR_STATE_CLASS) - is SensorStateClass.TOTAL_INCREASING + energy_this_month.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL ) assert ( energy_this_month.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -271,8 +266,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) assert energy_this_year.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( - energy_this_year.attributes.get(ATTR_STATE_CLASS) - is SensorStateClass.TOTAL_INCREASING + energy_this_year.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL ) assert ( energy_this_year.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -295,10 +289,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: assert gas_today.state == "1.1" assert gas_today.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_today.attributes.get(ATTR_ICON) == "mdi:counter" - assert ( - gas_today.attributes.get(ATTR_STATE_CLASS) - is SensorStateClass.TOTAL_INCREASING - ) + assert gas_today.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert ( gas_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS @@ -308,10 +299,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: assert gas_this_week.state == "5.6" assert gas_this_week.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_this_week.attributes.get(ATTR_ICON) == "mdi:counter" - assert ( - gas_this_week.attributes.get(ATTR_STATE_CLASS) - is SensorStateClass.TOTAL_INCREASING - ) + assert gas_this_week.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert ( gas_this_week.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS @@ -321,10 +309,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: assert gas_this_month.state == "39.1" assert gas_this_month.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_this_month.attributes.get(ATTR_ICON) == "mdi:counter" - assert ( - gas_this_month.attributes.get(ATTR_STATE_CLASS) - is SensorStateClass.TOTAL_INCREASING - ) + assert gas_this_month.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert ( gas_this_month.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS @@ -334,10 +319,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: assert gas_this_year.state == "116.7" assert gas_this_year.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_this_year.attributes.get(ATTR_ICON) == "mdi:counter" - assert ( - gas_this_year.attributes.get(ATTR_STATE_CLASS) - is SensorStateClass.TOTAL_INCREASING - ) + assert gas_this_year.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert ( gas_this_year.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS From 36512f71573e18ea1204a4d87a10e727c57dd22d Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 29 Oct 2023 00:05:37 -0700 Subject: [PATCH 046/265] Bump opower to 0.0.38 (#102983) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 88e038425043..a27d6f6f6808 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.0.37"] + "requirements": ["opower==0.0.38"] } diff --git a/requirements_all.txt b/requirements_all.txt index b69dbf4c2580..c2c34d359e3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1394,7 +1394,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.37 +opower==0.0.38 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ec473c9ef14..f6005b77f3c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1072,7 +1072,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.37 +opower==0.0.38 # homeassistant.components.oralb oralb-ble==0.17.6 From 5ac7e8b1ac2eb5c2217ad73b44969bcf5cd2c4e1 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 29 Oct 2023 13:28:35 +0100 Subject: [PATCH 047/265] Harden evohome against failures to retrieve high-precision temps (#102989) fix hass-logger-period --- homeassistant/components/evohome/__init__.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 2aa0cd42fe1a..4b79ef3df1bd 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -487,6 +487,18 @@ class EvoBroker: ) self.temps = None # these are now stale, will fall back to v2 temps + except KeyError as err: + _LOGGER.warning( + ( + "Unable to obtain high-precision temperatures. " + "It appears the JSON schema is not as expected, " + "so the high-precision feature will be disabled until next restart." + "Message is: %s" + ), + err, + ) + self.client_v1 = self.temps = None + else: if ( str(self.client_v1.location_id) @@ -495,7 +507,7 @@ class EvoBroker: _LOGGER.warning( "The v2 API's configured location doesn't match " "the v1 API's default location (there is more than one location), " - "so the high-precision feature will be disabled" + "so the high-precision feature will be disabled until next restart" ) self.client_v1 = self.temps = None else: From fefe930506814cfdf89715ca6eabe1cc376de553 Mon Sep 17 00:00:00 2001 From: Tom Puttemans Date: Sun, 29 Oct 2023 10:23:24 +0100 Subject: [PATCH 048/265] DSMR Gas currently delivered device state class conflict (#102991) Fixes #102985 --- 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 d89e30311e92..f12b2ad72bc7 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -210,7 +210,6 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/currently_delivered", translation_key="current_gas_usage", - device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, state_class=SensorStateClass.MEASUREMENT, ), From e81bfb959e0e71f5c0c2fcc554eb1750847f3941 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 29 Oct 2023 10:43:57 +0100 Subject: [PATCH 049/265] Fix proximity entity id (#102992) * fix proximity entity id * extend test to cover entity id --- homeassistant/components/proximity/__init__.py | 2 +- tests/components/proximity/test_init.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 07b5f931f791..23a8fc3bf649 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -88,7 +88,7 @@ def async_setup_proximity_component( proximity_zone, unit_of_measurement, ) - proximity.entity_id = f"{DOMAIN}.{proximity_zone}" + proximity.entity_id = f"{DOMAIN}.{zone_friendly_name}" proximity.async_write_ha_state() diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index 0ec8765e6047..cd96d0d7b81d 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -13,6 +13,11 @@ async def test_proximities(hass: HomeAssistant) -> None: "devices": ["device_tracker.test1", "device_tracker.test2"], "tolerance": "1", }, + "home_test2": { + "ignored_zones": ["work"], + "devices": ["device_tracker.test1", "device_tracker.test2"], + "tolerance": "1", + }, "work": { "devices": ["device_tracker.test1"], "tolerance": "1", @@ -23,7 +28,7 @@ async def test_proximities(hass: HomeAssistant) -> None: assert await async_setup_component(hass, DOMAIN, config) - proximities = ["home", "work"] + proximities = ["home", "home_test2", "work"] for prox in proximities: state = hass.states.get(f"proximity.{prox}") From 13580a334f37586bfbfe6b2b40d77cae7fe4683f Mon Sep 17 00:00:00 2001 From: Nortonko <52453201+Nortonko@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:32:24 +0100 Subject: [PATCH 050/265] Bump python-androidtv to 0.0.73 (#102999) * Update manifest.json Bump python-androidtv to version 0.0.73 * bump androidtv 0.0.73 * bump androidtv 0.0.73 --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index b8c020e6e1e9..2d0b062c7501 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -9,7 +9,7 @@ "loggers": ["adb_shell", "androidtv", "pure_python_adb"], "requirements": [ "adb-shell[async]==0.4.4", - "androidtv[async]==0.0.72", + "androidtv[async]==0.0.73", "pure-python-adb[async]==0.3.0.dev0" ] } diff --git a/requirements_all.txt b/requirements_all.txt index c2c34d359e3c..b248c8f1dabb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -414,7 +414,7 @@ amberelectric==1.0.4 amcrest==1.9.8 # homeassistant.components.androidtv -androidtv[async]==0.0.72 +androidtv[async]==0.0.73 # homeassistant.components.androidtv_remote androidtvremote2==0.0.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6005b77f3c3..cf6c995b3173 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -383,7 +383,7 @@ airtouch4pyapi==1.0.5 amberelectric==1.0.4 # homeassistant.components.androidtv -androidtv[async]==0.0.72 +androidtv[async]==0.0.73 # homeassistant.components.androidtv_remote androidtvremote2==0.0.14 From 031b1c26ce428b9c4652d4f011c1f4dbf8975d41 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 30 Oct 2023 08:46:20 +0000 Subject: [PATCH 051/265] Fix utility_meter reset when DST change occurs (#103012) --- .../components/utility_meter/sensor.py | 24 ++++++++++--------- tests/components/utility_meter/test_sensor.py | 14 ++++++++++- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index cd581d8c37f1..794a65db03a8 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -534,16 +534,25 @@ class UtilityMeterSensor(RestoreSensor): self.async_write_ha_state() - async def _async_reset_meter(self, event): - """Determine cycle - Helper function for larger than daily cycles.""" + async def _program_reset(self): + """Program the reset of the utility meter.""" if self._cron_pattern is not None: + tz = dt_util.get_time_zone(self.hass.config.time_zone) self.async_on_remove( async_track_point_in_time( self.hass, self._async_reset_meter, - croniter(self._cron_pattern, dt_util.now()).get_next(datetime), + croniter(self._cron_pattern, dt_util.now(tz)).get_next( + datetime + ), # we need timezone for DST purposes (see issue #102984) ) ) + + async def _async_reset_meter(self, event): + """Reset the utility meter status.""" + + await self._program_reset() + await self.async_reset_meter(self._tariff_entity) async def async_reset_meter(self, entity_id): @@ -566,14 +575,7 @@ class UtilityMeterSensor(RestoreSensor): """Handle entity which will be added.""" await super().async_added_to_hass() - if self._cron_pattern is not None: - self.async_on_remove( - async_track_point_in_time( - self.hass, - self._async_reset_meter, - croniter(self._cron_pattern, dt_util.now()).get_next(datetime), - ) - ) + await self._program_reset() self.async_on_remove( async_dispatcher_connect( diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 43d68d87362c..2c64338c4f3d 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1266,7 +1266,9 @@ async def _test_self_reset( state = hass.states.get("sensor.energy_bill") if expect_reset: assert state.attributes.get("last_period") == "2" - assert state.attributes.get("last_reset") == now.isoformat() + assert ( + state.attributes.get("last_reset") == dt_util.as_utc(now).isoformat() + ) # last_reset is kept in UTC assert state.state == "3" else: assert state.attributes.get("last_period") == "0" @@ -1348,6 +1350,16 @@ async def test_self_reset_hourly(hass: HomeAssistant) -> None: ) +async def test_self_reset_hourly_dst(hass: HomeAssistant) -> None: + """Test hourly reset of meter in DST change conditions.""" + + hass.config.time_zone = "Europe/Lisbon" + dt_util.set_default_time_zone(dt_util.get_time_zone(hass.config.time_zone)) + await _test_self_reset( + hass, gen_config("hourly"), "2023-10-29T01:59:00.000000+00:00" + ) + + async def test_self_reset_daily(hass: HomeAssistant) -> None: """Test daily reset of meter.""" await _test_self_reset( From 70e89781234df8cc7049a8d76d708e740364d489 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 29 Oct 2023 12:44:15 -0400 Subject: [PATCH 052/265] Fix zwave_js siren name (#103016) * Fix zwave_js.siren name * Fix test --- homeassistant/components/zwave_js/entity.py | 2 +- homeassistant/components/zwave_js/siren.py | 2 ++ tests/components/zwave_js/test_siren.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 0b9c68e96643..e7e110e7db6b 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -206,7 +206,7 @@ class ZWaveBaseEntity(Entity): ): name += f" ({primary_value.endpoint})" - return name + return name.strip() @property def available(self) -> bool: diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index 6de6b0f4e45b..7df88f7dca42 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -72,6 +72,8 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): if self._attr_available_tones: self._attr_supported_features |= SirenEntityFeature.TONES + self._attr_name = self.generate_name(include_value_name=True) + @property def is_on(self) -> bool | None: """Return whether device is on.""" diff --git a/tests/components/zwave_js/test_siren.py b/tests/components/zwave_js/test_siren.py index 210339e22d7d..6df5881107a1 100644 --- a/tests/components/zwave_js/test_siren.py +++ b/tests/components/zwave_js/test_siren.py @@ -9,7 +9,7 @@ from homeassistant.components.siren import ( from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant -SIREN_ENTITY = "siren.indoor_siren_6_2" +SIREN_ENTITY = "siren.indoor_siren_6_play_tone_2" TONE_ID_VALUE_ID = { "endpoint": 2, From f70c13214cae287f91be7f8c79432eb53274b393 Mon Sep 17 00:00:00 2001 From: kpine Date: Sun, 29 Oct 2023 11:15:19 -0700 Subject: [PATCH 053/265] Revert "Fix temperature setting for multi-setpoint z-wave device (#102395)" (#103022) This reverts commit 2d6dc2bcccff7518366655a67947d73506fc1e50. --- homeassistant/components/zwave_js/climate.py | 8 +- tests/components/zwave_js/conftest.py | 14 - .../climate_intermatic_pe653_state.json | 4508 ----------------- tests/components/zwave_js/test_climate.py | 193 - 4 files changed, 3 insertions(+), 4720 deletions(-) delete mode 100644 tests/components/zwave_js/fixtures/climate_intermatic_pe653_state.json diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 28084eecfa60..d511a030fb1a 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -259,11 +259,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): def _current_mode_setpoint_enums(self) -> list[ThermostatSetpointType]: """Return the list of enums that are relevant to the current thermostat mode.""" if self._current_mode is None or self._current_mode.value is None: - # Thermostat with no support for setting a mode is just a setpoint - if self.info.primary_value.property_key is None: - return [] - return [ThermostatSetpointType(int(self.info.primary_value.property_key))] - + # Thermostat(valve) with no support for setting a mode + # is considered heating-only + return [ThermostatSetpointType.HEATING] return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) @property diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 534f2fd2457c..5a424b38c5b8 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -662,12 +662,6 @@ def logic_group_zdb5100_state_fixture(): return json.loads(load_fixture("zwave_js/logic_group_zdb5100_state.json")) -@pytest.fixture(name="climate_intermatic_pe653_state", scope="session") -def climate_intermatic_pe653_state_fixture(): - """Load Intermatic PE653 Pool Control node state fixture data.""" - return json.loads(load_fixture("zwave_js/climate_intermatic_pe653_state.json")) - - @pytest.fixture(name="central_scene_node_state", scope="session") def central_scene_node_state_fixture(): """Load node with Central Scene CC node state fixture data.""" @@ -1312,14 +1306,6 @@ def logic_group_zdb5100_fixture(client, logic_group_zdb5100_state): return node -@pytest.fixture(name="climate_intermatic_pe653") -def climate_intermatic_pe653_fixture(client, climate_intermatic_pe653_state): - """Mock an Intermatic PE653 node.""" - node = Node(client, copy.deepcopy(climate_intermatic_pe653_state)) - client.driver.controller.nodes[node.node_id] = node - return node - - @pytest.fixture(name="central_scene_node") def central_scene_node_fixture(client, central_scene_node_state): """Mock a node with the Central Scene CC.""" diff --git a/tests/components/zwave_js/fixtures/climate_intermatic_pe653_state.json b/tests/components/zwave_js/fixtures/climate_intermatic_pe653_state.json deleted file mode 100644 index a5e86b9c0137..000000000000 --- a/tests/components/zwave_js/fixtures/climate_intermatic_pe653_state.json +++ /dev/null @@ -1,4508 +0,0 @@ -{ - "nodeId": 19, - "index": 0, - "status": 4, - "ready": true, - "isListening": true, - "isRouting": true, - "isSecure": false, - "manufacturerId": 5, - "productId": 1619, - "productType": 20549, - "firmwareVersion": "3.9", - "deviceConfig": { - "filename": "/data/db/devices/0x0005/pe653.json", - "isEmbedded": true, - "manufacturer": "Intermatic", - "manufacturerId": 5, - "label": "PE653", - "description": "Pool Control", - "devices": [ - { - "productType": 20549, - "productId": 1619 - } - ], - "firmwareVersion": { - "min": "0.0", - "max": "255.255" - }, - "preferred": false, - "associations": {}, - "paramInformation": { - "_map": {} - }, - "compat": { - "addCCs": {}, - "overrideQueries": { - "overrides": {} - } - } - }, - "label": "PE653", - "endpointCountIsDynamic": false, - "endpointsHaveIdenticalCapabilities": false, - "individualEndpointCount": 39, - "aggregatedEndpointCount": 0, - "interviewAttempts": 1, - "endpoints": [ - { - "nodeId": 19, - "index": 0, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 145, - "name": "Manufacturer Proprietary", - "version": 1, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 96, - "name": "Multi Channel", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 1, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 1, - "isSecure": false - }, - { - "id": 48, - "name": "Binary Sensor", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 1, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 145, - "name": "Manufacturer Proprietary", - "version": 1, - "isSecure": false - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": false - }, - { - "id": 112, - "name": "Configuration", - "version": 1, - "isSecure": false - }, - { - "id": 67, - "name": "Thermostat Setpoint", - "version": 1, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 1, - "isSecure": false - }, - { - "id": 48, - "name": "Binary Sensor", - "version": 1, - "isSecure": false - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 1, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 1, - "isSecure": false - }, - { - "id": 129, - "name": "Clock", - "version": 1, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 2, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 3, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 4, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 5, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 6, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 7, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 8, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 9, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 10, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 11, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 12, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 13, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 14, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 15, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 16, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 17, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 18, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 19, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 1, - "isSecure": false - }, - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 20, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 1, - "isSecure": false - }, - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 21, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 1, - "isSecure": false - }, - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 22, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 1, - "isSecure": false - }, - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 23, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 49, - "name": "Multilevel Sensor", - "version": 1, - "isSecure": false - }, - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 24, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 25, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 26, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 27, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 28, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 29, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 30, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 31, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 32, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 33, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 34, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 35, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 36, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 37, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 38, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - }, - { - "nodeId": 19, - "index": 39, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 37, - "name": "Binary Switch", - "version": 1, - "isSecure": false - } - ] - } - ], - "values": [ - { - "endpoint": 0, - "commandClass": 67, - "commandClassName": "Thermostat Setpoint", - "property": "setpoint", - "propertyKey": 7, - "propertyName": "setpoint", - "propertyKeyName": "Furnace", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Setpoint (Furnace)", - "ccSpecific": { - "setpointType": 7 - }, - "unit": "°F", - "stateful": true, - "secret": false - }, - "value": 60 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 1, - "propertyKey": 2, - "propertyName": "Installed Pump Type", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Installed Pump Type", - "default": 0, - "min": 0, - "max": 1, - "states": { - "0": "One Speed", - "1": "Two Speed" - }, - "valueSize": 2, - "format": 0, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Installed Pump Type" - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 1, - "propertyKey": 1, - "propertyName": "Booster (Cleaner) Pump Installed", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Booster (Cleaner) Pump Installed", - "default": 0, - "min": 0, - "max": 1, - "states": { - "0": "Disable", - "1": "Enable" - }, - "valueSize": 2, - "format": 1, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Booster (Cleaner) Pump Installed" - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 1, - "propertyKey": 65280, - "propertyName": "Booster (Cleaner) Pump Operation Mode", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Set the filter pump mode to use when the booster (cleaner) pump is running.", - "label": "Booster (Cleaner) Pump Operation Mode", - "default": 1, - "min": 1, - "max": 6, - "states": { - "1": "Disable", - "2": "Circuit 1", - "3": "VSP Speed 1", - "4": "VSP Speed 2", - "5": "VSP Speed 3", - "6": "VSP Speed 4" - }, - "valueSize": 2, - "format": 0, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Booster (Cleaner) Pump Operation Mode", - "info": "Set the filter pump mode to use when the booster (cleaner) pump is running." - }, - "value": 1 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 2, - "propertyKey": 65280, - "propertyName": "Heater Cooldown Period", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Heater Cooldown Period", - "default": -1, - "min": -1, - "max": 15, - "states": { - "0": "Heater installed with no cooldown", - "-1": "No heater installed" - }, - "unit": "minutes", - "valueSize": 2, - "format": 0, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Heater Cooldown Period" - }, - "value": 2 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 2, - "propertyKey": 1, - "propertyName": "Heater Safety Setting", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Prevent the heater from turning on while the pump is off.", - "label": "Heater Safety Setting", - "default": 0, - "min": 0, - "max": 1, - "states": { - "0": "Disable", - "1": "Enable" - }, - "valueSize": 2, - "format": 1, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Heater Safety Setting", - "info": "Prevent the heater from turning on while the pump is off." - }, - "value": 1 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 3, - "propertyKey": 4278190080, - "propertyName": "Water Temperature Offset", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Water Temperature Offset", - "default": 0, - "min": -5, - "max": 5, - "unit": "°F", - "valueSize": 4, - "format": 0, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Water Temperature Offset" - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 3, - "propertyKey": 16711680, - "propertyName": "Air/Freeze Temperature Offset", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Air/Freeze Temperature Offset", - "default": 0, - "min": -5, - "max": 5, - "unit": "°F", - "valueSize": 4, - "format": 0, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Air/Freeze Temperature Offset" - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 3, - "propertyKey": 65280, - "propertyName": "Solar Temperature Offset", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Solar Temperature Offset", - "default": 0, - "min": -5, - "max": 5, - "unit": "°F", - "valueSize": 4, - "format": 0, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Solar Temperature Offset" - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 22, - "propertyName": "Pool/Spa Configuration", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Pool/Spa Configuration", - "default": 0, - "min": 0, - "max": 2, - "states": { - "0": "Pool", - "1": "Spa", - "2": "Both" - }, - "valueSize": 1, - "format": 0, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Pool/Spa Configuration" - }, - "value": 2 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 23, - "propertyName": "Spa Mode Pump Speed", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Requires pool/spa configuration.", - "label": "Spa Mode Pump Speed", - "default": 1, - "min": 1, - "max": 6, - "states": { - "1": "Disabled", - "2": "Circuit 1", - "3": "VSP Speed 1", - "4": "VSP Speed 2", - "5": "VSP Speed 3", - "6": "VSP Speed 4" - }, - "valueSize": 1, - "format": 0, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Spa Mode Pump Speed", - "info": "Requires pool/spa configuration." - }, - "value": 1 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 32, - "propertyName": "Variable Speed Pump - Speed 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Requires connected variable speed pump.", - "label": "Variable Speed Pump - Speed 1", - "default": 750, - "min": 400, - "max": 3450, - "unit": "RPM", - "valueSize": 2, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump - Speed 1", - "info": "Requires connected variable speed pump." - }, - "value": 1400 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 33, - "propertyName": "Variable Speed Pump - Speed 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Requires connected variable speed pump.", - "label": "Variable Speed Pump - Speed 2", - "default": 1500, - "min": 400, - "max": 3450, - "unit": "RPM", - "valueSize": 2, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump - Speed 2", - "info": "Requires connected variable speed pump." - }, - "value": 1700 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 34, - "propertyName": "Variable Speed Pump - Speed 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Requires connected variable speed pump.", - "label": "Variable Speed Pump - Speed 3", - "default": 2350, - "min": 400, - "max": 3450, - "unit": "RPM", - "valueSize": 2, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump - Speed 3", - "info": "Requires connected variable speed pump." - }, - "value": 2500 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 35, - "propertyName": "Variable Speed Pump - Speed 4", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Requires connected variable speed pump.", - "label": "Variable Speed Pump - Speed 4", - "default": 3110, - "min": 400, - "max": 3450, - "unit": "RPM", - "valueSize": 2, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump - Speed 4", - "info": "Requires connected variable speed pump." - }, - "value": 2500 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 49, - "propertyName": "Variable Speed Pump - Max Speed", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Requires connected variable speed pump.", - "label": "Variable Speed Pump - Max Speed", - "default": 3450, - "min": 400, - "max": 3450, - "unit": "RPM", - "valueSize": 2, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump - Max Speed", - "info": "Requires connected variable speed pump." - }, - "value": 3000 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 50, - "propertyKey": 4278190080, - "propertyName": "Freeze Protection: Temperature", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Freeze Protection: Temperature", - "default": 0, - "min": 0, - "max": 44, - "states": { - "0": "Disabled", - "40": "40 °F", - "41": "41 °F", - "42": "42 °F", - "43": "43 °F", - "44": "44 °F" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Freeze Protection: Temperature" - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 50, - "propertyKey": 65536, - "propertyName": "Freeze Protection: Turn On Circuit 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Freeze Protection: Turn On Circuit 1", - "default": 0, - "min": 0, - "max": 1, - "states": { - "0": "Disable", - "1": "Enable" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Freeze Protection: Turn On Circuit 1" - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 50, - "propertyKey": 131072, - "propertyName": "Freeze Protection: Turn On Circuit 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Freeze Protection: Turn On Circuit 2", - "default": 0, - "min": 0, - "max": 1, - "states": { - "0": "Disable", - "1": "Enable" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Freeze Protection: Turn On Circuit 2" - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 50, - "propertyKey": 262144, - "propertyName": "Freeze Protection: Turn On Circuit 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Freeze Protection: Turn On Circuit 3", - "default": 0, - "min": 0, - "max": 1, - "states": { - "0": "Disable", - "1": "Enable" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Freeze Protection: Turn On Circuit 3" - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 50, - "propertyKey": 524288, - "propertyName": "Freeze Protection: Turn On Circuit 4", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Freeze Protection: Turn On Circuit 4", - "default": 0, - "min": 0, - "max": 1, - "states": { - "0": "Disable", - "1": "Enable" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Freeze Protection: Turn On Circuit 4" - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 50, - "propertyKey": 1048576, - "propertyName": "Freeze Protection: Turn On Circuit 5", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Freeze Protection: Turn On Circuit 5", - "default": 0, - "min": 0, - "max": 1, - "states": { - "0": "Disable", - "1": "Enable" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Freeze Protection: Turn On Circuit 5" - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 50, - "propertyKey": 65280, - "propertyName": "Freeze Protection: Turn On VSP Speed", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Requires variable speed pump and connected air/freeze sensor.", - "label": "Freeze Protection: Turn On VSP Speed", - "default": 0, - "min": 0, - "max": 5, - "states": { - "0": "None", - "2": "VSP Speed 1", - "3": "VSP Speed 2", - "4": "VSP Speed 3", - "5": "VSP Speed 4" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Freeze Protection: Turn On VSP Speed", - "info": "Requires variable speed pump and connected air/freeze sensor." - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 50, - "propertyKey": 128, - "propertyName": "Freeze Protection: Turn On Heater", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Requires heater and connected air/freeze sensor.", - "label": "Freeze Protection: Turn On Heater", - "default": 0, - "min": 0, - "max": 1, - "states": { - "0": "Disable", - "1": "Enable" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": false, - "isFromConfig": true, - "name": "Freeze Protection: Turn On Heater", - "info": "Requires heater and connected air/freeze sensor." - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 50, - "propertyKey": 127, - "propertyName": "Freeze Protection: Pool/Spa Cycle Time", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Requires pool/spa configuration and connected air/freeze sensor.", - "label": "Freeze Protection: Pool/Spa Cycle Time", - "default": 0, - "min": 0, - "max": 30, - "unit": "minutes", - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Freeze Protection: Pool/Spa Cycle Time", - "info": "Requires pool/spa configuration and connected air/freeze sensor." - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 4, - "propertyName": "Circuit 1 Schedule 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Start time (first two bytes, little endian) and stop time (last two bytes, little endian) of schedule in minutes past midnight, e.g. 12:05am (0x0500) to 3:00pm (0x8403) is entered as 83919875. Set to 4294967295 (0xffffffff) to disable.", - "label": "Circuit 1 Schedule 1", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 1 Schedule 1", - "info": "Start time (first two bytes, little endian) and stop time (last two bytes, little endian) of schedule in minutes past midnight, e.g. 12:05am (0x0500) to 3:00pm (0x8403) is entered as 83919875. Set to 4294967295 (0xffffffff) to disable." - }, - "value": 1979884035 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 5, - "propertyName": "Circuit 1 Schedule 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 1 Schedule 2", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 1 Schedule 2", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 6, - "propertyName": "Circuit 1 Schedule 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 1 Schedule 3", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 1 Schedule 3", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 7, - "propertyName": "Circuit 2 Schedule 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 2 Schedule 1", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 2 Schedule 1", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 8, - "propertyName": "Circuit 2 Schedule 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 2 Schedule 2", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 2 Schedule 2", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 9, - "propertyName": "Circuit 2 Schedule 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 2 Schedule 3", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 2 Schedule 3", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 10, - "propertyName": "Circuit 3 Schedule 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 3 Schedule 1", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 3 Schedule 1", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 11, - "propertyName": "Circuit 3 Schedule 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 3 Schedule 2", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 3 Schedule 2", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 12, - "propertyName": "Circuit 3 Schedule 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 3 Schedule 3", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 3 Schedule 3", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 13, - "propertyName": "Circuit 4 Schedule 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 4 Schedule 1", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 4 Schedule 1", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 14, - "propertyName": "Circuit 4 Schedule 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 4 Schedule 2", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 4 Schedule 2", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 15, - "propertyName": "Circuit 4 Schedule 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 4 Schedule 3", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 4 Schedule 3", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 16, - "propertyName": "Circuit 5 Schedule 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 5 Schedule 1", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 5 Schedule 1", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 17, - "propertyName": "Circuit 5 Schedule 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 5 Schedule 2", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 5 Schedule 2", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 18, - "propertyName": "Circuit 5 Schedule 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Circuit 5 Schedule 3", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Circuit 5 Schedule 3", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 19, - "propertyName": "Pool/Spa Mode Schedule 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Pool/Spa Mode Schedule 1", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Pool/Spa Mode Schedule 1", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 20, - "propertyName": "Pool/Spa Mode Schedule 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Pool/Spa Mode Schedule 2", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Pool/Spa Mode Schedule 2", - "info": "Refer to parameter 4 for usage." - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 21, - "propertyName": "Pool/Spa Mode Schedule 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Pool/Spa Mode Schedule 3", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Pool/Spa Mode Schedule 3", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 36, - "propertyName": "Variable Speed Pump Speed 1 Schedule 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 1 Schedule 1", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 1 Schedule 1", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 37, - "propertyName": "Variable Speed Pump Speed 1 Schedule 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 1 Schedule 2", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 1 Schedule 2", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 38, - "propertyName": "Variable Speed Pump Speed 1 Schedule 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 1 Schedule 3", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 1 Schedule 3", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 39, - "propertyName": "Variable Speed Pump Speed 2 Schedule 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 2 Schedule 1", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 2 Schedule 1", - "info": "Refer to parameter 4 for usage." - }, - "value": 1476575235 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 40, - "propertyName": "Variable Speed Pump Speed 2 Schedule 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 2 Schedule 2", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 2 Schedule 2", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 41, - "propertyName": "Variable Speed Pump Speed 2 Schedule 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 2 Schedule 3", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 2 Schedule 3", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 42, - "propertyName": "Variable Speed Pump Speed 3 Schedule 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 3 Schedule 1", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 3 Schedule 1", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 43, - "propertyName": "Variable Speed Pump Speed 3 Schedule 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 3 Schedule 2", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 3 Schedule 2", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 44, - "propertyName": "Variable Speed Pump Speed 3 Schedule 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 3 Schedule 3", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 3 Schedule 3", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 45, - "propertyName": "Variable Speed Pump Speed 4 Schedule 1", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 4 Schedule 1", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 4 Schedule 1", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 46, - "propertyName": "Variable Speed Pump Speed 4 Schedule 2", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 4 Schedule 2", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 4 Schedule 2", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 112, - "commandClassName": "Configuration", - "property": 47, - "propertyName": "Variable Speed Pump Speed 4 Schedule 3", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "description": "Refer to parameter 4 for usage.", - "label": "Variable Speed Pump Speed 4 Schedule 3", - "default": 4294967295, - "min": 0, - "max": 4294967295, - "states": { - "4294967295": "Disabled" - }, - "valueSize": 4, - "format": 1, - "allowManualEntry": true, - "isFromConfig": true, - "name": "Variable Speed Pump Speed 4 Schedule 3", - "info": "Refer to parameter 4 for usage." - }, - "value": 4294967295 - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "manufacturerId", - "propertyName": "manufacturerId", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Manufacturer ID", - "min": 0, - "max": 65535, - "stateful": true, - "secret": false - }, - "value": 5 - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "productType", - "propertyName": "productType", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Product type", - "min": 0, - "max": 65535, - "stateful": true, - "secret": false - }, - "value": 20549 - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "productId", - "propertyName": "productId", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Product ID", - "min": 0, - "max": 65535, - "stateful": true, - "secret": false - }, - "value": 1619 - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "libraryType", - "propertyName": "libraryType", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Library type", - "states": { - "0": "Unknown", - "1": "Static Controller", - "2": "Controller", - "3": "Enhanced Slave", - "4": "Slave", - "5": "Installer", - "6": "Routing Slave", - "7": "Bridge Controller", - "8": "Device under Test", - "9": "N/A", - "10": "AV Remote", - "11": "AV Device" - }, - "stateful": true, - "secret": false - }, - "value": 6 - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "protocolVersion", - "propertyName": "protocolVersion", - "ccVersion": 1, - "metadata": { - "type": "string", - "readable": true, - "writeable": false, - "label": "Z-Wave protocol version", - "stateful": true, - "secret": false - }, - "value": "2.78" - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "firmwareVersions", - "propertyName": "firmwareVersions", - "ccVersion": 1, - "metadata": { - "type": "string[]", - "readable": true, - "writeable": false, - "label": "Z-Wave chip firmware versions", - "stateful": true, - "secret": false - }, - "value": ["3.9"] - }, - { - "endpoint": 1, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": true - }, - { - "endpoint": 1, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 1, - "commandClass": 48, - "commandClassName": "Binary Sensor", - "property": "Any", - "propertyName": "Any", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Sensor state (Any)", - "ccSpecific": { - "sensorType": 255 - }, - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 1, - "commandClass": 49, - "commandClassName": "Multilevel Sensor", - "property": "Air temperature", - "propertyName": "Air temperature", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Air temperature", - "ccSpecific": { - "sensorType": 1, - "scale": 1 - }, - "unit": "°F", - "stateful": true, - "secret": false - }, - "value": 81, - "nodeId": 19 - }, - { - "endpoint": 1, - "commandClass": 67, - "commandClassName": "Thermostat Setpoint", - "property": "setpoint", - "propertyKey": 1, - "propertyName": "setpoint", - "propertyKeyName": "Heating", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Setpoint (Heating)", - "ccSpecific": { - "setpointType": 1 - }, - "unit": "°F", - "stateful": true, - "secret": false - }, - "value": 39 - }, - { - "endpoint": 2, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 2, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 2, - "commandClass": 49, - "commandClassName": "Multilevel Sensor", - "property": "Air temperature", - "propertyName": "Air temperature", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Air temperature", - "ccSpecific": { - "sensorType": 1, - "scale": 1 - }, - "unit": "°F", - "stateful": true, - "secret": false - }, - "value": 84, - "nodeId": 19 - }, - { - "endpoint": 3, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 3, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 3, - "commandClass": 49, - "commandClassName": "Multilevel Sensor", - "property": "Air temperature", - "propertyName": "Air temperature", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Air temperature", - "ccSpecific": { - "sensorType": 1, - "scale": 1 - }, - "unit": "°F", - "stateful": true, - "secret": false - }, - "value": 86, - "nodeId": 19 - }, - { - "endpoint": 4, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 4, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 4, - "commandClass": 49, - "commandClassName": "Multilevel Sensor", - "property": "Air temperature", - "propertyName": "Air temperature", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Air temperature", - "ccSpecific": { - "sensorType": 1, - "scale": 1 - }, - "unit": "°F", - "stateful": true, - "secret": false - }, - "value": 80, - "nodeId": 19 - }, - { - "endpoint": 5, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 5, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 5, - "commandClass": 49, - "commandClassName": "Multilevel Sensor", - "property": "Air temperature", - "propertyName": "Air temperature", - "ccVersion": 1, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Air temperature", - "ccSpecific": { - "sensorType": 1, - "scale": 1 - }, - "unit": "°F", - "stateful": true, - "secret": false - }, - "value": 83, - "nodeId": 19 - }, - { - "endpoint": 6, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 6, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 7, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 7, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 8, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 8, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 9, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 9, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 10, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 10, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 11, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 11, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 12, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 12, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 13, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 13, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 14, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 14, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 15, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 15, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 16, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 16, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 17, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": true - }, - { - "endpoint": 17, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 18, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 18, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 19, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 19, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 20, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 20, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 21, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 21, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 22, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 22, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 23, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 23, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 24, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 24, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 25, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 25, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 26, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 26, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 27, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 27, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 28, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 28, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 29, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 29, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 30, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 30, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 31, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - } - }, - { - "endpoint": 31, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 32, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 32, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 33, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 33, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 34, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 34, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 35, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 35, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 36, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 36, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 37, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 37, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 38, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 38, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 39, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": true - }, - { - "endpoint": 39, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 1, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - } - ], - "isFrequentListening": false, - "maxDataRate": 40000, - "supportedDataRates": [40000], - "protocolVersion": 2, - "supportsBeaming": true, - "supportsSecurity": false, - "nodeType": 1, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 16, - "label": "Binary Switch" - }, - "specific": { - "key": 1, - "label": "Binary Power Switch" - }, - "mandatorySupportedCCs": [32, 37, 39], - "mandatoryControlledCCs": [] - }, - "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0005:0x5045:0x0653:3.9", - "highestSecurityClass": -1, - "isControllerNode": false, - "keepAwake": false -} diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index cdc1e9959a73..e9040dfd397e 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -792,196 +792,3 @@ async def test_thermostat_raise_repair_issue_and_warning_when_setting_fan_preset "Dry and Fan preset modes are deprecated and will be removed in Home Assistant 2024.2. Please use the corresponding Dry and Fan HVAC modes instead" in caplog.text ) - - -async def test_multi_setpoint_thermostat( - hass: HomeAssistant, client, climate_intermatic_pe653, integration -) -> None: - """Test a thermostat with multiple setpoints.""" - node = climate_intermatic_pe653 - - heating_entity_id = "climate.pool_control_2" - heating = hass.states.get(heating_entity_id) - assert heating - assert heating.state == HVACMode.HEAT - assert heating.attributes[ATTR_TEMPERATURE] == 3.9 - assert heating.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT] - assert ( - heating.attributes[ATTR_SUPPORTED_FEATURES] - == ClimateEntityFeature.TARGET_TEMPERATURE - ) - - furnace_entity_id = "climate.pool_control" - furnace = hass.states.get(furnace_entity_id) - assert furnace - assert furnace.state == HVACMode.HEAT - assert furnace.attributes[ATTR_TEMPERATURE] == 15.6 - assert furnace.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT] - assert ( - furnace.attributes[ATTR_SUPPORTED_FEATURES] - == ClimateEntityFeature.TARGET_TEMPERATURE - ) - - client.async_send_command_no_wait.reset_mock() - - # Test setting temperature of heating setpoint - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - { - ATTR_ENTITY_ID: heating_entity_id, - ATTR_TEMPERATURE: 20.0, - }, - blocking=True, - ) - - # Test setting temperature of furnace setpoint - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - { - ATTR_ENTITY_ID: furnace_entity_id, - ATTR_TEMPERATURE: 2.0, - }, - blocking=True, - ) - - # Test setting illegal mode raises an error - with pytest.raises(ValueError): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - { - ATTR_ENTITY_ID: heating_entity_id, - ATTR_HVAC_MODE: HVACMode.COOL, - }, - blocking=True, - ) - - with pytest.raises(ValueError): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - { - ATTR_ENTITY_ID: furnace_entity_id, - ATTR_HVAC_MODE: HVACMode.COOL, - }, - blocking=True, - ) - - # this is a no-op since there's no mode - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - { - ATTR_ENTITY_ID: heating_entity_id, - ATTR_HVAC_MODE: HVACMode.HEAT, - }, - blocking=True, - ) - - # this is a no-op since there's no mode - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - { - ATTR_ENTITY_ID: furnace_entity_id, - ATTR_HVAC_MODE: HVACMode.HEAT, - }, - blocking=True, - ) - - assert len(client.async_send_command.call_args_list) == 2 - args = client.async_send_command.call_args_list[0][0][0] - assert args["command"] == "node.set_value" - assert args["nodeId"] == 19 - assert args["valueId"] == { - "endpoint": 1, - "commandClass": 67, - "property": "setpoint", - "propertyKey": 1, - } - assert args["value"] == 68.0 - - args = client.async_send_command.call_args_list[1][0][0] - assert args["command"] == "node.set_value" - assert args["nodeId"] == 19 - assert args["valueId"] == { - "endpoint": 0, - "commandClass": 67, - "property": "setpoint", - "propertyKey": 7, - } - assert args["value"] == 35.6 - - client.async_send_command.reset_mock() - - # Test heating setpoint value update from value updated event - event = Event( - type="value updated", - data={ - "source": "node", - "event": "value updated", - "nodeId": 19, - "args": { - "commandClassName": "Thermostat Setpoint", - "commandClass": 67, - "endpoint": 1, - "property": "setpoint", - "propertyKey": 1, - "propertyKeyName": "Heating", - "propertyName": "setpoint", - "newValue": 23, - "prevValue": 21.5, - }, - }, - ) - node.receive_event(event) - - state = hass.states.get(heating_entity_id) - assert state - assert state.state == HVACMode.HEAT - assert state.attributes[ATTR_TEMPERATURE] == -5 - - # furnace not changed - state = hass.states.get(furnace_entity_id) - assert state - assert state.state == HVACMode.HEAT - assert state.attributes[ATTR_TEMPERATURE] == 15.6 - - client.async_send_command.reset_mock() - - # Test furnace setpoint value update from value updated event - event = Event( - type="value updated", - data={ - "source": "node", - "event": "value updated", - "nodeId": 19, - "args": { - "commandClassName": "Thermostat Setpoint", - "commandClass": 67, - "endpoint": 0, - "property": "setpoint", - "propertyKey": 7, - "propertyKeyName": "Furnace", - "propertyName": "setpoint", - "newValue": 68, - "prevValue": 21.5, - }, - }, - ) - node.receive_event(event) - - # heating not changed - state = hass.states.get(heating_entity_id) - assert state - assert state.state == HVACMode.HEAT - assert state.attributes[ATTR_TEMPERATURE] == -5 - - state = hass.states.get(furnace_entity_id) - assert state - assert state.state == HVACMode.HEAT - assert state.attributes[ATTR_TEMPERATURE] == 20 - - client.async_send_command.reset_mock() From f5b3661836cffa5e6b4c94e78018b27eda4309a0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 29 Oct 2023 14:26:10 -0700 Subject: [PATCH 054/265] Fix bug in fitbit credential import for expired tokens (#103024) * Fix bug in fitbit credential import on token refresh * Use stable test ids * Update homeassistant/components/fitbit/sensor.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/fitbit/sensor.py | 11 +++++++---- tests/components/fitbit/conftest.py | 5 +++-- tests/components/fitbit/test_config_flow.py | 18 +++++++++++++----- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 4885c9fa16d0..d0d939ce67e3 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -581,7 +581,9 @@ async def async_setup_platform( refresh_cb=lambda x: None, ) try: - await hass.async_add_executor_job(authd_client.client.refresh_token) + updated_token = await hass.async_add_executor_job( + authd_client.client.refresh_token + ) except OAuth2Error as err: _LOGGER.debug("Unable to import fitbit OAuth2 credentials: %s", err) translation_key = "deprecated_yaml_import_issue_cannot_connect" @@ -599,9 +601,10 @@ async def async_setup_platform( data={ "auth_implementation": DOMAIN, CONF_TOKEN: { - ATTR_ACCESS_TOKEN: config_file[ATTR_ACCESS_TOKEN], - ATTR_REFRESH_TOKEN: config_file[ATTR_REFRESH_TOKEN], - "expires_at": config_file[ATTR_LAST_SAVED_AT], + ATTR_ACCESS_TOKEN: updated_token[ATTR_ACCESS_TOKEN], + ATTR_REFRESH_TOKEN: updated_token[ATTR_REFRESH_TOKEN], + "expires_at": updated_token["expires_at"], + "scope": " ".join(updated_token.get("scope", [])), }, CONF_CLOCK_FORMAT: config[CONF_CLOCK_FORMAT], CONF_UNIT_SYSTEM: config[CONF_UNIT_SYSTEM], diff --git a/tests/components/fitbit/conftest.py b/tests/components/fitbit/conftest.py index 155e54995434..682fb0edd3b0 100644 --- a/tests/components/fitbit/conftest.py +++ b/tests/components/fitbit/conftest.py @@ -41,10 +41,11 @@ TIMESERIES_API_URL_FORMAT = ( # These constants differ from values in the config entry or fitbit.conf SERVER_ACCESS_TOKEN = { - "refresh_token": "server-access-token", - "access_token": "server-refresh-token", + "refresh_token": "server-refresh-token", + "access_token": "server-access-token", "type": "Bearer", "expires_in": 60, + "scope": " ".join(OAUTH_SCOPES), } diff --git a/tests/components/fitbit/test_config_flow.py b/tests/components/fitbit/test_config_flow.py index 152439ec19a9..cf2d5d17f221 100644 --- a/tests/components/fitbit/test_config_flow.py +++ b/tests/components/fitbit/test_config_flow.py @@ -2,6 +2,7 @@ from collections.abc import Awaitable, Callable from http import HTTPStatus +import time from typing import Any from unittest.mock import patch @@ -16,9 +17,7 @@ from homeassistant.helpers import config_entry_oauth2_flow, issue_registry as ir from .conftest import ( CLIENT_ID, - FAKE_ACCESS_TOKEN, FAKE_AUTH_IMPL, - FAKE_REFRESH_TOKEN, PROFILE_API_URL, PROFILE_USER_ID, SERVER_ACCESS_TOKEN, @@ -204,6 +203,11 @@ async def test_config_entry_already_exists( assert result.get("reason") == "already_configured" +@pytest.mark.parametrize( + "token_expiration_time", + [time.time() + 86400, time.time() - 86400], + ids=("token_active", "token_expired"), +) async def test_import_fitbit_config( hass: HomeAssistant, fitbit_config_setup: None, @@ -235,16 +239,20 @@ async def test_import_fitbit_config( assert config_entry.unique_id == PROFILE_USER_ID data = dict(config_entry.data) + # Verify imported values from fitbit.conf and configuration.yaml and + # that the token is updated. assert "token" in data + expires_at = data["token"]["expires_at"] + assert expires_at > time.time() del data["token"]["expires_at"] - # Verify imported values from fitbit.conf and configuration.yaml assert dict(config_entry.data) == { "auth_implementation": DOMAIN, "clock_format": "24H", "monitored_resources": ["activities/steps"], "token": { - "access_token": FAKE_ACCESS_TOKEN, - "refresh_token": FAKE_REFRESH_TOKEN, + "access_token": "server-access-token", + "refresh_token": "server-refresh-token", + "scope": "activity heartrate nutrition profile settings sleep weight", }, "unit_system": "default", } From 6f73d2aac5a79a78fb8c4f652ca66b059c0826f7 Mon Sep 17 00:00:00 2001 From: G-Two <7310260+G-Two@users.noreply.github.com> Date: Mon, 30 Oct 2023 04:46:48 -0400 Subject: [PATCH 055/265] Bump to subarulink 0.7.8 (#103033) --- homeassistant/components/subaru/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/subaru/manifest.json b/homeassistant/components/subaru/manifest.json index 9fae6ca9f736..0c4367c77c8d 100644 --- a/homeassistant/components/subaru/manifest.json +++ b/homeassistant/components/subaru/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/subaru", "iot_class": "cloud_polling", "loggers": ["stdiomask", "subarulink"], - "requirements": ["subarulink==0.7.6"] + "requirements": ["subarulink==0.7.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index b248c8f1dabb..74749f3eafe7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2512,7 +2512,7 @@ streamlabswater==1.0.1 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.7.6 +subarulink==0.7.8 # homeassistant.components.solarlog sunwatcher==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf6c995b3173..1c2a04d2b515 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1872,7 +1872,7 @@ stookwijzer==1.3.0 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.7.6 +subarulink==0.7.8 # homeassistant.components.solarlog sunwatcher==0.2.1 From 483671bf9f03aa3411f010886d40dbcb297f3132 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 29 Oct 2023 15:48:01 -0700 Subject: [PATCH 056/265] Bump google-nest-sdm to 3.0.3 (#103035) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index bf24fc4a4e9e..892446422073 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -20,5 +20,5 @@ "iot_class": "cloud_push", "loggers": ["google_nest_sdm"], "quality_scale": "platinum", - "requirements": ["google-nest-sdm==3.0.2"] + "requirements": ["google-nest-sdm==3.0.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 74749f3eafe7..dc5ed080b1bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -910,7 +910,7 @@ google-cloud-texttospeech==2.12.3 google-generativeai==0.1.0 # homeassistant.components.nest -google-nest-sdm==3.0.2 +google-nest-sdm==3.0.3 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c2a04d2b515..c0d89daaa423 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -726,7 +726,7 @@ google-cloud-pubsub==2.13.11 google-generativeai==0.1.0 # homeassistant.components.nest -google-nest-sdm==3.0.2 +google-nest-sdm==3.0.3 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 5c16a8247addf7c81498674f9089af312d7592e4 Mon Sep 17 00:00:00 2001 From: Jirka Date: Mon, 30 Oct 2023 09:54:46 +0100 Subject: [PATCH 057/265] Update MQTT QoS description string (#103036) Update strings.json --- homeassistant/components/mqtt/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 68fa39bfdc93..6197e580b1d0 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -181,7 +181,7 @@ }, "qos": { "name": "QoS", - "description": "Quality of Service to use. O. At most once. 1: At least once. 2: Exactly once." + "description": "Quality of Service to use. 0: At most once. 1: At least once. 2: Exactly once." }, "retain": { "name": "Retain", From 891ad0b1be2c56f3b0c4dbc353149a19ae8cf60a Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Mon, 30 Oct 2023 20:56:50 +1300 Subject: [PATCH 058/265] Bump starlink-grpc-core to 1.1.3 (#103043) --- homeassistant/components/starlink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/starlink/manifest.json b/homeassistant/components/starlink/manifest.json index c719afa968dc..b8733dd24356 100644 --- a/homeassistant/components/starlink/manifest.json +++ b/homeassistant/components/starlink/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/starlink", "iot_class": "local_polling", "quality_scale": "silver", - "requirements": ["starlink-grpc-core==1.1.2"] + "requirements": ["starlink-grpc-core==1.1.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index dc5ed080b1bf..f9fa0cdc2b61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2488,7 +2488,7 @@ starline==0.1.5 starlingbank==3.2 # homeassistant.components.starlink -starlink-grpc-core==1.1.2 +starlink-grpc-core==1.1.3 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0d89daaa423..2c944ecf02c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1851,7 +1851,7 @@ srpenergy==1.3.6 starline==0.1.5 # homeassistant.components.starlink -starlink-grpc-core==1.1.2 +starlink-grpc-core==1.1.3 # homeassistant.components.statsd statsd==3.2.1 From f113d9aa713b7a93cf3dc7a6b1a3e482c2072632 Mon Sep 17 00:00:00 2001 From: Mike Woudenberg Date: Mon, 30 Oct 2023 15:57:00 +0100 Subject: [PATCH 059/265] Use correct config entry field to update when IP changes in loqed (#103051) --- homeassistant/components/loqed/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/loqed/config_flow.py b/homeassistant/components/loqed/config_flow.py index 911ccb0ff5b4..1c76f4805294 100644 --- a/homeassistant/components/loqed/config_flow.py +++ b/homeassistant/components/loqed/config_flow.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import webhook from homeassistant.components.zeroconf import ZeroconfServiceInfo -from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_NAME, CONF_WEBHOOK_ID +from homeassistant.const import CONF_API_TOKEN, CONF_NAME, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError @@ -95,7 +95,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Check if already exists await self.async_set_unique_id(lock_data["bridge_mac_wifi"]) - self._abort_if_unique_id_configured({CONF_HOST: host}) + self._abort_if_unique_id_configured({"bridge_ip": host}) return await self.async_step_user() From 31d8f4b35df7d4406ed68e0a2a5c5d3dd31ca799 Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 30 Oct 2023 08:08:51 -0700 Subject: [PATCH 060/265] Fix Opower not refreshing statistics when there are no forecast entities (#103058) Ensure _insert_statistics is periodically called --- homeassistant/components/opower/coordinator.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index 5ce35e949aff..239f23e75236 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -23,7 +23,7 @@ from homeassistant.components.recorder.statistics import ( statistics_during_period, ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -58,6 +58,16 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): entry_data.get(CONF_TOTP_SECRET), ) + @callback + def _dummy_listener() -> None: + pass + + # Force the coordinator to periodically update by registering at least one listener. + # Needed when the _async_update_data below returns {} for utilities that don't provide + # forecast, which results to no sensors added, no registered listeners, and thus + # _async_update_data not periodically getting called which is needed for _insert_statistics. + self.async_add_listener(_dummy_listener) + async def _async_update_data( self, ) -> dict[str, Forecast]: @@ -71,6 +81,8 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): raise ConfigEntryAuthFailed from err forecasts: list[Forecast] = await self.api.async_get_forecast() _LOGGER.debug("Updating sensor data with: %s", forecasts) + # Because Opower provides historical usage/cost with a delay of a couple of days + # we need to insert data into statistics. await self._insert_statistics() return {forecast.account.utility_account_id: forecast for forecast in forecasts} From 3728f3da69348ac035d5b07039504145f37751dd Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:47:33 +0100 Subject: [PATCH 061/265] Update PyViCare to v2.28.1 for ViCare integration (#103064) --- homeassistant/components/vicare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 418172975d89..e8bc4178073e 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/vicare", "iot_class": "cloud_polling", "loggers": ["PyViCare"], - "requirements": ["PyViCare==2.25.0"] + "requirements": ["PyViCare==2.28.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index f9fa0cdc2b61..dcbe08590383 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -113,7 +113,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.7.1 # homeassistant.components.vicare -PyViCare==2.25.0 +PyViCare==2.28.1 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c944ecf02c1..4715e175534f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.7.1 # homeassistant.components.vicare -PyViCare==2.25.0 +PyViCare==2.28.1 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 From c7b702f3c2ae38a97d52be9415a9bb9e6929aed2 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 30 Oct 2023 09:57:24 -0400 Subject: [PATCH 062/265] Bump pyschlage to 2023.10.0 (#103065) --- homeassistant/components/schlage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/schlage/manifest.json b/homeassistant/components/schlage/manifest.json index 3568692c6ca1..f474f739904e 100644 --- a/homeassistant/components/schlage/manifest.json +++ b/homeassistant/components/schlage/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/schlage", "iot_class": "cloud_polling", - "requirements": ["pyschlage==2023.9.1"] + "requirements": ["pyschlage==2023.10.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index dcbe08590383..34d110f4a3de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2004,7 +2004,7 @@ pysabnzbd==1.1.1 pysaj==0.0.16 # homeassistant.components.schlage -pyschlage==2023.9.1 +pyschlage==2023.10.0 # homeassistant.components.sensibo pysensibo==1.0.35 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4715e175534f..1328f70970e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1511,7 +1511,7 @@ pyrympro==0.0.7 pysabnzbd==1.1.1 # homeassistant.components.schlage -pyschlage==2023.9.1 +pyschlage==2023.10.0 # homeassistant.components.sensibo pysensibo==1.0.35 From bac39f0061bbf4c840dbb16f1d9725938daa76f9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 30 Oct 2023 19:40:27 +0100 Subject: [PATCH 063/265] Show a warning when no Withings data found (#103066) --- homeassistant/components/withings/sensor.py | 6 ++++++ tests/components/withings/test_sensor.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 1bef72c48ec3..707059a29303 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -40,6 +40,7 @@ from homeassistant.util import dt as dt_util from . import WithingsData from .const import ( DOMAIN, + LOGGER, SCORE_POINTS, UOM_BEATS_PER_MINUTE, UOM_BREATHS_PER_MINUTE, @@ -787,6 +788,11 @@ async def async_setup_entry( _async_add_workout_entities ) + if not entities: + LOGGER.warning( + "No data found for Withings entry %s, sensors will be added when new data is available" + ) + async_add_entities(entities) diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 0bf6b3231468..5d42ace495b6 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch +from aiowithings import Goals from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -341,3 +342,20 @@ async def test_workout_sensors_created_when_receive_workout_data( await hass.async_block_till_done() assert hass.states.get("sensor.henk_last_workout_type") + + +async def test_warning_if_no_entities_created( + hass: HomeAssistant, + withings: AsyncMock, + polling_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test we log a warning if no entities are created at startup.""" + withings.get_workouts_in_period.return_value = [] + withings.get_goals.return_value = Goals(None, None, None) + withings.get_measurement_in_period.return_value = [] + withings.get_sleep_summary_since.return_value = [] + withings.get_activities_since.return_value = [] + await setup_integration(hass, polling_config_entry, False) + + assert "No data found for Withings entry" in caplog.text From 8d781ff0638b8d6a6c7e76cd6eeef59b7e8786b0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 30 Oct 2023 15:47:08 +0100 Subject: [PATCH 064/265] Add 2 properties to Withings diagnostics (#103067) --- homeassistant/components/withings/diagnostics.py | 2 ++ tests/components/withings/snapshots/test_diagnostics.ambr | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/withings/diagnostics.py b/homeassistant/components/withings/diagnostics.py index 7ed9f6ce2c9c..31c9ffef5694 100644 --- a/homeassistant/components/withings/diagnostics.py +++ b/homeassistant/components/withings/diagnostics.py @@ -33,4 +33,6 @@ async def async_get_config_entry_diagnostics( "webhooks_connected": withings_data.measurement_coordinator.webhooks_connected, "received_measurements": list(withings_data.measurement_coordinator.data), "received_sleep_data": withings_data.sleep_coordinator.data is not None, + "received_workout_data": withings_data.workout_coordinator.data is not None, + "received_activity_data": withings_data.activity_coordinator.data is not None, } diff --git a/tests/components/withings/snapshots/test_diagnostics.ambr b/tests/components/withings/snapshots/test_diagnostics.ambr index 3b6a5390bd65..f9b4a1d9bbae 100644 --- a/tests/components/withings/snapshots/test_diagnostics.ambr +++ b/tests/components/withings/snapshots/test_diagnostics.ambr @@ -3,6 +3,7 @@ dict({ 'has_cloudhooks': True, 'has_valid_external_webhook_url': True, + 'received_activity_data': False, 'received_measurements': list([ 1, 8, @@ -26,6 +27,7 @@ 169, ]), 'received_sleep_data': True, + 'received_workout_data': True, 'webhooks_connected': True, }) # --- @@ -33,6 +35,7 @@ dict({ 'has_cloudhooks': False, 'has_valid_external_webhook_url': False, + 'received_activity_data': False, 'received_measurements': list([ 1, 8, @@ -56,6 +59,7 @@ 169, ]), 'received_sleep_data': True, + 'received_workout_data': True, 'webhooks_connected': False, }) # --- @@ -63,6 +67,7 @@ dict({ 'has_cloudhooks': False, 'has_valid_external_webhook_url': True, + 'received_activity_data': False, 'received_measurements': list([ 1, 8, @@ -86,6 +91,7 @@ 169, ]), 'received_sleep_data': True, + 'received_workout_data': True, 'webhooks_connected': True, }) # --- From a3ebfaebe7c928d9c8771b1f547ac9a61d0e3672 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 30 Oct 2023 19:59:32 +0100 Subject: [PATCH 065/265] Bumped version to 2023.11.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 cbc216871109..4587f1d37a15 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 92c11a19a12a..d6070c019aef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.11.0b2" +version = "2023.11.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 06f27e7e74f5a96754162d8c79b2eca92fa757b0 Mon Sep 17 00:00:00 2001 From: Paul Manzotti Date: Tue, 31 Oct 2023 07:09:03 +0000 Subject: [PATCH 066/265] Update geniushub-client to v0.7.1 (#103071) --- homeassistant/components/geniushub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 4029023bb073..280792938218 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/geniushub", "iot_class": "local_polling", "loggers": ["geniushubclient"], - "requirements": ["geniushub-client==0.7.0"] + "requirements": ["geniushub-client==0.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 34d110f4a3de..51697865b8aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -857,7 +857,7 @@ gassist-text==0.0.10 gcal-sync==5.0.0 # homeassistant.components.geniushub -geniushub-client==0.7.0 +geniushub-client==0.7.1 # homeassistant.components.geocaching geocachingapi==0.2.1 From 41500cbe9b8f9eb36df9664bc5030960b559cba3 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Tue, 31 Oct 2023 11:49:03 +0400 Subject: [PATCH 067/265] Code cleanup for transmission integration (#103078) --- homeassistant/components/transmission/__init__.py | 12 +----------- homeassistant/components/transmission/config_flow.py | 9 +++------ homeassistant/components/transmission/coordinator.py | 8 ++++---- homeassistant/components/transmission/strings.json | 2 -- 4 files changed, 8 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 7d019935e6c0..df78c5d96aaa 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -1,7 +1,6 @@ """Support for the Transmission BitTorrent client API.""" from __future__ import annotations -from datetime import timedelta from functools import partial import logging import re @@ -22,7 +21,6 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_SCAN_INTERVAL, CONF_USERNAME, Platform, ) @@ -69,7 +67,7 @@ MIGRATION_NAME_TO_KEY = { SERVICE_BASE_SCHEMA = vol.Schema( { - vol.Exclusive(CONF_ENTRY_ID, "identifier"): selector.ConfigEntrySelector(), + vol.Required(CONF_ENTRY_ID): selector.ConfigEntrySelector(), } ) @@ -135,7 +133,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) - config_entry.add_update_listener(async_options_updated) async def add_torrent(service: ServiceCall) -> None: """Add new torrent to download.""" @@ -244,10 +241,3 @@ async def get_api( except TransmissionError as error: _LOGGER.error(error) raise UnknownError from error - - -async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Triggered by config entry options updates.""" - coordinator: TransmissionDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - coordinator.update_interval = timedelta(seconds=entry.options[CONF_SCAN_INTERVAL]) - await coordinator.async_request_refresh() diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index d16981add877..a987233fef0b 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -55,12 +55,9 @@ 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] - and entry.data[CONF_PORT] == user_input[CONF_PORT] - ): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) try: await get_api(self.hass, user_input) diff --git a/homeassistant/components/transmission/coordinator.py b/homeassistant/components/transmission/coordinator.py index 9df509b9783e..91597d0e43d3 100644 --- a/homeassistant/components/transmission/coordinator.py +++ b/homeassistant/components/transmission/coordinator.py @@ -71,13 +71,13 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]): data = self.api.session_stats() self.torrents = self.api.get_torrents() self._session = self.api.get_session() - - self.check_completed_torrent() - self.check_started_torrent() - self.check_removed_torrent() except transmission_rpc.TransmissionError as err: raise UpdateFailed("Unable to connect to Transmission client") from err + self.check_completed_torrent() + self.check_started_torrent() + self.check_removed_torrent() + return data def init_torrent_list(self) -> None: diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 81d94b9aac46..77ffd6a8b2a0 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -30,9 +30,7 @@ "options": { "step": { "init": { - "title": "Configure options for Transmission", "data": { - "scan_interval": "Update frequency", "limit": "Limit", "order": "Order" } From 376a79eb42427423590478a63d0a30e51c2cfd40 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 30 Oct 2023 21:43:24 +0100 Subject: [PATCH 068/265] Refactor todo services and their schema (#103079) --- homeassistant/components/todo/__init__.py | 86 +++++------ homeassistant/components/todo/services.yaml | 27 ++-- homeassistant/components/todo/strings.json | 46 +++--- tests/components/todo/test_init.py | 152 +++++++++++--------- 4 files changed, 149 insertions(+), 162 deletions(-) diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index 12eac858f754..968256ce3d92 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -43,14 +43,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_handle_todo_item_move) component.async_register_entity_service( - "create_item", + "add_item", { - vol.Required("summary"): vol.All(cv.string, vol.Length(min=1)), - vol.Optional("status", default=TodoItemStatus.NEEDS_ACTION): vol.In( - {TodoItemStatus.NEEDS_ACTION, TodoItemStatus.COMPLETED} - ), + vol.Required("item"): vol.All(cv.string, vol.Length(min=1)), }, - _async_create_todo_item, + _async_add_todo_item, required_features=[TodoListEntityFeature.CREATE_TODO_ITEM], ) component.async_register_entity_service( @@ -58,30 +55,26 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: vol.All( cv.make_entity_service_schema( { - vol.Optional("uid"): cv.string, - vol.Optional("summary"): vol.All(cv.string, vol.Length(min=1)), + vol.Required("item"): vol.All(cv.string, vol.Length(min=1)), + vol.Optional("rename"): vol.All(cv.string, vol.Length(min=1)), vol.Optional("status"): vol.In( {TodoItemStatus.NEEDS_ACTION, TodoItemStatus.COMPLETED} ), } ), - cv.has_at_least_one_key("uid", "summary"), + cv.has_at_least_one_key("rename", "status"), ), _async_update_todo_item, required_features=[TodoListEntityFeature.UPDATE_TODO_ITEM], ) component.async_register_entity_service( - "delete_item", - vol.All( - cv.make_entity_service_schema( - { - vol.Optional("uid"): vol.All(cv.ensure_list, [cv.string]), - vol.Optional("summary"): vol.All(cv.ensure_list, [cv.string]), - } - ), - cv.has_at_least_one_key("uid", "summary"), + "remove_item", + cv.make_entity_service_schema( + { + vol.Required("item"): vol.All(cv.ensure_list, [cv.string]), + } ), - _async_delete_todo_items, + _async_remove_todo_items, required_features=[TodoListEntityFeature.DELETE_TODO_ITEM], ) @@ -114,13 +107,6 @@ class TodoItem: status: TodoItemStatus | None = None """A status or confirmation of the To-do item.""" - @classmethod - def from_dict(cls, obj: dict[str, Any]) -> "TodoItem": - """Create a To-do Item from a dictionary parsed by schema validators.""" - return cls( - summary=obj.get("summary"), status=obj.get("status"), uid=obj.get("uid") - ) - class TodoListEntity(Entity): """An entity that represents a To-do list.""" @@ -232,39 +218,43 @@ async def websocket_handle_todo_item_move( connection.send_result(msg["id"]) -def _find_by_summary(summary: str, items: list[TodoItem] | None) -> TodoItem | None: - """Find a To-do List item by summary name.""" +def _find_by_uid_or_summary( + value: str, items: list[TodoItem] | None +) -> TodoItem | None: + """Find a To-do List item by uid or summary name.""" for item in items or (): - if item.summary == summary: + if value in (item.uid, item.summary): return item return None -async def _async_create_todo_item(entity: TodoListEntity, call: ServiceCall) -> None: +async def _async_add_todo_item(entity: TodoListEntity, call: ServiceCall) -> None: """Add an item to the To-do list.""" - await entity.async_create_todo_item(item=TodoItem.from_dict(call.data)) + await entity.async_create_todo_item( + item=TodoItem(summary=call.data["item"], status=TodoItemStatus.NEEDS_ACTION) + ) async def _async_update_todo_item(entity: TodoListEntity, call: ServiceCall) -> None: """Update an item in the To-do list.""" - item = TodoItem.from_dict(call.data) - if not item.uid: - found = _find_by_summary(call.data["summary"], entity.todo_items) - if not found: - raise ValueError(f"Unable to find To-do item with summary '{item.summary}'") - item.uid = found.uid + item = call.data["item"] + found = _find_by_uid_or_summary(item, entity.todo_items) + if not found: + raise ValueError(f"Unable to find To-do item '{item}'") - await entity.async_update_todo_item(item=item) + update_item = TodoItem( + uid=found.uid, summary=call.data.get("rename"), status=call.data.get("status") + ) + + await entity.async_update_todo_item(item=update_item) -async def _async_delete_todo_items(entity: TodoListEntity, call: ServiceCall) -> None: - """Delete an item in the To-do list.""" - uids = call.data.get("uid", []) - if not uids: - summaries = call.data.get("summary", []) - for summary in summaries: - item = _find_by_summary(summary, entity.todo_items) - if not item: - raise ValueError(f"Unable to find To-do item with summary '{summary}") - uids.append(item.uid) +async def _async_remove_todo_items(entity: TodoListEntity, call: ServiceCall) -> None: + """Remove an item in the To-do list.""" + uids = [] + for item in call.data.get("item", []): + found = _find_by_uid_or_summary(item, entity.todo_items) + if not found or not found.uid: + raise ValueError(f"Unable to find To-do item '{item}") + uids.append(found.uid) await entity.async_delete_todo_items(uids=uids) diff --git a/homeassistant/components/todo/services.yaml b/homeassistant/components/todo/services.yaml index c31a7e888085..4d6237760ca5 100644 --- a/homeassistant/components/todo/services.yaml +++ b/homeassistant/components/todo/services.yaml @@ -1,23 +1,15 @@ -create_item: +add_item: target: entity: domain: todo supported_features: - todo.TodoListEntityFeature.CREATE_TODO_ITEM fields: - summary: + item: required: true example: "Submit income tax return" selector: text: - status: - example: "needs_action" - selector: - select: - translation_key: status - options: - - needs_action - - completed update_item: target: entity: @@ -25,11 +17,13 @@ update_item: supported_features: - todo.TodoListEntityFeature.UPDATE_TODO_ITEM fields: - uid: + item: + required: true + example: "Submit income tax return" selector: text: - summary: - example: "Submit income tax return" + rename: + example: "Something else" selector: text: status: @@ -40,16 +34,13 @@ update_item: options: - needs_action - completed -delete_item: +remove_item: target: entity: domain: todo supported_features: - todo.TodoListEntityFeature.DELETE_TODO_ITEM fields: - uid: - selector: - object: - summary: + item: selector: object: diff --git a/homeassistant/components/todo/strings.json b/homeassistant/components/todo/strings.json index 623c46375f0f..6ba8aaba1a51 100644 --- a/homeassistant/components/todo/strings.json +++ b/homeassistant/components/todo/strings.json @@ -6,49 +6,41 @@ } }, "services": { - "create_item": { - "name": "Create to-do list item", + "add_item": { + "name": "Add to-do list item", "description": "Add a new to-do list item.", "fields": { - "summary": { - "name": "Summary", - "description": "The short summary that represents the to-do item." - }, - "status": { - "name": "Status", - "description": "A status or confirmation of the to-do item." + "item": { + "name": "Item name", + "description": "The name that represents the to-do item." } } }, "update_item": { "name": "Update to-do list item", - "description": "Update an existing to-do list item based on either its unique ID or summary.", + "description": "Update an existing to-do list item based on its name.", "fields": { - "uid": { - "name": "To-do item unique ID", - "description": "Unique identifier for the to-do list item." + "item": { + "name": "Item name", + "description": "The name for the to-do list item." }, - "summary": { - "name": "Summary", - "description": "The short summary that represents the to-do item." + "rename": { + "name": "Rename item", + "description": "The new name of the to-do item" }, "status": { - "name": "Status", + "name": "Set status", "description": "A status or confirmation of the to-do item." } } }, - "delete_item": { - "name": "Delete a to-do list item", - "description": "Delete an existing to-do list item either by its unique ID or summary.", + "remove_item": { + "name": "Remove a to-do list item", + "description": "Remove an existing to-do list item by its name.", "fields": { - "uid": { - "name": "To-do item unique IDs", - "description": "Unique identifiers for the to-do list items." - }, - "summary": { - "name": "Summary", - "description": "The short summary that represents the to-do item." + "item": { + "name": "Item name", + "description": "The name for the to-do list items." } } } diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index f4d671ad352a..3e84049efa8a 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -197,28 +197,18 @@ async def test_unsupported_websocket( assert resp.get("error", {}).get("code") == "not_found" -@pytest.mark.parametrize( - ("item_data", "expected_status"), - [ - ({}, TodoItemStatus.NEEDS_ACTION), - ({"status": "needs_action"}, TodoItemStatus.NEEDS_ACTION), - ({"status": "completed"}, TodoItemStatus.COMPLETED), - ], -) -async def test_create_item_service( +async def test_add_item_service( hass: HomeAssistant, - item_data: dict[str, Any], - expected_status: TodoItemStatus, test_entity: TodoListEntity, ) -> None: - """Test creating an item in a To-do list.""" + """Test adding an item in a To-do list.""" await create_mock_platform(hass, [test_entity]) await hass.services.async_call( DOMAIN, - "create_item", - {"summary": "New item", **item_data}, + "add_item", + {"item": "New item"}, target={"entity_id": "todo.entity1"}, blocking=True, ) @@ -229,14 +219,14 @@ async def test_create_item_service( assert item assert item.uid is None assert item.summary == "New item" - assert item.status == expected_status + assert item.status == TodoItemStatus.NEEDS_ACTION -async def test_create_item_service_raises( +async def test_add_item_service_raises( hass: HomeAssistant, test_entity: TodoListEntity, ) -> None: - """Test creating an item in a To-do list that raises an error.""" + """Test adding an item in a To-do list that raises an error.""" await create_mock_platform(hass, [test_entity]) @@ -244,8 +234,8 @@ async def test_create_item_service_raises( with pytest.raises(HomeAssistantError, match="Ooops"): await hass.services.async_call( DOMAIN, - "create_item", - {"summary": "New item", "status": "needs_action"}, + "add_item", + {"item": "New item"}, target={"entity_id": "todo.entity1"}, blocking=True, ) @@ -255,27 +245,23 @@ async def test_create_item_service_raises( ("item_data", "expected_error"), [ ({}, "required key not provided"), - ({"status": "needs_action"}, "required key not provided"), - ( - {"summary": "", "status": "needs_action"}, - "length of value must be at least 1", - ), + ({"item": ""}, "length of value must be at least 1"), ], ) -async def test_create_item_service_invalid_input( +async def test_add_item_service_invalid_input( hass: HomeAssistant, test_entity: TodoListEntity, item_data: dict[str, Any], expected_error: str, ) -> None: - """Test invalid input to the create item service.""" + """Test invalid input to the add item service.""" await create_mock_platform(hass, [test_entity]) with pytest.raises(vol.Invalid, match=expected_error): await hass.services.async_call( DOMAIN, - "create_item", + "add_item", item_data, target={"entity_id": "todo.entity1"}, blocking=True, @@ -293,7 +279,7 @@ async def test_update_todo_item_service_by_id( await hass.services.async_call( DOMAIN, "update_item", - {"uid": "item-1", "summary": "Updated item", "status": "completed"}, + {"item": "1", "rename": "Updated item", "status": "completed"}, target={"entity_id": "todo.entity1"}, blocking=True, ) @@ -302,7 +288,7 @@ async def test_update_todo_item_service_by_id( assert args item = args.kwargs.get("item") assert item - assert item.uid == "item-1" + assert item.uid == "1" assert item.summary == "Updated item" assert item.status == TodoItemStatus.COMPLETED @@ -318,7 +304,7 @@ async def test_update_todo_item_service_by_id_status_only( await hass.services.async_call( DOMAIN, "update_item", - {"uid": "item-1", "status": "completed"}, + {"item": "1", "status": "completed"}, target={"entity_id": "todo.entity1"}, blocking=True, ) @@ -327,12 +313,12 @@ async def test_update_todo_item_service_by_id_status_only( assert args item = args.kwargs.get("item") assert item - assert item.uid == "item-1" + assert item.uid == "1" assert item.summary is None assert item.status == TodoItemStatus.COMPLETED -async def test_update_todo_item_service_by_id_summary_only( +async def test_update_todo_item_service_by_id_rename( hass: HomeAssistant, test_entity: TodoListEntity, ) -> None: @@ -343,7 +329,7 @@ async def test_update_todo_item_service_by_id_summary_only( await hass.services.async_call( DOMAIN, "update_item", - {"uid": "item-1", "summary": "Updated item"}, + {"item": "1", "rename": "Updated item"}, target={"entity_id": "todo.entity1"}, blocking=True, ) @@ -352,7 +338,7 @@ async def test_update_todo_item_service_by_id_summary_only( assert args item = args.kwargs.get("item") assert item - assert item.uid == "item-1" + assert item.uid == "1" assert item.summary == "Updated item" assert item.status is None @@ -368,7 +354,7 @@ async def test_update_todo_item_service_raises( await hass.services.async_call( DOMAIN, "update_item", - {"uid": "item-1", "summary": "Updated item", "status": "completed"}, + {"item": "1", "rename": "Updated item", "status": "completed"}, target={"entity_id": "todo.entity1"}, blocking=True, ) @@ -378,7 +364,7 @@ async def test_update_todo_item_service_raises( await hass.services.async_call( DOMAIN, "update_item", - {"uid": "item-1", "summary": "Updated item", "status": "completed"}, + {"item": "1", "rename": "Updated item", "status": "completed"}, target={"entity_id": "todo.entity1"}, blocking=True, ) @@ -395,7 +381,7 @@ async def test_update_todo_item_service_by_summary( await hass.services.async_call( DOMAIN, "update_item", - {"summary": "Item #1", "status": "completed"}, + {"item": "Item #1", "rename": "Something else", "status": "completed"}, target={"entity_id": "todo.entity1"}, blocking=True, ) @@ -405,10 +391,35 @@ async def test_update_todo_item_service_by_summary( item = args.kwargs.get("item") assert item assert item.uid == "1" - assert item.summary == "Item #1" + assert item.summary == "Something else" assert item.status == TodoItemStatus.COMPLETED +async def test_update_todo_item_service_by_summary_only_status( + hass: HomeAssistant, + test_entity: TodoListEntity, +) -> None: + """Test updating an item in a To-do list by summary.""" + + await create_mock_platform(hass, [test_entity]) + + await hass.services.async_call( + DOMAIN, + "update_item", + {"item": "Item #1", "rename": "Something else"}, + target={"entity_id": "todo.entity1"}, + blocking=True, + ) + + args = test_entity.async_update_todo_item.call_args + assert args + item = args.kwargs.get("item") + assert item + assert item.uid == "1" + assert item.summary == "Something else" + assert item.status is None + + async def test_update_todo_item_service_by_summary_not_found( hass: HomeAssistant, test_entity: TodoListEntity, @@ -421,7 +432,7 @@ async def test_update_todo_item_service_by_summary_not_found( await hass.services.async_call( DOMAIN, "update_item", - {"summary": "Item #7", "status": "completed"}, + {"item": "Item #7", "status": "completed"}, target={"entity_id": "todo.entity1"}, blocking=True, ) @@ -430,10 +441,11 @@ async def test_update_todo_item_service_by_summary_not_found( @pytest.mark.parametrize( ("item_data", "expected_error"), [ - ({}, "must contain at least one of"), - ({"status": "needs_action"}, "must contain at least one of"), + ({}, r"required key not provided @ data\['item'\]"), + ({"status": "needs_action"}, r"required key not provided @ data\['item'\]"), + ({"item": "Item #1"}, "must contain at least one of"), ( - {"summary": "", "status": "needs_action"}, + {"item": "", "status": "needs_action"}, "length of value must be at least 1", ), ], @@ -458,32 +470,32 @@ async def test_update_item_service_invalid_input( ) -async def test_delete_todo_item_service_by_id( +async def test_remove_todo_item_service_by_id( hass: HomeAssistant, test_entity: TodoListEntity, ) -> None: - """Test deleting an item in a To-do list.""" + """Test removing an item in a To-do list.""" await create_mock_platform(hass, [test_entity]) await hass.services.async_call( DOMAIN, - "delete_item", - {"uid": ["item-1", "item-2"]}, + "remove_item", + {"item": ["1", "2"]}, target={"entity_id": "todo.entity1"}, blocking=True, ) args = test_entity.async_delete_todo_items.call_args assert args - assert args.kwargs.get("uids") == ["item-1", "item-2"] + assert args.kwargs.get("uids") == ["1", "2"] -async def test_delete_todo_item_service_raises( +async def test_remove_todo_item_service_raises( hass: HomeAssistant, test_entity: TodoListEntity, ) -> None: - """Test deleting an item in a To-do list that raises an error.""" + """Test removing an item in a To-do list that raises an error.""" await create_mock_platform(hass, [test_entity]) @@ -491,43 +503,45 @@ async def test_delete_todo_item_service_raises( with pytest.raises(HomeAssistantError, match="Ooops"): await hass.services.async_call( DOMAIN, - "delete_item", - {"uid": ["item-1", "item-2"]}, + "remove_item", + {"item": ["1", "2"]}, target={"entity_id": "todo.entity1"}, blocking=True, ) -async def test_delete_todo_item_service_invalid_input( +async def test_remove_todo_item_service_invalid_input( hass: HomeAssistant, test_entity: TodoListEntity, ) -> None: - """Test invalid input to the delete item service.""" + """Test invalid input to the remove item service.""" await create_mock_platform(hass, [test_entity]) - with pytest.raises(vol.Invalid, match="must contain at least one of"): + with pytest.raises( + vol.Invalid, match=r"required key not provided @ data\['item'\]" + ): await hass.services.async_call( DOMAIN, - "delete_item", + "remove_item", {}, target={"entity_id": "todo.entity1"}, blocking=True, ) -async def test_delete_todo_item_service_by_summary( +async def test_remove_todo_item_service_by_summary( hass: HomeAssistant, test_entity: TodoListEntity, ) -> None: - """Test deleting an item in a To-do list by summary.""" + """Test removing an item in a To-do list by summary.""" await create_mock_platform(hass, [test_entity]) await hass.services.async_call( DOMAIN, - "delete_item", - {"summary": ["Item #1"]}, + "remove_item", + {"item": ["Item #1"]}, target={"entity_id": "todo.entity1"}, blocking=True, ) @@ -537,19 +551,19 @@ async def test_delete_todo_item_service_by_summary( assert args.kwargs.get("uids") == ["1"] -async def test_delete_todo_item_service_by_summary_not_found( +async def test_remove_todo_item_service_by_summary_not_found( hass: HomeAssistant, test_entity: TodoListEntity, ) -> None: - """Test deleting an item in a To-do list by summary which is not found.""" + """Test removing an item in a To-do list by summary which is not found.""" await create_mock_platform(hass, [test_entity]) with pytest.raises(ValueError, match="Unable to find"): await hass.services.async_call( DOMAIN, - "delete_item", - {"summary": ["Item #7"]}, + "remove_item", + {"item": ["Item #7"]}, target={"entity_id": "todo.entity1"}, blocking=True, ) @@ -656,22 +670,22 @@ async def test_move_todo_item_service_invalid_input( ("service_name", "payload"), [ ( - "create_item", + "add_item", { - "summary": "New item", + "item": "New item", }, ), ( - "delete_item", + "remove_item", { - "uid": ["1"], + "item": ["1"], }, ), ( "update_item", { - "uid": "1", - "summary": "Updated item", + "item": "1", + "rename": "Updated item", }, ), ], From 67edb98e5908ac0d6e9de3c96729c3e6eca69413 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 31 Oct 2023 08:31:53 +0100 Subject: [PATCH 069/265] Fix Met Device Info (#103082) --- homeassistant/components/met/__init__.py | 12 +++++++++++ homeassistant/components/met/weather.py | 14 ++++++------- tests/components/met/test_init.py | 26 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 16bfc93f715a..537642520431 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -68,6 +69,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + await cleanup_old_device(hass) + return True @@ -88,6 +91,15 @@ async def async_update_entry(hass: HomeAssistant, config_entry: ConfigEntry): await hass.config_entries.async_reload(config_entry.entry_id) +async def cleanup_old_device(hass: HomeAssistant) -> None: + """Cleanup device without proper device identifier.""" + device_reg = dr.async_get(hass) + device = device_reg.async_get_device(identifiers={(DOMAIN,)}) # type: ignore[arg-type] + if device: + _LOGGER.debug("Removing improper device %s", device.name) + device_reg.async_remove_device(device.id) + + class CannotConnect(HomeAssistantError): """Unable to connect to the web site.""" diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index def06634f425..8a5c405c1c10 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -60,7 +60,7 @@ async def async_setup_entry( if TYPE_CHECKING: assert isinstance(name, str) - entities = [MetWeather(coordinator, config_entry.data, False, name, is_metric)] + entities = [MetWeather(coordinator, config_entry, False, name, is_metric)] # Add hourly entity to legacy config entries if entity_registry.async_get_entity_id( @@ -69,9 +69,7 @@ async def async_setup_entry( _calculate_unique_id(config_entry.data, True), ): name = f"{name} hourly" - entities.append( - MetWeather(coordinator, config_entry.data, True, name, is_metric) - ) + entities.append(MetWeather(coordinator, config_entry, True, name, is_metric)) async_add_entities(entities) @@ -114,22 +112,22 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]): def __init__( self, coordinator: MetDataUpdateCoordinator, - config: MappingProxyType[str, Any], + config_entry: ConfigEntry, hourly: bool, name: str, is_metric: bool, ) -> None: """Initialise the platform with a data instance and site.""" super().__init__(coordinator) - self._attr_unique_id = _calculate_unique_id(config, hourly) - self._config = config + self._attr_unique_id = _calculate_unique_id(config_entry.data, hourly) + self._config = config_entry.data self._is_metric = is_metric self._hourly = hourly self._attr_entity_registry_enabled_default = not hourly self._attr_device_info = DeviceInfo( name="Forecast", entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN,)}, # type: ignore[arg-type] + identifiers={(DOMAIN, config_entry.entry_id)}, manufacturer="Met.no", model="Forecast", configuration_url="https://www.met.no/en", diff --git a/tests/components/met/test_init.py b/tests/components/met/test_init.py index d9085f8251f0..652763947dfe 100644 --- a/tests/components/met/test_init.py +++ b/tests/components/met/test_init.py @@ -9,6 +9,7 @@ from homeassistant.components.met.const import ( from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from . import init_integration @@ -48,3 +49,28 @@ async def test_fail_default_home_entry( "Skip setting up met.no integration; No Home location has been set" in caplog.text ) + + +async def test_removing_incorrect_devices( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_weather +) -> None: + """Test we remove incorrect devices.""" + entry = await init_integration(hass) + + device_reg = dr.async_get(hass) + device_reg.async_get_or_create( + config_entry_id=entry.entry_id, + name="Forecast_legacy", + entry_type=dr.DeviceEntryType.SERVICE, + identifiers={(DOMAIN,)}, + manufacturer="Met.no", + model="Forecast", + configuration_url="https://www.met.no/en", + ) + + assert await hass.config_entries.async_reload(entry.entry_id) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert not device_reg.async_get_device(identifiers={(DOMAIN,)}) + assert device_reg.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) + assert "Removing improper device Forecast_legacy" in caplog.text From d76c16fa3a2e74cc9df9e43ce1d297589774b3e9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 31 Oct 2023 10:05:16 +0100 Subject: [PATCH 070/265] Update frontend to 20231030.0 (#103086) --- 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 a47ef38264ee..b1eaaaf77e11 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20231027.0"] + "requirements": ["home-assistant-frontend==20231030.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5d68cead747e..cd1623c7d0df 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.74.0 hassil==1.2.5 home-assistant-bluetooth==1.10.4 -home-assistant-frontend==20231027.0 +home-assistant-frontend==20231030.0 home-assistant-intents==2023.10.16 httpx==0.25.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 51697865b8aa..4adfc7113097 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1007,7 +1007,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20231027.0 +home-assistant-frontend==20231030.0 # homeassistant.components.conversation home-assistant-intents==2023.10.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1328f70970e3..aa37475486d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -796,7 +796,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20231027.0 +home-assistant-frontend==20231030.0 # homeassistant.components.conversation home-assistant-intents==2023.10.16 From abaeacbd6b690c227de3ebb28f65e0b60a8502ff Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 Oct 2023 10:05:03 +0100 Subject: [PATCH 071/265] Fix restore state for light when saved attribute is None (#103096) --- .../components/light/reproduce_state.py | 18 +++---- .../components/light/test_reproduce_state.py | 50 +++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 15141b6d4288..f055f02ebdaf 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -149,31 +149,29 @@ async def _async_reproduce_state( service = SERVICE_TURN_ON for attr in ATTR_GROUP: # All attributes that are not colors - if attr in state.attributes: - service_data[attr] = state.attributes[attr] + if (attr_state := state.attributes.get(attr)) is not None: + service_data[attr] = attr_state if ( state.attributes.get(ATTR_COLOR_MODE, ColorMode.UNKNOWN) != ColorMode.UNKNOWN ): color_mode = state.attributes[ATTR_COLOR_MODE] - if color_mode_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode): - if color_mode_attr.state_attr not in state.attributes: + if cm_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode): + if (cm_attr_state := state.attributes.get(cm_attr.state_attr)) is None: _LOGGER.warning( "Color mode %s specified but attribute %s missing for: %s", color_mode, - color_mode_attr.state_attr, + cm_attr.state_attr, state.entity_id, ) return - service_data[color_mode_attr.parameter] = state.attributes[ - color_mode_attr.state_attr - ] + service_data[cm_attr.parameter] = cm_attr_state else: # Fall back to Choosing the first color that is specified for color_attr in COLOR_GROUP: - if color_attr in state.attributes: - service_data[color_attr] = state.attributes[color_attr] + if (color_attr_state := state.attributes.get(color_attr)) is not None: + service_data[color_attr] = color_attr_state break elif state.state == STATE_OFF: diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index f36b8180560c..816bde430e77 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -22,6 +22,20 @@ VALID_RGBW_COLOR = {"rgbw_color": (255, 63, 111, 10)} VALID_RGBWW_COLOR = {"rgbww_color": (255, 63, 111, 10, 20)} VALID_XY_COLOR = {"xy_color": (0.59, 0.274)} +NONE_BRIGHTNESS = {"brightness": None} +NONE_FLASH = {"flash": None} +NONE_EFFECT = {"effect": None} +NONE_TRANSITION = {"transition": None} +NONE_COLOR_NAME = {"color_name": None} +NONE_COLOR_TEMP = {"color_temp": None} +NONE_HS_COLOR = {"hs_color": None} +NONE_KELVIN = {"kelvin": None} +NONE_PROFILE = {"profile": None} +NONE_RGB_COLOR = {"rgb_color": None} +NONE_RGBW_COLOR = {"rgbw_color": None} +NONE_RGBWW_COLOR = {"rgbww_color": None} +NONE_XY_COLOR = {"xy_color": None} + async def test_reproducing_states( hass: HomeAssistant, caplog: pytest.LogCaptureFixture @@ -237,3 +251,39 @@ async def test_deprecation_warning( ) assert len(turn_on_calls) == 1 assert DEPRECATION_WARNING % ["brightness_pct"] in caplog.text + + +@pytest.mark.parametrize( + "saved_state", + ( + NONE_BRIGHTNESS, + NONE_FLASH, + NONE_EFFECT, + NONE_TRANSITION, + NONE_COLOR_NAME, + NONE_COLOR_TEMP, + NONE_HS_COLOR, + NONE_KELVIN, + NONE_PROFILE, + NONE_RGB_COLOR, + NONE_RGBW_COLOR, + NONE_RGBWW_COLOR, + NONE_XY_COLOR, + ), +) +async def test_filter_none(hass: HomeAssistant, saved_state) -> None: + """Test filtering of parameters which are None.""" + hass.states.async_set("light.entity", "off", {}) + + turn_on_calls = async_mock_service(hass, "light", "turn_on") + + await async_reproduce_state(hass, [State("light.entity", "on", saved_state)]) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "light" + assert dict(turn_on_calls[0].data) == {"entity_id": "light.entity"} + + # This should do nothing, the light is already in the desired state + hass.states.async_set("light.entity", "on", {}) + await async_reproduce_state(hass, [State("light.entity", "on", saved_state)]) + assert len(turn_on_calls) == 1 From 957998ea8dcd83942b10744c120e641718be01d2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 Oct 2023 02:03:34 +0100 Subject: [PATCH 072/265] Fix google_tasks todo tests (#103098) --- tests/components/google_tasks/test_todo.py | 28 +++++++++++++--------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/components/google_tasks/test_todo.py b/tests/components/google_tasks/test_todo.py index 5dc7f10fea06..e19ac1272cd5 100644 --- a/tests/components/google_tasks/test_todo.py +++ b/tests/components/google_tasks/test_todo.py @@ -30,6 +30,12 @@ LIST_TASKS_RESPONSE = { "items": [], } +LIST_TASKS_RESPONSE_WATER = { + "items": [ + {"id": "some-task-id", "title": "Water", "status": "needsAction"}, + ], +} + @pytest.fixture def platforms() -> list[str]: @@ -198,8 +204,8 @@ async def test_create_todo_list_item( await hass.services.async_call( TODO_DOMAIN, - "create_item", - {"summary": "Soda"}, + "add_item", + {"item": "Soda"}, target={"entity_id": "todo.my_tasks"}, blocking=True, ) @@ -215,7 +221,7 @@ async def test_create_todo_list_item( [ [ LIST_TASK_LIST_RESPONSE, - LIST_TASKS_RESPONSE, + LIST_TASKS_RESPONSE_WATER, EMPTY_RESPONSE, # update LIST_TASKS_RESPONSE, # refresh after update ] @@ -234,12 +240,12 @@ async def test_update_todo_list_item( state = hass.states.get("todo.my_tasks") assert state - assert state.state == "0" + assert state.state == "1" await hass.services.async_call( TODO_DOMAIN, "update_item", - {"uid": "some-task-id", "summary": "Soda", "status": "completed"}, + {"item": "some-task-id", "rename": "Soda", "status": "completed"}, target={"entity_id": "todo.my_tasks"}, blocking=True, ) @@ -255,7 +261,7 @@ async def test_update_todo_list_item( [ [ LIST_TASK_LIST_RESPONSE, - LIST_TASKS_RESPONSE, + LIST_TASKS_RESPONSE_WATER, EMPTY_RESPONSE, # update LIST_TASKS_RESPONSE, # refresh after update ] @@ -274,12 +280,12 @@ async def test_partial_update_title( state = hass.states.get("todo.my_tasks") assert state - assert state.state == "0" + assert state.state == "1" await hass.services.async_call( TODO_DOMAIN, "update_item", - {"uid": "some-task-id", "summary": "Soda"}, + {"item": "some-task-id", "rename": "Soda"}, target={"entity_id": "todo.my_tasks"}, blocking=True, ) @@ -295,7 +301,7 @@ async def test_partial_update_title( [ [ LIST_TASK_LIST_RESPONSE, - LIST_TASKS_RESPONSE, + LIST_TASKS_RESPONSE_WATER, EMPTY_RESPONSE, # update LIST_TASKS_RESPONSE, # refresh after update ] @@ -314,12 +320,12 @@ async def test_partial_update_status( state = hass.states.get("todo.my_tasks") assert state - assert state.state == "0" + assert state.state == "1" await hass.services.async_call( TODO_DOMAIN, "update_item", - {"uid": "some-task-id", "status": "needs_action"}, + {"item": "some-task-id", "status": "needs_action"}, target={"entity_id": "todo.my_tasks"}, blocking=True, ) From 26b7e94c4f19e26e809e02b5145db8a66b57b3c7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 Oct 2023 02:03:54 +0100 Subject: [PATCH 073/265] Fix shopping_list todo tests (#103100) --- tests/components/shopping_list/test_todo.py | 83 ++++++++------------- 1 file changed, 31 insertions(+), 52 deletions(-) diff --git a/tests/components/shopping_list/test_todo.py b/tests/components/shopping_list/test_todo.py index ab28c6cbe6da..681ccea60ac8 100644 --- a/tests/components/shopping_list/test_todo.py +++ b/tests/components/shopping_list/test_todo.py @@ -7,7 +7,6 @@ import pytest from homeassistant.components.todo import DOMAIN as TODO_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from tests.typing import WebSocketGenerator @@ -115,18 +114,18 @@ async def test_get_items( assert state.state == "1" -async def test_create_item( +async def test_add_item( hass: HomeAssistant, sl_setup: None, ws_req_id: Callable[[], int], ws_get_items: Callable[[], Awaitable[dict[str, str]]], ) -> None: - """Test creating shopping_list item and listing it.""" + """Test adding shopping_list item and listing it.""" await hass.services.async_call( TODO_DOMAIN, - "create_item", + "add_item", { - "summary": "soda", + "item": "soda", }, target={"entity_id": TEST_ENTITY}, blocking=True, @@ -142,38 +141,18 @@ async def test_create_item( assert state assert state.state == "1" - # Add a completed item - await hass.services.async_call( - TODO_DOMAIN, - "create_item", - {"summary": "paper", "status": "completed"}, - target={"entity_id": TEST_ENTITY}, - blocking=True, - ) - items = await ws_get_items() - assert len(items) == 2 - assert items[0]["summary"] == "soda" - assert items[0]["status"] == "needs_action" - assert items[1]["summary"] == "paper" - assert items[1]["status"] == "completed" - - state = hass.states.get(TEST_ENTITY) - assert state - assert state.state == "1" - - -async def test_delete_item( +async def test_remove_item( hass: HomeAssistant, sl_setup: None, ws_req_id: Callable[[], int], ws_get_items: Callable[[], Awaitable[dict[str, str]]], ) -> None: - """Test deleting a todo item.""" + """Test removing a todo item.""" await hass.services.async_call( TODO_DOMAIN, - "create_item", - {"summary": "soda", "status": "needs_action"}, + "add_item", + {"item": "soda"}, target={"entity_id": TEST_ENTITY}, blocking=True, ) @@ -189,9 +168,9 @@ async def test_delete_item( await hass.services.async_call( TODO_DOMAIN, - "delete_item", + "remove_item", { - "uid": [items[0]["uid"]], + "item": [items[0]["uid"]], }, target={"entity_id": TEST_ENTITY}, blocking=True, @@ -205,20 +184,20 @@ async def test_delete_item( assert state.state == "0" -async def test_bulk_delete( +async def test_bulk_remove( hass: HomeAssistant, sl_setup: None, ws_req_id: Callable[[], int], ws_get_items: Callable[[], Awaitable[dict[str, str]]], ) -> None: - """Test deleting a todo item.""" + """Test removing a todo item.""" for _i in range(0, 5): await hass.services.async_call( TODO_DOMAIN, - "create_item", + "add_item", { - "summary": "soda", + "item": "soda", }, target={"entity_id": TEST_ENTITY}, blocking=True, @@ -234,9 +213,9 @@ async def test_bulk_delete( await hass.services.async_call( TODO_DOMAIN, - "delete_item", + "remove_item", { - "uid": uids, + "item": uids, }, target={"entity_id": TEST_ENTITY}, blocking=True, @@ -261,9 +240,9 @@ async def test_update_item( # Create new item await hass.services.async_call( TODO_DOMAIN, - "create_item", + "add_item", { - "summary": "soda", + "item": "soda", }, target={"entity_id": TEST_ENTITY}, blocking=True, @@ -285,7 +264,7 @@ async def test_update_item( TODO_DOMAIN, "update_item", { - **item, + "item": "soda", "status": "completed", }, target={"entity_id": TEST_ENTITY}, @@ -315,9 +294,9 @@ async def test_partial_update_item( # Create new item await hass.services.async_call( TODO_DOMAIN, - "create_item", + "add_item", { - "summary": "soda", + "item": "soda", }, target={"entity_id": TEST_ENTITY}, blocking=True, @@ -339,7 +318,7 @@ async def test_partial_update_item( TODO_DOMAIN, "update_item", { - "uid": item["uid"], + "item": item["uid"], "status": "completed", }, target={"entity_id": TEST_ENTITY}, @@ -362,8 +341,8 @@ async def test_partial_update_item( TODO_DOMAIN, "update_item", { - "uid": item["uid"], - "summary": "other summary", + "item": item["uid"], + "rename": "other summary", }, target={"entity_id": TEST_ENTITY}, blocking=True, @@ -389,13 +368,13 @@ async def test_update_invalid_item( ) -> None: """Test updating a todo item that does not exist.""" - with pytest.raises(HomeAssistantError, match="was not found"): + with pytest.raises(ValueError, match="Unable to find"): await hass.services.async_call( TODO_DOMAIN, "update_item", { - "uid": "invalid-uid", - "summary": "Example task", + "item": "invalid-uid", + "rename": "Example task", }, target={"entity_id": TEST_ENTITY}, blocking=True, @@ -443,9 +422,9 @@ async def test_move_item( for i in range(1, 5): await hass.services.async_call( TODO_DOMAIN, - "create_item", + "add_item", { - "summary": f"item {i}", + "item": f"item {i}", }, target={"entity_id": TEST_ENTITY}, blocking=True, @@ -481,8 +460,8 @@ async def test_move_invalid_item( await hass.services.async_call( TODO_DOMAIN, - "create_item", - {"summary": "soda"}, + "add_item", + {"item": "soda"}, target={"entity_id": TEST_ENTITY}, blocking=True, ) From fa0f679a9a92119434b17b625be1815763004592 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 31 Oct 2023 10:06:42 +0100 Subject: [PATCH 074/265] Fix todo.remove_item frontend (#103108) --- homeassistant/components/todo/services.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/todo/services.yaml b/homeassistant/components/todo/services.yaml index 4d6237760ca5..1bdb8aca7792 100644 --- a/homeassistant/components/todo/services.yaml +++ b/homeassistant/components/todo/services.yaml @@ -42,5 +42,6 @@ remove_item: - todo.TodoListEntityFeature.DELETE_TODO_ITEM fields: item: + required: true selector: - object: + text: From 777ffe694605ba0a9adffe7e3896dbf9bb5ecd1d Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Tue, 31 Oct 2023 10:35:51 +0100 Subject: [PATCH 075/265] Fix client id label in ViCare integration (#103111) --- homeassistant/components/vicare/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index 0700d5d6f0e6..056a4df7920e 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -3,11 +3,11 @@ "flow_title": "{name} ({host})", "step": { "user": { - "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com", + "description": "Set up ViCare integration. To generate client ID go to https://app.developer.viessmann.com", "data": { "username": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]", - "client_id": "[%key:common::config_flow::data::api_key%]", + "client_id": "Client ID", "heating_type": "Heating type" } } From e309bd764be068c1899eb1e94ff2cfc1a000b58e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 31 Oct 2023 11:32:17 +0100 Subject: [PATCH 076/265] Abort config flow if Google Tasks API is not enabled (#103114) Co-authored-by: Martin Hjelmare --- .../components/google_tasks/config_flow.py | 28 ++++ .../components/google_tasks/strings.json | 4 +- .../fixtures/api_not_enabled_response.json | 15 +++ .../google_tasks/test_config_flow.py | 123 +++++++++++++++++- 4 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 tests/components/google_tasks/fixtures/api_not_enabled_response.json diff --git a/homeassistant/components/google_tasks/config_flow.py b/homeassistant/components/google_tasks/config_flow.py index 77570f0377f7..b8e5e26f42c6 100644 --- a/homeassistant/components/google_tasks/config_flow.py +++ b/homeassistant/components/google_tasks/config_flow.py @@ -2,6 +2,13 @@ import logging from typing import Any +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError +from googleapiclient.http import HttpRequest + +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN, OAUTH2_SCOPES @@ -28,3 +35,24 @@ class OAuth2FlowHandler( "access_type": "offline", "prompt": "consent", } + + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + """Create an entry for the flow.""" + try: + resource = build( + "tasks", + "v1", + credentials=Credentials(token=data[CONF_TOKEN][CONF_ACCESS_TOKEN]), + ) + cmd: HttpRequest = resource.tasklists().list() + await self.hass.async_add_executor_job(cmd.execute) + except HttpError as ex: + error = ex.reason + return self.async_abort( + reason="access_not_configured", + description_placeholders={"message": error}, + ) + except Exception as ex: # pylint: disable=broad-except + self.logger.exception("Unknown error occurred: %s", ex) + return self.async_abort(reason="unknown") + return self.async_create_entry(title=self.flow_impl.name, data=data) diff --git a/homeassistant/components/google_tasks/strings.json b/homeassistant/components/google_tasks/strings.json index e7dbbc2b6251..f15c31f42d4c 100644 --- a/homeassistant/components/google_tasks/strings.json +++ b/homeassistant/components/google_tasks/strings.json @@ -15,7 +15,9 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", - "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]" + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", + "access_not_configured": "Unable to access the Google API:\n\n{message}", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/tests/components/google_tasks/fixtures/api_not_enabled_response.json b/tests/components/google_tasks/fixtures/api_not_enabled_response.json new file mode 100644 index 000000000000..75ecfddab20d --- /dev/null +++ b/tests/components/google_tasks/fixtures/api_not_enabled_response.json @@ -0,0 +1,15 @@ +{ + "error": { + "code": 403, + "message": "Google Tasks API has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/tasks.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.", + "errors": [ + { + "message": "Google Tasks API has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/tasks.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.", + "domain": "usageLimits", + "reason": "accessNotConfigured", + "extendedHelp": "https://console.developers.google.com" + } + ], + "status": "PERMISSION_DENIED" + } +} diff --git a/tests/components/google_tasks/test_config_flow.py b/tests/components/google_tasks/test_config_flow.py index b05e1eb108d2..e92da6056971 100644 --- a/tests/components/google_tasks/test_config_flow.py +++ b/tests/components/google_tasks/test_config_flow.py @@ -2,6 +2,9 @@ from unittest.mock import patch +from googleapiclient.errors import HttpError +from httplib2 import Response + from homeassistant import config_entries from homeassistant.components.google_tasks.const import ( DOMAIN, @@ -9,8 +12,11 @@ from homeassistant.components.google_tasks.const import ( OAUTH2_TOKEN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from tests.common import load_fixture + CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -59,8 +65,119 @@ async def test_full_flow( with patch( "homeassistant.components.google_tasks.async_setup_entry", return_value=True - ) as mock_setup: - await hass.config_entries.flow.async_configure(result["flow_id"]) - + ) as mock_setup, patch("homeassistant.components.google_tasks.config_flow.build"): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.CREATE_ENTRY assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 + + +async def test_api_not_enabled( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, +) -> None: + """Check flow aborts if api is not enabled.""" + result = await hass.config_entries.flow.async_init( + "google_tasks", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=https://www.googleapis.com/auth/tasks" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.google_tasks.config_flow.build", + side_effect=HttpError( + Response({"status": "403"}), + bytes(load_fixture("google_tasks/api_not_enabled_response.json"), "utf-8"), + ), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "access_not_configured" + assert ( + result["description_placeholders"]["message"] + == "Google Tasks API has not been used in project 0 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/tasks.googleapis.com/overview?project=0 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." + ) + + +async def test_general_exception( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, + setup_credentials, +) -> None: + """Check flow aborts if exception happens.""" + result = await hass.config_entries.flow.async_init( + "google_tasks", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=https://www.googleapis.com/auth/tasks" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.google_tasks.config_flow.build", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "unknown" From 9ae29e243df7fdeb7e8c26ca67f41f77a769adc4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Oct 2023 13:30:10 +0100 Subject: [PATCH 077/265] Bumped version to 2023.11.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 4587f1d37a15..a21d7a8f647e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index d6070c019aef..b479c69aa54f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.11.0b3" +version = "2023.11.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 19479b2a68654fb51c23059ac9cd0b30cde2f96e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 Oct 2023 02:38:58 +0100 Subject: [PATCH 078/265] Fix local_todo todo tests (#103099) --- tests/components/local_todo/test_todo.py | 46 ++++++++++++------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/components/local_todo/test_todo.py b/tests/components/local_todo/test_todo.py index 8a7e38c9773d..39e9264d45a0 100644 --- a/tests/components/local_todo/test_todo.py +++ b/tests/components/local_todo/test_todo.py @@ -79,13 +79,13 @@ async def ws_move_item( return move -async def test_create_item( +async def test_add_item( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, setup_integration: None, ws_get_items: Callable[[], Awaitable[dict[str, str]]], ) -> None: - """Test creating a todo item.""" + """Test adding a todo item.""" state = hass.states.get(TEST_ENTITY) assert state @@ -93,8 +93,8 @@ async def test_create_item( await hass.services.async_call( TODO_DOMAIN, - "create_item", - {"summary": "replace batteries"}, + "add_item", + {"item": "replace batteries"}, target={"entity_id": TEST_ENTITY}, blocking=True, ) @@ -110,16 +110,16 @@ async def test_create_item( assert state.state == "1" -async def test_delete_item( +async def test_remove_item( hass: HomeAssistant, setup_integration: None, ws_get_items: Callable[[], Awaitable[dict[str, str]]], ) -> None: - """Test deleting a todo item.""" + """Test removing a todo item.""" await hass.services.async_call( TODO_DOMAIN, - "create_item", - {"summary": "replace batteries"}, + "add_item", + {"item": "replace batteries"}, target={"entity_id": TEST_ENTITY}, blocking=True, ) @@ -136,8 +136,8 @@ async def test_delete_item( await hass.services.async_call( TODO_DOMAIN, - "delete_item", - {"uid": [items[0]["uid"]]}, + "remove_item", + {"item": [items[0]["uid"]]}, target={"entity_id": TEST_ENTITY}, blocking=True, ) @@ -150,17 +150,17 @@ async def test_delete_item( assert state.state == "0" -async def test_bulk_delete( +async def test_bulk_remove( hass: HomeAssistant, setup_integration: None, ws_get_items: Callable[[], Awaitable[dict[str, str]]], ) -> None: - """Test deleting multiple todo items.""" + """Test removing multiple todo items.""" for i in range(0, 5): await hass.services.async_call( TODO_DOMAIN, - "create_item", - {"summary": f"soda #{i}"}, + "add_item", + {"item": f"soda #{i}"}, target={"entity_id": TEST_ENTITY}, blocking=True, ) @@ -175,8 +175,8 @@ async def test_bulk_delete( await hass.services.async_call( TODO_DOMAIN, - "delete_item", - {"uid": uids}, + "remove_item", + {"item": uids}, target={"entity_id": TEST_ENTITY}, blocking=True, ) @@ -199,8 +199,8 @@ async def test_update_item( # Create new item await hass.services.async_call( TODO_DOMAIN, - "create_item", - {"summary": "soda"}, + "add_item", + {"item": "soda"}, target={"entity_id": TEST_ENTITY}, blocking=True, ) @@ -220,7 +220,7 @@ async def test_update_item( await hass.services.async_call( TODO_DOMAIN, "update_item", - {"uid": item["uid"], "status": "completed"}, + {"item": item["uid"], "status": "completed"}, target={"entity_id": TEST_ENTITY}, blocking=True, ) @@ -276,8 +276,8 @@ async def test_move_item( for i in range(1, 5): await hass.services.async_call( TODO_DOMAIN, - "create_item", - {"summary": f"item {i}"}, + "add_item", + {"item": f"item {i}"}, target={"entity_id": TEST_ENTITY}, blocking=True, ) @@ -334,8 +334,8 @@ async def test_move_item_previous_unknown( await hass.services.async_call( TODO_DOMAIN, - "create_item", - {"summary": "item 1"}, + "add_item", + {"item": "item 1"}, target={"entity_id": TEST_ENTITY}, blocking=True, ) From a48e63aa28582f24d1a0948953361ce5c8cff0c6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 Oct 2023 02:38:18 +0100 Subject: [PATCH 079/265] Fix todoist todo tests (#103101) --- tests/components/todoist/test_todo.py | 46 +++++++-------------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/tests/components/todoist/test_todo.py b/tests/components/todoist/test_todo.py index bbfaf6c493ba..a14f362ea5be 100644 --- a/tests/components/todoist/test_todo.py +++ b/tests/components/todoist/test_todo.py @@ -56,12 +56,12 @@ async def test_todo_item_state( @pytest.mark.parametrize(("tasks"), [[]]) -async def test_create_todo_list_item( +async def test_add_todo_list_item( hass: HomeAssistant, setup_integration: None, api: AsyncMock, ) -> None: - """Test for creating a To-do Item.""" + """Test for adding a To-do Item.""" state = hass.states.get("todo.name") assert state @@ -75,8 +75,8 @@ async def test_create_todo_list_item( await hass.services.async_call( TODO_DOMAIN, - "create_item", - {"summary": "Soda"}, + "add_item", + {"item": "Soda"}, target={"entity_id": "todo.name"}, blocking=True, ) @@ -92,30 +92,6 @@ async def test_create_todo_list_item( assert state.state == "1" -@pytest.mark.parametrize(("tasks"), [[]]) -async def test_create_completed_item_unsupported( - hass: HomeAssistant, - setup_integration: None, - api: AsyncMock, -) -> None: - """Test for creating a To-do Item that is already completed.""" - - state = hass.states.get("todo.name") - assert state - assert state.state == "0" - - api.add_task = AsyncMock() - - with pytest.raises(ValueError, match="Only active tasks"): - await hass.services.async_call( - TODO_DOMAIN, - "create_item", - {"summary": "Soda", "status": "completed"}, - target={"entity_id": "todo.name"}, - blocking=True, - ) - - @pytest.mark.parametrize( ("tasks"), [[make_api_task(id="task-id-1", content="Soda", is_completed=False)]] ) @@ -141,7 +117,7 @@ async def test_update_todo_item_status( await hass.services.async_call( TODO_DOMAIN, "update_item", - {"uid": "task-id-1", "status": "completed"}, + {"item": "task-id-1", "status": "completed"}, target={"entity_id": "todo.name"}, blocking=True, ) @@ -164,7 +140,7 @@ async def test_update_todo_item_status( await hass.services.async_call( TODO_DOMAIN, "update_item", - {"uid": "task-id-1", "status": "needs_action"}, + {"item": "task-id-1", "status": "needs_action"}, target={"entity_id": "todo.name"}, blocking=True, ) @@ -203,7 +179,7 @@ async def test_update_todo_item_summary( await hass.services.async_call( TODO_DOMAIN, "update_item", - {"uid": "task-id-1", "summary": "Milk"}, + {"item": "task-id-1", "rename": "Milk"}, target={"entity_id": "todo.name"}, blocking=True, ) @@ -223,12 +199,12 @@ async def test_update_todo_item_summary( ] ], ) -async def test_delete_todo_item( +async def test_remove_todo_item( hass: HomeAssistant, setup_integration: None, api: AsyncMock, ) -> None: - """Test for deleting a To-do Item.""" + """Test for removing a To-do Item.""" state = hass.states.get("todo.name") assert state @@ -240,8 +216,8 @@ async def test_delete_todo_item( await hass.services.async_call( TODO_DOMAIN, - "delete_item", - {"uid": ["task-id-1", "task-id-2"]}, + "remove_item", + {"item": ["task-id-1", "task-id-2"]}, target={"entity_id": "todo.name"}, blocking=True, ) From 040ecb74e0a9d07211433b67973afee71b0882c1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 Oct 2023 07:55:03 +0100 Subject: [PATCH 080/265] Add todo to core files (#103102) --- .core_files.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.core_files.yaml b/.core_files.yaml index b3e854de04ba..f5ffdee91425 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -45,6 +45,7 @@ base_platforms: &base_platforms - homeassistant/components/switch/** - homeassistant/components/text/** - homeassistant/components/time/** + - homeassistant/components/todo/** - homeassistant/components/tts/** - homeassistant/components/update/** - homeassistant/components/vacuum/** From 09ed6e9f9b57ece487315a269971074f2d0f5e97 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Tue, 31 Oct 2023 13:42:52 +0100 Subject: [PATCH 081/265] Handle exception introduced with recent PyViCare update (#103110) --- .../components/vicare/config_flow.py | 7 +++++-- tests/components/vicare/test_config_flow.py | 21 ++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py index a0feb8f38eaf..5b2d3afa427f 100644 --- a/homeassistant/components/vicare/config_flow.py +++ b/homeassistant/components/vicare/config_flow.py @@ -4,7 +4,10 @@ from __future__ import annotations import logging from typing import Any -from PyViCare.PyViCareUtils import PyViCareInvalidCredentialsError +from PyViCare.PyViCareUtils import ( + PyViCareInvalidConfigurationError, + PyViCareInvalidCredentialsError, +) import voluptuous as vol from homeassistant import config_entries @@ -53,7 +56,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.hass.async_add_executor_job( vicare_login, self.hass, user_input ) - except PyViCareInvalidCredentialsError: + except (PyViCareInvalidConfigurationError, PyViCareInvalidCredentialsError): errors["base"] = "invalid_auth" else: return self.async_create_entry(title=VICARE_NAME, data=user_input) diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index 0774848ef116..7f70c13f0b01 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -2,7 +2,10 @@ from unittest.mock import AsyncMock, patch import pytest -from PyViCare.PyViCareUtils import PyViCareInvalidCredentialsError +from PyViCare.PyViCareUtils import ( + PyViCareInvalidConfigurationError, + PyViCareInvalidCredentialsError, +) from syrupy.assertion import SnapshotAssertion from homeassistant.components import dhcp @@ -43,6 +46,22 @@ async def test_user_create_entry( assert result["step_id"] == "user" assert result["errors"] == {} + # test PyViCareInvalidConfigurationError + with patch( + f"{MODULE}.config_flow.vicare_login", + side_effect=PyViCareInvalidConfigurationError( + {"error": "foo", "error_description": "bar"} + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + # test PyViCareInvalidCredentialsError with patch( f"{MODULE}.config_flow.vicare_login", From bfae1468d6fdac1b709dea87493cfca37e67ba2e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 31 Oct 2023 15:15:20 +0100 Subject: [PATCH 082/265] Bump reolink-aio to 0.7.12 (#103120) --- 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 9d9d8d59e88c..1c1d8dd96b12 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.11"] + "requirements": ["reolink-aio==0.7.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4adfc7113097..dff43a674508 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2319,7 +2319,7 @@ renault-api==0.2.0 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.7.11 +reolink-aio==0.7.12 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa37475486d2..6799e4782c39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1730,7 +1730,7 @@ renault-api==0.2.0 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.7.11 +reolink-aio==0.7.12 # homeassistant.components.rflink rflink==0.0.65 From e097dc02dd2fb12b958fbc2c149a6ca9edf4d134 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 31 Oct 2023 19:25:25 +0100 Subject: [PATCH 083/265] Don't try to load resources in safe mode (#103122) --- homeassistant/components/lovelace/websocket.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index c9b7cb103866..b756c2765e1c 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -62,6 +62,7 @@ async def websocket_lovelace_resources( if hass.config.safe_mode: connection.send_result(msg["id"], []) + return if not resources.loaded: await resources.async_load() From 8992d15ffc133d97988615d8f17c9bb2976bf546 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Oct 2023 12:56:15 -0500 Subject: [PATCH 084/265] Bump aiohomekit to 3.0.9 (#103123) --- homeassistant/components/homekit_controller/connection.py | 4 +++- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 1d0eb9cdd839..ef806cb52bc1 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -884,7 +884,9 @@ class HKDevice: self._config_changed_callbacks.add(callback_) return partial(self._remove_config_changed_callback, callback_) - async def get_characteristics(self, *args: Any, **kwargs: Any) -> dict[str, Any]: + async def get_characteristics( + self, *args: Any, **kwargs: Any + ) -> dict[tuple[int, int], dict[str, Any]]: """Read latest state from homekit accessory.""" return await self.pairing.get_characteristics(*args, **kwargs) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index ff918396640d..91fd199e17c0 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], - "requirements": ["aiohomekit==3.0.8"], + "requirements": ["aiohomekit==3.0.9"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index dff43a674508..751b7af15989 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -255,7 +255,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.0.8 +aiohomekit==3.0.9 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6799e4782c39..065a0a44d73b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -233,7 +233,7 @@ aioguardian==2022.07.0 aioharmony==0.2.10 # homeassistant.components.homekit_controller -aiohomekit==3.0.8 +aiohomekit==3.0.9 # homeassistant.components.emulated_hue # homeassistant.components.http From f0a06efa1f90892b994d597de400a76781129e0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Oct 2023 12:38:05 -0500 Subject: [PATCH 085/265] Fix race in starting reauth flows (#103130) --- homeassistant/config_entries.py | 31 +++++++++++++++++++++----- tests/components/smarttub/test_init.py | 1 + tests/test_config_entries.py | 14 ++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 02a9dd9dade6..2b8f1ec4065a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -223,6 +223,7 @@ class ConfigEntry: "_async_cancel_retry_setup", "_on_unload", "reload_lock", + "_reauth_lock", "_tasks", "_background_tasks", "_integration_for_domain", @@ -321,6 +322,8 @@ class ConfigEntry: # Reload lock to prevent conflicting reloads self.reload_lock = asyncio.Lock() + # Reauth lock to prevent concurrent reauth flows + self._reauth_lock = asyncio.Lock() self._tasks: set[asyncio.Future[Any]] = set() self._background_tasks: set[asyncio.Future[Any]] = set() @@ -727,12 +730,28 @@ class ConfigEntry: data: dict[str, Any] | None = None, ) -> None: """Start a reauth flow.""" + # We will check this again in the task when we hold the lock, + # but we also check it now to try to avoid creating the task. if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})): # Reauth flow already in progress for this entry return - hass.async_create_task( - hass.config_entries.flow.async_init( + self._async_init_reauth(hass, context, data), + f"config entry reauth {self.title} {self.domain} {self.entry_id}", + ) + + async def _async_init_reauth( + self, + hass: HomeAssistant, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + ) -> None: + """Start a reauth flow.""" + async with self._reauth_lock: + if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})): + # Reauth flow already in progress for this entry + return + await hass.config_entries.flow.async_init( self.domain, context={ "source": SOURCE_REAUTH, @@ -742,9 +761,7 @@ class ConfigEntry: } | (context or {}), data=self.data | (data or {}), - ), - f"config entry reauth {self.title} {self.domain} {self.entry_id}", - ) + ) @callback def async_get_active_flows( @@ -754,7 +771,9 @@ class ConfigEntry: return ( flow for flow in hass.config_entries.flow.async_progress_by_handler( - self.domain, match_context={"entry_id": self.entry_id} + self.domain, + match_context={"entry_id": self.entry_id}, + include_uninitialized=True, ) if flow["context"].get("source") in sources ) diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py index 0e88f3ed7c7b..929ad687e11e 100644 --- a/tests/components/smarttub/test_init.py +++ b/tests/components/smarttub/test_init.py @@ -42,6 +42,7 @@ async def test_setup_auth_failed( config_entry.add_to_hass(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.SETUP_ERROR mock_flow_init.assert_called_with( DOMAIN, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d17c724cb2aa..eb771b7e6a67 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3791,6 +3791,20 @@ async def test_reauth(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress()) == 2 + # Abort all existing flows + for flow in hass.config_entries.flow.async_progress(): + hass.config_entries.flow.async_abort(flow["flow_id"]) + await hass.async_block_till_done() + + # Check that we can't start duplicate reauth flows + # without blocking between flows + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + async def test_get_active_flows(hass: HomeAssistant) -> None: """Test the async_get_active_flows helper.""" From 9910f9e0aedd34613a789b11ef65017816005312 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Oct 2023 19:43:21 +0100 Subject: [PATCH 086/265] Bumped version to 2023.11.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 a21d7a8f647e..8022275b8246 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index b479c69aa54f..61c18f5d4bad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.11.0b4" +version = "2023.11.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c5f21fefbe637e145e552281648a082ed1bb1537 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 1 Nov 2023 01:54:51 +0100 Subject: [PATCH 087/265] Bump python-kasa to 0.5.4 for tplink (#103038) --- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tplink/test_init.py | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index d13adb8ec47d..e0ac41bdec6f 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -169,5 +169,5 @@ "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", - "requirements": ["python-kasa[speedups]==0.5.3"] + "requirements": ["python-kasa[speedups]==0.5.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 751b7af15989..f664cb7e7df3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2141,7 +2141,7 @@ python-join-api==0.0.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.5.3 +python-kasa[speedups]==0.5.4 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 065a0a44d73b..c8c884e92b28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1597,7 +1597,7 @@ python-izone==1.2.9 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa[speedups]==0.5.3 +python-kasa[speedups]==0.5.4 # homeassistant.components.matter python-matter-server==4.0.0 diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 4206c0de6ad3..c40560d2a89c 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -29,7 +29,9 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_configuring_tplink_causes_discovery(hass: HomeAssistant) -> None: """Test that specifying empty config does discovery.""" - with patch("homeassistant.components.tplink.Discover.discover") as discover: + with patch("homeassistant.components.tplink.Discover.discover") as discover, patch( + "homeassistant.components.tplink.Discover.discover_single" + ): discover.return_value = {MagicMock(): MagicMock()} await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() From ea2d2ba7b7c8c2df11d27d19e560dc091f75a387 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 31 Oct 2023 19:48:33 -0700 Subject: [PATCH 088/265] Improve fitbit oauth token error handling in config flow (#103131) * Improve fitbit oauth token error handling in config flow * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Update tests with updated error reason --------- Co-authored-by: Martin Hjelmare --- .../fitbit/application_credentials.py | 13 +++-- .../components/fitbit/config_flow.py | 15 ++++++ homeassistant/components/fitbit/strings.json | 5 +- tests/components/fitbit/test_config_flow.py | 53 ++++++++++++++++++- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/fitbit/application_credentials.py b/homeassistant/components/fitbit/application_credentials.py index e66b9ca9014d..caf0384eca26 100644 --- a/homeassistant/components/fitbit/application_credentials.py +++ b/homeassistant/components/fitbit/application_credentials.py @@ -59,13 +59,16 @@ class FitbitOAuth2Implementation(AuthImplementation): resp = await session.post(self.token_url, data=data, headers=self._headers) resp.raise_for_status() except aiohttp.ClientResponseError as err: - error_body = await resp.text() - _LOGGER.debug("Client response error body: %s", error_body) + if _LOGGER.isEnabledFor(logging.DEBUG): + error_body = await resp.text() if not session.closed else "" + _LOGGER.debug( + "Client response error status=%s, body=%s", err.status, error_body + ) if err.status == HTTPStatus.UNAUTHORIZED: - raise FitbitAuthException from err - raise FitbitApiException from err + raise FitbitAuthException(f"Unauthorized error: {err}") from err + raise FitbitApiException(f"Server error response: {err}") from err except aiohttp.ClientError as err: - raise FitbitApiException from err + raise FitbitApiException(f"Client connection error: {err}") from err return cast(dict, await resp.json()) @property diff --git a/homeassistant/components/fitbit/config_flow.py b/homeassistant/components/fitbit/config_flow.py index ee2340e7587b..dd7e79e2c65a 100644 --- a/homeassistant/components/fitbit/config_flow.py +++ b/homeassistant/components/fitbit/config_flow.py @@ -53,6 +53,21 @@ class OAuth2FlowHandler( return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() + async def async_step_creation( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Create config entry from external data with Fitbit specific error handling.""" + try: + return await super().async_step_creation() + except FitbitAuthException as err: + _LOGGER.error( + "Failed to authenticate when creating Fitbit credentials: %s", err + ) + return self.async_abort(reason="invalid_auth") + except FitbitApiException as err: + _LOGGER.error("Failed to create Fitbit credentials: %s", err) + return self.async_abort(reason="cannot_connect") + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for the flow, or update existing entry.""" diff --git a/homeassistant/components/fitbit/strings.json b/homeassistant/components/fitbit/strings.json index 2d74408a73f1..889b56f1bbd6 100644 --- a/homeassistant/components/fitbit/strings.json +++ b/homeassistant/components/fitbit/strings.json @@ -16,9 +16,10 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]", "wrong_account": "The user credentials provided do not match this Fitbit account." diff --git a/tests/components/fitbit/test_config_flow.py b/tests/components/fitbit/test_config_flow.py index cf2d5d17f221..d51379c9adc0 100644 --- a/tests/components/fitbit/test_config_flow.py +++ b/tests/components/fitbit/test_config_flow.py @@ -88,6 +88,57 @@ async def test_full_flow( } +@pytest.mark.parametrize( + ("status_code", "error_reason"), + [ + (HTTPStatus.UNAUTHORIZED, "invalid_auth"), + (HTTPStatus.INTERNAL_SERVER_ERROR, "cannot_connect"), + ], +) +async def test_token_error( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + profile: None, + setup_credentials: None, + status_code: HTTPStatus, + error_reason: str, +) -> None: + """Check full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": REDIRECT_URL, + }, + ) + assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + f"&redirect_uri={REDIRECT_URL}" + f"&state={state}" + "&scope=activity+heartrate+nutrition+profile+settings+sleep+weight&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + status=status_code, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == error_reason + + @pytest.mark.parametrize( ("http_status", "json", "error_reason"), [ @@ -460,7 +511,7 @@ async def test_reauth_flow( "refresh_token": "updated-refresh-token", "access_token": "updated-access-token", "type": "Bearer", - "expires_in": 60, + "expires_in": "60", }, ) From bcea021c14422ce0dbcf4db8af5f403529f19137 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Oct 2023 16:29:22 -0500 Subject: [PATCH 089/265] Allow non-admins to subscribe to the issue registry updated event (#103145) --- homeassistant/auth/permissions/events.py | 2 ++ homeassistant/components/websocket_api/commands.py | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/auth/permissions/events.py b/homeassistant/auth/permissions/events.py index d50da96a39f0..aec233316649 100644 --- a/homeassistant/auth/permissions/events.py +++ b/homeassistant/auth/permissions/events.py @@ -19,6 +19,7 @@ from homeassistant.const import ( from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. @@ -28,6 +29,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[str]] = { EVENT_CORE_CONFIG_UPDATE, EVENT_DEVICE_REGISTRY_UPDATED, EVENT_ENTITY_REGISTRY_UPDATED, + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, EVENT_LOVELACE_UPDATED, EVENT_PANELS_UPDATED, EVENT_RECORDER_5MIN_STATISTICS_GENERATED, diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index a29bee86116f..2dfa48c28fe5 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -57,6 +57,8 @@ from .messages import construct_event_message, construct_result_message ALL_SERVICE_DESCRIPTIONS_JSON_CACHE = "websocket_api_all_service_descriptions_json" +_LOGGER = logging.getLogger(__name__) + @callback def async_register_commands( @@ -132,7 +134,12 @@ def handle_subscribe_events( event_type = msg["event_type"] if event_type not in SUBSCRIBE_ALLOWLIST and not connection.user.is_admin: - raise Unauthorized + _LOGGER.error( + "Refusing to allow %s to subscribe to event %s", + connection.user.name, + event_type, + ) + raise Unauthorized(user_id=connection.user.id) if event_type == EVENT_STATE_CHANGED: forward_events = callback( From aa5ea5ebc33e52dec5c666a19b42384a722ebad8 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 1 Nov 2023 09:25:56 +0100 Subject: [PATCH 090/265] Fix mqtt is not reloading without yaml config (#103159) --- homeassistant/components/mqtt/__init__.py | 2 +- tests/components/mqtt/test_init.py | 36 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ac229cb677fb..be283271dee1 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -233,7 +233,7 @@ async def async_check_config_schema( ) -> None: """Validate manually configured MQTT items.""" mqtt_data = get_mqtt_data(hass) - mqtt_config: list[dict[str, list[ConfigType]]] = config_yaml[DOMAIN] + mqtt_config: list[dict[str, list[ConfigType]]] = config_yaml.get(DOMAIN, {}) for mqtt_config_item in mqtt_config: for domain, config_items in mqtt_config_item.items(): schema = mqtt_data.reload_schema[domain] diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b071252ea641..2aa8de388b10 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -3974,3 +3974,39 @@ async def test_reload_with_invalid_config( # Test nothing changed as loading the config failed assert hass.states.get("sensor.test") is not None + + +@pytest.mark.parametrize( + "hass_config", + [ + { + "mqtt": [ + { + "sensor": { + "name": "test", + "state_topic": "test-topic", + } + }, + ] + } + ], +) +async def test_reload_with_empty_config( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test reloading yaml config fails.""" + await mqtt_mock_entry() + assert hass.states.get("sensor.test") is not None + + # Reload with an empty config and assert again + with patch("homeassistant.config.load_yaml_config_file", return_value={}): + await hass.services.async_call( + "mqtt", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.test") is None From cfa2f2ce6189e9c54d4fb83d5a572478cd46ee3b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 1 Nov 2023 11:05:17 +0100 Subject: [PATCH 091/265] Update frontend to 20231030.1 (#103163) --- 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 b1eaaaf77e11..6fffc0e8acda 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20231030.0"] + "requirements": ["home-assistant-frontend==20231030.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cd1623c7d0df..a70bcf4524a9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.74.0 hassil==1.2.5 home-assistant-bluetooth==1.10.4 -home-assistant-frontend==20231030.0 +home-assistant-frontend==20231030.1 home-assistant-intents==2023.10.16 httpx==0.25.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index f664cb7e7df3..77633056811f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1007,7 +1007,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20231030.0 +home-assistant-frontend==20231030.1 # homeassistant.components.conversation home-assistant-intents==2023.10.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8c884e92b28..666c3ea4dc6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -796,7 +796,7 @@ hole==0.8.0 holidays==0.28 # homeassistant.components.frontend -home-assistant-frontend==20231030.0 +home-assistant-frontend==20231030.1 # homeassistant.components.conversation home-assistant-intents==2023.10.16 From 18acec32b862def367c6de2d2807c5d186cc5662 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Nov 2023 11:22:25 +0100 Subject: [PATCH 092/265] Bumped version to 2023.11.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 8022275b8246..1ec9532e11f3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 61c18f5d4bad..aa2a3c66b0ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.11.0b5" +version = "2023.11.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4ce859b4e4ae68f21af51a21c194a5f6a36cd554 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Nov 2023 11:24:41 +0100 Subject: [PATCH 093/265] Bumped version to 2023.11.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 1ec9532e11f3..28241ef15f44 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index aa2a3c66b0ef..7efa6915a467 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.11.0b6" +version = "2023.11.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From acd98e9b407d875ee28f723652f75604d0aed00e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 1 Nov 2023 12:37:59 +0100 Subject: [PATCH 094/265] Bump python-holidays to 0.35 (#103092) --- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 698ef17902fd..1c9a533d998c 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.28"] + "requirements": ["holidays==0.35"] } diff --git a/requirements_all.txt b/requirements_all.txt index 77633056811f..17de99f535a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1004,7 +1004,7 @@ hlk-sw16==0.0.9 hole==0.8.0 # homeassistant.components.workday -holidays==0.28 +holidays==0.35 # homeassistant.components.frontend home-assistant-frontend==20231030.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 666c3ea4dc6f..3c23ab27a0d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -793,7 +793,7 @@ hlk-sw16==0.0.9 hole==0.8.0 # homeassistant.components.workday -holidays==0.28 +holidays==0.35 # homeassistant.components.frontend home-assistant-frontend==20231030.1 From 0c8074bab44bee9b43123f888273246f999c63fc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 1 Nov 2023 13:25:33 +0100 Subject: [PATCH 095/265] Bump aiowaqi to 3.0.0 (#103166) --- homeassistant/components/waqi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index 1cac5be375bd..f5731da2a7e2 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waqi", "iot_class": "cloud_polling", "loggers": ["aiowaqi"], - "requirements": ["aiowaqi==2.1.0"] + "requirements": ["aiowaqi==3.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 17de99f535a8..7545800d5dcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -378,7 +378,7 @@ aiovlc==0.1.0 aiovodafone==0.4.2 # homeassistant.components.waqi -aiowaqi==2.1.0 +aiowaqi==3.0.0 # homeassistant.components.watttime aiowatttime==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c23ab27a0d8..6ed2583ec3c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -353,7 +353,7 @@ aiovlc==0.1.0 aiovodafone==0.4.2 # homeassistant.components.waqi -aiowaqi==2.1.0 +aiowaqi==3.0.0 # homeassistant.components.watttime aiowatttime==0.1.1 From 355b51d4c8e700d5aadd6800f0cafdc0c53b2af8 Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 1 Nov 2023 10:41:41 -0400 Subject: [PATCH 096/265] Catch unexpected response in Honeywell (#103169) catch unexpected response --- homeassistant/components/honeywell/climate.py | 10 +++++++ tests/components/honeywell/test_climate.py | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index ab23c878c157..e9af4b2fd95a 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -353,6 +353,11 @@ class HoneywellUSThermostat(ClimateEntity): if mode == "heat": await self._device.set_setpoint_heat(temperature) + except UnexpectedResponse as err: + raise HomeAssistantError( + "Honeywell set temperature failed: Invalid Response" + ) from err + except SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) raise ValueError( @@ -369,6 +374,11 @@ class HoneywellUSThermostat(ClimateEntity): if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): await self._device.set_setpoint_heat(temperature) + except UnexpectedResponse as err: + raise HomeAssistantError( + "Honeywell set temperature failed: Invalid Response" + ) from err + except SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) raise ValueError( diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index 53cb70475c98..45ce862dba83 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -358,7 +358,24 @@ async def test_service_calls_off_mode( device.set_setpoint_heat.assert_called_with(77) assert "Invalid temperature" in caplog.text + device.set_setpoint_heat.reset_mock() + device.set_setpoint_heat.side_effect = aiosomecomfort.UnexpectedResponse caplog.clear() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_LOW: 25.0, + ATTR_TARGET_TEMP_HIGH: 35.0, + }, + blocking=True, + ) + device.set_setpoint_cool.assert_called_with(95) + device.set_setpoint_heat.assert_called_with(77) + reset_mock(device) await hass.services.async_call( CLIMATE_DOMAIN, @@ -702,6 +719,17 @@ async def test_service_calls_heat_mode( device.set_hold_heat.reset_mock() assert "Invalid temperature" in caplog.text + device.set_hold_heat.side_effect = aiosomecomfort.UnexpectedResponse + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + device.set_hold_heat.assert_called_once_with(datetime.time(2, 30), 59) + device.set_hold_heat.reset_mock() + caplog.clear() await hass.services.async_call( CLIMATE_DOMAIN, From 5b4df0f7ffe267fed732204a04a6e3fb8b29c307 Mon Sep 17 00:00:00 2001 From: Xitee <59659167+Xitee1@users.noreply.github.com> Date: Wed, 1 Nov 2023 20:15:18 +0100 Subject: [PATCH 097/265] Fix roomba translation key mismatch (#103191) --- homeassistant/components/roomba/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index 3b2b34af67b3..7d103111301b 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -106,7 +106,7 @@ SENSORS: list[RoombaSensorEntityDescription] = [ ), RoombaSensorEntityDescription( key="scrubs_count", - translation_key="scrubs", + translation_key="scrubs_count", icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="Scrubs", From 2be229c5b5d3b073ffe331260a85dc68e5717ab1 Mon Sep 17 00:00:00 2001 From: Xitee <59659167+Xitee1@users.noreply.github.com> Date: Wed, 1 Nov 2023 21:12:57 +0100 Subject: [PATCH 098/265] Fix roomba error if battery stats are not available (#103196) --- homeassistant/components/roomba/irobot_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index ffa4e2d8292e..13e78ced379d 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -119,7 +119,7 @@ class IRobotEntity(Entity): @property def battery_stats(self): """Return the battery stats.""" - return self.vacuum_state.get("bbchg3") + return self.vacuum_state.get("bbchg3", {}) @property def _robot_state(self): From 239fa04d0292775e7fc1557cc24d593887e63761 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 2 Nov 2023 10:57:00 +0100 Subject: [PATCH 099/265] Fix mqtt config validation error handling (#103210) * Fix MQTT config check * Fix handling invalid enity_category for sensors * Improve docstr * Update comment * Use correct util for yaml dump --- .../components/mqtt/binary_sensor.py | 10 ++++- homeassistant/components/mqtt/climate.py | 6 +-- homeassistant/components/mqtt/fan.py | 6 +-- homeassistant/components/mqtt/humidifier.py | 6 +-- homeassistant/components/mqtt/mixins.py | 19 ++++++++-- homeassistant/components/mqtt/sensor.py | 4 +- homeassistant/components/mqtt/text.py | 4 +- .../mqtt/test_alarm_control_panel.py | 2 +- tests/components/mqtt/test_climate.py | 2 +- tests/components/mqtt/test_fan.py | 4 +- tests/components/mqtt/test_init.py | 38 ++++++++++++++++++- tests/components/mqtt/test_text.py | 4 +- 12 files changed, 80 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 7ab2e9ebf90e..a89fb8a22fc5 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -42,6 +42,7 @@ from .mixins import ( MqttAvailability, MqttEntity, async_setup_entity_entry_helper, + validate_sensor_entity_category, write_state_on_attr_change, ) from .models import MqttValueTemplate, ReceiveMessage @@ -55,7 +56,7 @@ DEFAULT_PAYLOAD_ON = "ON" DEFAULT_FORCE_UPDATE = False CONF_EXPIRE_AFTER = "expire_after" -PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, @@ -67,7 +68,12 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) +DISCOVERY_SCHEMA = vol.All( + validate_sensor_entity_category, + _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), +) + +PLATFORM_SCHEMA_MODERN = vol.All(validate_sensor_entity_category, _PLATFORM_SCHEMA_BASE) async def async_setup_entry( diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index dae768a13594..358fa6eb675d 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -232,16 +232,16 @@ TOPIC_KEYS = ( def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: """Validate that the preset mode reset payload is not one of the preset modes.""" if PRESET_NONE in config[CONF_PRESET_MODES_LIST]: - raise ValueError("preset_modes must not include preset mode 'none'") + raise vol.Invalid("preset_modes must not include preset mode 'none'") return config def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: """Validate a target_humidity range configuration, throws otherwise.""" if config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX]: - raise ValueError("target_humidity_max must be > target_humidity_min") + raise vol.Invalid("target_humidity_max must be > target_humidity_min") if config[CONF_HUMIDITY_MAX] > 100: - raise ValueError("max_humidity must be <= 100") + raise vol.Invalid("max_humidity must be <= 100") return config diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 021926767847..0e9e7d708e98 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -116,16 +116,16 @@ _LOGGER = logging.getLogger(__name__) def valid_speed_range_configuration(config: ConfigType) -> ConfigType: """Validate that the fan speed_range configuration is valid, throws if it isn't.""" if config[CONF_SPEED_RANGE_MIN] == 0: - raise ValueError("speed_range_min must be > 0") + raise vol.Invalid("speed_range_min must be > 0") if config[CONF_SPEED_RANGE_MIN] >= config[CONF_SPEED_RANGE_MAX]: - raise ValueError("speed_range_max must be > speed_range_min") + raise vol.Invalid("speed_range_max must be > speed_range_min") return config def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: """Validate that the preset mode reset payload is not one of the preset modes.""" if config[CONF_PAYLOAD_RESET_PRESET_MODE] in config[CONF_PRESET_MODES_LIST]: - raise ValueError("preset_modes must not contain payload_reset_preset_mode") + raise vol.Invalid("preset_modes must not contain payload_reset_preset_mode") return config diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 77a74b151978..75a74a0dcaac 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -102,7 +102,7 @@ _LOGGER = logging.getLogger(__name__) def valid_mode_configuration(config: ConfigType) -> ConfigType: """Validate that the mode reset payload is not one of the available modes.""" if config[CONF_PAYLOAD_RESET_MODE] in config[CONF_AVAILABLE_MODES_LIST]: - raise ValueError("modes must not contain payload_reset_mode") + raise vol.Invalid("modes must not contain payload_reset_mode") return config @@ -113,9 +113,9 @@ def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: throws if it isn't. """ if config[CONF_TARGET_HUMIDITY_MIN] >= config[CONF_TARGET_HUMIDITY_MAX]: - raise ValueError("target_humidity_max must be > target_humidity_min") + raise vol.Invalid("target_humidity_max must be > target_humidity_min") if config[CONF_TARGET_HUMIDITY_MAX] > 100: - raise ValueError("max_humidity must be <= 100") + raise vol.Invalid("max_humidity must be <= 100") return config diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 908e3c768b8d..91a5511001b2 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -9,7 +9,6 @@ import logging from typing import TYPE_CHECKING, Any, Protocol, cast, final import voluptuous as vol -import yaml from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -28,6 +27,7 @@ from homeassistant.const import ( CONF_NAME, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, + EntityCategory, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import ( @@ -63,6 +63,7 @@ from homeassistant.helpers.typing import ( UndefinedType, ) from homeassistant.util.json import json_loads +from homeassistant.util.yaml import dump as yaml_dump from . import debug_info, subscription from .client import async_publish @@ -207,6 +208,16 @@ def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType ) +def validate_sensor_entity_category(config: ConfigType) -> ConfigType: + """Check the sensor's entity category is not set to `config` which is invalid for sensors.""" + if ( + CONF_ENTITY_CATEGORY in config + and config[CONF_ENTITY_CATEGORY] == EntityCategory.CONFIG + ): + raise vol.Invalid("Entity category `config` is invalid") + return config + + MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( cv.deprecated(CONF_DEPRECATED_VIA_HUB, CONF_VIA_DEVICE), vol.Schema( @@ -404,8 +415,8 @@ async def async_setup_entity_entry_helper( error = str(ex) config_file = getattr(yaml_config, "__config_file__", "?") line = getattr(yaml_config, "__line__", "?") - issue_id = hex(hash(frozenset(yaml_config.items()))) - yaml_config_str = yaml.dump(dict(yaml_config)) + issue_id = hex(hash(frozenset(yaml_config))) + yaml_config_str = yaml_dump(yaml_config) learn_more_url = ( f"https://www.home-assistant.io/integrations/{domain}.mqtt/" ) @@ -427,7 +438,7 @@ async def async_setup_entity_entry_helper( translation_key="invalid_platform_config", ) _LOGGER.error( - "%s for manual configured MQTT %s item, in %s, line %s Got %s", + "%s for manually configured MQTT %s item, in %s, line %s Got %s", error, domain, config_file, diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 93151c515421..e1c7ba64abaf 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -44,6 +44,7 @@ from .mixins import ( MqttAvailability, MqttEntity, async_setup_entity_entry_helper, + validate_sensor_entity_category, write_state_on_attr_change, ) from .models import ( @@ -70,7 +71,6 @@ MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset( DEFAULT_NAME = "MQTT Sensor" DEFAULT_FORCE_UPDATE = False - _PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), @@ -88,6 +88,7 @@ PLATFORM_SCHEMA_MODERN = vol.All( # Deprecated in HA Core 2021.11.0 https://github.com/home-assistant/core/pull/54840 # Removed in HA Core 2023.6.0 cv.removed(CONF_LAST_RESET_TOPIC), + validate_sensor_entity_category, _PLATFORM_SCHEMA_BASE, ) @@ -95,6 +96,7 @@ DISCOVERY_SCHEMA = vol.All( # Deprecated in HA Core 2021.11.0 https://github.com/home-assistant/core/pull/54840 # Removed in HA Core 2023.6.0 cv.removed(CONF_LAST_RESET_TOPIC), + validate_sensor_entity_category, _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index f6aeac3be7c6..da93a6b619e3 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -71,9 +71,9 @@ MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset( def valid_text_size_configuration(config: ConfigType) -> ConfigType: """Validate that the text length configuration is valid, throws if it isn't.""" if config[CONF_MIN] >= config[CONF_MAX]: - raise ValueError("text length min must be >= max") + raise vol.Invalid("text length min must be >= max") if config[CONF_MAX] > MAX_LENGTH_STATE_STATE: - raise ValueError(f"max text length must be <= {MAX_LENGTH_STATE_STATE}") + raise vol.Invalid(f"max text length must be <= {MAX_LENGTH_STATE_STATE}") return config diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 0d5c9ee2e8d5..40049431edb5 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1297,7 +1297,7 @@ async def test_reload_after_invalid_config( assert hass.states.get("alarm_control_panel.test") is None assert ( "extra keys not allowed @ data['invalid_topic'] for " - "manual configured MQTT alarm_control_panel item, " + "manually configured MQTT alarm_control_panel item, " "in ?, line ? Got {'name': 'test', 'invalid_topic': 'test-topic'}" in caplog.text ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 89eaf87fb3ac..6d6c74753661 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -139,7 +139,7 @@ async def test_preset_none_in_preset_modes( ) -> None: """Test the preset mode payload reset configuration.""" assert await mqtt_mock_entry() - assert "not a valid value" in caplog.text + assert "preset_modes must not include preset mode 'none'" in caplog.text @pytest.mark.parametrize( diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 6642d778f534..21d3bcce3a97 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1788,7 +1788,7 @@ async def test_attributes( }, False, None, - "not a valid value", + "speed_range_max must be > speed_range_min", ), ( "test14", @@ -1805,7 +1805,7 @@ async def test_attributes( }, False, None, - "not a valid value", + "speed_range_min must be > 0", ), ( "test15", diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 2aa8de388b10..93d730948851 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2134,7 +2134,7 @@ async def test_setup_manual_mqtt_with_platform_key( """Test set up a manual MQTT item with a platform key.""" assert await mqtt_mock_entry() assert ( - "extra keys not allowed @ data['platform'] for manual configured MQTT light item" + "extra keys not allowed @ data['platform'] for manually configured MQTT light item" in caplog.text ) @@ -2151,6 +2151,42 @@ async def test_setup_manual_mqtt_with_invalid_config( assert "required key not provided" in caplog.text +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + "sensor": { + "name": "test", + "state_topic": "test-topic", + "entity_category": "config", + } + } + }, + { + mqtt.DOMAIN: { + "binary_sensor": { + "name": "test", + "state_topic": "test-topic", + "entity_category": "config", + } + } + }, + ], +) +@patch( + "homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR, Platform.SENSOR] +) +async def test_setup_manual_mqtt_with_invalid_entity_category( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test set up a manual sensor item with an invalid entity category.""" + assert await mqtt_mock_entry() + assert "Entity category `config` is invalid" in caplog.text + + @patch("homeassistant.components.mqtt.PLATFORMS", []) @pytest.mark.parametrize( ("mqtt_config_entry_data", "protocol"), diff --git a/tests/components/mqtt/test_text.py b/tests/components/mqtt/test_text.py index 80f38dffcf9f..a602f1e3065f 100644 --- a/tests/components/mqtt/test_text.py +++ b/tests/components/mqtt/test_text.py @@ -211,7 +211,7 @@ async def test_attribute_validation_max_greater_then_min( ) -> None: """Test the validation of min and max configuration attributes.""" assert await mqtt_mock_entry() - assert "not a valid value" in caplog.text + assert "text length min must be >= max" in caplog.text @pytest.mark.parametrize( @@ -236,7 +236,7 @@ async def test_attribute_validation_max_not_greater_then_max_state_length( ) -> None: """Test the max value of of max configuration attribute.""" assert await mqtt_mock_entry() - assert "not a valid value" in caplog.text + assert "max text length must be <= 255" in caplog.text @pytest.mark.parametrize( From dc30ddc24bb8ed0e534187b1cc4db9730296143a Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 2 Nov 2023 09:13:04 +0100 Subject: [PATCH 100/265] Fix Fronius entity initialisation (#103211) * Use None instead of raising ValueError if value invalid * use async_dispatcher_send --- homeassistant/components/fronius/__init__.py | 4 ++-- homeassistant/components/fronius/sensor.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 793f381d52f0..c05f18107a04 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -16,7 +16,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from .const import ( @@ -204,7 +204,7 @@ class FroniusSolarNet: # Only for re-scans. Initial setup adds entities through sensor.async_setup_entry if self.config_entry.state == ConfigEntryState.LOADED: - dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator) + async_dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator) _LOGGER.debug( "New inverter added (UID: %s)", diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index dfc76ae14157..f11855ce7e22 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -661,7 +661,7 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn if new_value is None: return self.entity_description.default_value if self.entity_description.invalid_when_falsy and not new_value: - raise ValueError(f"Ignoring zero value for {self.entity_id}.") + return None if isinstance(new_value, float): return round(new_value, 4) return new_value @@ -671,10 +671,9 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn """Handle updated data from the coordinator.""" try: self._attr_native_value = self._get_entity_value() - except (KeyError, ValueError): + except KeyError: # sets state to `None` if no default_value is defined in entity description # KeyError: raised when omitted in response - eg. at night when no production - # ValueError: raised when invalid zero value received self._attr_native_value = self.entity_description.default_value self.async_write_ha_state() From c811e0db497df62a11214c45d5b6d8c1758b2eb8 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 2 Nov 2023 15:46:58 -0400 Subject: [PATCH 101/265] Bump pyenphase to 1.14.1 (#103239) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 0700bd4e71a7..4cffcce2d5c6 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.13.1"], + "requirements": ["pyenphase==1.14.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 7545800d5dcf..328d08177058 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1693,7 +1693,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.13.1 +pyenphase==1.14.1 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ed2583ec3c6..3e5689ea2662 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1275,7 +1275,7 @@ pyeconet==0.1.22 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.13.1 +pyenphase==1.14.1 # homeassistant.components.everlights pyeverlights==0.1.0 From 1dcd66d75c643a01e646e9bcb7166df673a4e12d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 2 Nov 2023 16:47:33 +0100 Subject: [PATCH 102/265] Remove measurement flag from timestamp in gardena bluetooth (#103245) Remove measurement flag from timestamp --- homeassistant/components/gardena_bluetooth/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/gardena_bluetooth/sensor.py b/homeassistant/components/gardena_bluetooth/sensor.py index 396d8469ffcf..495a1fcb1eb9 100644 --- a/homeassistant/components/gardena_bluetooth/sensor.py +++ b/homeassistant/components/gardena_bluetooth/sensor.py @@ -88,7 +88,6 @@ DESCRIPTIONS = ( GardenaBluetoothSensorEntityDescription( key=Sensor.measurement_timestamp.uuid, translation_key="sensor_measurement_timestamp", - state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, char=Sensor.measurement_timestamp, From 06d26b7c7f8b5da968dd2a7d2c12f32f964842a2 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 3 Nov 2023 12:17:36 +0100 Subject: [PATCH 103/265] Fix Plugwise Schedule selection (#103262) --- homeassistant/components/plugwise/select.py | 2 +- tests/components/plugwise/test_select.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index 6646cce33693..138e5fe3b593 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -40,7 +40,7 @@ SELECT_TYPES = ( key="select_schedule", translation_key="select_schedule", icon="mdi:calendar-clock", - command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON), + command=lambda api, loc, opt: api.set_schedule_state(loc, STATE_ON, opt), options_key="available_schedules", ), PlugwiseSelectEntityDescription( diff --git a/tests/components/plugwise/test_select.py b/tests/components/plugwise/test_select.py index 7ec5559a608a..9df20a5ffc8d 100644 --- a/tests/components/plugwise/test_select.py +++ b/tests/components/plugwise/test_select.py @@ -40,5 +40,7 @@ async def test_adam_change_select_entity( assert mock_smile_adam.set_schedule_state.call_count == 1 mock_smile_adam.set_schedule_state.assert_called_with( - "c50f167537524366a5af7aa3942feb1e", "Badkamer Schema", "on" + "c50f167537524366a5af7aa3942feb1e", + "on", + "Badkamer Schema", ) From 92486b1ff0e76a8068fe03fa5173c630f86e605b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Nov 2023 22:00:43 -0500 Subject: [PATCH 104/265] Bump yalexs-ble to 2.3.2 (#103267) --- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 50df1f4bd1df..aacebb4bb5c5 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.10.0", "yalexs-ble==2.3.1"] + "requirements": ["yalexs==1.10.0", "yalexs-ble==2.3.2"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 8d15fbb9a9f7..be388ec563c5 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", "iot_class": "local_push", - "requirements": ["yalexs-ble==2.3.1"] + "requirements": ["yalexs-ble==2.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 328d08177058..354f72252a33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2758,7 +2758,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.3.1 +yalexs-ble==2.3.2 # homeassistant.components.august yalexs==1.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e5689ea2662..00b5b61e9d11 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2058,7 +2058,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.3.1 +yalexs-ble==2.3.2 # homeassistant.components.august yalexs==1.10.0 From ba634ac34612825dc445c8c1e3fe00296916ee45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Janu=C3=A1rio?= Date: Thu, 2 Nov 2023 22:32:46 +0000 Subject: [PATCH 105/265] add library logger info on ecoforest integration manifest (#103274) --- homeassistant/components/ecoforest/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ecoforest/manifest.json b/homeassistant/components/ecoforest/manifest.json index 518f4d97a04e..99b63fade5fa 100644 --- a/homeassistant/components/ecoforest/manifest.json +++ b/homeassistant/components/ecoforest/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecoforest", "iot_class": "local_polling", + "loggers": ["pyecoforest"], "requirements": ["pyecoforest==0.3.0"] } From 1a823376d8a8d1a4e0f7b414cec8b58f73005178 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 3 Nov 2023 12:09:31 +0100 Subject: [PATCH 106/265] Fix Matter 1.2 locks with specific unlatch/unbolt support (#103275) --- homeassistant/components/matter/lock.py | 58 +- tests/components/matter/conftest.py | 10 + .../fixtures/nodes/door-lock-with-unbolt.json | 510 ++++++++++++++++++ tests/components/matter/test_door_lock.py | 49 ++ 4 files changed, 605 insertions(+), 22 deletions(-) create mode 100644 tests/components/matter/fixtures/nodes/door-lock-with-unbolt.json diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index a5f625f9e73d..8491f58e3879 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -1,12 +1,15 @@ """Matter lock.""" from __future__ import annotations -from enum import IntFlag from typing import Any from chip.clusters import Objects as clusters -from homeassistant.components.lock import LockEntity, LockEntityDescription +from homeassistant.components.lock import ( + LockEntity, + LockEntityDescription, + LockEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE, Platform from homeassistant.core import HomeAssistant, callback @@ -17,6 +20,8 @@ from .entity import MatterEntity from .helpers import get_matter from .models import MatterDiscoverySchema +DoorLockFeature = clusters.DoorLock.Bitmaps.Feature + async def async_setup_entry( hass: HomeAssistant, @@ -61,6 +66,14 @@ class MatterLock(MatterEntity, LockEntity): return bool(self.features & DoorLockFeature.kDoorPositionSensor) + @property + def supports_unbolt(self) -> bool: + """Return True if the lock supports unbolt.""" + if self.features is None: + return False + + return bool(self.features & DoorLockFeature.kUnbolt) + async def send_device_command( self, command: clusters.ClusterCommand, @@ -92,6 +105,25 @@ class MatterLock(MatterEntity, LockEntity): self._lock_option_default_code, ) code_bytes = code.encode() if code else None + if self.supports_unbolt: + # if the lock reports it has separate unbolt support, + # the unlock command should unbolt only on the unlock command + # and unlatch on the HA 'open' command. + await self.send_device_command( + command=clusters.DoorLock.Commands.UnboltDoor(code_bytes) + ) + else: + await self.send_device_command( + command=clusters.DoorLock.Commands.UnlockDoor(code_bytes) + ) + + async def async_open(self, **kwargs: Any) -> None: + """Open the door latch.""" + code: str = kwargs.get( + ATTR_CODE, + self._lock_option_default_code, + ) + code_bytes = code.encode() if code else None await self.send_device_command( command=clusters.DoorLock.Commands.UnlockDoor(code_bytes) ) @@ -104,6 +136,8 @@ class MatterLock(MatterEntity, LockEntity): self.features = int( self.get_matter_attribute_value(clusters.DoorLock.Attributes.FeatureMap) ) + if self.supports_unbolt: + self._attr_supported_features = LockEntityFeature.OPEN lock_state = self.get_matter_attribute_value( clusters.DoorLock.Attributes.LockState @@ -144,26 +178,6 @@ class MatterLock(MatterEntity, LockEntity): ) -class DoorLockFeature(IntFlag): - """Temp enum that represents the features of a door lock. - - Should be replaced by the library provided one once that is released. - """ - - kPinCredential = 0x1 # noqa: N815 - kRfidCredential = 0x2 # noqa: N815 - kFingerCredentials = 0x4 # noqa: N815 - kLogging = 0x8 # noqa: N815 - kWeekDayAccessSchedules = 0x10 # noqa: N815 - kDoorPositionSensor = 0x20 # noqa: N815 - kFaceCredentials = 0x40 # noqa: N815 - kCredentialsOverTheAirAccess = 0x80 # noqa: N815 - kUser = 0x100 # noqa: N815 - kNotification = 0x200 # noqa: N815 - kYearDayAccessSchedules = 0x400 # noqa: N815 - kHolidaySchedules = 0x800 # noqa: N815 - - DISCOVERY_SCHEMAS = [ MatterDiscoverySchema( platform=Platform.LOCK, diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 6a14148585a2..03443e4c4b99 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -223,6 +223,16 @@ async def door_lock_fixture( return await setup_integration_with_node_fixture(hass, "door-lock", matter_client) +@pytest.fixture(name="door_lock_with_unbolt") +async def door_lock_with_unbolt_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a door lock node with unbolt feature.""" + return await setup_integration_with_node_fixture( + hass, "door-lock-with-unbolt", matter_client + ) + + @pytest.fixture(name="eve_contact_sensor_node") async def eve_contact_sensor_node_fixture( hass: HomeAssistant, matter_client: MagicMock diff --git a/tests/components/matter/fixtures/nodes/door-lock-with-unbolt.json b/tests/components/matter/fixtures/nodes/door-lock-with-unbolt.json new file mode 100644 index 000000000000..6cbd75ab09c2 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/door-lock-with-unbolt.json @@ -0,0 +1,510 @@ +{ + "node_id": 1, + "date_commissioned": "2023-03-07T09:06:06.059454", + "last_interview": "2023-03-07T09:06:06.059456", + "interview_version": 2, + "available": true, + "attributes": { + "0/29/0": [ + { + "deviceType": 22, + "revision": 1 + } + ], + "0/29/1": [ + 29, 31, 40, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 60, 62, + 63, 64, 65 + ], + "0/29/2": [41], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 1, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "0/31/0": [ + { + "privilege": 5, + "authMode": 2, + "subjects": [112233], + "targets": null, + "fabricIndex": 1 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533], + "0/40/0": 1, + "0/40/1": "TEST_VENDOR", + "0/40/2": 65521, + "0/40/3": "Mock Door Lock", + "0/40/4": 32769, + "0/40/5": "Mock Door Lock", + "0/40/6": "**REDACTED**", + "0/40/7": 0, + "0/40/8": "TEST_VERSION", + "0/40/9": 1, + "0/40/10": "1.0", + "0/40/11": "20200101", + "0/40/12": "", + "0/40/13": "", + "0/40/14": "", + "0/40/15": "TEST_SN", + "0/40/16": false, + "0/40/17": true, + "0/40/18": "mock-door-lock", + "0/40/19": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 65535 + }, + "0/40/65532": 0, + "0/40/65533": 1, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 0, + "0/42/3": 0, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "0/43/0": "en-US", + "0/43/1": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ], + "0/43/65532": 0, + "0/43/65533": 1, + "0/43/65528": [], + "0/43/65529": [], + "0/43/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533], + "0/44/0": 0, + "0/44/1": 0, + "0/44/2": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7], + "0/44/65532": 0, + "0/44/65533": 1, + "0/44/65528": [], + "0/44/65529": [], + "0/44/65531": [0, 1, 2, 65528, 65529, 65530, 65531, 65532, 65533], + "0/46/0": [0, 1], + "0/46/65532": 0, + "0/46/65533": 1, + "0/46/65528": [], + "0/46/65529": [], + "0/46/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "0/47/0": 1, + "0/47/1": 0, + "0/47/2": "USB", + "0/47/6": 0, + "0/47/65532": 1, + "0/47/65533": 1, + "0/47/65528": [], + "0/47/65529": [], + "0/47/65531": [0, 1, 2, 6, 65528, 65529, 65530, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "failSafeExpiryLengthSeconds": 60, + "maxCumulativeFailsafeSeconds": 900 + }, + "0/48/2": 0, + "0/48/3": 2, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65530, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [], + "0/49/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": null, + "0/49/6": null, + "0/49/7": null, + "0/49/65532": 2, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 3, 4, 6, 8], + "0/49/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/50/65532": 0, + "0/50/65533": 1, + "0/50/65528": [1], + "0/50/65529": [0], + "0/50/65531": [65528, 65529, 65530, 65531, 65532, 65533], + "0/51/0": [ + { + "name": "eth0", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "/mQDt/2Q", + "IPv4Addresses": ["CjwBaQ=="], + "IPv6Addresses": [ + "/VqgxiAxQib8ZAP//rf9kA==", + "IAEEcLs7AAb8ZAP//rf9kA==", + "/oAAAAAAAAD8ZAP//rf9kA==" + ], + "type": 2 + }, + { + "name": "lo", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "AAAAAAAA", + "IPv4Addresses": ["fwAAAQ=="], + "IPv6Addresses": ["AAAAAAAAAAAAAAAAAAAAAQ=="], + "type": 0 + } + ], + "0/51/1": 1, + "0/51/2": 25, + "0/51/3": 0, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [0], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/52/0": [ + { + "id": 26957, + "name": "26957", + "stackFreeCurrent": null, + "stackFreeMinimum": null, + "stackSize": null + }, + { + "id": 26956, + "name": "26956", + "stackFreeCurrent": null, + "stackFreeMinimum": null, + "stackSize": null + }, + { + "id": 26955, + "name": "26955", + "stackFreeCurrent": null, + "stackFreeMinimum": null, + "stackSize": null + }, + { + "id": 26953, + "name": "26953", + "stackFreeCurrent": null, + "stackFreeMinimum": null, + "stackSize": null + }, + { + "id": 26952, + "name": "26952", + "stackFreeCurrent": null, + "stackFreeMinimum": null, + "stackSize": null + } + ], + "0/52/1": 351120, + "0/52/2": 529520, + "0/52/3": 529520, + "0/52/65532": 1, + "0/52/65533": 1, + "0/52/65528": [], + "0/52/65529": [0], + "0/52/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "0/53/0": null, + "0/53/1": null, + "0/53/2": null, + "0/53/3": null, + "0/53/4": null, + "0/53/5": null, + "0/53/6": 0, + "0/53/7": [], + "0/53/8": [], + "0/53/9": null, + "0/53/10": null, + "0/53/11": null, + "0/53/12": null, + "0/53/13": null, + "0/53/14": 0, + "0/53/15": 0, + "0/53/16": 0, + "0/53/17": 0, + "0/53/18": 0, + "0/53/19": 0, + "0/53/20": 0, + "0/53/21": 0, + "0/53/22": 0, + "0/53/23": 0, + "0/53/24": 0, + "0/53/25": 0, + "0/53/26": 0, + "0/53/27": 0, + "0/53/28": 0, + "0/53/29": 0, + "0/53/30": 0, + "0/53/31": 0, + "0/53/32": 0, + "0/53/33": 0, + "0/53/34": 0, + "0/53/35": 0, + "0/53/36": 0, + "0/53/37": 0, + "0/53/38": 0, + "0/53/39": 0, + "0/53/40": 0, + "0/53/41": 0, + "0/53/42": 0, + "0/53/43": 0, + "0/53/44": 0, + "0/53/45": 0, + "0/53/46": 0, + "0/53/47": 0, + "0/53/48": 0, + "0/53/49": 0, + "0/53/50": 0, + "0/53/51": 0, + "0/53/52": 0, + "0/53/53": 0, + "0/53/54": 0, + "0/53/55": 0, + "0/53/56": null, + "0/53/57": null, + "0/53/58": null, + "0/53/59": null, + "0/53/60": null, + "0/53/61": null, + "0/53/62": [], + "0/53/65532": 15, + "0/53/65533": 1, + "0/53/65528": [], + "0/53/65529": [0], + "0/53/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/54/0": null, + "0/54/1": null, + "0/54/2": 3, + "0/54/3": null, + "0/54/4": null, + "0/54/5": null, + "0/54/6": null, + "0/54/7": null, + "0/54/8": null, + "0/54/9": null, + "0/54/10": null, + "0/54/11": null, + "0/54/12": null, + "0/54/65532": 3, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [0], + "0/54/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65530, 65531, + 65532, 65533 + ], + "0/55/0": null, + "0/55/1": false, + "0/55/2": 823, + "0/55/3": 969, + "0/55/4": 0, + "0/55/5": 0, + "0/55/6": 0, + "0/55/7": null, + "0/55/8": 25, + "0/55/65532": 3, + "0/55/65533": 1, + "0/55/65528": [], + "0/55/65529": [0], + "0/55/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65530, 65531, 65532, 65533], + "0/62/0": [ + { + "noc": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRARgkBwEkCAEwCUEE55h6CbNLPZH/uM3/rDdA+jeuuD2QSPN8gBeEB0bmGJqWz/gCT4/ySB77rK3XiwVWVAmJhJ/eMcTIA0XXWMqKPDcKNQEoARgkAgE2AwQCBAEYMAQUqnKiC76YFhcTHt4AQ/kAbtrZ2MowBRSL6EWyWm8+uC0Puc2/BncMqYbpmhgwC0AA05Z+y1mcyHUeOFJ5kyDJJMN/oNCwN5h8UpYN/868iuQArr180/fbaN1+db9lab4D2lf0HK7wgHIR3HsOa2w9GA==", + "icac": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE5R1DrUQE/L8tx95WR1g1dZJf4d+6LEB7JAYZN/nw9ZBUg5VOHDrB1xIw5KguYJzt10K+0KqQBBEbuwW+wLLobTcKNQEpARgkAmAwBBSL6EWyWm8+uC0Puc2/BncMqYbpmjAFFM0I6fPFzfOv2IWbX1huxb3eW0fqGDALQHXLE0TgIDW6XOnvtsOJCyKoENts8d4TQWBgTKviv1LF/+MS9eFYi+kO+1Idq5mVgwN+lH7eyecShQR0iqq6WLUY", + "fabricIndex": 1 + } + ], + "0/62/1": [ + { + "rootPublicKey": "BJ/jL2MdDrdq9TahKSa5c/dBc166NRCU0W9l7hK2kcuVtN915DLqiS+RAJ2iPEvWK5FawZHF/QdKLZmTkZHudxY=", + "vendorId": 65521, + "fabricId": 1, + "nodeId": 1, + "label": "", + "fabricIndex": 1 + } + ], + "0/62/2": 16, + "0/62/3": 1, + "0/62/4": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEn+MvYx0Ot2r1NqEpJrlz90FzXro1EJTRb2XuEraRy5W033XkMuqJL5EAnaI8S9YrkVrBkcX9B0otmZORke53FjcKNQEpARgkAmAwBBTNCOnzxc3zr9iFm19YbsW93ltH6jAFFM0I6fPFzfOv2IWbX1huxb3eW0fqGDALQILjpR3BTSHHl6DQtvwzWkjmA+i5jjXdc3qjemFGFjFVAnV6dPLQo7tctC8Y0uL4ZNERga2/NZAt1gRD72S0YR4Y" + ], + "0/62/5": 1, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65530, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 1, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "0/64/0": [ + { + "label": "room", + "value": "bedroom 2" + }, + { + "label": "orientation", + "value": "North" + }, + { + "label": "floor", + "value": "2" + }, + { + "label": "direction", + "value": "up" + } + ], + "0/64/65532": 0, + "0/64/65533": 1, + "0/64/65528": [], + "0/64/65529": [], + "0/64/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "0/65/0": [], + "0/65/65532": 0, + "0/65/65533": 1, + "0/65/65528": [], + "0/65/65529": [], + "0/65/65531": [0, 65528, 65529, 65530, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 0, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0], + "1/3/65531": [0, 1, 65528, 65529, 65530, 65531, 65532, 65533], + "1/6/0": false, + "1/6/16384": true, + "1/6/16385": 0, + "1/6/16386": 0, + "1/6/16387": 0, + "1/6/65532": 0, + "1/6/65533": 4, + "1/6/65528": [], + "1/6/65529": [0, 1, 2], + "1/6/65531": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "1/29/0": [ + { + "deviceType": 10, + "revision": 1 + } + ], + "1/29/1": [3, 6, 29, 47, 257], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 1, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65530, 65531, 65532, 65533], + "1/47/0": 1, + "1/47/1": 1, + "1/47/2": "Battery", + "1/47/14": 0, + "1/47/15": false, + "1/47/16": 0, + "1/47/19": "", + "1/47/65532": 10, + "1/47/65533": 1, + "1/47/65528": [], + "1/47/65529": [], + "1/47/65531": [ + 0, 1, 2, 14, 15, 16, 19, 65528, 65529, 65530, 65531, 65532, 65533 + ], + "1/257/0": 1, + "1/257/1": 0, + "1/257/2": true, + "1/257/3": 1, + "1/257/17": 10, + "1/257/18": 10, + "1/257/19": 10, + "1/257/20": 10, + "1/257/21": 10, + "1/257/22": 10, + "1/257/23": 8, + "1/257/24": 6, + "1/257/25": 20, + "1/257/26": 10, + "1/257/27": 1, + "1/257/28": 5, + "1/257/33": "en", + "1/257/35": 60, + "1/257/36": 0, + "1/257/37": 0, + "1/257/38": 65526, + "1/257/41": false, + "1/257/43": false, + "1/257/48": 3, + "1/257/49": 10, + "1/257/51": false, + "1/257/65532": 7603, + "1/257/65533": 6, + "1/257/65528": [12, 15, 18, 28, 35, 37], + "1/257/65529": [ + 0, 1, 3, 11, 12, 13, 14, 15, 16, 17, 18, 19, 26, 27, 29, 34, 36, 38 + ], + "1/257/65531": [ + 0, 1, 2, 3, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 33, 35, 36, + 37, 38, 41, 43, 48, 49, 51, 65528, 65529, 65530, 65531, 65532, 65533 + ] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index 221ae891d67d..a9753824edcf 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -10,6 +10,7 @@ from homeassistant.components.lock import ( STATE_LOCKING, STATE_UNLOCKED, STATE_UNLOCKING, + LockEntityFeature, ) from homeassistant.const import ATTR_CODE, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -135,3 +136,51 @@ async def test_lock_requires_pin( command=clusters.DoorLock.Commands.LockDoor(code.encode()), timed_request_timeout_ms=1000, ) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_lock_with_unbolt( + hass: HomeAssistant, + matter_client: MagicMock, + door_lock_with_unbolt: MatterNode, +) -> None: + """Test door lock.""" + state = hass.states.get("lock.mock_door_lock") + assert state + assert state.state == STATE_LOCKED + assert state.attributes["supported_features"] & LockEntityFeature.OPEN + # test unlock/unbolt + await hass.services.async_call( + "lock", + "unlock", + { + "entity_id": "lock.mock_door_lock", + }, + blocking=True, + ) + assert matter_client.send_device_command.call_count == 1 + # unlock should unbolt on a lock with unbolt feature + assert matter_client.send_device_command.call_args == call( + node_id=door_lock_with_unbolt.node_id, + endpoint_id=1, + command=clusters.DoorLock.Commands.UnboltDoor(), + timed_request_timeout_ms=1000, + ) + matter_client.send_device_command.reset_mock() + # test open / unlatch + await hass.services.async_call( + "lock", + "open", + { + "entity_id": "lock.mock_door_lock", + }, + blocking=True, + ) + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=door_lock_with_unbolt.node_id, + endpoint_id=1, + command=clusters.DoorLock.Commands.UnlockDoor(), + timed_request_timeout_ms=1000, + ) From 910654bf785f46113179077c3e29c58bed6249ca Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 3 Nov 2023 07:08:40 -0400 Subject: [PATCH 107/265] Fix firmware update failure (#103277) --- homeassistant/components/zwave_js/update.py | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index 6efae29e46e9..e49eb8a2017f 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import Counter from collections.abc import Callable -from dataclasses import asdict, dataclass +from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any, Final @@ -54,7 +54,7 @@ class ZWaveNodeFirmwareUpdateExtraStoredData(ExtraStoredData): def as_dict(self) -> dict[str, Any]: """Return a dict representation of the extra data.""" return { - ATTR_LATEST_VERSION_FIRMWARE: asdict(self.latest_version_firmware) + ATTR_LATEST_VERSION_FIRMWARE: self.latest_version_firmware.to_dict() if self.latest_version_firmware else None } @@ -339,19 +339,25 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity): and (latest_version := state.attributes.get(ATTR_LATEST_VERSION)) is not None and (extra_data := await self.async_get_last_extra_data()) - ): - self._attr_latest_version = latest_version - self._latest_version_firmware = ( - ZWaveNodeFirmwareUpdateExtraStoredData.from_dict( + and ( + latest_version_firmware := ZWaveNodeFirmwareUpdateExtraStoredData.from_dict( extra_data.as_dict() ).latest_version_firmware ) - # If we have no state or latest version to restore, we can set the latest + ): + self._attr_latest_version = latest_version + self._latest_version_firmware = latest_version_firmware + # If we have no state or latest version to restore, or the latest version is + # the same as the installed version, we can set the latest # version to installed so that the entity starts as off. If we have partial # restore data due to an upgrade to an HA version where this feature is released # from one that is not the entity will start in an unknown state until we can # correct on next update - elif not state or not latest_version: + elif ( + not state + or not latest_version + or latest_version == self._attr_installed_version + ): self._attr_latest_version = self._attr_installed_version # Spread updates out in 5 minute increments to avoid flooding the network From 4a56d0ec1d1e84d7397cfb633b615cf765413969 Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 3 Nov 2023 04:15:49 -0700 Subject: [PATCH 108/265] Bump opower to 0.0.39 (#103292) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index a27d6f6f6808..1022ab07e2ce 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.0.38"] + "requirements": ["opower==0.0.39"] } diff --git a/requirements_all.txt b/requirements_all.txt index 354f72252a33..374f5ee062e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1394,7 +1394,7 @@ openwrt-luci-rpc==1.1.16 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.0.38 +opower==0.0.39 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00b5b61e9d11..b19fe7fffcf9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1072,7 +1072,7 @@ openerz-api==0.2.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.0.38 +opower==0.0.39 # homeassistant.components.oralb oralb-ble==0.17.6 From 0b0f099d2722de000aec715482aafb5ccf825f71 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Nov 2023 13:02:22 +0100 Subject: [PATCH 109/265] Bumped version to 2023.11.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 28241ef15f44..99c1312dc98a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 11 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 7efa6915a467..616dd219bafd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.11.0" +version = "2023.11.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 810681b3574afb2b687a40758cba3949a480d9ee Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 3 Nov 2023 17:05:27 +0100 Subject: [PATCH 110/265] Bump reolink-aio to 0.7.14 and improve typing of Reolink (#103129) * Improve typing * fix mypy * Further improve typing * Restore Literal typing * Bump reolink_aio to 0.7.13 * Bump reolink-aio to 0.7.14 --- homeassistant/components/reolink/__init__.py | 11 ++++++++--- .../components/reolink/binary_sensor.py | 6 +++--- homeassistant/components/reolink/host.py | 18 ++++++++++-------- homeassistant/components/reolink/light.py | 16 ++++++++-------- homeassistant/components/reolink/manifest.json | 2 +- homeassistant/components/reolink/number.py | 4 ++-- homeassistant/components/reolink/sensor.py | 2 +- homeassistant/components/reolink/update.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 37 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index fd62f8451fbf..8425f29fbe89 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -10,6 +10,7 @@ from typing import Literal from reolink_aio.api import RETRY_ATTEMPTS from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError +from reolink_aio.software_version import NewSoftwareVersion from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform @@ -45,7 +46,9 @@ class ReolinkData: host: ReolinkHost device_coordinator: DataUpdateCoordinator[None] - firmware_coordinator: DataUpdateCoordinator[str | Literal[False]] + firmware_coordinator: DataUpdateCoordinator[ + str | Literal[False] | NewSoftwareVersion + ] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: @@ -86,7 +89,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)): await host.renew() - async def async_check_firmware_update() -> str | Literal[False]: + async def async_check_firmware_update() -> str | Literal[ + False + ] | NewSoftwareVersion: """Check for firmware updates.""" if not host.api.supported(None, "update"): return False @@ -153,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry): +async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Update the configuration of the host entity.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py index 49e964e2b3ff..7f2ff3e0053b 100644 --- a/homeassistant/components/reolink/binary_sensor.py +++ b/homeassistant/components/reolink/binary_sensor.py @@ -32,7 +32,7 @@ from .entity import ReolinkChannelCoordinatorEntity class ReolinkBinarySensorEntityDescriptionMixin: """Mixin values for Reolink binary sensor entities.""" - value: Callable[[Host, int | None], bool] + value: Callable[[Host, int], bool] @dataclass @@ -43,7 +43,7 @@ class ReolinkBinarySensorEntityDescription( icon: str = "mdi:motion-sensor" icon_off: str = "mdi:motion-sensor-off" - supported: Callable[[Host, int | None], bool] = lambda host, ch: True + supported: Callable[[Host, int], bool] = lambda host, ch: True BINARY_SENSORS = ( @@ -169,6 +169,6 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt ) ) - async def _async_handle_event(self, event): + async def _async_handle_event(self, event: str) -> None: """Handle incoming event for motion detection.""" self.async_write_ha_state() diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index d470711267d5..0075bbac4e6b 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Mapping import logging -from typing import Any +from typing import Any, Literal import aiohttp from aiohttp.web import Request @@ -81,7 +81,7 @@ class ReolinkHost: return self._unique_id @property - def api(self): + def api(self) -> Host: """Return the API object.""" return self._api @@ -313,7 +313,7 @@ class ReolinkHost: """Call the API of the camera device to update the internal states.""" await self._api.get_states() - async def disconnect(self): + async def disconnect(self) -> None: """Disconnect from the API, so the connection will be released.""" try: await self._api.unsubscribe() @@ -335,7 +335,7 @@ class ReolinkHost: err, ) - async def _async_start_long_polling(self, initial=False): + async def _async_start_long_polling(self, initial=False) -> None: """Start ONVIF long polling task.""" if self._long_poll_task is None: try: @@ -364,7 +364,7 @@ class ReolinkHost: self._lost_subscription = False self._long_poll_task = asyncio.create_task(self._async_long_polling()) - async def _async_stop_long_polling(self): + async def _async_stop_long_polling(self) -> None: """Stop ONVIF long polling task.""" if self._long_poll_task is not None: self._long_poll_task.cancel() @@ -372,7 +372,7 @@ class ReolinkHost: await self._api.unsubscribe(sub_type=SubType.long_poll) - async def stop(self, event=None): + async def stop(self, event=None) -> None: """Disconnect the API.""" if self._cancel_poll is not None: self._cancel_poll() @@ -433,7 +433,7 @@ class ReolinkHost: else: self._lost_subscription = False - async def _renew(self, sub_type: SubType) -> None: + async def _renew(self, sub_type: Literal[SubType.push, SubType.long_poll]) -> None: """Execute the renew of the subscription.""" if not self._api.subscribed(sub_type): _LOGGER.debug( @@ -512,8 +512,10 @@ class ReolinkHost: _LOGGER.debug("Registered webhook: %s", event_id) - def unregister_webhook(self): + def unregister_webhook(self) -> None: """Unregister the webhook for motion events.""" + if self.webhook_id is None: + return _LOGGER.debug("Unregistering webhook %s", self.webhook_id) webhook.async_unregister(self._hass, self.webhook_id) self.webhook_id = None diff --git a/homeassistant/components/reolink/light.py b/homeassistant/components/reolink/light.py index 4ac8166410f5..938093df4a36 100644 --- a/homeassistant/components/reolink/light.py +++ b/homeassistant/components/reolink/light.py @@ -38,8 +38,8 @@ class ReolinkLightEntityDescription( """A class that describes light entities.""" supported_fn: Callable[[Host, int], bool] = lambda api, ch: True - get_brightness_fn: Callable[[Host, int], int] | None = None - set_brightness_fn: Callable[[Host, int, float], Any] | None = None + get_brightness_fn: Callable[[Host, int], int | None] | None = None + set_brightness_fn: Callable[[Host, int, int], Any] | None = None LIGHT_ENTITIES = ( @@ -127,13 +127,13 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity): if self.entity_description.get_brightness_fn is None: return None - return round( - 255 - * ( - self.entity_description.get_brightness_fn(self._host.api, self._channel) - / 100.0 - ) + bright_pct = self.entity_description.get_brightness_fn( + self._host.api, self._channel ) + if bright_pct is None: + return None + + return round(255 * bright_pct / 100.0) async def async_turn_off(self, **kwargs: Any) -> None: """Turn light off.""" diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 1c1d8dd96b12..9189de89efa5 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -18,5 +18,5 @@ "documentation": "https://www.home-assistant.io/integrations/reolink", "iot_class": "local_push", "loggers": ["reolink_aio"], - "requirements": ["reolink-aio==0.7.12"] + "requirements": ["reolink-aio==0.7.14"] } diff --git a/homeassistant/components/reolink/number.py b/homeassistant/components/reolink/number.py index 24e5d1bd72be..6be0cef1670b 100644 --- a/homeassistant/components/reolink/number.py +++ b/homeassistant/components/reolink/number.py @@ -26,7 +26,7 @@ from .entity import ReolinkChannelCoordinatorEntity class ReolinkNumberEntityDescriptionMixin: """Mixin values for Reolink number entities.""" - value: Callable[[Host, int], float] + value: Callable[[Host, int], float | None] method: Callable[[Host, int, float], Any] @@ -354,7 +354,7 @@ class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity): ) @property - def native_value(self) -> float: + def native_value(self) -> float | None: """State of the number entity.""" return self.entity_description.value(self._host.api, self._channel) diff --git a/homeassistant/components/reolink/sensor.py b/homeassistant/components/reolink/sensor.py index 6282f29e4424..b9e8ddb8e731 100644 --- a/homeassistant/components/reolink/sensor.py +++ b/homeassistant/components/reolink/sensor.py @@ -44,7 +44,7 @@ class ReolinkSensorEntityDescription( class ReolinkHostSensorEntityDescriptionMixin: """Mixin values for Reolink host sensor entities.""" - value: Callable[[Host], int] + value: Callable[[Host], int | None] @dataclass diff --git a/homeassistant/components/reolink/update.py b/homeassistant/components/reolink/update.py index 57efe1d9e924..1c10671550d6 100644 --- a/homeassistant/components/reolink/update.py +++ b/homeassistant/components/reolink/update.py @@ -35,7 +35,8 @@ async def async_setup_entry( class ReolinkUpdateEntity( - ReolinkBaseCoordinatorEntity[str | Literal[False]], UpdateEntity + ReolinkBaseCoordinatorEntity[str | Literal[False] | NewSoftwareVersion], + UpdateEntity, ): """Update entity for a Netgear device.""" diff --git a/requirements_all.txt b/requirements_all.txt index 374f5ee062e8..14b75e6cd6d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2319,7 +2319,7 @@ renault-api==0.2.0 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.7.12 +reolink-aio==0.7.14 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b19fe7fffcf9..bf601b3156b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1730,7 +1730,7 @@ renault-api==0.2.0 renson-endura-delta==1.6.0 # homeassistant.components.reolink -reolink-aio==0.7.12 +reolink-aio==0.7.14 # homeassistant.components.rflink rflink==0.0.65 From 67ce51899f07d49c04fe421061f896ca1ef805c5 Mon Sep 17 00:00:00 2001 From: Ian Date: Fri, 3 Nov 2023 09:03:02 -0700 Subject: [PATCH 111/265] Bump py_nextbusnext to v1.0.2 to fix TypeError (#103214) * Bump py_nextbusnext to v1.0.1 to fix TypeError Currently throwing an error as a set is passed into the method that is currently expecting a Sequence. That method is technically compatible with Iterable, so the latest patch relaxes that restriction. * Bump version to v1.0.2 to fix error message --- homeassistant/components/nextbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 9d1490a4ae6a..d8f4018ada2a 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nextbus", "iot_class": "cloud_polling", "loggers": ["py_nextbus"], - "requirements": ["py-nextbusnext==1.0.0"] + "requirements": ["py-nextbusnext==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 14b75e6cd6d5..b5386244ee09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1521,7 +1521,7 @@ py-improv-ble-client==1.0.3 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==1.0.0 +py-nextbusnext==1.0.2 # homeassistant.components.nightscout py-nightscout==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf601b3156b4..d0dc45880224 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1166,7 +1166,7 @@ py-improv-ble-client==1.0.3 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==1.0.0 +py-nextbusnext==1.0.2 # homeassistant.components.nightscout py-nightscout==1.2.2 From d95d4d0184eb09cfce8d58aa21260bb17ddcd03f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 25 Oct 2023 16:07:22 -0400 Subject: [PATCH 112/265] Add script to convert zwave_js device diagnostics to fixture (#102799) --- homeassistant/components/zwave_js/README.md | 15 +- .../components/zwave_js/scripts/__init__.py | 1 + .../convert_device_diagnostics_to_fixture.py | 91 + .../zwave_js/fixtures/device_diagnostics.json | 2315 +++++++++++++++++ .../zwave_js/fixtures/zooz_zse44_state.json | 1330 ++++++++++ tests/components/zwave_js/scripts/__init__.py | 1 + ...t_convert_device_diagnostics_to_fixture.py | 80 + 7 files changed, 3832 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/zwave_js/scripts/__init__.py create mode 100644 homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py create mode 100644 tests/components/zwave_js/fixtures/device_diagnostics.json create mode 100644 tests/components/zwave_js/fixtures/zooz_zse44_state.json create mode 100644 tests/components/zwave_js/scripts/__init__.py create mode 100644 tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py diff --git a/homeassistant/components/zwave_js/README.md b/homeassistant/components/zwave_js/README.md index f82f421f7528..da49e67c60a3 100644 --- a/homeassistant/components/zwave_js/README.md +++ b/homeassistant/components/zwave_js/README.md @@ -10,7 +10,20 @@ The Z-Wave integration uses a discovery mechanism to create the necessary entiti In cases where an entity's functionality requires interaction with multiple Values, the discovery rule for that particular entity type is based on the primary Value, or the Value that must be there to indicate that this entity needs to be created, and then the rest of the Values required are discovered by the class instance for that entity. A good example of this is the discovery logic for the `climate` entity. Currently, the discovery logic is tied to the discovery of a Value with a property of `mode` and a command class of `Thermostat Mode`, but the actual entity uses many more Values than that to be fully functional as evident in the [code](./climate.py). -There are several ways that device support can be improved within Home Assistant, but regardless of the reason, it is important to add device specific tests in these use cases. To do so, add the device's data (from device diagnostics) to the [fixtures folder](../../../tests/components/zwave_js/fixtures) and then define the new fixtures in [conftest.py](../../../tests/components/zwave_js/conftest.py). Use existing tests as the model but the tests can go in the [test_discovery.py module](../../../tests/components/zwave_js/test_discovery.py). +There are several ways that device support can be improved within Home Assistant, but regardless of the reason, it is important to add device specific tests in these use cases. To do so, add the device's data to the [fixtures folder](../../../tests/components/zwave_js/fixtures) and then define the new fixtures in [conftest.py](../../../tests/components/zwave_js/conftest.py). Use existing tests as the model but the tests can go in the [test_discovery.py module](../../../tests/components/zwave_js/test_discovery.py). To learn how to generate fixtures, see the following section. + +### Generating device fixtures + +To generate a device fixture, download a diagnostics dump of the device from your Home Assistant instance. The dumped data will need to be modified to match the expected format. You can always do this transformation by hand, but the integration provides a [helper script](scripts/convert_device_diagnostics_to_fixture.py) that will generate the appropriate fixture data from a device diagnostics dump for you. To use it, run the script with the path to the diagnostics dump you downloaded: + +`python homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py ` + +The script will print the fixture data to standard output, and you can use Unix piping to create a file from the fixture data: + +`python homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py > ` + +You can alternatively pass the `--file` flag to the script and it will create the file for you in the [fixtures folder](../../../tests/components/zwave_js/fixtures): +`python homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py --file` ### Switching HA support for a device from one entity type to another. diff --git a/homeassistant/components/zwave_js/scripts/__init__.py b/homeassistant/components/zwave_js/scripts/__init__.py new file mode 100644 index 000000000000..fda5d0f5c39a --- /dev/null +++ b/homeassistant/components/zwave_js/scripts/__init__.py @@ -0,0 +1 @@ +"""Scripts module for Z-Wave JS.""" diff --git a/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py b/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py new file mode 100644 index 000000000000..1e8d295227fe --- /dev/null +++ b/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py @@ -0,0 +1,91 @@ +"""Script to convert a device diagnostics file to a fixture.""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + +from homeassistant.util import slugify + + +def get_arguments() -> argparse.Namespace: + """Get parsed passed in arguments.""" + parser = argparse.ArgumentParser(description="Z-Wave JS Fixture generator") + parser.add_argument( + "diagnostics_file", type=Path, help="Device diagnostics file to convert" + ) + parser.add_argument( + "--file", + action="store_true", + help=( + "Dump fixture to file in fixtures folder. By default, the fixture will be " + "printed to standard output." + ), + ) + + arguments = parser.parse_args() + + return arguments + + +def get_fixtures_dir_path(data: dict) -> Path: + """Get path to fixtures directory.""" + device_config = data["deviceConfig"] + filename = slugify( + f"{device_config['manufacturer']}-{device_config['label']}_state" + ) + path = Path(__file__).parents[1] + index = path.parts.index("homeassistant") + return Path( + *path.parts[:index], + "tests", + *path.parts[index + 1 :], + "fixtures", + f"{filename}.json", + ) + + +def load_file(path: Path) -> Any: + """Load file from path.""" + return json.loads(path.read_text("utf8")) + + +def extract_fixture_data(diagnostics_data: Any) -> dict: + """Extract fixture data from file.""" + if ( + not isinstance(diagnostics_data, dict) + or "data" not in diagnostics_data + or "state" not in diagnostics_data["data"] + ): + raise ValueError("Invalid diagnostics file format") + state: dict = diagnostics_data["data"]["state"] + if isinstance(state["values"], list): + return state + values_dict: dict[str, dict] = state.pop("values") + state["values"] = list(values_dict.values()) + + return state + + +def create_fixture_file(path: Path, state_text: str) -> None: + """Create a file for the state dump in the fixtures directory.""" + path.write_text(state_text, "utf8") + + +def main() -> None: + """Run the main script.""" + args = get_arguments() + diagnostics_path: Path = args.diagnostics_file + diagnostics = load_file(diagnostics_path) + fixture_data = extract_fixture_data(diagnostics) + fixture_text = json.dumps(fixture_data, indent=2) + if args.file: + fixture_path = get_fixtures_dir_path(fixture_data) + create_fixture_file(fixture_path, fixture_text) + return + print(fixture_text) # noqa: T201 + + +if __name__ == "__main__": + main() diff --git a/tests/components/zwave_js/fixtures/device_diagnostics.json b/tests/components/zwave_js/fixtures/device_diagnostics.json new file mode 100644 index 000000000000..a206cb8353c3 --- /dev/null +++ b/tests/components/zwave_js/fixtures/device_diagnostics.json @@ -0,0 +1,2315 @@ +{ + "home_assistant": { + "installation_type": "Home Assistant OS", + "version": "2023.10.5", + "dev": false, + "hassio": true, + "virtualenv": false, + "python_version": "3.11.5", + "docker": true, + "arch": "aarch64", + "timezone": "America/New_York", + "os_name": "Linux", + "os_version": "6.1.56", + "supervisor": "2023.10.1", + "host_os": "Home Assistant OS 11.0", + "docker_version": "24.0.6", + "chassis": "embedded", + "run_as_root": true + }, + "custom_components": { + "pyscript": { + "version": "1.5.0", + "requirements": ["croniter==1.3.8", "watchdog==2.3.1"] + } + }, + "integration_manifest": { + "domain": "zwave_js", + "name": "Z-Wave", + "codeowners": ["@home-assistant/z-wave"], + "config_flow": true, + "dependencies": ["http", "repairs", "usb", "websocket_api"], + "documentation": "https://www.home-assistant.io/integrations/zwave_js", + "integration_type": "hub", + "iot_class": "local_push", + "loggers": ["zwave_js_server"], + "quality_scale": "platinum", + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.52.1"], + "usb": [ + { + "vid": "0658", + "pid": "0200", + "known_devices": ["Aeotec Z-Stick Gen5+", "Z-WaveMe UZB"] + }, + { + "vid": "10C4", + "pid": "8A2A", + "description": "*z-wave*", + "known_devices": ["Nortek HUSBZB-1"] + } + ], + "zeroconf": ["_zwave-js-server._tcp.local."], + "is_built_in": true + }, + "data": { + "versionInfo": { + "driverVersion": "12.2.1", + "serverVersion": "1.33.0", + "minSchemaVersion": 0, + "maxSchemaVersion": 33 + }, + "entities": [ + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_sensor_heat_alarm_heat_sensor_status", + "original_name": "Heat Alarm Heat sensor status", + "original_device_class": "enum", + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-113-0-Heat Alarm-Heat sensor status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 0, + "property": "Heat Alarm", + "property_name": "Heat Alarm", + "property_key": "Heat sensor status", + "property_key_name": "Heat sensor status" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_sensor_weather_alarm_moisture_alarm_status", + "original_name": "Weather Alarm Moisture alarm status", + "original_device_class": "enum", + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-113-0-Weather Alarm-Moisture alarm status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 0, + "property": "Weather Alarm", + "property_name": "Weather Alarm", + "property_key": "Moisture alarm status", + "property_key_name": "Moisture alarm status" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_sensor_alarmtype", + "original_name": "Alarm Type", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-113-0-alarmType", + "primary_value": null + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_sensor_alarmlevel", + "original_name": "Alarm Level", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-113-0-alarmLevel", + "primary_value": null + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_sensor_battery_level", + "original_name": "Battery level", + "original_device_class": "battery", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": "%", + "value_id": "23-128-0-level", + "primary_value": { + "command_class": 128, + "command_class_name": "Battery", + "endpoint": 0, + "property": "level", + "property_name": "level", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_sensor_charging_status", + "original_name": "Charging status", + "original_device_class": "battery", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": "%", + "value_id": "23-128-0-chargingStatus", + "primary_value": { + "command_class": 128, + "command_class_name": "Battery", + "endpoint": 0, + "property": "chargingStatus", + "property_name": "chargingStatus", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_sensor_recharge_or_replace", + "original_name": "Recharge or replace", + "original_device_class": "battery", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": "%", + "value_id": "23-128-0-rechargeOrReplace", + "primary_value": { + "command_class": 128, + "command_class_name": "Battery", + "endpoint": 0, + "property": "rechargeOrReplace", + "property_name": "rechargeOrReplace", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_sensor_node_identify_on_off_period_duration", + "original_name": "Node Identify - On/Off Period: Duration", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-135-0-80-3", + "primary_value": { + "command_class": 135, + "command_class_name": "Indicator", + "endpoint": 0, + "property": 80, + "property_name": "Node Identify", + "property_key": 3, + "property_key_name": "On/Off Period: Duration" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_sensor_node_identify_on_off_cycle_count", + "original_name": "Node Identify - On/Off Cycle Count", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-135-0-80-4", + "primary_value": { + "command_class": 135, + "command_class_name": "Indicator", + "endpoint": 0, + "property": 80, + "property_name": "Node Identify", + "property_key": 4, + "property_key_name": "On/Off Cycle Count" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_sensor_node_identify_on_off_period_on_time", + "original_name": "Node Identify - On/Off Period: On time", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-135-0-80-5", + "primary_value": { + "command_class": 135, + "command_class_name": "Indicator", + "endpoint": 0, + "property": 80, + "property_name": "Node Identify", + "property_key": 5, + "property_key_name": "On/Off Period: On time" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_sensor_indicator_value", + "original_name": "Indicator value", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-135-0-value", + "primary_value": null + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.2nd_floor_sensor_low_battery_level", + "original_name": "Low battery level", + "original_device_class": "battery", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-128-0-isLow", + "primary_value": { + "command_class": 128, + "command_class_name": "Battery", + "endpoint": 0, + "property": "isLow", + "property_name": "isLow", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.2nd_floor_sensor_rechargeable", + "original_name": "Rechargeable", + "original_device_class": "battery", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-128-0-rechargeable", + "primary_value": { + "command_class": 128, + "command_class_name": "Battery", + "endpoint": 0, + "property": "rechargeable", + "property_name": "rechargeable", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.2nd_floor_sensor_used_as_backup", + "original_name": "Used as backup", + "original_device_class": "battery", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-128-0-backup", + "primary_value": { + "command_class": 128, + "command_class_name": "Battery", + "endpoint": 0, + "property": "backup", + "property_name": "backup", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.2nd_floor_sensor_overheating", + "original_name": "Overheating", + "original_device_class": "battery", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-128-0-overheating", + "primary_value": { + "command_class": 128, + "command_class_name": "Battery", + "endpoint": 0, + "property": "overheating", + "property_name": "overheating", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.2nd_floor_sensor_fluid_is_low", + "original_name": "Fluid is low", + "original_device_class": "battery", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-128-0-lowFluid", + "primary_value": { + "command_class": 128, + "command_class_name": "Battery", + "endpoint": 0, + "property": "lowFluid", + "property_name": "lowFluid", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.2nd_floor_sensor_battery_is_disconnected", + "original_name": "Battery is disconnected", + "original_device_class": "battery", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-128-0-disconnected", + "primary_value": { + "command_class": 128, + "command_class_name": "Battery", + "endpoint": 0, + "property": "disconnected", + "property_name": "disconnected", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.2nd_floor_sensor_battery_temperature_is_low", + "original_name": "Battery temperature is low", + "original_device_class": "battery", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-128-0-lowTemperatureStatus", + "primary_value": { + "command_class": 128, + "command_class_name": "Battery", + "endpoint": 0, + "property": "lowTemperatureStatus", + "property_name": "lowTemperatureStatus", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_air_temperature", + "original_name": "Air temperature", + "original_device_class": "temperature", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "\u00b0F", + "value_id": "23-49-0-Air temperature", + "primary_value": { + "command_class": 49, + "command_class_name": "Multilevel Sensor", + "endpoint": 0, + "property": "Air temperature", + "property_name": "Air temperature", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.2nd_floor_underheat_detected", + "original_name": "Underheat detected", + "original_device_class": "heat", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-113-0-Heat Alarm-Heat sensor status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 0, + "property": "Heat Alarm", + "property_name": "Heat Alarm", + "property_key": "Heat sensor status", + "property_key_name": "Heat sensor status", + "state_key": 6 + } + }, + { + "domain": "sensor", + "entity_id": "sensor.2nd_floor_humidity", + "original_name": "Humidity", + "original_device_class": "humidity", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "%", + "value_id": "23-49-0-Humidity", + "primary_value": { + "command_class": 49, + "command_class_name": "Multilevel Sensor", + "endpoint": 0, + "property": "Humidity", + "property_name": "Humidity", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.2nd_floor_moisture_alarm", + "original_name": "Moisture alarm", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-113-0-Weather Alarm-Moisture alarm status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 0, + "property": "Weather Alarm", + "property_name": "Weather Alarm", + "property_key": "Moisture alarm status", + "property_key_name": "Moisture alarm status", + "state_key": 2 + } + }, + { + "domain": "button", + "entity_id": "button.2nd_floor_sensor_idle_heat_sensor_status", + "original_name": "Idle Heat Alarm Heat sensor status", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-113-0-Heat Alarm-Heat sensor status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 0, + "property": "Heat Alarm", + "property_name": "Heat Alarm", + "property_key": "Heat sensor status", + "property_key_name": "Heat sensor status" + } + }, + { + "domain": "button", + "entity_id": "button.2nd_floor_sensor_idle_moisture_alarm_status", + "original_name": "Idle Weather Alarm Moisture alarm status", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-113-0-Weather Alarm-Moisture alarm status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 0, + "property": "Weather Alarm", + "property_name": "Weather Alarm", + "property_key": "Moisture alarm status", + "property_key_name": "Moisture alarm status" + } + }, + { + "domain": "select", + "entity_id": "select.2nd_floor_sensor_high_temperature_alert_reporting", + "original_name": "High Temperature Alert Reporting", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-112-0-6", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 6, + "property_name": "High Temperature Alert Reporting", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "select", + "entity_id": "select.2nd_floor_sensor_low_temperature_alert_reporting", + "original_name": "Low Temperature Alert Reporting", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-112-0-8", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 8, + "property_name": "Low Temperature Alert Reporting", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "select", + "entity_id": "select.2nd_floor_sensor_high_humidity_alert_reporting", + "original_name": "High Humidity Alert Reporting", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-112-0-10", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 10, + "property_name": "High Humidity Alert Reporting", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "select", + "entity_id": "select.2nd_floor_sensor_low_humidity_alert_reporting", + "original_name": "Low Humidity Alert Reporting", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-112-0-12", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 12, + "property_name": "Low Humidity Alert Reporting", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "select", + "entity_id": "select.2nd_floor_sensor_temperature_scale", + "original_name": "Temperature Scale", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "23-112-0-13", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 13, + "property_name": "Temperature Scale", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_battery_report_threshold", + "original_name": "Battery Report Threshold", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "%", + "value_id": "23-112-0-1", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 1, + "property_name": "Battery Report Threshold", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_low_battery_alarm_threshold", + "original_name": "Low Battery Alarm Threshold", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "%", + "value_id": "23-112-0-2", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 2, + "property_name": "Low Battery Alarm Threshold", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_temperature_report_threshold", + "original_name": "Temperature Report Threshold", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "0.1 \u00b0F/C", + "value_id": "23-112-0-3", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 3, + "property_name": "Temperature Report Threshold", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_humidity_report_threshold", + "original_name": "Humidity Report Threshold", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "%", + "value_id": "23-112-0-4", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 4, + "property_name": "Humidity Report Threshold", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_high_temperature_alert_threshold", + "original_name": "High Temperature Alert Threshold", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "\u00b0F/C", + "value_id": "23-112-0-5", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 5, + "property_name": "High Temperature Alert Threshold", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_low_temperature_alert_threshold", + "original_name": "Low Temperature Alert Threshold", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "\u00b0F/C", + "value_id": "23-112-0-7", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 7, + "property_name": "Low Temperature Alert Threshold", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_high_humidity_alert_threshold", + "original_name": "High Humidity Alert Threshold", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "%", + "value_id": "23-112-0-9", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 9, + "property_name": "High Humidity Alert Threshold", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_low_humidity_alert_threshold", + "original_name": "Low Humidity Alert Threshold", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "%", + "value_id": "23-112-0-11", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 11, + "property_name": "Low Humidity Alert Threshold", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_temperature_offset", + "original_name": "Temperature Offset", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "0.1 \u00b0F/C", + "value_id": "23-112-0-14", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 14, + "property_name": "Temperature Offset", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_humidity_offset", + "original_name": "Humidity Offset", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "0.1 %", + "value_id": "23-112-0-15", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 15, + "property_name": "Humidity Offset", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_temperature_reporting_interval", + "original_name": "Temperature Reporting Interval", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "minutes", + "value_id": "23-112-0-16", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 16, + "property_name": "Temperature Reporting Interval", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "number", + "entity_id": "number.2nd_floor_sensor_humidity_reporting_interval", + "original_name": "Humidity Reporting Interval", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": "minutes", + "value_id": "23-112-0-17", + "primary_value": { + "command_class": 112, + "command_class_name": "Configuration", + "endpoint": 0, + "property": 17, + "property_name": "Humidity Reporting Interval", + "property_key": null, + "property_key_name": null + } + } + ], + "state": { + "nodeId": 23, + "index": 0, + "installerIcon": 3327, + "userIcon": 3327, + "status": 1, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "manufacturerId": 634, + "productId": 57348, + "productType": 28672, + "firmwareVersion": "1.10", + "zwavePlusVersion": 2, + "name": "2nd Floor Sensor", + "location": "**REDACTED**", + "deviceConfig": { + "filename": "/usr/src/app/store/.config-db/devices/0x027a/zse44.json", + "isEmbedded": true, + "manufacturer": "Zooz", + "manufacturerId": 634, + "label": "ZSE44", + "description": "Temperature Humidity XS Sensor", + "devices": [ + { + "productType": 28672, + "productId": 57348 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "Initiate inclusion (pairing) in the app (or web interface). Not sure how? ask@getzooz.com\nWhile the hub is looking for new devices, click the Z-Wave button 3 times as quickly as possible. The LED indicator will start flashing to confirm inclusion mode and turn off once inclusion is completed.", + "exclusion": "1. Bring the sensor within direct range of your Z-Wave hub.\n2. Put the Z-Wave hub into exclusion mode (not sure how to do that? ask@getzooz.com).\n3. Click the Z-Wave button 3 times as quickly as possible.\n4. Your hub will confirm exclusion and the sensor will disappear from your controller's device list", + "reset": "When your network\u2019s primary controller is missing or otherwise inoperable, you may need to reset the device to factory settings manually. In order to complete the process, make sure the sensor is powered, then click the Z-Wave button twice and hold it the third time for 10 seconds. The LED indicator will blink continuously. Immediately after, click the Z-Wave button twice more to finalize the reset. The LED indicator will flash 3 times to confirm a successful reset", + "manual": "https://cdn.shopify.com/s/files/1/0218/7704/files/zooz-700-series-tilt-shock-xs-sensor-zse43-manual.pdf" + } + }, + "label": "ZSE44", + "interviewAttempts": 0, + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 6, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x027a:0x7000:0xe004:1.10", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + "lwr": { + "repeaters": [2], + "protocolDataRate": 3 + } + }, + "highestSecurityClass": 1, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2023-08-09T13:26:05.031Z", + "values": { + "23-49-0-Air temperature": { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 1 + }, + "unit": "\u00b0F", + "stateful": true, + "secret": false + }, + "value": 69.9 + }, + "23-49-0-Humidity": { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Humidity", + "propertyName": "Humidity", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Humidity", + "ccSpecific": { + "sensorType": 5, + "scale": 0 + }, + "unit": "%", + "stateful": true, + "secret": false + }, + "value": 54 + }, + "23-112-0-1": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Battery Report Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Report Threshold", + "default": 5, + "min": 1, + "max": 10, + "unit": "%", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + "23-112-0-2": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Low Battery Alarm Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Battery Alarm Threshold", + "default": 20, + "min": 10, + "max": 50, + "unit": "%", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + "23-112-0-3": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Temperature Report Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature Report Threshold", + "default": 20, + "min": 10, + "max": 100, + "unit": "0.1 \u00b0F/C", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + "23-112-0-4": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Humidity Report Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Humidity Report Threshold", + "default": 10, + "min": 1, + "max": 50, + "unit": "%", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + "23-112-0-5": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "High Temperature Alert Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "High Temperature Alert Threshold", + "default": 120, + "min": 50, + "max": 120, + "unit": "\u00b0F/C", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 120 + }, + "23-112-0-6": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "High Temperature Alert Reporting", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "High Temperature Alert Reporting", + "default": 7, + "min": 0, + "max": 7, + "states": { + "0": "Disable", + "1": "Lifeline only", + "2": "0xff (on) to Group 2 only", + "3": "0xff (on) to Lifeline and Group 2", + "4": "0x00 (off) to Group 2 only", + "5": "0x00 (off) to Lifeline and Group 2", + "6": "0xff (on) and 0x00 (off) to Group 2 only", + "7": "0xff (on) and 0x00 (off) to Lifeline and Group 2" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 7 + }, + "23-112-0-7": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Low Temperature Alert Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Temperature Alert Threshold", + "default": 10, + "min": 10, + "max": 100, + "unit": "\u00b0F/C", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + "23-112-0-8": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Low Temperature Alert Reporting", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Temperature Alert Reporting", + "default": 7, + "min": 0, + "max": 7, + "states": { + "0": "Disable", + "1": "Lifeline only", + "2": "0xff (on) to Group 3 only", + "3": "0xff (on) to Lifeline and Group 3", + "4": "0x00 (off) to Group 3 only", + "5": "0x00 (off) to Lifeline and Group 3", + "6": "0xff (on) and 0x00 (off) to Group 3 only", + "7": "0xff (on) and 0x00 (off) to Lifeline and Group 3" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 7 + }, + "23-112-0-9": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "High Humidity Alert Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "High Humidity Alert Threshold", + "default": 0, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + "23-112-0-10": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "High Humidity Alert Reporting", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "High Humidity Alert Reporting", + "default": 7, + "min": 0, + "max": 7, + "states": { + "0": "Disable", + "1": "Lifeline only", + "2": "0xff (on) to Group 4 only", + "3": "0xff (on) to Lifeline and Group 4", + "4": "0x00 (off) to Group 4 only", + "5": "0x00 (off) to Lifeline and Group 4", + "6": "0xff (on) and 0x00 (off) to Group 4 only", + "7": "0xff (on) and 0x00 (off) to Lifeline and Group 4" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 7 + }, + "23-112-0-11": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Low Humidity Alert Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Humidity Alert Threshold", + "default": 0, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + "23-112-0-12": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Low Humidity Alert Reporting", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Humidity Alert Reporting", + "default": 7, + "min": 0, + "max": 7, + "states": { + "0": "Disable", + "1": "Lifeline only", + "2": "0xff (on) to Group 5 only", + "3": "0xff (on) to Lifeline and Group 5", + "4": "0x00 (off) to Group 5 only", + "5": "0x00 (off) to Lifeline and Group 5", + "6": "0xff (on) and 0x00 (off) to Group 5 only", + "7": "0xff (on) and 0x00 (off) to Lifeline and Group 5" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 7 + }, + "23-112-0-13": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Temperature Scale", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature Scale", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Celsius", + "1": "Fahrenheit" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + "23-112-0-14": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Temperature Offset", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "0=-10, 100=0, 200=+10", + "label": "Temperature Offset", + "default": 100, + "min": 0, + "max": 200, + "unit": "0.1 \u00b0F/C", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + "23-112-0-15": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyName": "Humidity Offset", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "0=-10, 100=0, 200=+10", + "label": "Humidity Offset", + "default": 100, + "min": 0, + "max": 200, + "unit": "0.1 %", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + "23-112-0-16": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyName": "Temperature Reporting Interval", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature Reporting Interval", + "default": 240, + "min": 1, + "max": 480, + "unit": "minutes", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 240 + }, + "23-112-0-17": { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Humidity Reporting Interval", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Humidity Reporting Interval", + "default": 240, + "min": 1, + "max": 480, + "unit": "minutes", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 240 + }, + "23-113-0-Heat Alarm-Heat sensor status": { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Heat Alarm", + "propertyKey": "Heat sensor status", + "propertyName": "Heat Alarm", + "propertyKeyName": "Heat sensor status", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Heat sensor status", + "ccSpecific": { + "notificationType": 4 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "6": "Underheat detected" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "23-113-0-Weather Alarm-Moisture alarm status": { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Weather Alarm", + "propertyKey": "Moisture alarm status", + "propertyName": "Weather Alarm", + "propertyKeyName": "Moisture alarm status", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Moisture alarm status", + "ccSpecific": { + "notificationType": 16 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "2": "Moisture alarm" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "23-114-0-manufacturerId": { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 634 + }, + "23-114-0-productType": { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 28672 + }, + "23-114-0-productId": { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 57348 + }, + "23-128-0-level": { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%", + "stateful": true, + "secret": false + }, + "value": 0 + }, + "23-128-0-isLow": { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level", + "stateful": true, + "secret": false + }, + "value": true + }, + "23-128-0-chargingStatus": { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "chargingStatus", + "propertyName": "chargingStatus", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Charging status", + "min": 0, + "max": 255, + "states": { + "0": "Discharging", + "1": "Charging", + "2": "Maintaining" + }, + "stateful": true, + "secret": false + } + }, + "23-128-0-rechargeable": { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "rechargeable", + "propertyName": "rechargeable", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Rechargeable", + "stateful": true, + "secret": false + } + }, + "23-128-0-backup": { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "backup", + "propertyName": "backup", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Used as backup", + "stateful": true, + "secret": false + } + }, + "23-128-0-overheating": { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "overheating", + "propertyName": "overheating", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Overheating", + "stateful": true, + "secret": false + } + }, + "23-128-0-lowFluid": { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "lowFluid", + "propertyName": "lowFluid", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Fluid is low", + "stateful": true, + "secret": false + } + }, + "23-128-0-rechargeOrReplace": { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "rechargeOrReplace", + "propertyName": "rechargeOrReplace", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Recharge or replace", + "min": 0, + "max": 255, + "states": { + "0": "No", + "1": "Soon", + "2": "Now" + }, + "stateful": true, + "secret": false + } + }, + "23-128-0-disconnected": { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "disconnected", + "propertyName": "disconnected", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Battery is disconnected", + "stateful": true, + "secret": false + } + }, + "23-128-0-lowTemperatureStatus": { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "lowTemperatureStatus", + "propertyName": "lowTemperatureStatus", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Battery temperature is low", + "stateful": true, + "secret": false + } + }, + "23-132-0-wakeUpInterval": { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "ccVersion": 1, + "metadata": { + "type": "number", + "default": 21600, + "readable": false, + "writeable": true, + "min": 3600, + "max": 86400, + "steps": 60, + "stateful": true, + "secret": false + }, + "value": 21600 + }, + "23-132-0-controllerNodeId": { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller", + "stateful": true, + "secret": false + }, + "value": 1 + }, + "23-134-0-libraryType": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + }, + "23-134-0-protocolVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.13" + }, + "23-134-0-firmwareVersions": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["1.10"] + }, + "23-134-0-hardwareVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 1 + }, + "23-134-0-sdkVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version", + "stateful": true, + "secret": false + } + }, + "23-134-0-applicationFrameworkAPIVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version", + "stateful": true, + "secret": false + } + }, + "23-134-0-applicationFrameworkBuildNumber": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number", + "stateful": true, + "secret": false + } + }, + "23-134-0-hostInterfaceVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version", + "stateful": true, + "secret": false + } + }, + "23-134-0-hostInterfaceBuildNumber": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number", + "stateful": true, + "secret": false + } + }, + "23-134-0-zWaveProtocolVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + } + }, + "23-134-0-zWaveProtocolBuildNumber": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number", + "stateful": true, + "secret": false + } + }, + "23-134-0-applicationVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version", + "stateful": true, + "secret": false + } + }, + "23-134-0-applicationBuildNumber": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number", + "stateful": true, + "secret": false + } + }, + "23-135-0-80-3": { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 3, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "Node Identify - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "23-135-0-80-4": { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 4, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "Node Identify - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "23-135-0-80-5": { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 5, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "Node Identify - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + } + }, + "endpoints": { + "0": { + "nodeId": 23, + "index": 0, + "installerIcon": 3327, + "userIcon": 3327, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 3, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 4, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": true + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 11, + "isSecure": true + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 3, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": true + }, + { + "id": 135, + "name": "Indicator", + "version": 4, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": true + }, + { + "id": 132, + "name": "Wake Up", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 7, + "isSecure": true + } + ] + } + } + } + } +} diff --git a/tests/components/zwave_js/fixtures/zooz_zse44_state.json b/tests/components/zwave_js/fixtures/zooz_zse44_state.json new file mode 100644 index 000000000000..a2fb5421fb70 --- /dev/null +++ b/tests/components/zwave_js/fixtures/zooz_zse44_state.json @@ -0,0 +1,1330 @@ +{ + "nodeId": 23, + "index": 0, + "installerIcon": 3327, + "userIcon": 3327, + "status": 1, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "manufacturerId": 634, + "productId": 57348, + "productType": 28672, + "firmwareVersion": "1.10", + "zwavePlusVersion": 2, + "name": "2nd Floor Sensor", + "location": "**REDACTED**", + "deviceConfig": { + "filename": "/usr/src/app/store/.config-db/devices/0x027a/zse44.json", + "isEmbedded": true, + "manufacturer": "Zooz", + "manufacturerId": 634, + "label": "ZSE44", + "description": "Temperature Humidity XS Sensor", + "devices": [ + { + "productType": 28672, + "productId": 57348 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "Initiate inclusion (pairing) in the app (or web interface). Not sure how? ask@getzooz.com\nWhile the hub is looking for new devices, click the Z-Wave button 3 times as quickly as possible. The LED indicator will start flashing to confirm inclusion mode and turn off once inclusion is completed.", + "exclusion": "1. Bring the sensor within direct range of your Z-Wave hub.\n2. Put the Z-Wave hub into exclusion mode (not sure how to do that? ask@getzooz.com).\n3. Click the Z-Wave button 3 times as quickly as possible.\n4. Your hub will confirm exclusion and the sensor will disappear from your controller's device list", + "reset": "When your network\u2019s primary controller is missing or otherwise inoperable, you may need to reset the device to factory settings manually. In order to complete the process, make sure the sensor is powered, then click the Z-Wave button twice and hold it the third time for 10 seconds. The LED indicator will blink continuously. Immediately after, click the Z-Wave button twice more to finalize the reset. The LED indicator will flash 3 times to confirm a successful reset", + "manual": "https://cdn.shopify.com/s/files/1/0218/7704/files/zooz-700-series-tilt-shock-xs-sensor-zse43-manual.pdf" + } + }, + "label": "ZSE44", + "interviewAttempts": 0, + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 6, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x027a:0x7000:0xe004:1.10", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + "lwr": { + "repeaters": [2], + "protocolDataRate": 3 + } + }, + "highestSecurityClass": 1, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2023-08-09T13:26:05.031Z", + "endpoints": { + "0": { + "nodeId": 23, + "index": 0, + "installerIcon": 3327, + "userIcon": 3327, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 3, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 4, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": true + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 11, + "isSecure": true + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 3, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": true + }, + { + "id": 135, + "name": "Indicator", + "version": 4, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": true + }, + { + "id": 132, + "name": "Wake Up", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 7, + "isSecure": true + } + ] + } + }, + "values": [ + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 1 + }, + "unit": "\u00b0F", + "stateful": true, + "secret": false + }, + "value": 69.9 + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Humidity", + "propertyName": "Humidity", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Humidity", + "ccSpecific": { + "sensorType": 5, + "scale": 0 + }, + "unit": "%", + "stateful": true, + "secret": false + }, + "value": 54 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Battery Report Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Report Threshold", + "default": 5, + "min": 1, + "max": 10, + "unit": "%", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Low Battery Alarm Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Battery Alarm Threshold", + "default": 20, + "min": 10, + "max": 50, + "unit": "%", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Temperature Report Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature Report Threshold", + "default": 20, + "min": 10, + "max": 100, + "unit": "0.1 \u00b0F/C", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Humidity Report Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Humidity Report Threshold", + "default": 10, + "min": 1, + "max": 50, + "unit": "%", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "High Temperature Alert Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "High Temperature Alert Threshold", + "default": 120, + "min": 50, + "max": 120, + "unit": "\u00b0F/C", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 120 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "High Temperature Alert Reporting", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "High Temperature Alert Reporting", + "default": 7, + "min": 0, + "max": 7, + "states": { + "0": "Disable", + "1": "Lifeline only", + "2": "0xff (on) to Group 2 only", + "3": "0xff (on) to Lifeline and Group 2", + "4": "0x00 (off) to Group 2 only", + "5": "0x00 (off) to Lifeline and Group 2", + "6": "0xff (on) and 0x00 (off) to Group 2 only", + "7": "0xff (on) and 0x00 (off) to Lifeline and Group 2" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 7 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Low Temperature Alert Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Temperature Alert Threshold", + "default": 10, + "min": 10, + "max": 100, + "unit": "\u00b0F/C", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Low Temperature Alert Reporting", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Temperature Alert Reporting", + "default": 7, + "min": 0, + "max": 7, + "states": { + "0": "Disable", + "1": "Lifeline only", + "2": "0xff (on) to Group 3 only", + "3": "0xff (on) to Lifeline and Group 3", + "4": "0x00 (off) to Group 3 only", + "5": "0x00 (off) to Lifeline and Group 3", + "6": "0xff (on) and 0x00 (off) to Group 3 only", + "7": "0xff (on) and 0x00 (off) to Lifeline and Group 3" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 7 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "High Humidity Alert Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "High Humidity Alert Threshold", + "default": 0, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "High Humidity Alert Reporting", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "High Humidity Alert Reporting", + "default": 7, + "min": 0, + "max": 7, + "states": { + "0": "Disable", + "1": "Lifeline only", + "2": "0xff (on) to Group 4 only", + "3": "0xff (on) to Lifeline and Group 4", + "4": "0x00 (off) to Group 4 only", + "5": "0x00 (off) to Lifeline and Group 4", + "6": "0xff (on) and 0x00 (off) to Group 4 only", + "7": "0xff (on) and 0x00 (off) to Lifeline and Group 4" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 7 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Low Humidity Alert Threshold", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Humidity Alert Threshold", + "default": 0, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Low Humidity Alert Reporting", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Low Humidity Alert Reporting", + "default": 7, + "min": 0, + "max": 7, + "states": { + "0": "Disable", + "1": "Lifeline only", + "2": "0xff (on) to Group 5 only", + "3": "0xff (on) to Lifeline and Group 5", + "4": "0x00 (off) to Group 5 only", + "5": "0x00 (off) to Lifeline and Group 5", + "6": "0xff (on) and 0x00 (off) to Group 5 only", + "7": "0xff (on) and 0x00 (off) to Lifeline and Group 5" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 7 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Temperature Scale", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature Scale", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Celsius", + "1": "Fahrenheit" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Temperature Offset", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "0=-10, 100=0, 200=+10", + "label": "Temperature Offset", + "default": 100, + "min": 0, + "max": 200, + "unit": "0.1 \u00b0F/C", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyName": "Humidity Offset", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "0=-10, 100=0, 200=+10", + "label": "Humidity Offset", + "default": 100, + "min": 0, + "max": 200, + "unit": "0.1 %", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyName": "Temperature Reporting Interval", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature Reporting Interval", + "default": 240, + "min": 1, + "max": 480, + "unit": "minutes", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 240 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Humidity Reporting Interval", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Humidity Reporting Interval", + "default": 240, + "min": 1, + "max": 480, + "unit": "minutes", + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 240 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Heat Alarm", + "propertyKey": "Heat sensor status", + "propertyName": "Heat Alarm", + "propertyKeyName": "Heat sensor status", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Heat sensor status", + "ccSpecific": { + "notificationType": 4 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "6": "Underheat detected" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Weather Alarm", + "propertyKey": "Moisture alarm status", + "propertyName": "Weather Alarm", + "propertyKeyName": "Moisture alarm status", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Moisture alarm status", + "ccSpecific": { + "notificationType": 16 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "2": "Moisture alarm" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 634 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 28672 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 57348 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level", + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "chargingStatus", + "propertyName": "chargingStatus", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Charging status", + "min": 0, + "max": 255, + "states": { + "0": "Discharging", + "1": "Charging", + "2": "Maintaining" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "rechargeable", + "propertyName": "rechargeable", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Rechargeable", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "backup", + "propertyName": "backup", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Used as backup", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "overheating", + "propertyName": "overheating", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Overheating", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "lowFluid", + "propertyName": "lowFluid", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Fluid is low", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "rechargeOrReplace", + "propertyName": "rechargeOrReplace", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Recharge or replace", + "min": 0, + "max": 255, + "states": { + "0": "No", + "1": "Soon", + "2": "Now" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "disconnected", + "propertyName": "disconnected", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Battery is disconnected", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "lowTemperatureStatus", + "propertyName": "lowTemperatureStatus", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Battery temperature is low", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "ccVersion": 1, + "metadata": { + "type": "number", + "default": 21600, + "readable": false, + "writeable": true, + "min": 3600, + "max": 86400, + "steps": 60, + "stateful": true, + "secret": false + }, + "value": 21600 + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller", + "stateful": true, + "secret": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.13" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["1.10"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 3, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "Node Identify - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 4, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "Node Identify - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 5, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "Node Identify - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + } + ] +} diff --git a/tests/components/zwave_js/scripts/__init__.py b/tests/components/zwave_js/scripts/__init__.py new file mode 100644 index 000000000000..96d81d993e9f --- /dev/null +++ b/tests/components/zwave_js/scripts/__init__.py @@ -0,0 +1 @@ +"""Tests for zwave_js scripts.""" diff --git a/tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py b/tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py new file mode 100644 index 000000000000..d1e12e7abb46 --- /dev/null +++ b/tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py @@ -0,0 +1,80 @@ +"""Test convert_device_diagnostics_to_fixture script.""" +import copy +import json +from pathlib import Path +import sys +from unittest.mock import patch + +import pytest + +from homeassistant.components.zwave_js.scripts.convert_device_diagnostics_to_fixture import ( + extract_fixture_data, + get_fixtures_dir_path, + load_file, + main, +) + +from tests.common import load_fixture + + +def _minify(text: str) -> str: + """Minify string by removing whitespace and new lines.""" + return text.replace(" ", "").replace("\n", "") + + +def test_fixture_functions() -> None: + """Test functions related to the fixture.""" + diagnostics_data = json.loads(load_fixture("zwave_js/device_diagnostics.json")) + state = extract_fixture_data(copy.deepcopy(diagnostics_data)) + assert isinstance(state["values"], list) + assert ( + get_fixtures_dir_path(state) + == Path(__file__).parents[1] / "fixtures" / "zooz_zse44_state.json" + ) + + old_diagnostics_format_data = copy.deepcopy(diagnostics_data) + old_diagnostics_format_data["data"]["state"]["values"] = list( + old_diagnostics_format_data["data"]["state"]["values"].values() + ) + assert ( + extract_fixture_data(old_diagnostics_format_data) + == old_diagnostics_format_data["data"]["state"] + ) + + with pytest.raises(ValueError): + extract_fixture_data({}) + + +def test_load_file() -> None: + """Test load file.""" + assert load_file( + Path(__file__).parents[1] / "fixtures" / "device_diagnostics.json" + ) == json.loads(load_fixture("zwave_js/device_diagnostics.json")) + + +def test_main(capfd: pytest.CaptureFixture[str]) -> None: + """Test main function.""" + Path(__file__).parents[1] / "fixtures" / "zooz_zse44_state.json" + fixture_str = load_fixture("zwave_js/zooz_zse44_state.json") + fixture_dict = json.loads(fixture_str) + + # Test dump to stdout + args = [ + "homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py", + str(Path(__file__).parents[1] / "fixtures" / "device_diagnostics.json"), + ] + with patch.object(sys, "argv", args): + main() + + captured = capfd.readouterr() + assert _minify(captured.out) == _minify(fixture_str) + + # Check file dump + args.append("--file") + with patch.object(sys, "argv", args), patch( + "homeassistant.components.zwave_js.scripts.convert_device_diagnostics_to_fixture.Path.write_text" + ) as write_text_mock: + main() + + assert len(write_text_mock.call_args_list) == 1 + assert write_text_mock.call_args[0][0] == json.dumps(fixture_dict, indent=2) From 718901d2addc539d5fffdfbb311bfd726f16c18d Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Fri, 3 Nov 2023 17:16:20 +0100 Subject: [PATCH 113/265] Fix typo in Todoist config flow (#103317) --- homeassistant/components/todoist/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/todoist/strings.json b/homeassistant/components/todoist/strings.json index 442114eb1189..0f81702a4d03 100644 --- a/homeassistant/components/todoist/strings.json +++ b/homeassistant/components/todoist/strings.json @@ -5,7 +5,7 @@ "data": { "token": "[%key:common::config_flow::data::api_token%]" }, - "description": "Please entry your API token from your [Todoist Settings page]({settings_url})" + "description": "Please enter your API token from your [Todoist Settings page]({settings_url})" } }, "error": { From 730a3f787002294375ca361ea80d79c2ae431437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 3 Nov 2023 17:44:48 +0100 Subject: [PATCH 114/265] Remove extra from traccar webhook (#103319) --- homeassistant/components/traccar/__init__.py | 3 ++- tests/components/traccar/test_init.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index c428ce7a5b1b..5dffd629e803 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -50,7 +50,8 @@ WEBHOOK_SCHEMA = vol.Schema( vol.Optional(ATTR_BEARING): vol.Coerce(float), vol.Optional(ATTR_SPEED): vol.Coerce(float), vol.Optional(ATTR_TIMESTAMP): vol.Coerce(int), - } + }, + extra=vol.REMOVE_EXTRA, ) diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index ccae59932de8..1ac7adfb5493 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -153,6 +153,7 @@ async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None "speed": 100, "bearing": "105.32", "altitude": 102, + "charge": "true", } req = await client.post(url, params=data) @@ -165,6 +166,7 @@ async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None assert state.attributes["speed"] == 100.0 assert state.attributes["bearing"] == 105.32 assert state.attributes["altitude"] == 102.0 + assert "charge" not in state.attributes data = { "lat": str(HOME_LATITUDE), From 8a07c10d886a8990eba567d74065689e96f59d03 Mon Sep 17 00:00:00 2001 From: Jan Rieger Date: Sat, 4 Nov 2023 09:48:02 +0100 Subject: [PATCH 115/265] Report correct weather condition at night for Met (#103334) * Report correct weather condition at night for Met, fixes #68369, fixes #89001 * Update homeassistant/components/met/weather.py Co-authored-by: Jan-Philipp Benecke --------- Co-authored-by: Jan-Philipp Benecke --- homeassistant/components/met/weather.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 8a5c405c1c10..97b99e826cdc 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -31,13 +31,21 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import entity_registry as er, sun from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import METRIC_SYSTEM from . import MetDataUpdateCoordinator -from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP +from .const import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_SUNNY, + ATTR_MAP, + CONDITIONS_MAP, + CONF_TRACK_HOME, + DOMAIN, + FORECAST_MAP, +) DEFAULT_NAME = "Met.no" @@ -141,6 +149,10 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]): condition = self.coordinator.data.current_weather_data.get("condition") if condition is None: return None + + if condition == ATTR_CONDITION_SUNNY and not sun.is_up(self.hass): + condition = ATTR_CONDITION_CLEAR_NIGHT + return format_condition(condition) @property From 42243f14332b502c0cb424c67ca4f164cc6d57c4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 4 Nov 2023 12:19:56 +0100 Subject: [PATCH 116/265] Handle UniFi traffic rules not supported on older versions (#103346) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index f1fc47774676..ed8649896ddb 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==64"], + "requirements": ["aiounifi==65"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index b5386244ee09..c005a86cac3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -369,7 +369,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==64 +aiounifi==65 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0dc45880224..b41909be0785 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -344,7 +344,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==64 +aiounifi==65 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 1ef460cffe601d63e6df1c0e168b253e5b301df7 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Sat, 4 Nov 2023 12:43:20 +0400 Subject: [PATCH 117/265] Fix sensor unique id in Islamic prayer times (#103356) --- homeassistant/components/islamic_prayer_times/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index ee3c5d9071d6..45270863f01b 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -78,7 +78,7 @@ class IslamicPrayerTimeSensor( """Initialize the Islamic prayer time sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_unique_id = description.key + self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, name=NAME, From 9eff9ee374895bb055cee2640a4f34fa9e4156b7 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:36:48 -0400 Subject: [PATCH 118/265] Fix zwave_js cover bug for Window Covering CC values (#103289) * Fix cover bug for Window Covering CC values * update test * Fix fixture * Remove no-op line from test --- homeassistant/components/zwave_js/cover.py | 3 +- .../convert_device_diagnostics_to_fixture.py | 10 +- .../fixtures/cover_iblinds_v3_state.json | 708 ++++++++---------- .../zwave_js/fixtures/zooz_zse44_state.json | 268 +++---- ...t_convert_device_diagnostics_to_fixture.py | 5 +- tests/components/zwave_js/test_cover.py | 12 +- 6 files changed, 459 insertions(+), 547 deletions(-) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 9a8cb203c055..364eafd8cafc 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -18,7 +18,6 @@ from zwave_js_server.const.command_class.multilevel_switch import ( from zwave_js_server.const.command_class.window_covering import ( NO_POSITION_PROPERTY_KEYS, NO_POSITION_SUFFIX, - WINDOW_COVERING_OPEN_PROPERTY, SlatStates, ) from zwave_js_server.model.driver import Driver @@ -370,7 +369,7 @@ class ZWaveWindowCovering(CoverPositionMixin, CoverTiltMixin): set_values_func( value, stop_value=self.get_zwave_value( - WINDOW_COVERING_OPEN_PROPERTY, + "levelChangeUp", value_property_key=value.property_key, ), ) diff --git a/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py b/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py index 1e8d295227fe..826f3eebe0c2 100644 --- a/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py +++ b/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py @@ -60,10 +60,12 @@ def extract_fixture_data(diagnostics_data: Any) -> dict: ): raise ValueError("Invalid diagnostics file format") state: dict = diagnostics_data["data"]["state"] - if isinstance(state["values"], list): - return state - values_dict: dict[str, dict] = state.pop("values") - state["values"] = list(values_dict.values()) + if not isinstance(state["values"], list): + values_dict: dict[str, dict] = state.pop("values") + state["values"] = list(values_dict.values()) + if not isinstance(state["endpoints"], list): + endpoints_dict: dict[str, dict] = state.pop("endpoints") + state["endpoints"] = list(endpoints_dict.values()) return state diff --git a/tests/components/zwave_js/fixtures/cover_iblinds_v3_state.json b/tests/components/zwave_js/fixtures/cover_iblinds_v3_state.json index f0da41e4b6ff..cf04e885d794 100644 --- a/tests/components/zwave_js/fixtures/cover_iblinds_v3_state.json +++ b/tests/components/zwave_js/fixtures/cover_iblinds_v3_state.json @@ -1,5 +1,5 @@ { - "nodeId": 12, + "nodeId": 131, "index": 0, "installerIcon": 6656, "userIcon": 6656, @@ -7,12 +7,13 @@ "ready": true, "isListening": false, "isRouting": true, - "isSecure": true, + "isSecure": false, "manufacturerId": 647, "productId": 114, "productType": 4, "firmwareVersion": "3.12.1", "zwavePlusVersion": 2, + "name": "Blind West Bed 1", "deviceConfig": { "filename": "/data/db/devices/0x0287/iblindsv3.json", "isEmbedded": true, @@ -38,321 +39,61 @@ "associations": {}, "paramInformation": { "_map": {} + }, + "compat": { + "removeCCs": {} } }, "label": "iblinds V3", "interviewAttempts": 1, - "endpoints": [ - { - "nodeId": 12, - "index": 0, - "installerIcon": 6656, - "userIcon": 6656, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 17, - "label": "Multilevel Switch" - }, - "specific": { - "key": 7, - "label": "Motor Control Class C" - }, - "mandatorySupportedCCs": [32, 38, 37, 114, 134], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 38, - "name": "Multilevel Switch", - "version": 4, - "isSecure": true - }, - { - "id": 37, - "name": "Binary Switch", - "version": 2, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": true - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": true - }, - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 2, - "isSecure": true - }, - { - "id": 89, - "name": "Association Group Information", - "version": 3, - "isSecure": true - }, - { - "id": 85, - "name": "Transport Service", - "version": 2, - "isSecure": false - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": true - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": true - }, - { - "id": 159, - "name": "Security 2", - "version": 1, - "isSecure": true - }, - { - "id": 108, - "name": "Supervision", - "version": 1, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 5, - "isSecure": true - }, - { - "id": 128, - "name": "Battery", - "version": 1, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 4, - "isSecure": true - }, - { - "id": 135, - "name": "Indicator", - "version": 3, - "isSecure": true - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 3, - "isSecure": true - }, - { - "id": 106, - "name": "Window Covering", - "version": 1, - "isSecure": true - }, - { - "id": 152, - "name": "Security", - "version": 1, - "isSecure": true - } - ] + "isFrequentListening": "1000ms", + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + }, + "mandatorySupportedCCs": [32, 38, 37, 114, 134], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0287:0x0004:0x0072:3.12.1", + "statistics": { + "commandsTX": 95, + "commandsRX": 110, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + "rtt": 1295.6, + "lastSeen": "2023-11-02T18:41:40.552Z", + "rssi": -69, + "lwr": { + "protocolDataRate": 2, + "repeaters": [], + "rssi": -71, + "repeaterRSSI": [] } - ], + }, + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2023-11-02T18:41:40.552Z", "values": [ - { - "endpoint": 0, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 2, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Current value", - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 0, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 2, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - }, - "value": false - }, - { - "endpoint": 0, - "commandClass": 37, - "commandClassName": "Binary Switch", - "property": "duration", - "propertyName": "duration", - "ccVersion": 2, - "metadata": { - "type": "duration", - "readable": true, - "writeable": false, - "label": "Remaining duration", - "stateful": true, - "secret": false - }, - "value": { - "value": 0, - "unit": "seconds" - } - }, - { - "endpoint": 0, - "commandClass": 38, - "commandClassName": "Multilevel Switch", - "property": "targetValue", - "propertyName": "targetValue", - "ccVersion": 4, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Target value", - "valueChangeOptions": ["transitionDuration"], - "min": 0, - "max": 99, - "stateful": true, - "secret": false - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 38, - "commandClassName": "Multilevel Switch", - "property": "duration", - "propertyName": "duration", - "ccVersion": 4, - "metadata": { - "type": "duration", - "readable": true, - "writeable": false, - "label": "Remaining duration", - "stateful": true, - "secret": false - }, - "value": { - "value": 0, - "unit": "seconds" - } - }, - { - "endpoint": 0, - "commandClass": 38, - "commandClassName": "Multilevel Switch", - "property": "currentValue", - "propertyName": "currentValue", - "ccVersion": 4, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Current value", - "min": 0, - "max": 99, - "stateful": true, - "secret": false - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 38, - "commandClassName": "Multilevel Switch", - "property": "Up", - "propertyName": "Up", - "ccVersion": 4, - "metadata": { - "type": "boolean", - "readable": false, - "writeable": true, - "label": "Perform a level change (Up)", - "ccSpecific": { - "switchType": 2 - }, - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 0, - "commandClass": 38, - "commandClassName": "Multilevel Switch", - "property": "Down", - "propertyName": "Down", - "ccVersion": 4, - "metadata": { - "type": "boolean", - "readable": false, - "writeable": true, - "label": "Perform a level change (Down)", - "ccSpecific": { - "switchType": 2 - }, - "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - } - }, - { - "endpoint": 0, - "commandClass": 38, - "commandClassName": "Multilevel Switch", - "property": "restorePrevious", - "propertyName": "restorePrevious", - "ccVersion": 4, - "metadata": { - "type": "boolean", - "readable": false, - "writeable": true, - "label": "Restore previous value", - "stateful": true, - "secret": false - } - }, { "endpoint": 0, "commandClass": 106, @@ -361,7 +102,7 @@ "propertyKey": 23, "propertyName": "currentValue", "propertyKeyName": "Horizontal Slats Angle", - "ccVersion": 0, + "ccVersion": 1, "metadata": { "type": "number", "readable": true, @@ -373,9 +114,9 @@ "min": 0, "max": 99, "states": { - "0": "Closed (up)", + "0": "Closed (up inside)", "50": "Open", - "99": "Closed (down)" + "99": "Closed (down inside)" }, "stateful": true, "secret": false @@ -390,7 +131,7 @@ "propertyKey": 23, "propertyName": "targetValue", "propertyKeyName": "Horizontal Slats Angle", - "ccVersion": 0, + "ccVersion": 1, "metadata": { "type": "number", "readable": true, @@ -403,14 +144,14 @@ "min": 0, "max": 99, "states": { - "0": "Closed (up)", + "0": "Closed (up inside)", "50": "Open", - "99": "Closed (down)" + "99": "Closed (down inside)" }, "stateful": true, "secret": false }, - "value": 99 + "value": 0 }, { "endpoint": 0, @@ -420,7 +161,7 @@ "propertyKey": 23, "propertyName": "duration", "propertyKeyName": "Horizontal Slats Angle", - "ccVersion": 0, + "ccVersion": 1, "metadata": { "type": "duration", "readable": true, @@ -441,44 +182,24 @@ "endpoint": 0, "commandClass": 106, "commandClassName": "Window Covering", - "property": "open", + "property": "levelChangeUp", "propertyKey": 23, - "propertyName": "open", + "propertyName": "levelChangeUp", "propertyKeyName": "Horizontal Slats Angle", - "ccVersion": 0, + "ccVersion": 1, "metadata": { "type": "boolean", "readable": false, "writeable": true, - "label": "Open - Horizontal Slats Angle", + "label": "Change tilt (down inside) - Horizontal Slats Angle", "ccSpecific": { "parameter": 23 }, "valueChangeOptions": ["transitionDuration"], - "stateful": true, - "secret": false - }, - "nodeId": 12, - "value": true - }, - { - "endpoint": 0, - "commandClass": 106, - "commandClassName": "Window Covering", - "property": "close0", - "propertyKey": 23, - "propertyName": "close0", - "propertyKeyName": "Horizontal Slats Angle", - "ccVersion": 0, - "metadata": { - "type": "boolean", - "readable": false, - "writeable": true, - "label": "Close Up - Horizontal Slats Angle", - "ccSpecific": { - "parameter": 23 + "states": { + "true": "Start", + "false": "Stop" }, - "valueChangeOptions": ["transitionDuration"], "stateful": true, "secret": false } @@ -487,25 +208,27 @@ "endpoint": 0, "commandClass": 106, "commandClassName": "Window Covering", - "property": "close99", + "property": "levelChangeDown", "propertyKey": 23, - "propertyName": "close99", + "propertyName": "levelChangeDown", "propertyKeyName": "Horizontal Slats Angle", - "ccVersion": 0, + "ccVersion": 1, "metadata": { "type": "boolean", "readable": false, "writeable": true, - "label": "Close Down - Horizontal Slats Angle", + "label": "Change tilt (up inside) - Horizontal Slats Angle", "ccSpecific": { "parameter": 23 }, "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, "stateful": true, "secret": false - }, - "nodeId": 12, - "value": true + } }, { "endpoint": 0, @@ -604,7 +327,7 @@ "allowManualEntry": true, "isFromConfig": true }, - "value": 50 + "value": 45 }, { "endpoint": 0, @@ -656,6 +379,32 @@ }, "value": 0 }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "MC", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "MC", + "label": "MC", + "default": 1, + "min": 0, + "max": 1, + "valueSize": 1, + "format": 0, + "noBulkSupport": true, + "isAdvanced": false, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, { "endpoint": 0, "commandClass": 112, @@ -721,7 +470,9 @@ "format": 0, "allowManualEntry": true, "isFromConfig": true - } + }, + "nodeId": 131, + "value": 99 }, { "endpoint": 0, @@ -1169,7 +920,9 @@ "max": 255, "stateful": true, "secret": false - } + }, + "nodeId": 131, + "value": 47 }, { "endpoint": 0, @@ -1183,54 +936,209 @@ "readable": false, "writeable": true, "label": "Identify", + "states": { + "true": "Identify" + }, "stateful": true, "secret": false } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "Timeout", + "stateful": true, + "secret": false + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "property": "targetValue", + "endpoint": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "propertyName": "targetValue", + "nodeId": 131, + "value": 45 + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "property": "duration", + "endpoint": 0, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "propertyName": "duration", + "nodeId": 131, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "property": "currentValue", + "endpoint": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "propertyName": "currentValue", + "nodeId": 131, + "value": 45 } ], - "isFrequentListening": "1000ms", - "maxDataRate": 100000, - "supportedDataRates": [40000, 100000], - "protocolVersion": 3, - "supportsBeaming": true, - "supportsSecurity": false, - "nodeType": 1, - "zwavePlusNodeType": 0, - "zwavePlusRoleType": 7, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 17, - "label": "Multilevel Switch" - }, - "specific": { - "key": 7, - "label": "Motor Control Class C" - }, - "mandatorySupportedCCs": [32, 38, 37, 114, 134], - "mandatoryControlledCCs": [] - }, - "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0287:0x0004:0x0072:3.12.1", - "statistics": { - "commandsTX": 109, - "commandsRX": 101, - "commandsDroppedRX": 2, - "commandsDroppedTX": 0, - "timeoutResponse": 8, - "rtt": 1217.2, - "rssi": -43, - "lwr": { - "protocolDataRate": 2, - "repeaters": [], - "rssi": -45, - "repeaterRSSI": [] + "endpoints": [ + { + "nodeId": 131, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + }, + "mandatorySupportedCCs": [32, 38, 37, 114, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 106, + "name": "Window Covering", + "version": 1, + "isSecure": false + } + ] } - }, - "highestSecurityClass": 1, - "isControllerNode": false, - "keepAwake": false + ] } diff --git a/tests/components/zwave_js/fixtures/zooz_zse44_state.json b/tests/components/zwave_js/fixtures/zooz_zse44_state.json index a2fb5421fb70..982708aaa116 100644 --- a/tests/components/zwave_js/fixtures/zooz_zse44_state.json +++ b/tests/components/zwave_js/fixtures/zooz_zse44_state.json @@ -88,140 +88,6 @@ "isControllerNode": false, "keepAwake": false, "lastSeen": "2023-08-09T13:26:05.031Z", - "endpoints": { - "0": { - "nodeId": 23, - "index": 0, - "installerIcon": 3327, - "userIcon": 3327, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 7, - "label": "Notification Sensor" - }, - "specific": { - "key": 1, - "label": "Notification Sensor" - }, - "mandatorySupportedCCs": [], - "mandatoryControlledCCs": [] - }, - "commandClasses": [ - { - "id": 94, - "name": "Z-Wave Plus Info", - "version": 2, - "isSecure": false - }, - { - "id": 133, - "name": "Association", - "version": 3, - "isSecure": true - }, - { - "id": 142, - "name": "Multi Channel Association", - "version": 4, - "isSecure": true - }, - { - "id": 89, - "name": "Association Group Information", - "version": 3, - "isSecure": true - }, - { - "id": 49, - "name": "Multilevel Sensor", - "version": 11, - "isSecure": true - }, - { - "id": 85, - "name": "Transport Service", - "version": 2, - "isSecure": false - }, - { - "id": 134, - "name": "Version", - "version": 3, - "isSecure": true - }, - { - "id": 114, - "name": "Manufacturer Specific", - "version": 2, - "isSecure": true - }, - { - "id": 90, - "name": "Device Reset Locally", - "version": 1, - "isSecure": true - }, - { - "id": 115, - "name": "Powerlevel", - "version": 1, - "isSecure": true - }, - { - "id": 128, - "name": "Battery", - "version": 3, - "isSecure": true - }, - { - "id": 159, - "name": "Security 2", - "version": 1, - "isSecure": true - }, - { - "id": 113, - "name": "Notification", - "version": 8, - "isSecure": true - }, - { - "id": 135, - "name": "Indicator", - "version": 4, - "isSecure": true - }, - { - "id": 112, - "name": "Configuration", - "version": 4, - "isSecure": true - }, - { - "id": 132, - "name": "Wake Up", - "version": 1, - "isSecure": true - }, - { - "id": 108, - "name": "Supervision", - "version": 2, - "isSecure": false - }, - { - "id": 122, - "name": "Firmware Update Meta Data", - "version": 7, - "isSecure": true - } - ] - } - }, "values": [ { "endpoint": 0, @@ -1326,5 +1192,139 @@ }, "value": 0 } + ], + "endpoints": [ + { + "nodeId": 23, + "index": 0, + "installerIcon": 3327, + "userIcon": 3327, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 3, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 4, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": true + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 11, + "isSecure": true + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 3, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": true + }, + { + "id": 135, + "name": "Indicator", + "version": 4, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": true + }, + { + "id": 132, + "name": "Wake Up", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 7, + "isSecure": true + } + ] + } ] } diff --git a/tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py b/tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py index d1e12e7abb46..ee03d57f4c7d 100644 --- a/tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py +++ b/tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py @@ -36,6 +36,10 @@ def test_fixture_functions() -> None: old_diagnostics_format_data["data"]["state"]["values"] = list( old_diagnostics_format_data["data"]["state"]["values"].values() ) + old_diagnostics_format_data["data"]["state"]["endpoints"] = list( + old_diagnostics_format_data["data"]["state"]["endpoints"].values() + ) + assert ( extract_fixture_data(old_diagnostics_format_data) == old_diagnostics_format_data["data"]["state"] @@ -54,7 +58,6 @@ def test_load_file() -> None: def test_main(capfd: pytest.CaptureFixture[str]) -> None: """Test main function.""" - Path(__file__).parents[1] / "fixtures" / "zooz_zse44_state.json" fixture_str = load_fixture("zwave_js/zooz_zse44_state.json") fixture_dict = json.loads(fixture_str) diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index fc593de883bb..54be2b437659 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -829,7 +829,7 @@ async def test_iblinds_v3_cover( hass: HomeAssistant, client, iblinds_v3, integration ) -> None: """Test iBlinds v3 cover which uses Window Covering CC.""" - entity_id = "cover.window_blind_controller_horizontal_slats_angle" + entity_id = "cover.blind_west_bed_1_horizontal_slats_angle" state = hass.states.get(entity_id) assert state # This device has no state because there is no position value @@ -854,7 +854,7 @@ async def test_iblinds_v3_cover( assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" - assert args["nodeId"] == 12 + assert args["nodeId"] == 131 assert args["valueId"] == { "endpoint": 0, "commandClass": 106, @@ -875,7 +875,7 @@ async def test_iblinds_v3_cover( assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" - assert args["nodeId"] == 12 + assert args["nodeId"] == 131 assert args["valueId"] == { "endpoint": 0, "commandClass": 106, @@ -896,7 +896,7 @@ async def test_iblinds_v3_cover( assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" - assert args["nodeId"] == 12 + assert args["nodeId"] == 131 assert args["valueId"] == { "endpoint": 0, "commandClass": 106, @@ -917,11 +917,11 @@ async def test_iblinds_v3_cover( assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" - assert args["nodeId"] == 12 + assert args["nodeId"] == 131 assert args["valueId"] == { "endpoint": 0, "commandClass": 106, - "property": "open", + "property": "levelChangeUp", "propertyKey": 23, } assert args["value"] is False From b0bb91ec08918636a6bf693489890cc963992915 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 4 Nov 2023 17:08:06 +0000 Subject: [PATCH 119/265] Don't assume that the `sleep` value is a dictionary in Tractive integration (#103138) * Sleep value can be null * Catch TypeError --- homeassistant/components/tractive/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index f2853e0032c5..300d7ebafc7f 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -240,7 +240,7 @@ class TractiveClient: self._config_entry.data[CONF_EMAIL], ) return - except KeyError as error: + except (KeyError, TypeError) as error: _LOGGER.error("Error while listening for events: %s", error) continue except aiotractive.exceptions.TractiveError: @@ -284,11 +284,16 @@ class TractiveClient: ) def _send_wellness_update(self, event: dict[str, Any]) -> None: + sleep_day = None + sleep_night = None + if isinstance(event["sleep"], dict): + sleep_day = event["sleep"]["minutes_day_sleep"] + sleep_night = event["sleep"]["minutes_night_sleep"] payload = { ATTR_ACTIVITY_LABEL: event["wellness"].get("activity_label"), ATTR_CALORIES: event["activity"]["calories"], - ATTR_MINUTES_DAY_SLEEP: event["sleep"]["minutes_day_sleep"], - ATTR_MINUTES_NIGHT_SLEEP: event["sleep"]["minutes_night_sleep"], + ATTR_MINUTES_DAY_SLEEP: sleep_day, + ATTR_MINUTES_NIGHT_SLEEP: sleep_night, ATTR_MINUTES_REST: event["activity"]["minutes_rest"], ATTR_SLEEP_LABEL: event["wellness"].get("sleep_label"), } From aa623cc15c51a2004cc35a9b323d37335072bfc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 2 Nov 2023 20:53:26 +0100 Subject: [PATCH 120/265] Update aioairzone-cloud to v0.3.2 (#103258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index eb9593421226..bbc8a84a3dce 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_polling", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.3.1"] + "requirements": ["aioairzone-cloud==0.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index c005a86cac3a..0e9c7378b8de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.3.1 +aioairzone-cloud==0.3.2 # homeassistant.components.airzone aioairzone==0.6.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b41909be0785..ce384b2e248f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.3.1 +aioairzone-cloud==0.3.2 # homeassistant.components.airzone aioairzone==0.6.9 From 2b36befe959e8843173a792ab8e252cf7db7f8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sun, 5 Nov 2023 14:09:29 +0100 Subject: [PATCH 121/265] Update aioairzone-cloud to v0.3.5 (#103315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update aioairzone-cloud to v0.3.3 Signed-off-by: Álvaro Fernández Rojas * Update aioairzone-cloud to v0.3.4 Reverts accidental TaskGroup changes. Signed-off-by: Álvaro Fernández Rojas * Update aioairzone-cloud to v0.3.5 Signed-off-by: Álvaro Fernández Rojas * Trigger Github CI --------- Signed-off-by: Álvaro Fernández Rojas Co-authored-by: J. Nick Koston --- homeassistant/components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index bbc8a84a3dce..ea22487f4a23 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_polling", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.3.2"] + "requirements": ["aioairzone-cloud==0.3.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0e9c7378b8de..1e388942667a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -192,7 +192,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.3.2 +aioairzone-cloud==0.3.5 # homeassistant.components.airzone aioairzone==0.6.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce384b2e248f..62ac01261a02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aio-georss-gdacs==0.8 aioairq==0.2.4 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.3.2 +aioairzone-cloud==0.3.5 # homeassistant.components.airzone aioairzone==0.6.9 From 412fa4c65a301af4772b95ff5c958fce0bd0280c Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 6 Nov 2023 06:48:00 -0700 Subject: [PATCH 122/265] Handle null data in WeatherFlow sensors (#103349) Co-authored-by: J. Nick Koston --- homeassistant/components/weatherflow/sensor.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/weatherflow/sensor.py b/homeassistant/components/weatherflow/sensor.py index bc5d38e99e52..f3e5b8744e6e 100644 --- a/homeassistant/components/weatherflow/sensor.py +++ b/homeassistant/components/weatherflow/sensor.py @@ -71,7 +71,8 @@ class WeatherFlowSensorEntityDescription( def get_native_value(self, device: WeatherFlowDevice) -> datetime | StateType: """Return the parsed sensor value.""" - raw_sensor_data = getattr(device, self.key) + if (raw_sensor_data := getattr(device, self.key)) is None: + return None return self.raw_data_conv_fn(raw_sensor_data) @@ -371,14 +372,17 @@ class WeatherFlowSensorEntity(SensorEntity): return self.device.last_report return None - @property - def native_value(self) -> datetime | StateType: - """Return the state of the sensor.""" - return self.entity_description.get_native_value(self.device) + def _async_update_state(self) -> None: + """Update entity state.""" + value = self.entity_description.get_native_value(self.device) + self._attr_available = value is not None + self._attr_native_value = value + self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Subscribe to events.""" + self._async_update_state() for event in self.entity_description.event_subscriptions: self.async_on_remove( - self.device.on(event, lambda _: self.async_write_ha_state()) + self.device.on(event, lambda _: self._async_update_state()) ) From 334a02bc2be14ab45acf3534d8141f78dd3e45cf Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 5 Nov 2023 19:11:50 -0800 Subject: [PATCH 123/265] Handle smarttub sensor values being None (#103385) * [smarttub] handle sensor values being None * empty commit to rerun CI * lint * use const in test * reorder checks * use None instead of STATE_UNKNOWN * empty commit to rerun CI * check for STATE_UNKNOWN in test * empty commit to rerun CI --- .../components/smarttub/manifest.json | 2 +- homeassistant/components/smarttub/sensor.py | 6 ++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/smarttub/test_sensor.py | 22 +++++++++++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 3b8b727015b5..e8db096f31da 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["smarttub"], "quality_scale": "platinum", - "requirements": ["python-smarttub==0.0.33"] + "requirements": ["python-smarttub==0.0.35"] } diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index a72555962eb1..c362e1ea8f08 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -89,10 +89,14 @@ class SmartTubSensor(SmartTubSensorBase, SensorEntity): """Generic class for SmartTub status sensors.""" @property - def native_value(self) -> str: + def native_value(self) -> str | None: """Return the current state of the sensor.""" + if self._state is None: + return None + if isinstance(self._state, Enum): return self._state.name.lower() + return self._state.lower() diff --git a/requirements_all.txt b/requirements_all.txt index 1e388942667a..7baa4592aec6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2184,7 +2184,7 @@ python-ripple-api==0.0.3 python-roborock==0.35.0 # homeassistant.components.smarttub -python-smarttub==0.0.33 +python-smarttub==0.0.35 # homeassistant.components.songpal python-songpal==0.15.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62ac01261a02..bfcae4f5396a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1628,7 +1628,7 @@ python-qbittorrent==0.4.3 python-roborock==0.35.0 # homeassistant.components.smarttub -python-smarttub==0.0.33 +python-smarttub==0.0.35 # homeassistant.components.songpal python-songpal==0.15.2 diff --git a/tests/components/smarttub/test_sensor.py b/tests/components/smarttub/test_sensor.py index 5c5359df3811..5e476dcaaa5e 100644 --- a/tests/components/smarttub/test_sensor.py +++ b/tests/components/smarttub/test_sensor.py @@ -2,6 +2,7 @@ import pytest import smarttub +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -27,6 +28,27 @@ async def test_sensor( assert state.state == expected_state +# https://github.com/home-assistant/core/issues/102339 +async def test_null_blowoutcycle( + spa, + spa_state, + config_entry, + hass: HomeAssistant, +) -> None: + """Test blowoutCycle having null value.""" + + spa_state.blowout_cycle = None + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_id = f"sensor.{spa.brand}_{spa.model}_blowout_cycle" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNKNOWN + + async def test_primary_filtration( spa, spa_state, setup_entry, hass: HomeAssistant ) -> None: From 050f1085d037b6dab1b92797c799a7003c7fa6df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Nov 2023 18:38:28 -0500 Subject: [PATCH 124/265] Pin jaraco.functools to fix builds and CI (#103406) --- homeassistant/components/abode/manifest.json | 2 +- requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index d27def552510..c7d51c7ea1f5 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -9,5 +9,5 @@ }, "iot_class": "cloud_push", "loggers": ["jaraco.abode", "lomond"], - "requirements": ["jaraco.abode==3.3.0"] + "requirements": ["jaraco.abode==3.3.0", "jaraco.functools==3.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7baa4592aec6..7568d76b55ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1094,6 +1094,9 @@ janus==1.0.0 # homeassistant.components.abode jaraco.abode==3.3.0 +# homeassistant.components.abode +jaraco.functools==3.9.0 + # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfcae4f5396a..06fd96dfe82a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -862,6 +862,9 @@ janus==1.0.0 # homeassistant.components.abode jaraco.abode==3.3.0 +# homeassistant.components.abode +jaraco.functools==3.9.0 + # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 From 7e2c12b0a9c69dee0fe3856b8ac21e06de604233 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 6 Nov 2023 11:12:33 +0100 Subject: [PATCH 125/265] Update tailscale to 0.6.0 (#103409) --- .../components/tailscale/diagnostics.py | 2 +- .../components/tailscale/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tailscale/conftest.py | 4 +- .../tailscale/snapshots/test_diagnostics.ambr | 87 +++++++++++++++++ .../components/tailscale/test_diagnostics.py | 95 ++----------------- 7 files changed, 99 insertions(+), 95 deletions(-) create mode 100644 tests/components/tailscale/snapshots/test_diagnostics.ambr diff --git a/homeassistant/components/tailscale/diagnostics.py b/homeassistant/components/tailscale/diagnostics.py index 0fd69a128251..687cee7741f4 100644 --- a/homeassistant/components/tailscale/diagnostics.py +++ b/homeassistant/components/tailscale/diagnostics.py @@ -32,5 +32,5 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: TailscaleDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] # Round-trip via JSON to trigger serialization - devices = [json.loads(device.json()) for device in coordinator.data.values()] + devices = [json.loads(device.to_json()) for device in coordinator.data.values()] return async_redact_data({"devices": devices}, TO_REDACT) diff --git a/homeassistant/components/tailscale/manifest.json b/homeassistant/components/tailscale/manifest.json index 088389060f54..14f4206f44f0 100644 --- a/homeassistant/components/tailscale/manifest.json +++ b/homeassistant/components/tailscale/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["tailscale==0.2.0"] + "requirements": ["tailscale==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7568d76b55ee..b1927de3b45c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2536,7 +2536,7 @@ synology-srm==0.2.0 systembridgeconnector==3.8.4 # homeassistant.components.tailscale -tailscale==0.2.0 +tailscale==0.6.0 # homeassistant.components.tank_utility tank-utility==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06fd96dfe82a..a21b1a9cd70c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1890,7 +1890,7 @@ switchbot-api==1.2.1 systembridgeconnector==3.8.4 # homeassistant.components.tailscale -tailscale==0.2.0 +tailscale==0.6.0 # homeassistant.components.tellduslive tellduslive==0.10.11 diff --git a/tests/components/tailscale/conftest.py b/tests/components/tailscale/conftest.py index 12f11a5656da..ec3b6afa1392 100644 --- a/tests/components/tailscale/conftest.py +++ b/tests/components/tailscale/conftest.py @@ -41,7 +41,7 @@ def mock_tailscale_config_flow() -> Generator[None, MagicMock, None]: "homeassistant.components.tailscale.config_flow.Tailscale", autospec=True ) as tailscale_mock: tailscale = tailscale_mock.return_value - tailscale.devices.return_value = Devices.parse_raw( + tailscale.devices.return_value = Devices.from_json( load_fixture("tailscale/devices.json") ).devices yield tailscale @@ -54,7 +54,7 @@ def mock_tailscale(request: pytest.FixtureRequest) -> Generator[None, MagicMock, if hasattr(request, "param") and request.param: fixture = request.param - devices = Devices.parse_raw(load_fixture(fixture)).devices + devices = Devices.from_json(load_fixture(fixture)).devices with patch( "homeassistant.components.tailscale.coordinator.Tailscale", autospec=True ) as tailscale_mock: diff --git a/tests/components/tailscale/snapshots/test_diagnostics.ambr b/tests/components/tailscale/snapshots/test_diagnostics.ambr new file mode 100644 index 000000000000..eba8d9bd145f --- /dev/null +++ b/tests/components/tailscale/snapshots/test_diagnostics.ambr @@ -0,0 +1,87 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'devices': list([ + dict({ + 'addresses': '**REDACTED**', + 'advertised_routes': list([ + ]), + 'authorized': True, + 'blocks_incoming_connections': False, + 'client_connectivity': dict({ + 'client_supports': dict({ + 'hair_pinning': False, + 'ipv6': False, + 'pcp': False, + 'pmp': False, + 'udp': True, + 'upnp': False, + }), + 'endpoints': '**REDACTED**', + 'mapping_varies_by_dest_ip': False, + }), + 'client_version': '1.12.3-td91ea7286-ge1bbbd90c', + 'created': '2021-08-19T09:25:22+00:00', + 'device_id': '**REDACTED**', + 'enabled_routes': list([ + ]), + 'expires': '2022-02-15T09:25:22+00:00', + 'hostname': '**REDACTED**', + 'is_external': False, + 'key_expiry_disabled': False, + 'last_seen': '2021-09-16T06:11:23+00:00', + 'machine_key': '**REDACTED**', + 'name': '**REDACTED**', + 'node_key': '**REDACTED**', + 'os': 'iOS', + 'tags': list([ + ]), + 'update_available': True, + 'user': '**REDACTED**', + }), + dict({ + 'addresses': '**REDACTED**', + 'advertised_routes': list([ + '0.0.0.0/0', + '10.10.10.0/23', + '::/0', + ]), + 'authorized': True, + 'blocks_incoming_connections': False, + 'client_connectivity': dict({ + 'client_supports': dict({ + 'hair_pinning': True, + 'ipv6': False, + 'pcp': False, + 'pmp': False, + 'udp': True, + 'upnp': False, + }), + 'endpoints': '**REDACTED**', + 'mapping_varies_by_dest_ip': False, + }), + 'client_version': '1.14.0-t5cff36945-g809e87bba', + 'created': '2021-08-29T09:49:06+00:00', + 'device_id': '**REDACTED**', + 'enabled_routes': list([ + '0.0.0.0/0', + '10.10.10.0/23', + '::/0', + ]), + 'expires': '2022-02-25T09:49:06+00:00', + 'hostname': '**REDACTED**', + 'is_external': False, + 'key_expiry_disabled': False, + 'last_seen': '2021-11-15T20:37:03+00:00', + 'machine_key': '**REDACTED**', + 'name': '**REDACTED**', + 'node_key': '**REDACTED**', + 'os': 'linux', + 'tags': list([ + ]), + 'update_available': True, + 'user': '**REDACTED**', + }), + ]), + }) +# --- diff --git a/tests/components/tailscale/test_diagnostics.py b/tests/components/tailscale/test_diagnostics.py index a6b892dbc863..4f900db74015 100644 --- a/tests/components/tailscale/test_diagnostics.py +++ b/tests/components/tailscale/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the Tailscale integration.""" +from syrupy import SnapshotAssertion -from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -12,93 +12,10 @@ async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - assert await get_diagnostics_for_config_entry( - hass, hass_client, init_integration - ) == { - "devices": [ - { - "addresses": REDACTED, - "device_id": REDACTED, - "user": REDACTED, - "name": REDACTED, - "hostname": REDACTED, - "client_version": "1.12.3-td91ea7286-ge1bbbd90c", - "update_available": True, - "os": "iOS", - "created": "2021-08-19T09:25:22+00:00", - "last_seen": "2021-09-16T06:11:23+00:00", - "key_expiry_disabled": False, - "expires": "2022-02-15T09:25:22+00:00", - "authorized": True, - "is_external": False, - "machine_key": REDACTED, - "node_key": REDACTED, - "blocks_incoming_connections": False, - "enabled_routes": [], - "advertised_routes": [], - "client_connectivity": { - "endpoints": REDACTED, - "derp": "", - "mapping_varies_by_dest_ip": False, - "latency": {}, - "client_supports": { - "hair_pinning": False, - "ipv6": False, - "pcp": False, - "pmp": False, - "udp": True, - "upnp": False, - }, - }, - }, - { - "addresses": REDACTED, - "device_id": REDACTED, - "user": REDACTED, - "name": REDACTED, - "hostname": REDACTED, - "client_version": "1.14.0-t5cff36945-g809e87bba", - "update_available": True, - "os": "linux", - "created": "2021-08-29T09:49:06+00:00", - "last_seen": "2021-11-15T20:37:03+00:00", - "key_expiry_disabled": False, - "expires": "2022-02-25T09:49:06+00:00", - "authorized": True, - "is_external": False, - "machine_key": REDACTED, - "node_key": REDACTED, - "blocks_incoming_connections": False, - "enabled_routes": ["0.0.0.0/0", "10.10.10.0/23", "::/0"], - "advertised_routes": ["0.0.0.0/0", "10.10.10.0/23", "::/0"], - "client_connectivity": { - "endpoints": REDACTED, - "derp": "", - "mapping_varies_by_dest_ip": False, - "latency": { - "Bangalore": {"latencyMs": 143.42505599999998}, - "Chicago": {"latencyMs": 101.123646}, - "Dallas": {"latencyMs": 136.85886}, - "Frankfurt": {"latencyMs": 18.968314}, - "London": {"preferred": True, "latencyMs": 14.314574}, - "New York City": {"latencyMs": 83.078912}, - "San Francisco": {"latencyMs": 148.215522}, - "Seattle": {"latencyMs": 181.553595}, - "Singapore": {"latencyMs": 164.566539}, - "São Paulo": {"latencyMs": 207.250179}, - "Tokyo": {"latencyMs": 226.90714300000002}, - }, - "client_supports": { - "hair_pinning": True, - "ipv6": False, - "pcp": False, - "pmp": False, - "udp": True, - "upnp": False, - }, - }, - }, - ] - } + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) From f45114371e3513c2b6e6c555beaf2e6d416a9f1b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 5 Nov 2023 21:13:45 +0100 Subject: [PATCH 126/265] Bump pyatmo to v7.6.0 (#103410) Signed-off-by: Tobias Sauerwein --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/netatmo/snapshots/test_diagnostics.ambr | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 7fe1f9b8c04b..d031632ed750 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pyatmo"], - "requirements": ["pyatmo==7.5.0"] + "requirements": ["pyatmo==7.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index b1927de3b45c..0e7821f82d97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1600,7 +1600,7 @@ pyairvisual==2023.08.1 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.5.0 +pyatmo==7.6.0 # homeassistant.components.apple_tv pyatv==0.14.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a21b1a9cd70c..d0bc7b85dcd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1218,7 +1218,7 @@ pyairvisual==2023.08.1 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.5.0 +pyatmo==7.6.0 # homeassistant.components.apple_tv pyatv==0.14.3 diff --git a/tests/components/netatmo/snapshots/test_diagnostics.ambr b/tests/components/netatmo/snapshots/test_diagnostics.ambr index 228fc7563e0e..bd9005bd3890 100644 --- a/tests/components/netatmo/snapshots/test_diagnostics.ambr +++ b/tests/components/netatmo/snapshots/test_diagnostics.ambr @@ -572,6 +572,7 @@ 'read_smokedetector', 'read_station', 'read_thermostat', + 'read_mhs1', 'write_bubendorff', 'write_camera', 'write_magellan', @@ -579,6 +580,7 @@ 'write_presence', 'write_smarther', 'write_thermostat', + 'write_mhs1', ]), 'type': 'Bearer', }), From e56e75114a419b2013aa71afaa7b4f9d5d262a45 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 5 Nov 2023 12:41:15 +0100 Subject: [PATCH 127/265] Fix serial in Flo device information (#103427) --- homeassistant/components/flo/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index 99e86d4b6b5f..bcc52f512a1f 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -139,9 +139,9 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): return self._device_information["fwVersion"] @property - def serial_number(self) -> str: + def serial_number(self) -> str | None: """Return the serial number for the device.""" - return self._device_information["serialNumber"] + return self._device_information.get("serialNumber") @property def pending_info_alerts_count(self) -> int: From 9327c51115b0e7b95debe83587ea36e11fbb6515 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 6 Nov 2023 07:52:15 +0100 Subject: [PATCH 128/265] modbus Allow swap: byte for datatype: string. (#103441) --- homeassistant/components/modbus/validators.py | 6 +++--- tests/components/modbus/test_sensor.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index bef58b3fa564..5fa314d589cc 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -63,8 +63,8 @@ PARM_IS_LEGAL = namedtuple( ], ) # PARM_IS_LEGAL defines if the keywords: -# count: .. -# structure: .. +# count: +# structure: # swap: byte # swap: word # swap: word_byte (identical to swap: word) @@ -84,7 +84,7 @@ DEFAULT_STRUCT_FORMAT = { DataType.INT64: ENTRY("q", 4, PARM_IS_LEGAL(False, False, True, True, True)), DataType.UINT64: ENTRY("Q", 4, PARM_IS_LEGAL(False, False, True, True, True)), DataType.FLOAT64: ENTRY("d", 4, PARM_IS_LEGAL(False, False, True, True, True)), - DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, False, False)), + DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, True, False)), DataType.CUSTOM: ENTRY("?", 0, PARM_IS_LEGAL(True, True, False, False, False)), } diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 0f79a125c86a..72aebbd396fa 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -513,6 +513,20 @@ async def test_config_wrong_struct_sensor( False, "07-05-2020 14:35", ), + ( + { + CONF_COUNT: 8, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_DATA_TYPE: DataType.STRING, + CONF_SWAP: CONF_SWAP_BYTE, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + [0x3730, 0x302D, 0x2D35, 0x3032, 0x3032, 0x3120, 0x3A34, 0x3533], + False, + "07-05-2020 14:35", + ), ( { CONF_COUNT: 8, From 9a37868244d2ee030be0eac7cf83380b55fdde78 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 5 Nov 2023 20:20:13 +0100 Subject: [PATCH 129/265] Modbus set device_class in slaves (#103442) --- homeassistant/components/modbus/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index d7a6b4cca0f1..52aa37535d68 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -168,6 +168,7 @@ class SlaveSensor( self._attr_unique_id = f"{self._attr_unique_id}_{idx}" self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = entry.get(CONF_STATE_CLASS) + self._attr_device_class = entry.get(CONF_DEVICE_CLASS) self._attr_available = False super().__init__(coordinator) From 6fd8973a0059b12d36fe0f0565efb43f86530d2b Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 6 Nov 2023 08:39:40 +0100 Subject: [PATCH 130/265] Fix KNX expose default value when attribute is `None` (#103446) Fix KNX expose default value when attribute is `null` --- homeassistant/components/knx/expose.py | 10 +++++----- tests/components/knx/test_expose.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index e14ee501d7b9..d5c871d59ba9 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -122,12 +122,12 @@ class KNXExposeSensor: """Extract value from state.""" if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): value = self.expose_default + elif self.expose_attribute is not None: + _attr = state.attributes.get(self.expose_attribute) + value = _attr if _attr is not None else self.expose_default else: - value = ( - state.state - if self.expose_attribute is None - else state.attributes.get(self.expose_attribute, self.expose_default) - ) + value = state.state + if self.expose_type == "binary": if value in (1, STATE_ON, "True"): return True diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index ca3fc5c7f587..4359c54164a3 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -85,6 +85,14 @@ async def test_expose_attribute(hass: HomeAssistant, knx: KNXTestKit) -> None: hass.states.async_set(entity_id, "off", {}) await knx.assert_telegram_count(0) + # Change attribute; keep state + hass.states.async_set(entity_id, "on", {attribute: 1}) + await knx.assert_write("1/1/8", (1,)) + + # Change state to "off"; null attribute + hass.states.async_set(entity_id, "off", {attribute: None}) + await knx.assert_telegram_count(0) + async def test_expose_attribute_with_default( hass: HomeAssistant, knx: KNXTestKit @@ -132,6 +140,14 @@ async def test_expose_attribute_with_default( hass.states.async_set(entity_id, "off", {}) await knx.assert_write("1/1/8", (0,)) + # Change state and attribute + hass.states.async_set(entity_id, "on", {attribute: 1}) + await knx.assert_write("1/1/8", (1,)) + + # Change state to "off"; null attribute + hass.states.async_set(entity_id, "off", {attribute: None}) + await knx.assert_write("1/1/8", (0,)) + async def test_expose_string(hass: HomeAssistant, knx: KNXTestKit) -> None: """Test an expose to send string values of up to 14 bytes only.""" From e2270a305dd37763f71e607694bde60ea76eee46 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 6 Nov 2023 07:58:13 +0100 Subject: [PATCH 131/265] Sort Withings sleep data on end date (#103454) * Sort Withings sleep data on end date * Sort Withings sleep data on end date --- .../components/withings/coordinator.py | 5 +- .../withings/fixtures/sleep_summaries.json | 78 +++++++++---------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index 35eeb6e62b63..7dec48a3489f 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -147,7 +147,10 @@ class WithingsSleepDataUpdateCoordinator( ) if not response: return None - return response[0] + + return sorted( + response, key=lambda sleep_summary: sleep_summary.end_date, reverse=True + )[0] class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[None]): diff --git a/tests/components/withings/fixtures/sleep_summaries.json b/tests/components/withings/fixtures/sleep_summaries.json index 1bcfcfcc1d2e..4e7f05142d3a 100644 --- a/tests/components/withings/fixtures/sleep_summaries.json +++ b/tests/components/withings/fixtures/sleep_summaries.json @@ -1,43 +1,4 @@ [ - { - "id": 2081804182, - "timezone": "Europe/Paris", - "model": 32, - "model_id": 63, - "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", - "startdate": 1618691453, - "enddate": 1618713173, - "date": "2021-04-18", - "data": { - "wakeupduration": 3060, - "wakeupcount": 1, - "durationtosleep": 540, - "remsleepduration": 2400, - "durationtowakeup": 1140, - "total_sleep_time": 18660, - "sleep_efficiency": 0.86, - "sleep_latency": 540, - "wakeup_latency": 1140, - "waso": 1380, - "nb_rem_episodes": 1, - "out_of_bed_count": 0, - "lightsleepduration": 10440, - "deepsleepduration": 5820, - "hr_average": 103, - "hr_min": 70, - "hr_max": 120, - "rr_average": 14, - "rr_min": 10, - "rr_max": 20, - "breathing_disturbances_intensity": 9, - "snoring": 1080, - "snoringepisodecount": 18, - "sleep_score": 37, - "apnea_hypopnea_index": 9 - }, - "created": 1620237476, - "modified": 1620237476 - }, { "id": 2081804265, "timezone": "Europe/Paris", @@ -77,6 +38,45 @@ "created": 1620237480, "modified": 1620237479 }, + { + "id": 2081804182, + "timezone": "Europe/Paris", + "model": 32, + "model_id": 63, + "hash_deviceid": "201d0b9a0556d6b755166b2cf8d22d3bdf0487ee", + "startdate": 1618691453, + "enddate": 1618713173, + "date": "2021-04-18", + "data": { + "wakeupduration": 3060, + "wakeupcount": 1, + "durationtosleep": 540, + "remsleepduration": 2400, + "durationtowakeup": 1140, + "total_sleep_time": 18660, + "sleep_efficiency": 0.86, + "sleep_latency": 540, + "wakeup_latency": 1140, + "waso": 1380, + "nb_rem_episodes": 1, + "out_of_bed_count": 0, + "lightsleepduration": 10440, + "deepsleepduration": 5820, + "hr_average": 103, + "hr_min": 70, + "hr_max": 120, + "rr_average": 14, + "rr_min": 10, + "rr_max": 20, + "breathing_disturbances_intensity": 9, + "snoring": 1080, + "snoringepisodecount": 18, + "sleep_score": 37, + "apnea_hypopnea_index": 9 + }, + "created": 1620237476, + "modified": 1620237476 + }, { "id": 2081804358, "timezone": "Europe/Paris", From 27d8d1011e9d62fd2edfdbd657d551a978abab5e Mon Sep 17 00:00:00 2001 From: dupondje Date: Mon, 6 Nov 2023 14:19:47 +0100 Subject: [PATCH 132/265] Use right equipment identifier in DSMR setup (#103494) --- homeassistant/components/dsmr/config_flow.py | 2 + tests/components/dsmr/conftest.py | 10 +++++ tests/components/dsmr/test_config_flow.py | 44 ++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index c7b9ab4e3803..3b32d3547663 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -53,6 +53,8 @@ class DSMRConnection: self._protocol = protocol self._telegram: dict[str, DSMRObject] = {} self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER + if dsmr_version == "5B": + self._equipment_identifier = obis_ref.BELGIUM_EQUIPMENT_IDENTIFIER if dsmr_version == "5L": self._equipment_identifier = obis_ref.LUXEMBOURG_EQUIPMENT_IDENTIFIER if dsmr_version == "Q3D": diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index 67e8b724a977..01aff5ae48e6 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch from dsmr_parser.clients.protocol import DSMRProtocol from dsmr_parser.clients.rfxtrx_protocol import RFXtrxDSMRProtocol from dsmr_parser.obis_references import ( + BELGIUM_EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER_GAS, LUXEMBOURG_EQUIPMENT_IDENTIFIER, @@ -81,6 +82,15 @@ async def dsmr_connection_send_validate_fixture(hass): async def connection_factory(*args, **kwargs): """Return mocked out Asyncio classes.""" + if args[1] == "5B": + protocol.telegram = { + BELGIUM_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_EQUIPMENT_IDENTIFIER, [{"value": "12345678", "unit": ""}] + ), + EQUIPMENT_IDENTIFIER_GAS: CosemObject( + EQUIPMENT_IDENTIFIER_GAS, [{"value": "123456789", "unit": ""}] + ), + } if args[1] == "5L": protocol.telegram = { LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemObject( diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 8ad7c7214a3c..c4bbe9a7086a 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -215,6 +215,50 @@ async def test_setup_serial_rfxtrx( assert result["data"] == {**entry_data, **SERIAL_DATA} +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +async def test_setup_5B( + com_mock, hass: HomeAssistant, dsmr_connection_send_validate_fixture +) -> None: + """Test we can setup serial.""" + port = com_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"type": "Serial"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "setup_serial" + assert result["errors"] == {} + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"port": port.device, "dsmr_version": "5B"}, + ) + await hass.async_block_till_done() + + entry_data = { + "port": port.device, + "dsmr_version": "5B", + "protocol": "dsmr_protocol", + "serial_id": "12345678", + "serial_id_gas": "123456789", + } + + assert result["type"] == "create_entry" + assert result["title"] == port.device + assert result["data"] == entry_data + + @patch("serial.tools.list_ports.comports", return_value=[com_port()]) async def test_setup_5L( com_mock, hass: HomeAssistant, dsmr_connection_send_validate_fixture From c17def27fc8045cb244a88a5bbc5d43770492191 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 5 Nov 2023 09:42:07 -0800 Subject: [PATCH 133/265] Fix litterrobot test failure due to time zone dependence (#103444) * fix litterrobot test * use a date in northern hemisehpere summer --- tests/components/litterrobot/test_time.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/litterrobot/test_time.py b/tests/components/litterrobot/test_time.py index 6532f2a3bc7b..53f254008e7d 100644 --- a/tests/components/litterrobot/test_time.py +++ b/tests/components/litterrobot/test_time.py @@ -1,10 +1,11 @@ """Test the Litter-Robot time entity.""" from __future__ import annotations -from datetime import time +from datetime import datetime, time from unittest.mock import MagicMock from pylitterbot import LitterRobot3 +import pytest from homeassistant.components.time import DOMAIN as PLATFORM_DOMAIN from homeassistant.const import ATTR_ENTITY_ID @@ -15,6 +16,7 @@ from .conftest import setup_integration SLEEP_START_TIME_ENTITY_ID = "time.test_sleep_mode_start_time" +@pytest.mark.freeze_time(datetime(2023, 7, 1, 12)) async def test_sleep_mode_start_time( hass: HomeAssistant, mock_account: MagicMock ) -> None: From 8f684ab102d583f777f74fb6e3ea6433dc057621 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 6 Nov 2023 17:56:53 +0100 Subject: [PATCH 134/265] Revert binary_sensor part of #103210 (#103499) --- homeassistant/components/mqtt/binary_sensor.py | 10 ++-------- tests/components/mqtt/test_init.py | 9 --------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index a89fb8a22fc5..7ab2e9ebf90e 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -42,7 +42,6 @@ from .mixins import ( MqttAvailability, MqttEntity, async_setup_entity_entry_helper, - validate_sensor_entity_category, write_state_on_attr_change, ) from .models import MqttValueTemplate, ReceiveMessage @@ -56,7 +55,7 @@ DEFAULT_PAYLOAD_ON = "ON" DEFAULT_FORCE_UPDATE = False CONF_EXPIRE_AFTER = "expire_after" -_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, @@ -68,12 +67,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -DISCOVERY_SCHEMA = vol.All( - validate_sensor_entity_category, - _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), -) - -PLATFORM_SCHEMA_MODERN = vol.All(validate_sensor_entity_category, _PLATFORM_SCHEMA_BASE) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) async def async_setup_entry( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 93d730948851..8112a289e628 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2163,15 +2163,6 @@ async def test_setup_manual_mqtt_with_invalid_config( } } }, - { - mqtt.DOMAIN: { - "binary_sensor": { - "name": "test", - "state_topic": "test-topic", - "entity_category": "config", - } - } - }, ], ) @patch( From d019045199c589fb60ecacd819bcbb04767b8276 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 26 Oct 2023 20:19:31 +1300 Subject: [PATCH 135/265] ESPHome: Add suggested_area from device info (#102834) --- homeassistant/components/esphome/manager.py | 5 +++++ homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 812cf430d09c..d2eca7d39f92 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -596,6 +596,10 @@ def _async_setup_device_registry( model = project_name[1] hw_version = device_info.project_version + suggested_area = None + if device_info.suggested_area: + suggested_area = device_info.suggested_area + device_registry = dr.async_get(hass) device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -606,6 +610,7 @@ def _async_setup_device_registry( model=model, sw_version=sw_version, hw_version=hw_version, + suggested_area=suggested_area, ) return device_entry.id diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 702f75b166ef..8968fa7da4f0 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==18.1.0", + "aioesphomeapi==18.2.0", "bluetooth-data-tools==1.13.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 0e7821f82d97..74339e7a29b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.1.0 +aioesphomeapi==18.2.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0bc7b85dcd2..f92b787a6ef7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -218,7 +218,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.1.0 +aioesphomeapi==18.2.0 # homeassistant.components.flo aioflo==2021.11.0 From 3cac87cf30a7377108c985781619c278e71fede0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Nov 2023 02:56:48 -0500 Subject: [PATCH 136/265] Bump aioesphomeapi to 18.2.1 (#103156) --- 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 8968fa7da4f0..4619ffef4c56 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==18.2.0", + "aioesphomeapi==18.2.1", "bluetooth-data-tools==1.13.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 74339e7a29b1..930ade42cc44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.2.0 +aioesphomeapi==18.2.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f92b787a6ef7..0b45c9579b82 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -218,7 +218,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.2.0 +aioesphomeapi==18.2.1 # homeassistant.components.flo aioflo==2021.11.0 From dbdd9d74cf8c15a52238b1092eeaafeea0c23a58 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Nov 2023 07:07:44 -0600 Subject: [PATCH 137/265] Bump bluetooth-data-tools to 0.14.0 (#103413) changelog: https://github.com/Bluetooth-Devices/bluetooth-data-tools/compare/v1.13.0...v1.14.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/ld2410_ble/manifest.json | 2 +- homeassistant/components/led_ble/manifest.json | 2 +- homeassistant/components/private_ble_device/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 06e7d34e68df..813bc9009000 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak-retry-connector==3.3.0", "bluetooth-adapters==0.16.1", "bluetooth-auto-recovery==1.2.3", - "bluetooth-data-tools==1.13.0", + "bluetooth-data-tools==1.14.0", "dbus-fast==2.12.0" ] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 4619ffef4c56..4e8d3c8dde44 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "requirements": [ "async-interrupt==1.1.1", "aioesphomeapi==18.2.1", - "bluetooth-data-tools==1.13.0", + "bluetooth-data-tools==1.14.0", "esphome-dashboard-api==1.2.3" ], "zeroconf": ["_esphomelib._tcp.local."] diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json index f82b2fff62b3..7996376b6ac9 100644 --- a/homeassistant/components/ld2410_ble/manifest.json +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "integration_type": "device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.13.0", "ld2410-ble==0.1.1"] + "requirements": ["bluetooth-data-tools==1.14.0", "ld2410-ble==0.1.1"] } diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index a0f7685a2ec9..21543ad67887 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -32,5 +32,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/led_ble", "iot_class": "local_polling", - "requirements": ["bluetooth-data-tools==1.13.0", "led-ble==1.0.1"] + "requirements": ["bluetooth-data-tools==1.14.0", "led-ble==1.0.1"] } diff --git a/homeassistant/components/private_ble_device/manifest.json b/homeassistant/components/private_ble_device/manifest.json index 91ef843a8649..663461ceaa1f 100644 --- a/homeassistant/components/private_ble_device/manifest.json +++ b/homeassistant/components/private_ble_device/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/private_ble_device", "iot_class": "local_push", - "requirements": ["bluetooth-data-tools==1.13.0"] + "requirements": ["bluetooth-data-tools==1.14.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a70bcf4524a9..12b1c6a4d0a6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ bleak-retry-connector==3.3.0 bleak==0.21.1 bluetooth-adapters==0.16.1 bluetooth-auto-recovery==1.2.3 -bluetooth-data-tools==1.13.0 +bluetooth-data-tools==1.14.0 certifi>=2021.5.30 ciso8601==2.3.0 cryptography==41.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 930ade42cc44..e02ddb809e2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -562,7 +562,7 @@ bluetooth-auto-recovery==1.2.3 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.13.0 +bluetooth-data-tools==1.14.0 # homeassistant.components.bond bond-async==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b45c9579b82..28f515c826dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -476,7 +476,7 @@ bluetooth-auto-recovery==1.2.3 # homeassistant.components.ld2410_ble # homeassistant.components.led_ble # homeassistant.components.private_ble_device -bluetooth-data-tools==1.13.0 +bluetooth-data-tools==1.14.0 # homeassistant.components.bond bond-async==0.2.1 From 93a0bd351a199871353e3c8795b2f6c71fcf54b7 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 7 Nov 2023 18:04:23 -0500 Subject: [PATCH 138/265] Bump blinkpy to 0.22.3 (#103438) --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 54f36ec6e2e2..bb8fd4a5a512 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/blink", "iot_class": "cloud_polling", "loggers": ["blinkpy"], - "requirements": ["blinkpy==0.22.2"] + "requirements": ["blinkpy==0.22.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index e02ddb809e2e..c6c465e7f9d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -539,7 +539,7 @@ bleak==0.21.1 blebox-uniapi==2.2.0 # homeassistant.components.blink -blinkpy==0.22.2 +blinkpy==0.22.3 # homeassistant.components.bitcoin blockchain==1.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28f515c826dc..3c22bbb9a057 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -460,7 +460,7 @@ bleak==0.21.1 blebox-uniapi==2.2.0 # homeassistant.components.blink -blinkpy==0.22.2 +blinkpy==0.22.3 # homeassistant.components.bluemaestro bluemaestro-ble==0.2.3 From 35c0c9958d64ba5fce2af996323beea29de7f8f0 Mon Sep 17 00:00:00 2001 From: dupondje Date: Wed, 8 Nov 2023 09:13:51 +0100 Subject: [PATCH 139/265] Fix 5B Gas meter in dsmr (#103506) * Fix 5B Gas meter in dsmr In commit 1b73219 the gas meter broke for 5B. As the change can't be reverted easily without removing the peak usage sensors, we implement a workaround. The first MBUS_METER_READING2 value will contain the gas meter data just like the previous BELGIUM_5MIN_GAS_METER_READING did. But this without the need to touch dsmr_parser (version). Fixes: #103306, #103293 * Use parametrize * Apply suggestions from code review Co-authored-by: Jan Bouwhuis * Add additional tests + typo fix --------- Co-authored-by: Jan Bouwhuis --- homeassistant/components/dsmr/const.py | 3 - homeassistant/components/dsmr/sensor.py | 42 +++++-- tests/components/dsmr/test_sensor.py | 152 +++++++++++++++++++++++- 3 files changed, 179 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 7bc0247aea65..5e1a54aedc4f 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -34,6 +34,3 @@ DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"} DSMR_PROTOCOL = "dsmr_protocol" RFXTRX_DSMR_PROTOCOL = "rfxtrx_dsmr_protocol" - -# Temp obis until sensors replaced by mbus variants -BELGIUM_5MIN_GAS_METER_READING = r"\d-\d:24\.2\.3.+?\r\n" diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 99af30b8111c..fa58bd8c5a6d 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -44,7 +44,6 @@ from homeassistant.helpers.typing import StateType from homeassistant.util import Throttle from .const import ( - BELGIUM_5MIN_GAS_METER_READING, CONF_DSMR_VERSION, CONF_PRECISION, CONF_PROTOCOL, @@ -382,16 +381,6 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, ), - DSMRSensorEntityDescription( - key="belgium_5min_gas_meter_reading", - translation_key="gas_meter_reading", - obis_reference=BELGIUM_5MIN_GAS_METER_READING, - dsmr_versions={"5B"}, - is_gas=True, - force_update=True, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), DSMRSensorEntityDescription( key="gas_meter_reading", translation_key="gas_meter_reading", @@ -405,6 +394,31 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ) +def add_gas_sensor_5B(telegram: dict[str, DSMRObject]) -> DSMRSensorEntityDescription: + """Return correct entity for 5B Gas meter.""" + ref = None + if obis_references.BELGIUM_MBUS1_METER_READING2 in telegram: + ref = obis_references.BELGIUM_MBUS1_METER_READING2 + elif obis_references.BELGIUM_MBUS2_METER_READING2 in telegram: + ref = obis_references.BELGIUM_MBUS2_METER_READING2 + elif obis_references.BELGIUM_MBUS3_METER_READING2 in telegram: + ref = obis_references.BELGIUM_MBUS3_METER_READING2 + elif obis_references.BELGIUM_MBUS4_METER_READING2 in telegram: + ref = obis_references.BELGIUM_MBUS4_METER_READING2 + elif ref is None: + ref = obis_references.BELGIUM_MBUS1_METER_READING2 + return DSMRSensorEntityDescription( + key="belgium_5min_gas_meter_reading", + translation_key="gas_meter_reading", + obis_reference=ref, + dsmr_versions={"5B"}, + is_gas=True, + force_update=True, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -438,6 +452,10 @@ async def async_setup_entry( return (entity_description.device_class, UNIT_CONVERSION[uom]) return (entity_description.device_class, uom) + all_sensors = SENSORS + if dsmr_version == "5B": + all_sensors += (add_gas_sensor_5B(telegram),) + entities.extend( [ DSMREntity( @@ -448,7 +466,7 @@ async def async_setup_entry( telegram, description ), # type: ignore[arg-type] ) - for description in SENSORS + for description in all_sensors if ( description.dsmr_versions is None or dsmr_version in description.dsmr_versions diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 9c8c4e6fc702..e7f0e715f59a 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -8,10 +8,22 @@ import asyncio import datetime from decimal import Decimal from itertools import chain, repeat +from typing import Literal from unittest.mock import DEFAULT, MagicMock +from dsmr_parser.obis_references import ( + BELGIUM_MBUS1_METER_READING1, + BELGIUM_MBUS1_METER_READING2, + BELGIUM_MBUS2_METER_READING1, + BELGIUM_MBUS2_METER_READING2, + BELGIUM_MBUS3_METER_READING1, + BELGIUM_MBUS3_METER_READING2, + BELGIUM_MBUS4_METER_READING1, + BELGIUM_MBUS4_METER_READING2, +) +import pytest + from homeassistant import config_entries -from homeassistant.components.dsmr.const import BELGIUM_5MIN_GAS_METER_READING from homeassistant.components.sensor import ( ATTR_OPTIONS, ATTR_STATE_CLASS, @@ -483,6 +495,10 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No from dsmr_parser.obis_references import ( BELGIUM_CURRENT_AVERAGE_DEMAND, BELGIUM_MAXIMUM_DEMAND_MONTH, + BELGIUM_MBUS1_METER_READING2, + BELGIUM_MBUS2_METER_READING2, + BELGIUM_MBUS3_METER_READING2, + BELGIUM_MBUS4_METER_READING2, ELECTRICITY_ACTIVE_TARIFF, ) from dsmr_parser.objects import CosemObject, MBusObject @@ -500,13 +516,34 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No } telegram = { - BELGIUM_5MIN_GAS_METER_READING: MBusObject( - BELGIUM_5MIN_GAS_METER_READING, + BELGIUM_MBUS1_METER_READING2: MBusObject( + BELGIUM_MBUS1_METER_READING2, [ {"value": datetime.datetime.fromtimestamp(1551642213)}, {"value": Decimal(745.695), "unit": "m3"}, ], ), + BELGIUM_MBUS2_METER_READING2: MBusObject( + BELGIUM_MBUS2_METER_READING2, + [ + {"value": datetime.datetime.fromtimestamp(1551642214)}, + {"value": Decimal(745.696), "unit": "m3"}, + ], + ), + BELGIUM_MBUS3_METER_READING2: MBusObject( + BELGIUM_MBUS3_METER_READING2, + [ + {"value": datetime.datetime.fromtimestamp(1551642215)}, + {"value": Decimal(745.697), "unit": "m3"}, + ], + ), + BELGIUM_MBUS4_METER_READING2: MBusObject( + BELGIUM_MBUS4_METER_READING2, + [ + {"value": datetime.datetime.fromtimestamp(1551642216)}, + {"value": Decimal(745.698), "unit": "m3"}, + ], + ), BELGIUM_CURRENT_AVERAGE_DEMAND: CosemObject( BELGIUM_CURRENT_AVERAGE_DEMAND, [{"value": Decimal(1.75), "unit": "kW"}], @@ -577,6 +614,115 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No ) +@pytest.mark.parametrize( + ("key1", "key2", "key3", "gas_value"), + [ + ( + BELGIUM_MBUS1_METER_READING1, + BELGIUM_MBUS2_METER_READING2, + BELGIUM_MBUS3_METER_READING1, + "745.696", + ), + ( + BELGIUM_MBUS1_METER_READING2, + BELGIUM_MBUS2_METER_READING1, + BELGIUM_MBUS3_METER_READING2, + "745.695", + ), + ( + BELGIUM_MBUS4_METER_READING2, + BELGIUM_MBUS2_METER_READING1, + BELGIUM_MBUS3_METER_READING1, + "745.695", + ), + ( + BELGIUM_MBUS4_METER_READING1, + BELGIUM_MBUS2_METER_READING1, + BELGIUM_MBUS3_METER_READING2, + "745.697", + ), + ], +) +async def test_belgian_meter_alt( + hass: HomeAssistant, + dsmr_connection_fixture, + key1: Literal, + key2: Literal, + key3: Literal, + gas_value: str, +) -> None: + """Test if Belgian meter is correctly parsed.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.objects import MBusObject + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5B", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + } + entry_options = { + "time_between_update": 0, + } + + telegram = { + key1: MBusObject( + key1, + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ], + ), + key2: MBusObject( + key2, + [ + {"value": datetime.datetime.fromtimestamp(1551642214)}, + {"value": Decimal(745.696), "unit": "m3"}, + ], + ), + key3: MBusObject( + key3, + [ + {"value": datetime.datetime.fromtimestamp(1551642215)}, + {"value": Decimal(745.697), "unit": "m3"}, + ], + ), + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to be created + await hass.async_block_till_done() + + # check if gas consumption is parsed correctly + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") + assert gas_consumption.state == gas_value + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS + ) + + async def test_belgian_meter_low(hass: HomeAssistant, dsmr_connection_fixture) -> None: """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture From da1c282c1b8d937f5395793d122e3ecfcd81ac42 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 6 Nov 2023 23:43:56 +0100 Subject: [PATCH 140/265] Fix invalid MAC in samsungtv (#103512) * Fix invalid MAC in samsungtv * Also adjust __init__ --- .../components/samsungtv/__init__.py | 4 +- .../components/samsungtv/config_flow.py | 5 +- .../components/samsungtv/test_config_flow.py | 62 ++++++++++++++++--- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index b7d400ce8316..2ced868ada7e 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -211,7 +211,9 @@ async def _async_create_bridge_with_updated_data( partial(getmac.get_mac_address, ip=host) ) - if mac: + if mac and mac != "none": + # Samsung sometimes returns a value of "none" for the mac address + # this should be ignored LOGGER.info("Updated mac to %s for %s", mac, host) updated_data[CONF_MAC] = mac else: diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 124dab73004e..f20a79cc9e6d 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -219,7 +219,10 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._title = f"{self._name} ({self._model})" self._udn = _strip_uuid(dev_info.get("udn", info["id"])) if mac := mac_from_device_info(info): - self._mac = mac + # Samsung sometimes returns a value of "none" for the mac address + # this should be ignored - but also shouldn't trigger getmac + if mac != "none": + self._mac = mac elif mac := await self.hass.async_add_executor_job( partial(getmac.get_mac_address, ip=self._host) ): diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index a70a0042fcd3..0eacd63b42dc 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Samsung TV config flow.""" +from copy import deepcopy from ipaddress import ip_address from unittest.mock import ANY, AsyncMock, Mock, call, patch @@ -165,14 +166,6 @@ MOCK_DEVICE_INFO = { }, "id": "123", } -MOCK_DEVICE_INFO_2 = { - "device": { - "type": "Samsung SmartTV", - "name": "fake2_name", - "modelName": "fake2_model", - }, - "id": "345", -} AUTODETECT_LEGACY = { "name": "HomeAssistant", @@ -1968,3 +1961,56 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( assert result["step_id"] == "confirm" assert entry.data[CONF_MAC] == "aa:bb:ss:ss:dd:pp" assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + + +@pytest.mark.usefixtures("remotews", "remoteencws_failing") +async def test_ssdp_update_mac(hass: HomeAssistant) -> None: + """Ensure that MAC address is correctly updated from SSDP.""" + with patch( + "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 + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + entry = result["result"] + assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER + assert entry.data[CONF_MODEL] == "fake_model" + assert entry.data[CONF_MAC] is None + assert entry.unique_id == "123" + + device_info = deepcopy(MOCK_DEVICE_INFO) + device_info["device"]["wifiMac"] = "none" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", + return_value=device_info, + ): + # Updated + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == RESULT_ALREADY_CONFIGURED + + # ensure mac wasn't updated with "none" + assert entry.data[CONF_MAC] is None + assert entry.unique_id == "123" + + device_info = deepcopy(MOCK_DEVICE_INFO) + device_info["device"]["wifiMac"] = "aa:bb:cc:dd:ee:ff" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", + return_value=device_info, + ): + # Updated + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == RESULT_ALREADY_CONFIGURED + + # ensure mac was updated with new wifiMac value + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id == "123" From c8d3e377f0fcbae33aa447deef944307066fbcfe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Nov 2023 04:22:41 -0600 Subject: [PATCH 141/265] Bump aioesphomeapi to 18.2.4 (#103552) --- .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/conftest.py | 25 +++++++++++++++++-- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 4e8d3c8dde44..cb1a741c447c 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ "async-interrupt==1.1.1", - "aioesphomeapi==18.2.1", + "aioesphomeapi==18.2.4", "bluetooth-data-tools==1.14.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index c6c465e7f9d8..df1e8835288c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.2.1 +aioesphomeapi==18.2.4 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c22bbb9a057..7d343781c60e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -218,7 +218,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==18.2.1 +aioesphomeapi==18.2.4 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 4ff6b503b3cd..48b0868e406c 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -77,6 +77,17 @@ def mock_config_entry(hass) -> MockConfigEntry: return config_entry +class BaseMockReconnectLogic(ReconnectLogic): + """Mock ReconnectLogic.""" + + def stop_callback(self) -> None: + """Stop the reconnect logic.""" + # For the purposes of testing, we don't want to wait + # for the reconnect logic to finish trying to connect + self._cancel_connect("forced disconnect from test") + self._is_stopped = True + + @pytest.fixture def mock_device_info() -> DeviceInfo: """Return the default mocked device info.""" @@ -132,7 +143,10 @@ def mock_client(mock_device_info) -> APIClient: mock_client.address = "127.0.0.1" mock_client.api_version = APIVersion(99, 99) - with patch("homeassistant.components.esphome.APIClient", mock_client), patch( + with patch( + "homeassistant.components.esphome.manager.ReconnectLogic", + BaseMockReconnectLogic, + ), patch("homeassistant.components.esphome.APIClient", mock_client), patch( "homeassistant.components.esphome.config_flow.APIClient", mock_client ): yield mock_client @@ -234,7 +248,7 @@ async def _mock_generic_device_entry( try_connect_done = Event() - class MockReconnectLogic(ReconnectLogic): + class MockReconnectLogic(BaseMockReconnectLogic): """Mock ReconnectLogic.""" def __init__(self, *args, **kwargs): @@ -250,6 +264,13 @@ async def _mock_generic_device_entry( try_connect_done.set() return result + def stop_callback(self) -> None: + """Stop the reconnect logic.""" + # For the purposes of testing, we don't want to wait + # for the reconnect logic to finish trying to connect + self._cancel_connect("forced disconnect from test") + self._is_stopped = True + with patch( "homeassistant.components.esphome.manager.ReconnectLogic", MockReconnectLogic ): From 95d4254074f79bafe25ca2e8e7d92a0d3b469bdd Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 6 Nov 2023 18:53:44 -0500 Subject: [PATCH 142/265] Bump pyenphase to 1.14.2 (#103553) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 4cffcce2d5c6..718c33d28115 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.14.1"], + "requirements": ["pyenphase==1.14.2"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index df1e8835288c..74d9af9953a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1696,7 +1696,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.14.1 +pyenphase==1.14.2 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d343781c60e..4c356309f249 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1278,7 +1278,7 @@ pyeconet==0.1.22 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.14.1 +pyenphase==1.14.2 # homeassistant.components.everlights pyeverlights==0.1.0 From 70f0ee81c9885e1dae0eb2fbfc05515315e9f090 Mon Sep 17 00:00:00 2001 From: dupondje Date: Tue, 7 Nov 2023 10:38:37 +0100 Subject: [PATCH 143/265] Update dsmr-parser to 1.3.1 to fix parsing issues (#103572) --- homeassistant/components/dsmr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index b3f59a15b801..90fd2d6cdced 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["dsmr_parser"], - "requirements": ["dsmr-parser==1.3.0"] + "requirements": ["dsmr-parser==1.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 74d9af9953a0..8a6230bb9d75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -701,7 +701,7 @@ dovado==0.4.1 dremel3dpy==2.1.1 # homeassistant.components.dsmr -dsmr-parser==1.3.0 +dsmr-parser==1.3.1 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c356309f249..77ab2e7e38f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -572,7 +572,7 @@ discovery30303==0.2.1 dremel3dpy==2.1.1 # homeassistant.components.dsmr -dsmr-parser==1.3.0 +dsmr-parser==1.3.1 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.6 From f9c70fd3c85859c81ccd8c0f042dd901115e5d77 Mon Sep 17 00:00:00 2001 From: suaveolent <2163625+suaveolent@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:46:02 +0100 Subject: [PATCH 144/265] fix: get_devices only checks for the first type (#103583) --- homeassistant/components/lupusec/switch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index b42942160039..981a2a8633a0 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -28,9 +28,10 @@ def setup_platform( data = hass.data[LUPUSEC_DOMAIN] - devices = [] + device_types = [CONST.TYPE_SWITCH] - for device in data.lupusec.get_devices(generic_type=CONST.TYPE_SWITCH): + devices = [] + for device in data.lupusec.get_devices(generic_type=device_types): devices.append(LupusecSwitch(data, device)) add_entities(devices) From d1a3a5895b4de08344140f229ff2f8c4dcb83577 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 7 Nov 2023 16:33:46 +0100 Subject: [PATCH 145/265] Raise exception when data can't be fetched in Opensky (#103596) --- homeassistant/components/opensky/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opensky/__init__.py b/homeassistant/components/opensky/__init__.py index cb9c6173694b..6e60c2ec4f13 100644 --- a/homeassistant/components/opensky/__init__.py +++ b/homeassistant/components/opensky/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from aiohttp import BasicAuth from python_opensky import OpenSky -from python_opensky.exceptions import OpenSkyUnauthenticatedError +from python_opensky.exceptions import OpenSkyError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), contributing_user=entry.options.get(CONF_CONTRIBUTING_USER, False), ) - except OpenSkyUnauthenticatedError as exc: + except OpenSkyError as exc: raise ConfigEntryNotReady from exc coordinator = OpenSkyDataUpdateCoordinator(hass, client) From 0ffc1bae7644a67e40fc7ec7ba33b891f4f1548f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 7 Nov 2023 22:50:09 +0100 Subject: [PATCH 146/265] Bump yt-dlp to 2023.10.13 (#103616) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 37a8a0d67735..d16439800a93 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -7,5 +7,5 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2023.9.24"] + "requirements": ["yt-dlp==2023.10.13"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8a6230bb9d75..f48915d56292 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2782,7 +2782,7 @@ youless-api==1.0.1 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2023.9.24 +yt-dlp==2023.10.13 # homeassistant.components.zamg zamg==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77ab2e7e38f3..ad95793bfee5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2079,7 +2079,7 @@ youless-api==1.0.1 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2023.9.24 +yt-dlp==2023.10.13 # homeassistant.components.zamg zamg==0.3.0 From f946ed9e16409f58a8bedee158c2775056aceb8e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 10 Nov 2023 09:27:33 +0100 Subject: [PATCH 147/265] Fix Reolink DHCP IP update (#103654) --- .../components/reolink/config_flow.py | 4 ++- tests/components/reolink/conftest.py | 16 ++++++++--- tests/components/reolink/test_config_flow.py | 27 +++++++++++++++++-- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 59fbdc227478..a27c84b9593f 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -113,7 +113,9 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): raise AbortFlow("already_configured") # check if the camera is reachable at the new IP - host = ReolinkHost(self.hass, existing_entry.data, existing_entry.options) + new_config = dict(existing_entry.data) + new_config[CONF_HOST] = discovery_info.ip + host = ReolinkHost(self.hass, new_config, existing_entry.options) try: await host.api.get_state("GetLocalLink") await host.api.logout() diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 25719c4cff7a..3efc1e481df5 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -34,8 +34,10 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture -def reolink_connect(mock_get_source_ip: None) -> Generator[MagicMock, None, None]: - """Mock reolink connection.""" +def reolink_connect_class( + mock_get_source_ip: None, +) -> Generator[MagicMock, None, None]: + """Mock reolink connection and return both the host_mock and host_mock_class.""" with patch( "homeassistant.components.reolink.host.webhook.async_register", return_value=True, @@ -65,7 +67,15 @@ def reolink_connect(mock_get_source_ip: None) -> Generator[MagicMock, None, None host_mock.session_active = True host_mock.timeout = 60 host_mock.renewtimer.return_value = 600 - yield host_mock + yield host_mock_class + + +@pytest.fixture +def reolink_connect( + reolink_connect_class: MagicMock, +) -> Generator[MagicMock, None, None]: + """Mock reolink connection.""" + return reolink_connect_class.return_value @pytest.fixture diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 1a4bf999cceb..9b449d4b8517 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -2,7 +2,7 @@ from datetime import timedelta import json from typing import Any -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, call import pytest from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError @@ -12,6 +12,7 @@ from homeassistant.components import dhcp from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL, const from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL from homeassistant.components.reolink.exceptions import ReolinkWebhookException +from homeassistant.components.reolink.host import DEFAULT_TIMEOUT from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -380,41 +381,47 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No @pytest.mark.parametrize( - ("last_update_success", "attr", "value", "expected"), + ("last_update_success", "attr", "value", "expected", "host_call_list"), [ ( False, None, None, TEST_HOST2, + [TEST_HOST, TEST_HOST2], ), ( True, None, None, TEST_HOST, + [TEST_HOST], ), ( False, "get_state", AsyncMock(side_effect=ReolinkError("Test error")), TEST_HOST, + [TEST_HOST, TEST_HOST2], ), ( False, "mac_address", "aa:aa:aa:aa:aa:aa", TEST_HOST, + [TEST_HOST, TEST_HOST2], ), ], ) async def test_dhcp_ip_update( hass: HomeAssistant, + reolink_connect_class: MagicMock, reolink_connect: MagicMock, last_update_success: bool, attr: str, value: Any, expected: str, + host_call_list: list[str], ) -> None: """Test dhcp discovery aborts if already configured where the IP is updated if appropriate.""" config_entry = MockConfigEntry( @@ -459,6 +466,22 @@ async def test_dhcp_ip_update( const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) + expected_calls = [] + for host in host_call_list: + expected_calls.append( + call( + host, + TEST_USERNAME, + TEST_PASSWORD, + port=TEST_PORT, + use_https=TEST_USE_HTTPS, + protocol=DEFAULT_PROTOCOL, + timeout=DEFAULT_TIMEOUT, + ) + ) + + assert reolink_connect_class.call_args_list == expected_calls + assert result["type"] is data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" From 8ce746972f147e3cce547fcb23ed1a7d8eb087af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 8 Nov 2023 18:13:11 -0600 Subject: [PATCH 148/265] Incease tplink setup timeout (#103671) It can take longer than 5s to do the first update of the device especially when the device is overloaded as seen in #103668 Wait 10 seconds for the discovery since when the power strips are loaded they cannot respond in time --- homeassistant/components/tplink/__init__.py | 2 +- tests/components/tplink/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index d8285cbed70e..f2a1e6823043 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -87,7 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up TPLink from a config entry.""" host = entry.data[CONF_HOST] try: - device: SmartDevice = await Discover.discover_single(host) + device: SmartDevice = await Discover.discover_single(host, timeout=10) except SmartDeviceException as ex: raise ConfigEntryNotReady from ex diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 816251ae3bb7..9006a058c578 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -202,7 +202,7 @@ def _patch_discovery(device=None, no_device=False): def _patch_single_discovery(device=None, no_device=False): - async def _discover_single(*_): + async def _discover_single(*args, **kwargs): if no_device: raise SmartDeviceException return device if device else _mocked_bulb() From 30dc05cdd729ae18171a7219ad79317c98e7a598 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 9 Nov 2023 12:44:50 +0100 Subject: [PATCH 149/265] Add name to Withings coordinator (#103692) --- homeassistant/components/withings/coordinator.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index 7dec48a3489f..2639ccccf7df 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -37,11 +37,15 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]): _default_update_interval: timedelta | None = UPDATE_INTERVAL _last_valid_update: datetime | None = None webhooks_connected: bool = False + coordinator_name: str = "" def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: """Initialize the Withings data coordinator.""" super().__init__( - hass, LOGGER, name="Withings", update_interval=self._default_update_interval + hass, + LOGGER, + name=f"Withings {self.coordinator_name}", + update_interval=self._default_update_interval, ) self._client = client self.notification_categories: set[NotificationCategory] = set() @@ -77,6 +81,8 @@ class WithingsMeasurementDataUpdateCoordinator( ): """Withings measurement coordinator.""" + coordinator_name: str = "measurements" + def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: """Initialize the Withings data coordinator.""" super().__init__(hass, client) @@ -109,6 +115,8 @@ class WithingsSleepDataUpdateCoordinator( ): """Withings sleep coordinator.""" + coordinator_name: str = "sleep" + def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: """Initialize the Withings data coordinator.""" super().__init__(hass, client) @@ -156,6 +164,7 @@ class WithingsSleepDataUpdateCoordinator( class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[None]): """Withings bed presence coordinator.""" + coordinator_name: str = "bed presence" in_bed: bool | None = None _default_update_interval = None @@ -181,6 +190,7 @@ class WithingsBedPresenceDataUpdateCoordinator(WithingsDataUpdateCoordinator[Non class WithingsGoalsDataUpdateCoordinator(WithingsDataUpdateCoordinator[Goals]): """Withings goals coordinator.""" + coordinator_name: str = "goals" _default_update_interval = timedelta(hours=1) def webhook_subscription_listener(self, connected: bool) -> None: @@ -197,6 +207,7 @@ class WithingsActivityDataUpdateCoordinator( ): """Withings activity coordinator.""" + coordinator_name: str = "activity" _previous_data: Activity | None = None def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: @@ -235,6 +246,7 @@ class WithingsWorkoutDataUpdateCoordinator( ): """Withings workout coordinator.""" + coordinator_name: str = "workout" _previous_data: Workout | None = None def __init__(self, hass: HomeAssistant, client: WithingsClient) -> None: From f12055875097db1de9bffd43cfcbe6effc136d70 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 10 Nov 2023 09:04:33 +0100 Subject: [PATCH 150/265] Update frontend to 20231030.2 (#103706) --- 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 6fffc0e8acda..469deab23e1d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20231030.1"] + "requirements": ["home-assistant-frontend==20231030.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 12b1c6a4d0a6..67050e43eeb1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ ha-av==10.1.1 hass-nabucasa==0.74.0 hassil==1.2.5 home-assistant-bluetooth==1.10.4 -home-assistant-frontend==20231030.1 +home-assistant-frontend==20231030.2 home-assistant-intents==2023.10.16 httpx==0.25.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index f48915d56292..ab4afc4dfa91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1007,7 +1007,7 @@ hole==0.8.0 holidays==0.35 # homeassistant.components.frontend -home-assistant-frontend==20231030.1 +home-assistant-frontend==20231030.2 # homeassistant.components.conversation home-assistant-intents==2023.10.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad95793bfee5..798a6851b629 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -796,7 +796,7 @@ hole==0.8.0 holidays==0.35 # homeassistant.components.frontend -home-assistant-frontend==20231030.1 +home-assistant-frontend==20231030.2 # homeassistant.components.conversation home-assistant-intents==2023.10.16 From eaf711335d9675099586d68e862cec2aab7bfd72 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 10 Nov 2023 10:04:50 +0100 Subject: [PATCH 151/265] Bumped version to 2023.11.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 99c1312dc98a..479bfcbac6e2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 11 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 616dd219bafd..55dd7a81a37c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.11.1" +version = "2023.11.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e55567176540bd6509a4e5a70459d6c6f854ce30 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 6 Nov 2023 20:02:02 +0000 Subject: [PATCH 152/265] Bump accuweather to version 2.0.1 (#103532) --- homeassistant/components/accuweather/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index 5a5a1de2a01a..307d68c4b7bf 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["accuweather"], "quality_scale": "platinum", - "requirements": ["accuweather==2.0.0"] + "requirements": ["accuweather==2.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index ab4afc4dfa91..870fcc948978 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ TwitterAPI==2.7.12 WSDiscovery==2.0.0 # homeassistant.components.accuweather -accuweather==2.0.0 +accuweather==2.0.1 # homeassistant.components.adax adax==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 798a6851b629..96a135845788 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ Tami4EdgeAPI==2.1 WSDiscovery==2.0.0 # homeassistant.components.accuweather -accuweather==2.0.0 +accuweather==2.0.1 # homeassistant.components.adax adax==0.3.0 From d63d7841c38ded8d664315b153317acd13387b73 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 7 Nov 2023 14:24:34 -0800 Subject: [PATCH 153/265] Remove rainbird yaml config test fixtures (#103607) --- tests/components/rainbird/conftest.py | 27 +++-------- .../components/rainbird/test_binary_sensor.py | 23 +++++----- tests/components/rainbird/test_calendar.py | 40 ++++++++--------- tests/components/rainbird/test_config_flow.py | 20 +++------ tests/components/rainbird/test_init.py | 45 +++++++------------ tests/components/rainbird/test_number.py | 28 +++++------- tests/components/rainbird/test_sensor.py | 20 ++++++--- tests/components/rainbird/test_switch.py | 43 ++++++------------ 8 files changed, 97 insertions(+), 149 deletions(-) diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index f25bdfb1d86f..6e8d58219c18 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable, Generator from http import HTTPStatus from typing import Any from unittest.mock import patch @@ -17,13 +16,10 @@ from homeassistant.components.rainbird.const import ( ) from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse -ComponentSetup = Callable[[], Awaitable[bool]] - HOST = "example.com" URL = "http://example.com/stick" PASSWORD = "password" @@ -79,12 +75,6 @@ def platforms() -> list[Platform]: return [] -@pytest.fixture -def yaml_config() -> dict[str, Any]: - """Fixture for configuration.yaml.""" - return {} - - @pytest.fixture async def config_entry_unique_id() -> str: """Fixture for serial number used in the config entry.""" @@ -122,22 +112,15 @@ async def add_config_entry( config_entry.add_to_hass(hass) -@pytest.fixture -async def setup_integration( +@pytest.fixture(autouse=True) +def setup_platforms( hass: HomeAssistant, platforms: list[str], - yaml_config: dict[str, Any], -) -> Generator[ComponentSetup, None, None]: - """Fixture for setting up the component.""" +) -> None: + """Fixture for setting up the default platforms.""" with patch(f"homeassistant.components.{DOMAIN}.PLATFORMS", platforms): - - async def func() -> bool: - result = await async_setup_component(hass, DOMAIN, yaml_config) - await hass.async_block_till_done() - return result - - yield func + yield def rainbird_response(data: str) -> bytes: diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py index 24cd1750ed49..7b9fb41ed1f1 100644 --- a/tests/components/rainbird/test_binary_sensor.py +++ b/tests/components/rainbird/test_binary_sensor.py @@ -3,12 +3,14 @@ import pytest +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import RAIN_SENSOR_OFF, RAIN_SENSOR_ON, SERIAL_NUMBER, ComponentSetup +from .conftest import RAIN_SENSOR_OFF, RAIN_SENSOR_ON, SERIAL_NUMBER +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMockResponse @@ -18,21 +20,27 @@ def platforms() -> list[Platform]: return [Platform.BINARY_SENSOR] +@pytest.fixture(autouse=True) +async def setup_config_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> list[Platform]: + """Fixture to setup the config entry.""" + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + + @pytest.mark.parametrize( ("rain_response", "expected_state"), [(RAIN_SENSOR_OFF, "off"), (RAIN_SENSOR_ON, "on")], ) async def test_rainsensor( hass: HomeAssistant, - setup_integration: ComponentSetup, responses: list[AiohttpClientMockResponse], entity_registry: er.EntityRegistry, expected_state: bool, ) -> None: """Test rainsensor binary sensor.""" - assert await setup_integration() - rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor") assert rainsensor is not None assert rainsensor.state == expected_state @@ -53,14 +61,10 @@ async def test_rainsensor( ) async def test_unique_id( hass: HomeAssistant, - setup_integration: ComponentSetup, entity_registry: er.EntityRegistry, entity_unique_id: str, ) -> None: """Test rainsensor binary sensor.""" - - assert await setup_integration() - rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor") assert rainsensor is not None assert rainsensor.attributes == { @@ -83,14 +87,11 @@ async def test_unique_id( ) async def test_no_unique_id( hass: HomeAssistant, - setup_integration: ComponentSetup, responses: list[AiohttpClientMockResponse], entity_registry: er.EntityRegistry, ) -> None: """Test rainsensor binary sensor with no unique id.""" - assert await setup_integration() - rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor") assert rainsensor is not None assert ( diff --git a/tests/components/rainbird/test_calendar.py b/tests/components/rainbird/test_calendar.py index 2e486226a7be..d6c14834342c 100644 --- a/tests/components/rainbird/test_calendar.py +++ b/tests/components/rainbird/test_calendar.py @@ -12,12 +12,14 @@ from aiohttp import ClientSession from freezegun.api import FrozenDateTimeFactory import pytest +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import ComponentSetup, mock_response, mock_response_error +from .conftest import mock_response, mock_response_error +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMockResponse TEST_ENTITY = "calendar.rain_bird_controller" @@ -80,6 +82,15 @@ def platforms() -> list[str]: return [Platform.CALENDAR] +@pytest.fixture(autouse=True) +async def setup_config_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> list[Platform]: + """Fixture to setup the config entry.""" + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + + @pytest.fixture(autouse=True) def set_time_zone(hass: HomeAssistant): """Set the time zone for the tests.""" @@ -121,13 +132,9 @@ def get_events_fixture( @pytest.mark.freeze_time("2023-01-21 09:32:00") -async def test_get_events( - hass: HomeAssistant, setup_integration: ComponentSetup, get_events: GetEventsFn -) -> None: +async def test_get_events(hass: HomeAssistant, get_events: GetEventsFn) -> None: """Test calendar event fetching APIs.""" - assert await setup_integration() - events = await get_events("2023-01-20T00:00:00Z", "2023-02-05T00:00:00Z") assert events == [ # Monday @@ -158,31 +165,34 @@ async def test_get_events( @pytest.mark.parametrize( - ("freeze_time", "expected_state"), + ("freeze_time", "expected_state", "setup_config_entry"), [ ( datetime.datetime(2023, 1, 23, 3, 50, tzinfo=ZoneInfo("America/Regina")), "off", + None, ), ( datetime.datetime(2023, 1, 23, 4, 30, tzinfo=ZoneInfo("America/Regina")), "on", + None, ), ], ) async def test_event_state( hass: HomeAssistant, - setup_integration: ComponentSetup, get_events: GetEventsFn, freezer: FrozenDateTimeFactory, freeze_time: datetime.datetime, expected_state: str, entity_registry: er.EntityRegistry, + config_entry: MockConfigEntry, ) -> None: """Test calendar upcoming event state.""" freezer.move_to(freeze_time) - assert await setup_integration() + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED state = hass.states.get(TEST_ENTITY) assert state is not None @@ -213,13 +223,10 @@ async def test_event_state( ) async def test_calendar_not_supported_by_device( hass: HomeAssistant, - setup_integration: ComponentSetup, has_entity: bool, ) -> None: """Test calendar upcoming event state.""" - assert await setup_integration() - state = hass.states.get(TEST_ENTITY) assert (state is not None) == has_entity @@ -229,7 +236,6 @@ async def test_calendar_not_supported_by_device( ) async def test_no_schedule( hass: HomeAssistant, - setup_integration: ComponentSetup, get_events: GetEventsFn, responses: list[AiohttpClientMockResponse], hass_client: Callable[..., Awaitable[ClientSession]], @@ -237,8 +243,6 @@ async def test_no_schedule( """Test calendar error when fetching the calendar.""" responses.extend([mock_response_error(HTTPStatus.BAD_GATEWAY)]) # Arbitrary error - assert await setup_integration() - state = hass.states.get(TEST_ENTITY) assert state.state == "unavailable" assert state.attributes == { @@ -260,13 +264,10 @@ async def test_no_schedule( ) async def test_program_schedule_disabled( hass: HomeAssistant, - setup_integration: ComponentSetup, get_events: GetEventsFn, ) -> None: """Test calendar when the program is disabled with no upcoming events.""" - assert await setup_integration() - events = await get_events("2023-01-20T00:00:00Z", "2023-02-05T00:00:00Z") assert events == [] @@ -286,14 +287,11 @@ async def test_program_schedule_disabled( ) async def test_no_unique_id( hass: HomeAssistant, - setup_integration: ComponentSetup, get_events: GetEventsFn, entity_registry: er.EntityRegistry, ) -> None: """Test calendar entity with no unique id.""" - assert await setup_integration() - state = hass.states.get(TEST_ENTITY) assert state is not None assert state.attributes.get("friendly_name") == "Rain Bird Controller" diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index cfc4ff3b5cb3..f93da8d9839c 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -24,10 +24,10 @@ from .conftest import ( SERIAL_RESPONSE, URL, ZERO_SERIAL_RESPONSE, - ComponentSetup, mock_response, ) +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse @@ -129,17 +129,14 @@ async def test_controller_flow( ) async def test_multiple_config_entries( hass: HomeAssistant, - setup_integration: ComponentSetup, + config_entry: MockConfigEntry, responses: list[AiohttpClientMockResponse], config_flow_responses: list[AiohttpClientMockResponse], expected_config_entry: dict[str, Any] | None, ) -> None: """Test setting up multiple config entries that refer to different devices.""" - assert await setup_integration() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED responses.clear() responses.extend(config_flow_responses) @@ -177,16 +174,13 @@ async def test_multiple_config_entries( ) async def test_duplicate_config_entries( hass: HomeAssistant, - setup_integration: ComponentSetup, + config_entry: MockConfigEntry, responses: list[AiohttpClientMockResponse], config_flow_responses: list[AiohttpClientMockResponse], ) -> None: """Test that a device can not be registered twice.""" - assert await setup_integration() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED responses.clear() responses.extend(config_flow_responses) diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index f548d3aacda5..7ec22b88867b 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -6,31 +6,30 @@ from http import HTTPStatus import pytest -from homeassistant.components.rainbird import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from .conftest import ( CONFIG_ENTRY_DATA, MODEL_AND_VERSION_RESPONSE, - ComponentSetup, mock_response, mock_response_error, ) +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMockResponse @pytest.mark.parametrize( - ("yaml_config", "config_entry_data", "initial_response"), + ("config_entry_data", "initial_response"), [ - ({}, CONFIG_ENTRY_DATA, None), + (CONFIG_ENTRY_DATA, None), ], ids=["config_entry"], ) async def test_init_success( hass: HomeAssistant, - setup_integration: ComponentSetup, + config_entry: MockConfigEntry, responses: list[AiohttpClientMockResponse], initial_response: AiohttpClientMockResponse | None, ) -> None: @@ -38,49 +37,42 @@ async def test_init_success( if initial_response: responses.insert(0, initial_response) - assert await setup_integration() + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.LOADED - - await hass.config_entries.async_unload(entries[0].entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert entries[0].state is ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( - ("yaml_config", "config_entry_data", "responses", "config_entry_states"), + ("config_entry_data", "responses", "config_entry_state"), [ ( - {}, CONFIG_ENTRY_DATA, [mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)], - [ConfigEntryState.SETUP_RETRY], + ConfigEntryState.SETUP_RETRY, ), ( - {}, CONFIG_ENTRY_DATA, [mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR)], - [ConfigEntryState.SETUP_RETRY], + ConfigEntryState.SETUP_RETRY, ), ( - {}, CONFIG_ENTRY_DATA, [ mock_response(MODEL_AND_VERSION_RESPONSE), mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE), ], - [ConfigEntryState.SETUP_RETRY], + ConfigEntryState.SETUP_RETRY, ), ( - {}, CONFIG_ENTRY_DATA, [ mock_response(MODEL_AND_VERSION_RESPONSE), mock_response_error(HTTPStatus.INTERNAL_SERVER_ERROR), ], - [ConfigEntryState.SETUP_RETRY], + ConfigEntryState.SETUP_RETRY, ), ], ids=[ @@ -92,13 +84,10 @@ async def test_init_success( ) async def test_communication_failure( hass: HomeAssistant, - setup_integration: ComponentSetup, - config_entry_states: list[ConfigEntryState], + config_entry: MockConfigEntry, + config_entry_state: list[ConfigEntryState], ) -> None: """Test unable to talk to device on startup, which fails setup.""" - assert await setup_integration() - - assert [ - entry.state for entry in hass.config_entries.async_entries(DOMAIN) - ] == config_entry_states + await config_entry.async_setup(hass) + assert config_entry.state == config_entry_state diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index b3cfd56832d4..0beae1f5a95a 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components import number from homeassistant.components.rainbird import DOMAIN -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -17,11 +17,11 @@ from .conftest import ( RAIN_DELAY, RAIN_DELAY_OFF, SERIAL_NUMBER, - ComponentSetup, mock_response, mock_response_error, ) +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -31,20 +31,26 @@ def platforms() -> list[str]: return [Platform.NUMBER] +@pytest.fixture(autouse=True) +async def setup_config_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> list[Platform]: + """Fixture to setup the config entry.""" + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + + @pytest.mark.parametrize( ("rain_delay_response", "expected_state"), [(RAIN_DELAY, "16"), (RAIN_DELAY_OFF, "0")], ) async def test_number_values( hass: HomeAssistant, - setup_integration: ComponentSetup, expected_state: str, entity_registry: er.EntityRegistry, ) -> None: """Test number platform.""" - assert await setup_integration() - raindelay = hass.states.get("number.rain_bird_controller_rain_delay") assert raindelay is not None assert raindelay.state == expected_state @@ -74,14 +80,11 @@ async def test_number_values( ) async def test_unique_id( hass: HomeAssistant, - setup_integration: ComponentSetup, entity_registry: er.EntityRegistry, entity_unique_id: str, ) -> None: """Test number platform.""" - assert await setup_integration() - raindelay = hass.states.get("number.rain_bird_controller_rain_delay") assert raindelay is not None assert ( @@ -95,15 +98,12 @@ async def test_unique_id( async def test_set_value( hass: HomeAssistant, - setup_integration: ComponentSetup, aioclient_mock: AiohttpClientMocker, responses: list[str], config_entry: ConfigEntry, ) -> None: """Test setting the rain delay number.""" - assert await setup_integration() - device_registry = dr.async_get(hass) device = device_registry.async_get_device(identifiers={(DOMAIN, SERIAL_NUMBER)}) assert device @@ -136,7 +136,6 @@ async def test_set_value( ) async def test_set_value_error( hass: HomeAssistant, - setup_integration: ComponentSetup, aioclient_mock: AiohttpClientMocker, responses: list[str], config_entry: ConfigEntry, @@ -145,8 +144,6 @@ async def test_set_value_error( ) -> None: """Test an error while talking to the device.""" - assert await setup_integration() - aioclient_mock.mock_calls.clear() responses.append(mock_response_error(status=status)) @@ -172,13 +169,10 @@ async def test_set_value_error( ) async def test_no_unique_id( hass: HomeAssistant, - setup_integration: ComponentSetup, entity_registry: er.EntityRegistry, ) -> None: """Test number platform with no unique id.""" - assert await setup_integration() - raindelay = hass.states.get("number.rain_bird_controller_rain_delay") assert raindelay is not None assert ( diff --git a/tests/components/rainbird/test_sensor.py b/tests/components/rainbird/test_sensor.py index d8fb053c0ffd..00d778335c5e 100644 --- a/tests/components/rainbird/test_sensor.py +++ b/tests/components/rainbird/test_sensor.py @@ -3,11 +3,14 @@ import pytest +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import CONFIG_ENTRY_DATA, RAIN_DELAY, RAIN_DELAY_OFF, ComponentSetup +from .conftest import CONFIG_ENTRY_DATA, RAIN_DELAY, RAIN_DELAY_OFF + +from tests.common import MockConfigEntry @pytest.fixture @@ -16,20 +19,26 @@ def platforms() -> list[str]: return [Platform.SENSOR] +@pytest.fixture(autouse=True) +async def setup_config_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> list[Platform]: + """Fixture to setup the config entry.""" + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + + @pytest.mark.parametrize( ("rain_delay_response", "expected_state"), [(RAIN_DELAY, "16"), (RAIN_DELAY_OFF, "0")], ) async def test_sensors( hass: HomeAssistant, - setup_integration: ComponentSetup, entity_registry: er.EntityRegistry, expected_state: str, ) -> None: """Test sensor platform.""" - assert await setup_integration() - raindelay = hass.states.get("sensor.rain_bird_controller_raindelay") assert raindelay is not None assert raindelay.state == expected_state @@ -66,14 +75,11 @@ async def test_sensors( ) async def test_sensor_no_unique_id( hass: HomeAssistant, - setup_integration: ComponentSetup, entity_registry: er.EntityRegistry, config_entry_unique_id: str | None, ) -> None: """Test sensor platform with no unique id.""" - assert await setup_integration() - raindelay = hass.states.get("sensor.rain_bird_controller_raindelay") assert raindelay is not None assert raindelay.attributes.get("friendly_name") == "Rain Bird Controller Raindelay" diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index 31b64dded99d..e2b6a99d01a1 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -5,6 +5,7 @@ from http import HTTPStatus import pytest from homeassistant.components.rainbird import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -21,11 +22,11 @@ from .conftest import ( ZONE_3_ON_RESPONSE, ZONE_5_ON_RESPONSE, ZONE_OFF_RESPONSE, - ComponentSetup, mock_response, mock_response_error, ) +from tests.common import MockConfigEntry from tests.components.switch import common as switch_common from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse @@ -36,18 +37,24 @@ def platforms() -> list[str]: return [Platform.SWITCH] +@pytest.fixture(autouse=True) +async def setup_config_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> list[Platform]: + """Fixture to setup the config entry.""" + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + + @pytest.mark.parametrize( "stations_response", [EMPTY_STATIONS_RESPONSE], ) async def test_no_zones( hass: HomeAssistant, - setup_integration: ComponentSetup, ) -> None: """Test case where listing stations returns no stations.""" - assert await setup_integration() - zone = hass.states.get("switch.rain_bird_sprinkler_1") assert zone is None @@ -58,13 +65,10 @@ async def test_no_zones( ) async def test_zones( hass: HomeAssistant, - setup_integration: ComponentSetup, entity_registry: er.EntityRegistry, ) -> None: """Test switch platform with fake data that creates 7 zones with one enabled.""" - assert await setup_integration() - zone = hass.states.get("switch.rain_bird_sprinkler_1") assert zone is not None assert zone.state == "off" @@ -110,14 +114,11 @@ async def test_zones( async def test_switch_on( hass: HomeAssistant, - setup_integration: ComponentSetup, aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], ) -> None: """Test turning on irrigation switch.""" - assert await setup_integration() - # Initially all zones are off. Pick zone3 as an arbitrary to assert # state, then update below as a switch. zone = hass.states.get("switch.rain_bird_sprinkler_3") @@ -149,14 +150,11 @@ async def test_switch_on( ) async def test_switch_off( hass: HomeAssistant, - setup_integration: ComponentSetup, aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], ) -> None: """Test turning off irrigation switch.""" - assert await setup_integration() - # Initially the test zone is on zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None @@ -182,15 +180,12 @@ async def test_switch_off( async def test_irrigation_service( hass: HomeAssistant, - setup_integration: ComponentSetup, aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], api_responses: list[str], ) -> None: """Test calling the irrigation service.""" - assert await setup_integration() - zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.state == "off" @@ -219,10 +214,9 @@ async def test_irrigation_service( @pytest.mark.parametrize( - ("yaml_config", "config_entry_data"), + ("config_entry_data"), [ ( - {}, { "host": HOST, "password": PASSWORD, @@ -232,17 +226,15 @@ async def test_irrigation_service( "1": "Garden Sprinkler", "2": "Back Yard", }, - }, + } ) ], ) async def test_yaml_imported_config( hass: HomeAssistant, - setup_integration: ComponentSetup, responses: list[AiohttpClientMockResponse], ) -> None: """Test a config entry that was previously imported from yaml.""" - assert await setup_integration() assert hass.states.get("switch.garden_sprinkler") assert not hass.states.get("switch.rain_bird_sprinkler_1") @@ -260,7 +252,6 @@ async def test_yaml_imported_config( ) async def test_switch_error( hass: HomeAssistant, - setup_integration: ComponentSetup, aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], status: HTTPStatus, @@ -268,8 +259,6 @@ async def test_switch_error( ) -> None: """Test an error talking to the device.""" - assert await setup_integration() - aioclient_mock.mock_calls.clear() responses.append(mock_response_error(status=status)) @@ -292,15 +281,12 @@ async def test_switch_error( ) async def test_no_unique_id( hass: HomeAssistant, - setup_integration: ComponentSetup, aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], entity_registry: er.EntityRegistry, ) -> None: """Test an irrigation switch with no unique id.""" - assert await setup_integration() - zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.attributes.get("friendly_name") == "Rain Bird Sprinkler 3" @@ -321,7 +307,6 @@ async def test_no_unique_id( ) async def test_has_unique_id( hass: HomeAssistant, - setup_integration: ComponentSetup, aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], entity_registry: er.EntityRegistry, @@ -329,8 +314,6 @@ async def test_has_unique_id( ) -> None: """Test an irrigation switch with no unique id.""" - assert await setup_integration() - zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.attributes.get("friendly_name") == "Rain Bird Sprinkler 3" From 3993c14f1d389ddc552bdf303e9ffe29f60dd4b3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 11 Nov 2023 09:39:41 +0100 Subject: [PATCH 154/265] Lock Withings token refresh (#103688) Lock Withings refresh --- homeassistant/components/withings/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 496aba290bad..701f7f444cfd 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -166,12 +166,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: implementation = await async_get_config_entry_implementation(hass, entry) oauth_session = OAuth2Session(hass, entry, implementation) + refresh_lock = asyncio.Lock() + async def _refresh_token() -> str: - await oauth_session.async_ensure_token_valid() - token = oauth_session.token[CONF_ACCESS_TOKEN] - if TYPE_CHECKING: - assert isinstance(token, str) - return token + async with refresh_lock: + await oauth_session.async_ensure_token_valid() + token = oauth_session.token[CONF_ACCESS_TOKEN] + if TYPE_CHECKING: + assert isinstance(token, str) + return token client.refresh_token_function = _refresh_token withings_data = WithingsData( From 6f086a27d459e8c65ef4185fc2b4c0ed31d15345 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 10 Nov 2023 20:46:15 +0000 Subject: [PATCH 155/265] Bump accuweather to version 2.1.0 (#103744) --- homeassistant/components/accuweather/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index 307d68c4b7bf..b74711ccbe6f 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["accuweather"], "quality_scale": "platinum", - "requirements": ["accuweather==2.0.1"] + "requirements": ["accuweather==2.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 870fcc948978..48df0873e15b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -147,7 +147,7 @@ TwitterAPI==2.7.12 WSDiscovery==2.0.0 # homeassistant.components.accuweather -accuweather==2.0.1 +accuweather==2.1.0 # homeassistant.components.adax adax==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96a135845788..4f2ca912a17a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ Tami4EdgeAPI==2.1 WSDiscovery==2.0.0 # homeassistant.components.accuweather -accuweather==2.0.1 +accuweather==2.1.0 # homeassistant.components.adax adax==0.3.0 From d8a6d3e1bc28182f8aef76cf493f68a92265a2f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Nov 2023 18:36:50 +0100 Subject: [PATCH 156/265] Bump python-matter-server to 4.0.2 (#103760) Co-authored-by: Marcel van der Veldt --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../matter/fixtures/config_entry_diagnostics_redacted.json | 1 + tests/components/matter/fixtures/nodes/device_diagnostics.json | 1 + 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 6f494153a97c..174ebb1cab92 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==4.0.0"] + "requirements": ["python-matter-server==4.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 48df0873e15b..0be089e87844 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2150,7 +2150,7 @@ python-kasa[speedups]==0.5.4 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==4.0.0 +python-matter-server==4.0.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f2ca912a17a..994d04dee98e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1603,7 +1603,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.5.4 # homeassistant.components.matter -python-matter-server==4.0.0 +python-matter-server==4.0.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json index 3c5b82ad5b89..8a67ef0fb638 100644 --- a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json +++ b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json @@ -14,6 +14,7 @@ "node_id": 5, "date_commissioned": "2023-01-16T21:07:57.508440", "last_interview": "2023-01-16T21:07:57.508448", + "last_subscription_attempt": 0, "interview_version": 2, "attributes": { "0/4/0": 128, diff --git a/tests/components/matter/fixtures/nodes/device_diagnostics.json b/tests/components/matter/fixtures/nodes/device_diagnostics.json index 4b834cd90903..3abecbdf66fb 100644 --- a/tests/components/matter/fixtures/nodes/device_diagnostics.json +++ b/tests/components/matter/fixtures/nodes/device_diagnostics.json @@ -3,6 +3,7 @@ "date_commissioned": "2023-01-16T21:07:57.508440", "last_interview": "2023-01-16T21:07:57.508448", "interview_version": 2, + "last_subscription_attempt": 0, "attributes": { "0/4/0": 128, "0/4/65532": 1, From db604170bab9bb6dab4324ed33857d0fbe9759ca Mon Sep 17 00:00:00 2001 From: G-Two <7310260+G-Two@users.noreply.github.com> Date: Fri, 10 Nov 2023 15:32:10 -0500 Subject: [PATCH 157/265] Bump subarulink to 0.7.9 (#103761) --- homeassistant/components/subaru/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/subaru/manifest.json b/homeassistant/components/subaru/manifest.json index 0c4367c77c8d..0cffe2576d12 100644 --- a/homeassistant/components/subaru/manifest.json +++ b/homeassistant/components/subaru/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/subaru", "iot_class": "cloud_polling", "loggers": ["stdiomask", "subarulink"], - "requirements": ["subarulink==0.7.8"] + "requirements": ["subarulink==0.7.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0be089e87844..85be51deb9bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2515,7 +2515,7 @@ streamlabswater==1.0.1 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.7.8 +subarulink==0.7.9 # homeassistant.components.solarlog sunwatcher==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 994d04dee98e..1c4a21c0a2aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1875,7 +1875,7 @@ stookwijzer==1.3.0 stringcase==1.2.0 # homeassistant.components.subaru -subarulink==0.7.8 +subarulink==0.7.9 # homeassistant.components.solarlog sunwatcher==0.2.1 From df3e49b24f3dd720bb777c868111c1465d6109e5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 21 Nov 2023 20:25:07 +0100 Subject: [PATCH 158/265] Fix discovery schema for Matter switches (#103762) * Fix discovery schema for matter switches * fix typo in function that generates device name * add test for switchunit --- homeassistant/components/matter/adapter.py | 4 +- homeassistant/components/matter/switch.py | 11 +- .../matter/fixtures/nodes/switch-unit.json | 119 ++++++++++++++++++ tests/components/matter/test_switch.py | 41 ++++-- 4 files changed, 162 insertions(+), 13 deletions(-) create mode 100644 tests/components/matter/fixtures/nodes/switch-unit.json diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 52b8e905b4b6..2831ebe9a38b 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -145,9 +145,7 @@ class MatterAdapter: get_clean_name(basic_info.nodeLabel) or get_clean_name(basic_info.productLabel) or get_clean_name(basic_info.productName) - or device_type.__name__ - if device_type - else None + or (device_type.__name__ if device_type else None) ) # handle bridged devices diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index e1fb4464b831..61922e8e8c98 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -67,7 +67,15 @@ DISCOVERY_SCHEMAS = [ ), entity_class=MatterSwitch, required_attributes=(clusters.OnOff.Attributes.OnOff,), - # restrict device type to prevent discovery by the wrong platform + device_type=(device_types.OnOffPlugInUnit,), + ), + MatterDiscoverySchema( + platform=Platform.SWITCH, + entity_description=SwitchEntityDescription( + key="MatterSwitch", device_class=SwitchDeviceClass.SWITCH, name=None + ), + entity_class=MatterSwitch, + required_attributes=(clusters.OnOff.Attributes.OnOff,), not_device_type=( device_types.ColorTemperatureLight, device_types.DimmableLight, @@ -76,7 +84,6 @@ DISCOVERY_SCHEMAS = [ device_types.DoorLock, device_types.ColorDimmerSwitch, device_types.DimmerSwitch, - device_types.OnOffLightSwitch, device_types.Thermostat, ), ), diff --git a/tests/components/matter/fixtures/nodes/switch-unit.json b/tests/components/matter/fixtures/nodes/switch-unit.json new file mode 100644 index 000000000000..ceed22d2524c --- /dev/null +++ b/tests/components/matter/fixtures/nodes/switch-unit.json @@ -0,0 +1,119 @@ +{ + "node_id": 1, + "date_commissioned": "2022-11-29T21:23:48.485051", + "last_interview": "2022-11-29T21:23:48.485057", + "interview_version": 2, + "attributes": { + "0/29/0": [ + { + "deviceType": 99999, + "revision": 1 + } + ], + "0/29/1": [ + 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, 63, + 64, 65 + ], + "0/29/2": [41], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 1, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 1, + "0/40/1": "Nabu Casa", + "0/40/2": 65521, + "0/40/3": "Mock SwitchUnit", + "0/40/4": 32768, + "0/40/5": "Mock SwitchUnit", + "0/40/6": "XX", + "0/40/7": 0, + "0/40/8": "v1.0", + "0/40/9": 1, + "0/40/10": "v1.0", + "0/40/11": "20221206", + "0/40/12": "", + "0/40/13": "", + "0/40/14": "", + "0/40/15": "TEST_SN", + "0/40/16": false, + "0/40/17": true, + "0/40/18": "mock-switch-unit", + "0/40/19": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + }, + "0/40/65532": 0, + "0/40/65533": 1, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 65528, 65529, 65531, 65532, 65533 + ], + "1/3/0": 0, + "1/3/1": 2, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/4/0": 128, + "1/4/65532": 1, + "1/4/65533": 4, + "1/4/65528": [0, 1, 2, 3], + "1/4/65529": [0, 1, 2, 3, 4, 5], + "1/4/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/5/0": 0, + "1/5/1": 0, + "1/5/2": 0, + "1/5/3": false, + "1/5/4": 0, + "1/5/65532": 0, + "1/5/65533": 4, + "1/5/65528": [0, 1, 2, 3, 4, 6], + "1/5/65529": [0, 1, 2, 3, 4, 5, 6], + "1/5/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "1/6/0": false, + "1/6/16384": true, + "1/6/16385": 0, + "1/6/16386": 0, + "1/6/16387": null, + "1/6/65532": 1, + "1/6/65533": 4, + "1/6/65528": [], + "1/6/65529": [0, 1, 2, 64, 65, 66], + "1/6/65531": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533 + ], + "1/7/0": 0, + "1/7/16": 0, + "1/7/65532": 0, + "1/7/65533": 1, + "1/7/65528": [], + "1/7/65529": [], + "1/7/65531": [0, 16, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "deviceType": 9999999, + "revision": 1 + } + ], + "1/29/1": [ + 3, 4, 5, 6, 7, 8, 15, 29, 30, 37, 47, 59, 64, 65, 69, 80, 257, 258, 259, + 512, 513, 514, 516, 768, 1024, 1026, 1027, 1028, 1029, 1030, 1283, 1284, + 1285, 1286, 1287, 1288, 1289, 1290, 1291, 1292, 1293, 1294, 2820, + 4294048773 + ], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 1, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "available": true, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py index 6fbe5d58f289..ac03d731ee1b 100644 --- a/tests/components/matter/test_switch.py +++ b/tests/components/matter/test_switch.py @@ -14,22 +14,30 @@ from .common import ( ) -@pytest.fixture(name="switch_node") -async def switch_node_fixture( +@pytest.fixture(name="powerplug_node") +async def powerplug_node_fixture( hass: HomeAssistant, matter_client: MagicMock ) -> MatterNode: - """Fixture for a switch node.""" + """Fixture for a Powerplug node.""" return await setup_integration_with_node_fixture( hass, "on-off-plugin-unit", matter_client ) +@pytest.fixture(name="switch_unit") +async def switch_unit_fixture( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a Switch Unit node.""" + return await setup_integration_with_node_fixture(hass, "switch-unit", matter_client) + + # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_turn_on( hass: HomeAssistant, matter_client: MagicMock, - switch_node: MatterNode, + powerplug_node: MatterNode, ) -> None: """Test turning on a switch.""" state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") @@ -47,12 +55,12 @@ async def test_turn_on( assert matter_client.send_device_command.call_count == 1 assert matter_client.send_device_command.call_args == call( - node_id=switch_node.node_id, + node_id=powerplug_node.node_id, endpoint_id=1, command=clusters.OnOff.Commands.On(), ) - set_node_attribute(switch_node, 1, 6, 0, True) + set_node_attribute(powerplug_node, 1, 6, 0, True) await trigger_subscription_callback(hass, matter_client) state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") @@ -65,7 +73,7 @@ async def test_turn_on( async def test_turn_off( hass: HomeAssistant, matter_client: MagicMock, - switch_node: MatterNode, + powerplug_node: MatterNode, ) -> None: """Test turning off a switch.""" state = hass.states.get("switch.mock_onoffpluginunit_powerplug_switch") @@ -83,7 +91,24 @@ async def test_turn_off( assert matter_client.send_device_command.call_count == 1 assert matter_client.send_device_command.call_args == call( - node_id=switch_node.node_id, + node_id=powerplug_node.node_id, endpoint_id=1, command=clusters.OnOff.Commands.Off(), ) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_switch_unit( + hass: HomeAssistant, + matter_client: MagicMock, + switch_unit: MatterNode, +) -> None: + """Test if a switch entity is discovered from any (non-light) OnOf cluster device.""" + # A switch entity should be discovered as fallback for ANY Matter device (endpoint) + # that has the OnOff cluster and does not fall into an explicit discovery schema + # by another platform (e.g. light, lock etc.). + state = hass.states.get("switch.mock_switchunit") + assert state + assert state.state == "off" + assert state.attributes["friendly_name"] == "Mock SwitchUnit" From 8a152a68d89d3992fc04812c3b20718c29ff1f86 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 13 Nov 2023 22:59:03 +0100 Subject: [PATCH 159/265] Fix raising vol.Invalid during mqtt config validation instead of ValueError (#103764) --- homeassistant/components/mqtt/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 358fa6eb675d..3fa3ebfd30cd 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -256,7 +256,7 @@ def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: CONF_HUMIDITY_STATE_TOPIC in config and CONF_HUMIDITY_COMMAND_TOPIC not in config ): - raise ValueError( + raise vol.Invalid( f"{CONF_HUMIDITY_STATE_TOPIC} cannot be used without" f" {CONF_HUMIDITY_COMMAND_TOPIC}" ) From fb1dfb016e5a4d7adb5a52845880a29f5724403a Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 13 Nov 2023 15:42:51 +0100 Subject: [PATCH 160/265] Fix race condition in Matter unsubscribe method (#103770) --- homeassistant/components/matter/entity.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 102e0c83b7b0..7e7b7a688df6 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import abstractmethod from collections.abc import Callable +from contextlib import suppress from dataclasses import dataclass import logging from typing import TYPE_CHECKING, Any, cast @@ -110,7 +111,9 @@ class MatterEntity(Entity): async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" for unsub in self._unsubscribes: - unsub() + with suppress(ValueError): + # suppress ValueError to prevent race conditions + unsub() @callback def _on_matter_event(self, event: EventType, data: Any = None) -> None: From 31ac03fe504684e4e41ed9b5b944f61153dcaf90 Mon Sep 17 00:00:00 2001 From: Jan Rieger Date: Sat, 11 Nov 2023 14:24:23 +0100 Subject: [PATCH 161/265] Fix typo in calendar translation (#103789) --- homeassistant/components/calendar/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/calendar/strings.json b/homeassistant/components/calendar/strings.json index 81334c123796..20679ed09b2e 100644 --- a/homeassistant/components/calendar/strings.json +++ b/homeassistant/components/calendar/strings.json @@ -24,7 +24,7 @@ "location": { "name": "Location" }, - "messages": { + "message": { "name": "Message" }, "start_time": { From 2a26dea5874166975fe3ce309c6b705258cf24fb Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 11 Nov 2023 23:36:30 -0800 Subject: [PATCH 162/265] Fix Rainbird unique to use a more reliable source (mac address) (#101603) * Fix rainbird unique id to use a mac address for new entries * Fix typo * Normalize mac address before using as unique id * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Update test check and remove dead code * Update all config entries to the new format * Update config entry tests for migration * Fix rainbird entity unique ids * Add test coverage for repair failure * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Remove unnecessary migration failure checks * Remove invalid config entries * Update entry when entering a different hostname for an existing host. --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/rainbird/__init__.py | 88 +++++++- .../components/rainbird/config_flow.py | 45 ++-- tests/components/rainbird/conftest.py | 51 ++++- .../components/rainbird/test_binary_sensor.py | 49 ++-- tests/components/rainbird/test_calendar.py | 16 +- tests/components/rainbird/test_config_flow.py | 79 ++++++- tests/components/rainbird/test_init.py | 212 +++++++++++++++++- tests/components/rainbird/test_number.py | 57 ++--- tests/components/rainbird/test_sensor.py | 32 ++- tests/components/rainbird/test_switch.py | 47 ++-- 10 files changed, 515 insertions(+), 161 deletions(-) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index a97af14f4495..e7a7c1200b96 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -1,17 +1,25 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" from __future__ import annotations +import logging + from pyrainbird.async_client import AsyncRainbirdClient, AsyncRainbirdController from pyrainbird.exceptions import RainbirdApiException from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.entity_registry import async_entries_for_config_entry +from .const import CONF_SERIAL_NUMBER from .coordinator import RainbirdData +_LOGGER = logging.getLogger(__name__) + PLATFORMS = [ Platform.SWITCH, Platform.SENSOR, @@ -36,6 +44,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PASSWORD], ) ) + + if not (await _async_fix_unique_id(hass, controller, entry)): + return False + if mac_address := entry.data.get(CONF_MAC): + _async_fix_entity_unique_id( + hass, + er.async_get(hass), + entry.entry_id, + format_mac(mac_address), + str(entry.data[CONF_SERIAL_NUMBER]), + ) + try: model_info = await controller.get_model_and_version() except RainbirdApiException as err: @@ -51,6 +71,72 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def _async_fix_unique_id( + hass: HomeAssistant, controller: AsyncRainbirdController, entry: ConfigEntry +) -> bool: + """Update the config entry with a unique id based on the mac address.""" + _LOGGER.debug("Checking for migration of config entry (%s)", entry.unique_id) + if not (mac_address := entry.data.get(CONF_MAC)): + try: + wifi_params = await controller.get_wifi_params() + except RainbirdApiException as err: + _LOGGER.warning("Unable to fix missing unique id: %s", err) + return True + + if (mac_address := wifi_params.mac_address) is None: + _LOGGER.warning("Unable to fix missing unique id (mac address was None)") + return True + + new_unique_id = format_mac(mac_address) + if entry.unique_id == new_unique_id and CONF_MAC in entry.data: + _LOGGER.debug("Config entry already in correct state") + return True + + entries = hass.config_entries.async_entries(DOMAIN) + for existing_entry in entries: + if existing_entry.unique_id == new_unique_id: + _LOGGER.warning( + "Unable to fix missing unique id (already exists); Removing duplicate entry" + ) + hass.async_create_background_task( + hass.config_entries.async_remove(entry.entry_id), + "Remove rainbird config entry", + ) + return False + + _LOGGER.debug("Updating unique id to %s", new_unique_id) + hass.config_entries.async_update_entry( + entry, + unique_id=new_unique_id, + data={ + **entry.data, + CONF_MAC: mac_address, + }, + ) + return True + + +def _async_fix_entity_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_id: str, + mac_address: str, + serial_number: str, +) -> None: + """Migrate existing entity if current one can't be found and an old one exists.""" + entity_entries = async_entries_for_config_entry(entity_registry, config_entry_id) + for entity_entry in entity_entries: + unique_id = str(entity_entry.unique_id) + if unique_id.startswith(mac_address): + continue + if (suffix := unique_id.removeprefix(str(serial_number))) != unique_id: + new_unique_id = f"{mac_address}{suffix}" + _LOGGER.debug("Updating unique id from %s to %s", unique_id, new_unique_id) + entity_registry.async_update_entity( + entity_entry.entity_id, new_unique_id=new_unique_id + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" diff --git a/homeassistant/components/rainbird/config_flow.py b/homeassistant/components/rainbird/config_flow.py index bf6682e7a6f8..f90e13d37f3e 100644 --- a/homeassistant/components/rainbird/config_flow.py +++ b/homeassistant/components/rainbird/config_flow.py @@ -11,15 +11,17 @@ from pyrainbird.async_client import ( AsyncRainbirdController, RainbirdApiException, ) +from pyrainbird.data import WifiParams import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac from .const import ( ATTR_DURATION, @@ -69,7 +71,7 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): error_code: str | None = None if user_input: try: - serial_number = await self._test_connection( + serial_number, wifi_params = await self._test_connection( user_input[CONF_HOST], user_input[CONF_PASSWORD] ) except ConfigFlowError as err: @@ -77,11 +79,11 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): error_code = err.error_code else: return await self.async_finish( - serial_number, data={ CONF_HOST: user_input[CONF_HOST], CONF_PASSWORD: user_input[CONF_PASSWORD], CONF_SERIAL_NUMBER: serial_number, + CONF_MAC: wifi_params.mac_address, }, options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES}, ) @@ -92,8 +94,10 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors={"base": error_code} if error_code else None, ) - async def _test_connection(self, host: str, password: str) -> str: - """Test the connection and return the device serial number. + async def _test_connection( + self, host: str, password: str + ) -> tuple[str, WifiParams]: + """Test the connection and return the device identifiers. Raises a ConfigFlowError on failure. """ @@ -106,7 +110,10 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) try: async with asyncio.timeout(TIMEOUT_SECONDS): - return await controller.get_serial_number() + return await asyncio.gather( + controller.get_serial_number(), + controller.get_wifi_params(), + ) except asyncio.TimeoutError as err: raise ConfigFlowError( f"Timeout connecting to Rain Bird controller: {str(err)}", @@ -120,18 +127,28 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_finish( self, - serial_number: str, data: dict[str, Any], options: dict[str, Any], ) -> FlowResult: """Create the config entry.""" - # Prevent devices with the same serial number. If the device does not have a serial number - # then we can at least prevent configuring the same host twice. - if serial_number: - await self.async_set_unique_id(serial_number) - self._abort_if_unique_id_configured() - else: - self._async_abort_entries_match(data) + # The integration has historically used a serial number, but not all devices + # historically had a valid one. Now the mac address is used as a unique id + # and serial is still persisted in config entry data in case it is needed + # in the future. + # Either way, also prevent configuring the same host twice. + await self.async_set_unique_id(format_mac(data[CONF_MAC])) + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: data[CONF_HOST], + CONF_PASSWORD: data[CONF_PASSWORD], + } + ) + self._async_abort_entries_match( + { + CONF_HOST: data[CONF_HOST], + CONF_PASSWORD: data[CONF_PASSWORD], + } + ) return self.async_create_entry( title=data[CONF_HOST], data=data, diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index 6e8d58219c18..52b98e5c6b68 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations from http import HTTPStatus +import json from typing import Any from unittest.mock import patch @@ -24,6 +25,8 @@ HOST = "example.com" URL = "http://example.com/stick" PASSWORD = "password" SERIAL_NUMBER = 0x12635436566 +MAC_ADDRESS = "4C:A1:61:00:11:22" +MAC_ADDRESS_UNIQUE_ID = "4c:a1:61:00:11:22" # # Response payloads below come from pyrainbird test cases. @@ -50,6 +53,20 @@ RAIN_DELAY = "B60010" # 0x10 is 16 RAIN_DELAY_OFF = "B60000" # ACK command 0x10, Echo 0x06 ACK_ECHO = "0106" +WIFI_PARAMS_RESPONSE = { + "macAddress": MAC_ADDRESS, + "localIpAddress": "1.1.1.38", + "localNetmask": "255.255.255.0", + "localGateway": "1.1.1.1", + "rssi": -61, + "wifiSsid": "wifi-ssid-name", + "wifiPassword": "wifi-password-name", + "wifiSecurity": "wpa2-aes", + "apTimeoutNoLan": 20, + "apTimeoutIdle": 20, + "apSecurity": "unknown", + "stickVersion": "Rain Bird Stick Rev C/1.63", +} CONFIG = { @@ -62,10 +79,16 @@ CONFIG = { } } +CONFIG_ENTRY_DATA_OLD_FORMAT = { + "host": HOST, + "password": PASSWORD, + "serial_number": SERIAL_NUMBER, +} CONFIG_ENTRY_DATA = { "host": HOST, "password": PASSWORD, "serial_number": SERIAL_NUMBER, + "mac": MAC_ADDRESS, } @@ -77,14 +100,23 @@ def platforms() -> list[Platform]: @pytest.fixture async def config_entry_unique_id() -> str: - """Fixture for serial number used in the config entry.""" + """Fixture for config entry unique id.""" + return MAC_ADDRESS_UNIQUE_ID + + +@pytest.fixture +async def serial_number() -> int: + """Fixture for serial number used in the config entry data.""" return SERIAL_NUMBER @pytest.fixture -async def config_entry_data() -> dict[str, Any]: +async def config_entry_data(serial_number: int) -> dict[str, Any]: """Fixture for MockConfigEntry data.""" - return CONFIG_ENTRY_DATA + return { + **CONFIG_ENTRY_DATA, + "serial_number": serial_number, + } @pytest.fixture @@ -123,17 +155,24 @@ def setup_platforms( yield -def rainbird_response(data: str) -> bytes: +def rainbird_json_response(result: dict[str, str]) -> bytes: """Create a fake API response.""" return encryption.encrypt( - '{"jsonrpc": "2.0", "result": {"data":"%s"}, "id": 1} ' % data, + '{"jsonrpc": "2.0", "result": %s, "id": 1} ' % json.dumps(result), PASSWORD, ) +def mock_json_response(result: dict[str, str]) -> AiohttpClientMockResponse: + """Create a fake AiohttpClientMockResponse.""" + return AiohttpClientMockResponse( + "POST", URL, response=rainbird_json_response(result) + ) + + def mock_response(data: str) -> AiohttpClientMockResponse: """Create a fake AiohttpClientMockResponse.""" - return AiohttpClientMockResponse("POST", URL, response=rainbird_response(data)) + return mock_json_response({"data": data}) def mock_response_error( diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py index 7b9fb41ed1f1..afe183373770 100644 --- a/tests/components/rainbird/test_binary_sensor.py +++ b/tests/components/rainbird/test_binary_sensor.py @@ -1,6 +1,8 @@ """Tests for rainbird sensor platform.""" +from http import HTTPStatus + import pytest from homeassistant.config_entries import ConfigEntryState @@ -8,7 +10,12 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import RAIN_SENSOR_OFF, RAIN_SENSOR_ON, SERIAL_NUMBER +from .conftest import ( + CONFIG_ENTRY_DATA_OLD_FORMAT, + RAIN_SENSOR_OFF, + RAIN_SENSOR_ON, + mock_response_error, +) from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMockResponse @@ -51,47 +58,25 @@ async def test_rainsensor( @pytest.mark.parametrize( - ("config_entry_unique_id", "entity_unique_id"), + ("config_entry_data", "config_entry_unique_id", "setup_config_entry"), [ - (SERIAL_NUMBER, "1263613994342-rainsensor"), - # Some existing config entries may have a "0" serial number but preserve - # their unique id - (0, "0-rainsensor"), - ], -) -async def test_unique_id( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - entity_unique_id: str, -) -> None: - """Test rainsensor binary sensor.""" - rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor") - assert rainsensor is not None - assert rainsensor.attributes == { - "friendly_name": "Rain Bird Controller Rainsensor", - "icon": "mdi:water", - } - - entity_entry = entity_registry.async_get( - "binary_sensor.rain_bird_controller_rainsensor" - ) - assert entity_entry - assert entity_entry.unique_id == entity_unique_id - - -@pytest.mark.parametrize( - ("config_entry_unique_id"), - [ - (None), + (CONFIG_ENTRY_DATA_OLD_FORMAT, None, None), ], ) async def test_no_unique_id( hass: HomeAssistant, responses: list[AiohttpClientMockResponse], entity_registry: er.EntityRegistry, + config_entry: MockConfigEntry, ) -> None: """Test rainsensor binary sensor with no unique id.""" + # Failure to migrate config entry to a unique id + responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) + + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + rainsensor = hass.states.get("binary_sensor.rain_bird_controller_rainsensor") assert rainsensor is not None assert ( diff --git a/tests/components/rainbird/test_calendar.py b/tests/components/rainbird/test_calendar.py index d6c14834342c..04e423a399c6 100644 --- a/tests/components/rainbird/test_calendar.py +++ b/tests/components/rainbird/test_calendar.py @@ -17,7 +17,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import mock_response, mock_response_error +from .conftest import CONFIG_ENTRY_DATA_OLD_FORMAT, mock_response, mock_response_error from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMockResponse @@ -210,7 +210,7 @@ async def test_event_state( entity = entity_registry.async_get(TEST_ENTITY) assert entity - assert entity.unique_id == 1263613994342 + assert entity.unique_id == "4c:a1:61:00:11:22" @pytest.mark.parametrize( @@ -280,18 +280,26 @@ async def test_program_schedule_disabled( @pytest.mark.parametrize( - ("config_entry_unique_id"), + ("config_entry_data", "config_entry_unique_id", "setup_config_entry"), [ - (None), + (CONFIG_ENTRY_DATA_OLD_FORMAT, None, None), ], ) async def test_no_unique_id( hass: HomeAssistant, get_events: GetEventsFn, + responses: list[AiohttpClientMockResponse], entity_registry: er.EntityRegistry, + config_entry: MockConfigEntry, ) -> None: """Test calendar entity with no unique id.""" + # Failure to migrate config entry to a unique id + responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) + + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + state = hass.states.get(TEST_ENTITY) assert state is not None assert state.attributes.get("friendly_name") == "Rain Bird Controller" diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py index f93da8d9839c..6c0e13fef395 100644 --- a/tests/components/rainbird/test_config_flow.py +++ b/tests/components/rainbird/test_config_flow.py @@ -19,11 +19,14 @@ from homeassistant.data_entry_flow import FlowResult, FlowResultType from .conftest import ( CONFIG_ENTRY_DATA, HOST, + MAC_ADDRESS_UNIQUE_ID, PASSWORD, SERIAL_NUMBER, SERIAL_RESPONSE, URL, + WIFI_PARAMS_RESPONSE, ZERO_SERIAL_RESPONSE, + mock_json_response, mock_response, ) @@ -34,7 +37,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockRespon @pytest.fixture(name="responses") def mock_responses() -> list[AiohttpClientMockResponse]: """Set up fake serial number response when testing the connection.""" - return [mock_response(SERIAL_RESPONSE)] + return [mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)] @pytest.fixture(autouse=True) @@ -74,14 +77,20 @@ async def complete_flow(hass: HomeAssistant) -> FlowResult: ("responses", "expected_config_entry", "expected_unique_id"), [ ( - [mock_response(SERIAL_RESPONSE)], + [ + mock_response(SERIAL_RESPONSE), + mock_json_response(WIFI_PARAMS_RESPONSE), + ], CONFIG_ENTRY_DATA, - SERIAL_NUMBER, + MAC_ADDRESS_UNIQUE_ID, ), ( - [mock_response(ZERO_SERIAL_RESPONSE)], + [ + mock_response(ZERO_SERIAL_RESPONSE), + mock_json_response(WIFI_PARAMS_RESPONSE), + ], {**CONFIG_ENTRY_DATA, "serial_number": 0}, - None, + MAC_ADDRESS_UNIQUE_ID, ), ], ) @@ -115,17 +124,32 @@ async def test_controller_flow( ( "other-serial-number", {**CONFIG_ENTRY_DATA, "host": "other-host"}, - [mock_response(SERIAL_RESPONSE)], + [mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)], + CONFIG_ENTRY_DATA, + ), + ( + "11:22:33:44:55:66", + { + **CONFIG_ENTRY_DATA, + "host": "other-host", + }, + [ + mock_response(SERIAL_RESPONSE), + mock_json_response(WIFI_PARAMS_RESPONSE), + ], CONFIG_ENTRY_DATA, ), ( None, {**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"}, - [mock_response(ZERO_SERIAL_RESPONSE)], + [ + mock_response(ZERO_SERIAL_RESPONSE), + mock_json_response(WIFI_PARAMS_RESPONSE), + ], {**CONFIG_ENTRY_DATA, "serial_number": 0}, ), ], - ids=["with-serial", "zero-serial"], + ids=["with-serial", "with-mac-address", "zero-serial"], ) async def test_multiple_config_entries( hass: HomeAssistant, @@ -154,22 +178,52 @@ async def test_multiple_config_entries( "config_entry_unique_id", "config_entry_data", "config_flow_responses", + "expected_config_entry_data", ), [ + # Config entry is a pure duplicate with the same mac address unique id + ( + MAC_ADDRESS_UNIQUE_ID, + CONFIG_ENTRY_DATA, + [ + mock_response(SERIAL_RESPONSE), + mock_json_response(WIFI_PARAMS_RESPONSE), + ], + CONFIG_ENTRY_DATA, + ), + # Old unique id with serial, but same host ( SERIAL_NUMBER, CONFIG_ENTRY_DATA, - [mock_response(SERIAL_RESPONSE)], + [mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)], + CONFIG_ENTRY_DATA, ), + # Old unique id with no serial, but same host ( None, {**CONFIG_ENTRY_DATA, "serial_number": 0}, - [mock_response(ZERO_SERIAL_RESPONSE)], + [ + mock_response(ZERO_SERIAL_RESPONSE), + mock_json_response(WIFI_PARAMS_RESPONSE), + ], + {**CONFIG_ENTRY_DATA, "serial_number": 0}, + ), + # Enters a different hostname that points to the same mac address + ( + MAC_ADDRESS_UNIQUE_ID, + { + **CONFIG_ENTRY_DATA, + "host": f"other-{HOST}", + }, + [mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)], + CONFIG_ENTRY_DATA, # Updated the host ), ], ids=[ - "duplicate-serial-number", + "duplicate-mac-unique-id", + "duplicate-host-legacy-serial-number", "duplicate-host-port-no-serial", + "duplicate-duplicate-hostname", ], ) async def test_duplicate_config_entries( @@ -177,6 +231,7 @@ async def test_duplicate_config_entries( config_entry: MockConfigEntry, responses: list[AiohttpClientMockResponse], config_flow_responses: list[AiohttpClientMockResponse], + expected_config_entry_data: dict[str, Any], ) -> None: """Test that a device can not be registered twice.""" await config_entry.async_setup(hass) @@ -186,8 +241,10 @@ async def test_duplicate_config_entries( responses.extend(config_flow_responses) result = await complete_flow(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" + assert dict(config_entry.data) == expected_config_entry_data async def test_controller_cannot_connect( diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index 7ec22b88867b..db9c4c8739eb 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -6,12 +6,21 @@ from http import HTTPStatus import pytest +from homeassistant.components.rainbird.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_MAC from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .conftest import ( CONFIG_ENTRY_DATA, + CONFIG_ENTRY_DATA_OLD_FORMAT, + MAC_ADDRESS, + MAC_ADDRESS_UNIQUE_ID, MODEL_AND_VERSION_RESPONSE, + SERIAL_NUMBER, + WIFI_PARAMS_RESPONSE, + mock_json_response, mock_response, mock_response_error, ) @@ -20,22 +29,11 @@ from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMockResponse -@pytest.mark.parametrize( - ("config_entry_data", "initial_response"), - [ - (CONFIG_ENTRY_DATA, None), - ], - ids=["config_entry"], -) async def test_init_success( hass: HomeAssistant, config_entry: MockConfigEntry, - responses: list[AiohttpClientMockResponse], - initial_response: AiohttpClientMockResponse | None, ) -> None: """Test successful setup and unload.""" - if initial_response: - responses.insert(0, initial_response) await config_entry.async_setup(hass) assert config_entry.state == ConfigEntryState.LOADED @@ -88,6 +86,196 @@ async def test_communication_failure( config_entry_state: list[ConfigEntryState], ) -> None: """Test unable to talk to device on startup, which fails setup.""" - await config_entry.async_setup(hass) assert config_entry.state == config_entry_state + + +@pytest.mark.parametrize( + ("config_entry_unique_id", "config_entry_data"), + [ + ( + None, + {**CONFIG_ENTRY_DATA, "mac": None}, + ), + ], + ids=["config_entry"], +) +async def test_fix_unique_id( + hass: HomeAssistant, + responses: list[AiohttpClientMockResponse], + config_entry: MockConfigEntry, +) -> None: + """Test fix of a config entry with no unique id.""" + + responses.insert(0, mock_json_response(WIFI_PARAMS_RESPONSE)) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.NOT_LOADED + assert entries[0].unique_id is None + assert entries[0].data.get(CONF_MAC) is None + + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + + # Verify config entry now has a unique id + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert entries[0].unique_id == MAC_ADDRESS_UNIQUE_ID + assert entries[0].data.get(CONF_MAC) == MAC_ADDRESS + + +@pytest.mark.parametrize( + ( + "config_entry_unique_id", + "config_entry_data", + "initial_response", + "expected_warning", + ), + [ + ( + None, + CONFIG_ENTRY_DATA_OLD_FORMAT, + mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE), + "Unable to fix missing unique id:", + ), + ( + None, + CONFIG_ENTRY_DATA_OLD_FORMAT, + mock_response_error(HTTPStatus.NOT_FOUND), + "Unable to fix missing unique id:", + ), + ( + None, + CONFIG_ENTRY_DATA_OLD_FORMAT, + mock_response("bogus"), + "Unable to fix missing unique id (mac address was None)", + ), + ], + ids=["service_unavailable", "not_found", "unexpected_response_format"], +) +async def test_fix_unique_id_failure( + hass: HomeAssistant, + initial_response: AiohttpClientMockResponse, + responses: list[AiohttpClientMockResponse], + expected_warning: str, + caplog: pytest.LogCaptureFixture, + config_entry: MockConfigEntry, +) -> None: + """Test a failure during fix of a config entry with no unique id.""" + + responses.insert(0, initial_response) + + await config_entry.async_setup(hass) + # Config entry is loaded, but not updated + assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.unique_id is None + + assert expected_warning in caplog.text + + +@pytest.mark.parametrize( + ("config_entry_unique_id"), + [(MAC_ADDRESS_UNIQUE_ID)], +) +async def test_fix_unique_id_duplicate( + hass: HomeAssistant, + config_entry: MockConfigEntry, + responses: list[AiohttpClientMockResponse], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that a config entry unique id already exists during fix.""" + # Add a second config entry that has no unique id, but has the same + # mac address. When fixing the unique id, it can't use the mac address + # since it already exists. + other_entry = MockConfigEntry( + unique_id=None, + domain=DOMAIN, + data=CONFIG_ENTRY_DATA_OLD_FORMAT, + ) + other_entry.add_to_hass(hass) + + # Responses for the second config entry. This first fetches wifi params + # to repair the unique id. + responses_copy = [*responses] + responses.append(mock_json_response(WIFI_PARAMS_RESPONSE)) + responses.extend(responses_copy) + + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.unique_id == MAC_ADDRESS_UNIQUE_ID + + await other_entry.async_setup(hass) + # Config entry unique id could not be updated since it already exists + assert other_entry.state == ConfigEntryState.SETUP_ERROR + + assert "Unable to fix missing unique id (already exists)" in caplog.text + + await hass.async_block_till_done() + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +@pytest.mark.parametrize( + ( + "config_entry_unique_id", + "serial_number", + "entity_unique_id", + "expected_unique_id", + ), + [ + (SERIAL_NUMBER, SERIAL_NUMBER, SERIAL_NUMBER, MAC_ADDRESS_UNIQUE_ID), + ( + SERIAL_NUMBER, + SERIAL_NUMBER, + f"{SERIAL_NUMBER}-rain-delay", + f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", + ), + ("0", 0, "0", MAC_ADDRESS_UNIQUE_ID), + ( + "0", + 0, + "0-rain-delay", + f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", + ), + ( + MAC_ADDRESS_UNIQUE_ID, + SERIAL_NUMBER, + MAC_ADDRESS_UNIQUE_ID, + MAC_ADDRESS_UNIQUE_ID, + ), + ( + MAC_ADDRESS_UNIQUE_ID, + SERIAL_NUMBER, + f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", + f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", + ), + ], + ids=( + "serial-number", + "serial-number-with-suffix", + "zero-serial", + "zero-serial-suffix", + "new-format", + "new-format-suffx", + ), +) +async def test_fix_entity_unique_ids( + hass: HomeAssistant, + config_entry: MockConfigEntry, + entity_unique_id: str, + expected_unique_id: str, +) -> None: + """Test fixing entity unique ids from old unique id formats.""" + + entity_registry = er.async_get(hass) + entity_entry = entity_registry.async_get_or_create( + DOMAIN, "number", unique_id=entity_unique_id, config_entry=config_entry + ) + + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + + entity_entry = entity_registry.async_get(entity_entry.id) + assert entity_entry + assert entity_entry.unique_id == expected_unique_id diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index 0beae1f5a95a..79b8fd5ec37b 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components import number from homeassistant.components.rainbird import DOMAIN -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -14,15 +14,16 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from .conftest import ( ACK_ECHO, + CONFIG_ENTRY_DATA_OLD_FORMAT, + MAC_ADDRESS, RAIN_DELAY, RAIN_DELAY_OFF, - SERIAL_NUMBER, mock_response, mock_response_error, ) from tests.common import MockConfigEntry -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse @pytest.fixture @@ -66,46 +67,23 @@ async def test_number_values( entity_entry = entity_registry.async_get("number.rain_bird_controller_rain_delay") assert entity_entry - assert entity_entry.unique_id == "1263613994342-rain-delay" - - -@pytest.mark.parametrize( - ("config_entry_unique_id", "entity_unique_id"), - [ - (SERIAL_NUMBER, "1263613994342-rain-delay"), - # Some existing config entries may have a "0" serial number but preserve - # their unique id - (0, "0-rain-delay"), - ], -) -async def test_unique_id( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - entity_unique_id: str, -) -> None: - """Test number platform.""" - - raindelay = hass.states.get("number.rain_bird_controller_rain_delay") - assert raindelay is not None - assert ( - raindelay.attributes.get("friendly_name") == "Rain Bird Controller Rain delay" - ) - - entity_entry = entity_registry.async_get("number.rain_bird_controller_rain_delay") - assert entity_entry - assert entity_entry.unique_id == entity_unique_id + assert entity_entry.unique_id == "4c:a1:61:00:11:22-rain-delay" async def test_set_value( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, responses: list[str], - config_entry: ConfigEntry, ) -> None: """Test setting the rain delay number.""" + raindelay = hass.states.get("number.rain_bird_controller_rain_delay") + assert raindelay is not None + device_registry = dr.async_get(hass) - device = device_registry.async_get_device(identifiers={(DOMAIN, SERIAL_NUMBER)}) + device = device_registry.async_get_device( + identifiers={(DOMAIN, MAC_ADDRESS.lower())} + ) assert device assert device.name == "Rain Bird Controller" assert device.model == "ESP-TM2" @@ -138,7 +116,6 @@ async def test_set_value_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, responses: list[str], - config_entry: ConfigEntry, status: HTTPStatus, expected_msg: str, ) -> None: @@ -162,17 +139,25 @@ async def test_set_value_error( @pytest.mark.parametrize( - ("config_entry_unique_id"), + ("config_entry_data", "config_entry_unique_id", "setup_config_entry"), [ - (None), + (CONFIG_ENTRY_DATA_OLD_FORMAT, None, None), ], ) async def test_no_unique_id( hass: HomeAssistant, + responses: list[AiohttpClientMockResponse], entity_registry: er.EntityRegistry, + config_entry: MockConfigEntry, ) -> None: """Test number platform with no unique id.""" + # Failure to migrate config entry to a unique id + responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) + + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + raindelay = hass.states.get("number.rain_bird_controller_rain_delay") assert raindelay is not None assert ( diff --git a/tests/components/rainbird/test_sensor.py b/tests/components/rainbird/test_sensor.py index 00d778335c5e..2a0195f8d97d 100644 --- a/tests/components/rainbird/test_sensor.py +++ b/tests/components/rainbird/test_sensor.py @@ -1,5 +1,6 @@ """Tests for rainbird sensor platform.""" +from http import HTTPStatus import pytest @@ -8,9 +9,15 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import CONFIG_ENTRY_DATA, RAIN_DELAY, RAIN_DELAY_OFF +from .conftest import ( + CONFIG_ENTRY_DATA_OLD_FORMAT, + RAIN_DELAY, + RAIN_DELAY_OFF, + mock_response_error, +) from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMockResponse @pytest.fixture @@ -49,37 +56,38 @@ async def test_sensors( entity_entry = entity_registry.async_get("sensor.rain_bird_controller_raindelay") assert entity_entry - assert entity_entry.unique_id == "1263613994342-raindelay" + assert entity_entry.unique_id == "4c:a1:61:00:11:22-raindelay" @pytest.mark.parametrize( - ("config_entry_unique_id", "config_entry_data"), + ("config_entry_unique_id", "config_entry_data", "setup_config_entry"), [ # Config entry setup without a unique id since it had no serial number ( None, { - **CONFIG_ENTRY_DATA, - "serial_number": 0, - }, - ), - # Legacy case for old config entries with serial number 0 preserves old behavior - ( - "0", - { - **CONFIG_ENTRY_DATA, + **CONFIG_ENTRY_DATA_OLD_FORMAT, "serial_number": 0, }, + None, ), ], ) async def test_sensor_no_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, + responses: list[AiohttpClientMockResponse], config_entry_unique_id: str | None, + config_entry: MockConfigEntry, ) -> None: """Test sensor platform with no unique id.""" + # Failure to migrate config entry to a unique id + responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) + + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED + raindelay = hass.states.get("sensor.rain_bird_controller_raindelay") assert raindelay is not None assert raindelay.attributes.get("friendly_name") == "Rain Bird Controller Raindelay" diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index e2b6a99d01a1..f9c03f63dd34 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -13,12 +13,13 @@ from homeassistant.helpers import entity_registry as er from .conftest import ( ACK_ECHO, + CONFIG_ENTRY_DATA_OLD_FORMAT, EMPTY_STATIONS_RESPONSE, HOST, + MAC_ADDRESS, PASSWORD, RAIN_DELAY_OFF, RAIN_SENSOR_OFF, - SERIAL_NUMBER, ZONE_3_ON_RESPONSE, ZONE_5_ON_RESPONSE, ZONE_OFF_RESPONSE, @@ -109,7 +110,7 @@ async def test_zones( # Verify unique id for one of the switches entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3") - assert entity_entry.unique_id == "1263613994342-3" + assert entity_entry.unique_id == "4c:a1:61:00:11:22-3" async def test_switch_on( @@ -226,6 +227,7 @@ async def test_irrigation_service( "1": "Garden Sprinkler", "2": "Back Yard", }, + "mac": MAC_ADDRESS, } ) ], @@ -274,9 +276,9 @@ async def test_switch_error( @pytest.mark.parametrize( - ("config_entry_unique_id"), + ("config_entry_data", "config_entry_unique_id", "setup_config_entry"), [ - (None), + (CONFIG_ENTRY_DATA_OLD_FORMAT, None, None), ], ) async def test_no_unique_id( @@ -284,8 +286,15 @@ async def test_no_unique_id( aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], entity_registry: er.EntityRegistry, + config_entry: MockConfigEntry, ) -> None: - """Test an irrigation switch with no unique id.""" + """Test an irrigation switch with no unique id due to migration failure.""" + + # Failure to migrate config entry to a unique id + responses.insert(0, mock_response_error(HTTPStatus.SERVICE_UNAVAILABLE)) + + await config_entry.async_setup(hass) + assert config_entry.state == ConfigEntryState.LOADED zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None @@ -294,31 +303,3 @@ async def test_no_unique_id( entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3") assert entity_entry is None - - -@pytest.mark.parametrize( - ("config_entry_unique_id", "entity_unique_id"), - [ - (SERIAL_NUMBER, "1263613994342-3"), - # Some existing config entries may have a "0" serial number but preserve - # their unique id - (0, "0-3"), - ], -) -async def test_has_unique_id( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - responses: list[AiohttpClientMockResponse], - entity_registry: er.EntityRegistry, - entity_unique_id: str, -) -> None: - """Test an irrigation switch with no unique id.""" - - zone = hass.states.get("switch.rain_bird_sprinkler_3") - assert zone is not None - assert zone.attributes.get("friendly_name") == "Rain Bird Sprinkler 3" - assert zone.state == "off" - - entity_entry = entity_registry.async_get("switch.rain_bird_sprinkler_3") - assert entity_entry - assert entity_entry.unique_id == entity_unique_id From 3dddf6b9f646bf98bf7901e1e132329b0adb3c35 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 7 Nov 2023 13:12:26 +0100 Subject: [PATCH 163/265] Bump pyOverkiz to 1.13.0 (#103582) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 3b3afddc489b..f57e351a2827 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -13,7 +13,7 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.12.1"], + "requirements": ["pyoverkiz==1.13.0"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 85be51deb9bc..64ad1b8cc639 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1935,7 +1935,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.12.1 +pyoverkiz==1.13.0 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c4a21c0a2aa..d819b2005eb1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1460,7 +1460,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.12.1 +pyoverkiz==1.13.0 # homeassistant.components.openweathermap pyowm==3.2.0 From 149aef9a128d5bc26beb9173842181f77b0933ef Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 13 Nov 2023 15:41:06 +0100 Subject: [PATCH 164/265] Bump pyOverkiz to 1.13.2 (#103790) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index f57e351a2827..cc9a410392a9 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -13,7 +13,7 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.13.0"], + "requirements": ["pyoverkiz==1.13.2"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 64ad1b8cc639..b1eca178fc42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1935,7 +1935,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.0 +pyoverkiz==1.13.2 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d819b2005eb1..7b054c4eda8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1460,7 +1460,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.0 +pyoverkiz==1.13.2 # homeassistant.components.openweathermap pyowm==3.2.0 From 57c76b2ea35a55efce5ee5de437cd6c17dd0427a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 13 Nov 2023 19:09:46 +0100 Subject: [PATCH 165/265] Bump aiocomelit to 0.5.2 (#103791) * Bump aoicomelit to 0.5.0 * bump to 0.5.2 --- homeassistant/components/comelit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index 5978f17cfc47..77796ac7e7f0 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/comelit", "iot_class": "local_polling", "loggers": ["aiocomelit"], - "requirements": ["aiocomelit==0.3.0"] + "requirements": ["aiocomelit==0.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index b1eca178fc42..d5fa9a91e5c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -216,7 +216,7 @@ aiobafi6==0.9.0 aiobotocore==2.6.0 # homeassistant.components.comelit -aiocomelit==0.3.0 +aiocomelit==0.5.2 # homeassistant.components.dhcp aiodiscover==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b054c4eda8e..de51a342b44c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -197,7 +197,7 @@ aiobafi6==0.9.0 aiobotocore==2.6.0 # homeassistant.components.comelit -aiocomelit==0.3.0 +aiocomelit==0.5.2 # homeassistant.components.dhcp aiodiscover==1.5.1 From 6133ce02580b5ffaad9ad2f97d0b9fdf06898e8b Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 13 Nov 2023 13:53:49 +0100 Subject: [PATCH 166/265] Bump velbusaio to 2023.11.0 (#103798) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 3c773e39e33b..1f0dd001853d 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2023.10.2"], + "requirements": ["velbus-aio==2023.11.0"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index d5fa9a91e5c8..799a7e880599 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2664,7 +2664,7 @@ vallox-websocket-api==3.3.0 vehicle==2.0.0 # homeassistant.components.velbus -velbus-aio==2023.10.2 +velbus-aio==2023.11.0 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de51a342b44c..63d7557c6ede 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1982,7 +1982,7 @@ vallox-websocket-api==3.3.0 vehicle==2.0.0 # homeassistant.components.velbus -velbus-aio==2023.10.2 +velbus-aio==2023.11.0 # homeassistant.components.venstar venstarcolortouch==0.19 From 92780dd21720da522c024ab691eb4240ea7202aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Nov 2023 02:33:48 -0600 Subject: [PATCH 167/265] Bump pyunifiprotect to 4.21.0 (#103832) changelog: https://github.com/AngellusMortis/pyunifiprotect/compare/v4.20.0...v4.21.0 --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index b63700720e61..ee6f6d055489 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.20.0", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.21.0", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 799a7e880599..4332a347cb7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2229,7 +2229,7 @@ pytrafikverket==0.3.7 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.20.0 +pyunifiprotect==4.21.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63d7557c6ede..f7f24dc8ccff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1664,7 +1664,7 @@ pytrafikverket==0.3.7 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.20.0 +pyunifiprotect==4.21.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 339e9e7b48c4358397a978768334e537f2caf1e1 Mon Sep 17 00:00:00 2001 From: suaveolent <2163625+suaveolent@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:37:49 +0100 Subject: [PATCH 168/265] Bump lupupy to 0.3.1 (#103835) Co-authored-by: suaveolent --- homeassistant/components/lupusec/binary_sensor.py | 2 +- homeassistant/components/lupusec/manifest.json | 2 +- homeassistant/components/lupusec/switch.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py index 2c6e7b2fff83..c98e634dcb3b 100644 --- a/homeassistant/components/lupusec/binary_sensor.py +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -27,7 +27,7 @@ def setup_platform( data = hass.data[LUPUSEC_DOMAIN] - device_types = [CONST.TYPE_OPENING] + device_types = CONST.TYPE_OPENING devices = [] for device in data.lupusec.get_devices(generic_type=device_types): diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 6fa6c55de2e4..e73feef55a1e 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/lupusec", "iot_class": "local_polling", "loggers": ["lupupy"], - "requirements": ["lupupy==0.3.0"] + "requirements": ["lupupy==0.3.1"] } diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index 981a2a8633a0..37a3b2ec9694 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -28,7 +28,7 @@ def setup_platform( data = hass.data[LUPUSEC_DOMAIN] - device_types = [CONST.TYPE_SWITCH] + device_types = CONST.TYPE_SWITCH devices = [] for device in data.lupusec.get_devices(generic_type=device_types): diff --git a/requirements_all.txt b/requirements_all.txt index 4332a347cb7e..b1f7d4646213 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1185,7 +1185,7 @@ loqedAPI==2.1.8 luftdaten==0.7.4 # homeassistant.components.lupusec -lupupy==0.3.0 +lupupy==0.3.1 # homeassistant.components.lw12wifi lw12==0.9.2 From e89b47138dc0a09f894033cb112f95f5bcce7919 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 12 Nov 2023 09:48:42 -0800 Subject: [PATCH 169/265] Bump gcal_sync to 6.0.1 (#103861) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 509100a5174a..fc9107bb8d25 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==5.0.0", "oauth2client==4.1.3"] + "requirements": ["gcal-sync==6.0.1", "oauth2client==4.1.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index b1f7d4646213..728d79fe1e03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -854,7 +854,7 @@ gardena-bluetooth==1.4.0 gassist-text==0.0.10 # homeassistant.components.google -gcal-sync==5.0.0 +gcal-sync==6.0.1 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7f24dc8ccff..06286df206c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -679,7 +679,7 @@ gardena-bluetooth==1.4.0 gassist-text==0.0.10 # homeassistant.components.google -gcal-sync==5.0.0 +gcal-sync==6.0.1 # homeassistant.components.geocaching geocachingapi==0.2.1 From c352cf0bd8f58424439233a21cd00cbddba2f1e0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 12 Nov 2023 10:27:02 -0800 Subject: [PATCH 170/265] Fix bug in Fitbit config flow, and switch to prefer display name (#103869) --- homeassistant/components/fitbit/api.py | 2 +- .../components/fitbit/config_flow.py | 2 +- homeassistant/components/fitbit/model.py | 4 +- tests/components/fitbit/conftest.py | 39 +++++++++--- tests/components/fitbit/test_config_flow.py | 63 ++++++++++++++++++- 5 files changed, 96 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fitbit/api.py b/homeassistant/components/fitbit/api.py index ceb619c43853..49e51a0fd986 100644 --- a/homeassistant/components/fitbit/api.py +++ b/homeassistant/components/fitbit/api.py @@ -69,7 +69,7 @@ class FitbitApi(ABC): profile = response["user"] self._profile = FitbitProfile( encoded_id=profile["encodedId"], - full_name=profile["fullName"], + display_name=profile["displayName"], locale=profile.get("locale"), ) return self._profile diff --git a/homeassistant/components/fitbit/config_flow.py b/homeassistant/components/fitbit/config_flow.py index dd7e79e2c65a..7ef6ecbfa28f 100644 --- a/homeassistant/components/fitbit/config_flow.py +++ b/homeassistant/components/fitbit/config_flow.py @@ -90,7 +90,7 @@ class OAuth2FlowHandler( await self.async_set_unique_id(profile.encoded_id) self._abort_if_unique_id_configured() - return self.async_create_entry(title=profile.full_name, data=data) + return self.async_create_entry(title=profile.display_name, data=data) async def async_step_import(self, data: dict[str, Any]) -> FlowResult: """Handle import from YAML.""" diff --git a/homeassistant/components/fitbit/model.py b/homeassistant/components/fitbit/model.py index 38b1d0bb7861..cd8ece163a4b 100644 --- a/homeassistant/components/fitbit/model.py +++ b/homeassistant/components/fitbit/model.py @@ -14,8 +14,8 @@ class FitbitProfile: encoded_id: str """The ID representing the Fitbit user.""" - full_name: str - """The first name value specified in the user's account settings.""" + display_name: str + """The name shown when the user's friends look at their Fitbit profile.""" locale: str | None """The locale defined in the user's Fitbit account settings.""" diff --git a/tests/components/fitbit/conftest.py b/tests/components/fitbit/conftest.py index 682fb0edd3b0..a076be7f63d2 100644 --- a/tests/components/fitbit/conftest.py +++ b/tests/components/fitbit/conftest.py @@ -32,6 +32,15 @@ PROFILE_USER_ID = "fitbit-api-user-id-1" FAKE_ACCESS_TOKEN = "some-access-token" FAKE_REFRESH_TOKEN = "some-refresh-token" FAKE_AUTH_IMPL = "conftest-imported-cred" +FULL_NAME = "First Last" +DISPLAY_NAME = "First L." +PROFILE_DATA = { + "fullName": FULL_NAME, + "displayName": DISPLAY_NAME, + "displayNameSetting": "name", + "firstName": "First", + "lastName": "Last", +} PROFILE_API_URL = "https://api.fitbit.com/1/user/-/profile.json" DEVICES_API_URL = "https://api.fitbit.com/1/user/-/devices.json" @@ -214,20 +223,34 @@ def mock_profile_locale() -> str: return "en_US" +@pytest.fixture(name="profile_data") +def mock_profile_data() -> dict[str, Any]: + """Fixture to return other profile data fields.""" + return PROFILE_DATA + + +@pytest.fixture(name="profile_response") +def mock_profile_response( + profile_id: str, profile_locale: str, profile_data: dict[str, Any] +) -> dict[str, Any]: + """Fixture to construct the fake profile API response.""" + return { + "user": { + "encodedId": profile_id, + "locale": profile_locale, + **profile_data, + }, + } + + @pytest.fixture(name="profile", autouse=True) -def mock_profile(requests_mock: Mocker, profile_id: str, profile_locale: str) -> None: +def mock_profile(requests_mock: Mocker, profile_response: dict[str, Any]) -> None: """Fixture to setup fake requests made to Fitbit API during config flow.""" requests_mock.register_uri( "GET", PROFILE_API_URL, status_code=HTTPStatus.OK, - json={ - "user": { - "encodedId": profile_id, - "fullName": "My name", - "locale": profile_locale, - }, - }, + json=profile_response, ) diff --git a/tests/components/fitbit/test_config_flow.py b/tests/components/fitbit/test_config_flow.py index d51379c9adc0..78d20b0fb58d 100644 --- a/tests/components/fitbit/test_config_flow.py +++ b/tests/components/fitbit/test_config_flow.py @@ -17,8 +17,10 @@ from homeassistant.helpers import config_entry_oauth2_flow, issue_registry as ir from .conftest import ( CLIENT_ID, + DISPLAY_NAME, FAKE_AUTH_IMPL, PROFILE_API_URL, + PROFILE_DATA, PROFILE_USER_ID, SERVER_ACCESS_TOKEN, ) @@ -76,7 +78,7 @@ async def test_full_flow( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 config_entry = entries[0] - assert config_entry.title == "My name" + assert config_entry.title == DISPLAY_NAME assert config_entry.unique_id == PROFILE_USER_ID data = dict(config_entry.data) @@ -286,7 +288,7 @@ async def test_import_fitbit_config( # Verify valid profile can be fetched from the API config_entry = entries[0] - assert config_entry.title == "My name" + assert config_entry.title == DISPLAY_NAME assert config_entry.unique_id == PROFILE_USER_ID data = dict(config_entry.data) @@ -598,3 +600,60 @@ async def test_reauth_wrong_user_id( assert result.get("reason") == "wrong_account" assert len(mock_setup.mock_calls) == 0 + + +@pytest.mark.parametrize( + ("profile_data", "expected_title"), + [ + (PROFILE_DATA, DISPLAY_NAME), + ({"displayName": DISPLAY_NAME}, DISPLAY_NAME), + ], + ids=("full_profile_data", "display_name_only"), +) +async def test_partial_profile_data( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + profile: None, + setup_credentials: None, + expected_title: str, +) -> None: + """Check full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": REDIRECT_URL, + }, + ) + assert result["type"] == FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + f"&redirect_uri={REDIRECT_URL}" + f"&state={state}" + "&scope=activity+heartrate+nutrition+profile+settings+sleep+weight&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + + aioclient_mock.post( + OAUTH2_TOKEN, + json=SERVER_ACCESS_TOKEN, + ) + + with patch( + "homeassistant.components.fitbit.async_setup_entry", return_value=True + ) as mock_setup: + await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(mock_setup.mock_calls) == 1 + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + config_entry = entries[0] + assert config_entry.title == expected_title From 29a65d56201c57fae8f620a8938596356e9d6223 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 12 Nov 2023 10:44:26 -0800 Subject: [PATCH 171/265] Fix for Google Calendar API returning invalid RRULE:DATE rules (#103870) --- homeassistant/components/google/calendar.py | 9 +++- tests/components/google/test_calendar.py | 48 +++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index bd0fe18912e4..3e34a7234a47 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -521,8 +521,13 @@ class GoogleCalendarEntity( def _get_calendar_event(event: Event) -> CalendarEvent: """Return a CalendarEvent from an API event.""" rrule: str | None = None - if len(event.recurrence) == 1: - rrule = event.recurrence[0].lstrip(RRULE_PREFIX) + # Home Assistant expects a single RRULE: and all other rule types are unsupported or ignored + if ( + len(event.recurrence) == 1 + and (raw_rule := event.recurrence[0]) + and raw_rule.startswith(RRULE_PREFIX) + ): + rrule = raw_rule.removeprefix(RRULE_PREFIX) return CalendarEvent( uid=event.ical_uuid, recurrence_id=event.id if event.recurring_event_id else None, diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 3a9673441c0a..83544087104c 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -1301,3 +1301,51 @@ async def test_event_differs_timezone( "description": event["description"], "supported_features": 3, } + + +async def test_invalid_rrule_fix( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_events_list_items, + component_setup, +) -> None: + """Test that an invalid RRULE returned from Google Calendar API is handled correctly end to end.""" + week_from_today = dt_util.now().date() + datetime.timedelta(days=7) + end_event = week_from_today + datetime.timedelta(days=1) + event = { + **TEST_EVENT, + "start": {"date": week_from_today.isoformat()}, + "end": {"date": end_event.isoformat()}, + "recurrence": [ + "RRULE:DATE;TZID=Europe/Warsaw:20230818T020000,20230915T020000,20231013T020000,20231110T010000,20231208T010000", + ], + } + mock_events_list_items([event]) + + assert await component_setup() + + state = hass.states.get(TEST_ENTITY) + assert state.name == TEST_ENTITY_NAME + assert state.state == STATE_OFF + + # Pick a date range that contains two instances of the event + web_client = await hass_client() + response = await web_client.get( + get_events_url(TEST_ENTITY, "2023-08-10T00:00:00Z", "2023-09-20T00:00:00Z") + ) + assert response.status == HTTPStatus.OK + events = await response.json() + + # Both instances are returned, however the RDATE rule is ignored by Home + # Assistant so they are just treateded as flattened events. + assert len(events) == 2 + + event = events[0] + assert event["uid"] == "cydrevtfuybguinhomj@google.com" + assert event["recurrence_id"] == "_c8rinwq863h45qnucyoi43ny8_20230818" + assert event["rrule"] is None + + event = events[1] + assert event["uid"] == "cydrevtfuybguinhomj@google.com" + assert event["recurrence_id"] == "_c8rinwq863h45qnucyoi43ny8_20230915" + assert event["rrule"] is None From cf35e9b1540948baeab6031cb5182bc8fbe922bc Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 12 Nov 2023 12:49:49 -0800 Subject: [PATCH 172/265] Update Fitbit to avoid a KeyError when `restingHeartRate` is not present (#103872) * Update Fitbit to avoid a KeyError when `restingHeartRate` is not present * Explicitly handle none response values --- homeassistant/components/fitbit/sensor.py | 13 +++++- tests/components/fitbit/test_sensor.py | 57 +++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index d0d939ce67e3..1bac147306a1 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -134,6 +134,17 @@ def _water_unit(unit_system: FitbitUnitSystem) -> UnitOfVolume: return UnitOfVolume.MILLILITERS +def _int_value_or_none(field: str) -> Callable[[dict[str, Any]], int | None]: + """Value function that will parse the specified field if present.""" + + def convert(result: dict[str, Any]) -> int | None: + if (value := result["value"].get(field)) is not None: + return int(value) + return None + + return convert + + @dataclass class FitbitSensorEntityDescription(SensorEntityDescription): """Describes Fitbit sensor entity.""" @@ -206,7 +217,7 @@ FITBIT_RESOURCES_LIST: Final[tuple[FitbitSensorEntityDescription, ...]] = ( name="Resting Heart Rate", native_unit_of_measurement="bpm", icon="mdi:heart-pulse", - value_fn=lambda result: int(result["value"]["restingHeartRate"]), + value_fn=_int_value_or_none("restingHeartRate"), scope=FitbitScope.HEART_RATE, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/tests/components/fitbit/test_sensor.py b/tests/components/fitbit/test_sensor.py index 5421a6521257..d14c7ae78da3 100644 --- a/tests/components/fitbit/test_sensor.py +++ b/tests/components/fitbit/test_sensor.py @@ -771,3 +771,60 @@ async def test_device_battery_level_reauth_required( flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["step_id"] == "reauth_confirm" + + +@pytest.mark.parametrize( + ("scopes", "response_data", "expected_state"), + [ + (["heartrate"], {}, "unknown"), + ( + ["heartrate"], + { + "restingHeartRate": 120, + }, + "120", + ), + ( + ["heartrate"], + { + "restingHeartRate": 0, + }, + "0", + ), + ], + ids=("missing", "valid", "zero"), +) +async def test_resting_heart_rate_responses( + hass: HomeAssistant, + setup_credentials: None, + integration_setup: Callable[[], Awaitable[bool]], + register_timeseries: Callable[[str, dict[str, Any]], None], + response_data: dict[str, Any], + expected_state: str, +) -> None: + """Test resting heart rate sensor with various values from response.""" + + register_timeseries( + "activities/heart", + timeseries_response( + "activities-heart", + { + "customHeartRateZones": [], + "heartRateZones": [ + { + "caloriesOut": 0, + "max": 220, + "min": 159, + "minutes": 0, + "name": "Peak", + }, + ], + **response_data, + }, + ), + ) + assert await integration_setup() + + state = hass.states.get("sensor.resting_heart_rate") + assert state + assert state.state == expected_state From 56298b2c88318e5126891aef913f53e2a42f1c74 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 13 Nov 2023 14:04:12 +0100 Subject: [PATCH 173/265] fix Comelit cover stop (#103911) --- homeassistant/components/comelit/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/comelit/cover.py b/homeassistant/components/comelit/cover.py index 4a3c8eed63ce..72bbf56e08a3 100644 --- a/homeassistant/components/comelit/cover.py +++ b/homeassistant/components/comelit/cover.py @@ -109,7 +109,7 @@ class ComelitCoverEntity( if not self.is_closing and not self.is_opening: return - action = STATE_OFF if self.is_closing else STATE_ON + action = STATE_ON if self.is_closing else STATE_OFF await self._api.set_device_status(COVER, self._device.index, action) @callback From a5a8d38d08c1fbb163cd39f35ea3683548ea73da Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Mon, 13 Nov 2023 19:10:15 +0000 Subject: [PATCH 174/265] Fix Coinbase for new API Structure (#103930) --- .../components/coinbase/config_flow.py | 3 +- homeassistant/components/coinbase/const.py | 4 +- homeassistant/components/coinbase/sensor.py | 42 ++++++++++--------- tests/components/coinbase/common.py | 13 +++++- tests/components/coinbase/const.py | 9 ++-- .../coinbase/snapshots/test_diagnostics.ambr | 24 ++++------- 6 files changed, 50 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index 5dc60f535d74..380532954112 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -17,6 +17,7 @@ import homeassistant.helpers.config_validation as cv from . import get_accounts from .const import ( API_ACCOUNT_CURRENCY, + API_ACCOUNT_CURRENCY_CODE, API_RATES, API_RESOURCE_TYPE, API_TYPE_VAULT, @@ -81,7 +82,7 @@ async def validate_options( accounts = await hass.async_add_executor_job(get_accounts, client) accounts_currencies = [ - account[API_ACCOUNT_CURRENCY] + account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] for account in accounts if account[API_RESOURCE_TYPE] != API_TYPE_VAULT ] diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index c5fdec4d511b..3fc8158f9709 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -12,14 +12,16 @@ DOMAIN = "coinbase" API_ACCOUNT_AMOUNT = "amount" API_ACCOUNT_BALANCE = "balance" API_ACCOUNT_CURRENCY = "currency" +API_ACCOUNT_CURRENCY_CODE = "code" API_ACCOUNT_ID = "id" -API_ACCOUNT_NATIVE_BALANCE = "native_balance" +API_ACCOUNT_NATIVE_BALANCE = "balance" API_ACCOUNT_NAME = "name" API_ACCOUNTS_DATA = "data" API_RATES = "rates" API_RESOURCE_PATH = "resource_path" API_RESOURCE_TYPE = "type" API_TYPE_VAULT = "vault" +API_USD = "USD" WALLETS = { "1INCH": "1INCH", diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 47fd3b911298..1442a626f742 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -14,9 +14,9 @@ from .const import ( API_ACCOUNT_AMOUNT, API_ACCOUNT_BALANCE, API_ACCOUNT_CURRENCY, + API_ACCOUNT_CURRENCY_CODE, API_ACCOUNT_ID, API_ACCOUNT_NAME, - API_ACCOUNT_NATIVE_BALANCE, API_RATES, API_RESOURCE_TYPE, API_TYPE_VAULT, @@ -55,7 +55,7 @@ async def async_setup_entry( entities: list[SensorEntity] = [] provided_currencies: list[str] = [ - account[API_ACCOUNT_CURRENCY] + account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] for account in instance.accounts if account[API_RESOURCE_TYPE] != API_TYPE_VAULT ] @@ -106,26 +106,28 @@ class AccountSensor(SensorEntity): self._currency = currency for account in coinbase_data.accounts: if ( - account[API_ACCOUNT_CURRENCY] != currency + account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] != currency or account[API_RESOURCE_TYPE] == API_TYPE_VAULT ): continue self._attr_name = f"Coinbase {account[API_ACCOUNT_NAME]}" self._attr_unique_id = ( f"coinbase-{account[API_ACCOUNT_ID]}-wallet-" - f"{account[API_ACCOUNT_CURRENCY]}" + f"{account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]}" ) self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] - self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY] + self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY][ + API_ACCOUNT_CURRENCY_CODE + ] self._attr_icon = CURRENCY_ICONS.get( - account[API_ACCOUNT_CURRENCY], DEFAULT_COIN_ICON + account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE], + DEFAULT_COIN_ICON, + ) + self._native_balance = round( + float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]) + / float(coinbase_data.exchange_rates[API_RATES][currency]), + 2, ) - self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][ - API_ACCOUNT_AMOUNT - ] - self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][ - API_ACCOUNT_CURRENCY - ] break self._attr_state_class = SensorStateClass.TOTAL @@ -141,7 +143,7 @@ class AccountSensor(SensorEntity): def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes of the sensor.""" return { - ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._native_currency}", + ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._coinbase_data.exchange_base}", } def update(self) -> None: @@ -149,17 +151,17 @@ class AccountSensor(SensorEntity): self._coinbase_data.update() for account in self._coinbase_data.accounts: if ( - account[API_ACCOUNT_CURRENCY] != self._currency + account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] + != self._currency or account[API_RESOURCE_TYPE] == API_TYPE_VAULT ): continue self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT] - self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][ - API_ACCOUNT_AMOUNT - ] - self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][ - API_ACCOUNT_CURRENCY - ] + self._native_balance = round( + float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]) + / float(self._coinbase_data.exchange_rates[API_RATES][self._currency]), + 2, + ) break diff --git a/tests/components/coinbase/common.py b/tests/components/coinbase/common.py index 6ab33f3bc7c4..0f8930dbeffa 100644 --- a/tests/components/coinbase/common.py +++ b/tests/components/coinbase/common.py @@ -6,7 +6,12 @@ from homeassistant.components.coinbase.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN -from .const import GOOD_EXCHANGE_RATE, GOOD_EXCHANGE_RATE_2, MOCK_ACCOUNTS_RESPONSE +from .const import ( + GOOD_CURRENCY_2, + GOOD_EXCHANGE_RATE, + GOOD_EXCHANGE_RATE_2, + MOCK_ACCOUNTS_RESPONSE, +) from tests.common import MockConfigEntry @@ -60,7 +65,11 @@ def mock_get_exchange_rates(): """Return a heavily reduced mock list of exchange rates for testing.""" return { "currency": "USD", - "rates": {GOOD_EXCHANGE_RATE_2: "0.109", GOOD_EXCHANGE_RATE: "0.00002"}, + "rates": { + GOOD_CURRENCY_2: "1.0", + GOOD_EXCHANGE_RATE_2: "0.109", + GOOD_EXCHANGE_RATE: "0.00002", + }, } diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py index 2b437e154784..138b941c62cb 100644 --- a/tests/components/coinbase/const.py +++ b/tests/components/coinbase/const.py @@ -12,26 +12,23 @@ BAD_EXCHANGE_RATE = "ETH" MOCK_ACCOUNTS_RESPONSE = [ { "balance": {"amount": "0.00001", "currency": GOOD_CURRENCY}, - "currency": GOOD_CURRENCY, + "currency": {"code": GOOD_CURRENCY}, "id": "123456789", "name": "BTC Wallet", - "native_balance": {"amount": "100.12", "currency": GOOD_CURRENCY_2}, "type": "wallet", }, { "balance": {"amount": "100.00", "currency": GOOD_CURRENCY}, - "currency": GOOD_CURRENCY, + "currency": {"code": GOOD_CURRENCY}, "id": "abcdefg", "name": "BTC Vault", - "native_balance": {"amount": "100.12", "currency": GOOD_CURRENCY_2}, "type": "vault", }, { "balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2}, - "currency": "USD", + "currency": {"code": GOOD_CURRENCY_2}, "id": "987654321", "name": "USD Wallet", - "native_balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2}, "type": "fiat", }, ] diff --git a/tests/components/coinbase/snapshots/test_diagnostics.ambr b/tests/components/coinbase/snapshots/test_diagnostics.ambr index c214330d5f9e..38224a9992f6 100644 --- a/tests/components/coinbase/snapshots/test_diagnostics.ambr +++ b/tests/components/coinbase/snapshots/test_diagnostics.ambr @@ -7,13 +7,11 @@ 'amount': '**REDACTED**', 'currency': 'BTC', }), - 'currency': 'BTC', + 'currency': dict({ + 'code': 'BTC', + }), 'id': '**REDACTED**', 'name': 'BTC Wallet', - 'native_balance': dict({ - 'amount': '**REDACTED**', - 'currency': 'USD', - }), 'type': 'wallet', }), dict({ @@ -21,13 +19,11 @@ 'amount': '**REDACTED**', 'currency': 'BTC', }), - 'currency': 'BTC', + 'currency': dict({ + 'code': 'BTC', + }), 'id': '**REDACTED**', 'name': 'BTC Vault', - 'native_balance': dict({ - 'amount': '**REDACTED**', - 'currency': 'USD', - }), 'type': 'vault', }), dict({ @@ -35,13 +31,11 @@ 'amount': '**REDACTED**', 'currency': 'USD', }), - 'currency': 'USD', + 'currency': dict({ + 'code': 'USD', + }), 'id': '**REDACTED**', 'name': 'USD Wallet', - 'native_balance': dict({ - 'amount': '**REDACTED**', - 'currency': 'USD', - }), 'type': 'fiat', }), ]), From bcd371ac2b8a84abc34e7de7e9c7add97e627ca2 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 14 Nov 2023 02:30:15 -0500 Subject: [PATCH 175/265] Bump zwave-js-server-python to 0.54.0 (#103943) --- homeassistant/components/zwave_js/cover.py | 3 ++- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 364eafd8cafc..27919a176148 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -18,6 +18,7 @@ from zwave_js_server.const.command_class.multilevel_switch import ( from zwave_js_server.const.command_class.window_covering import ( NO_POSITION_PROPERTY_KEYS, NO_POSITION_SUFFIX, + WINDOW_COVERING_LEVEL_CHANGE_UP_PROPERTY, SlatStates, ) from zwave_js_server.model.driver import Driver @@ -369,7 +370,7 @@ class ZWaveWindowCovering(CoverPositionMixin, CoverTiltMixin): set_values_func( value, stop_value=self.get_zwave_value( - "levelChangeUp", + WINDOW_COVERING_LEVEL_CHANGE_UP_PROPERTY, value_property_key=value.property_key, ), ) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index f0c1dcec6b5a..f2d32d499c91 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.53.1"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.54.0"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index 728d79fe1e03..1f95654e7cde 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2824,7 +2824,7 @@ zigpy==0.59.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.53.1 +zwave-js-server-python==0.54.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06286df206c9..10e693885327 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2109,7 +2109,7 @@ zigpy-znp==0.11.6 zigpy==0.59.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.53.1 +zwave-js-server-python==0.54.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 From 19f268a1e1d48035d2b1a19ec2be791f9ffd115a Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Tue, 14 Nov 2023 01:17:44 -0800 Subject: [PATCH 176/265] Update smarttub to 0.0.36 (#103948) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index e8db096f31da..f2514063a409 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["smarttub"], "quality_scale": "platinum", - "requirements": ["python-smarttub==0.0.35"] + "requirements": ["python-smarttub==0.0.36"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1f95654e7cde..da17826e5222 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2187,7 +2187,7 @@ python-ripple-api==0.0.3 python-roborock==0.35.0 # homeassistant.components.smarttub -python-smarttub==0.0.35 +python-smarttub==0.0.36 # homeassistant.components.songpal python-songpal==0.15.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10e693885327..2ec1f05ad262 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1631,7 +1631,7 @@ python-qbittorrent==0.4.3 python-roborock==0.35.0 # homeassistant.components.smarttub -python-smarttub==0.0.35 +python-smarttub==0.0.36 # homeassistant.components.songpal python-songpal==0.15.2 From 2f380d4b750a37b4476981dd6b15bc236bbbd69b Mon Sep 17 00:00:00 2001 From: Chuck Foster <75957355+fosterchuck@users.noreply.github.com> Date: Tue, 14 Nov 2023 08:13:14 -0800 Subject: [PATCH 177/265] Fix duplicate Ban file entries (#103953) --- homeassistant/components/http/ban.py | 5 +++-- tests/components/http/test_ban.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 85feb19a24ba..0fa3e95eaf22 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -242,5 +242,6 @@ class IpBanManager: async def async_add_ban(self, remote_addr: IPv4Address | IPv6Address) -> None: """Add a new IP address to the banned list.""" - new_ban = self.ip_bans_lookup[remote_addr] = IpBan(remote_addr) - await self.hass.async_add_executor_job(self._add_ban, new_ban) + if remote_addr not in self.ip_bans_lookup: + new_ban = self.ip_bans_lookup[remote_addr] = IpBan(remote_addr) + await self.hass.async_add_executor_job(self._add_ban, new_ban) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index e6e237a7b677..c5fb56a28fc5 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -378,3 +378,29 @@ async def test_failed_login_attempts_counter( resp = await client.get("/auth_true") assert resp.status == HTTPStatus.OK assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2 + + +async def test_single_ban_file_entry( + hass: HomeAssistant, +) -> None: + """Test that only one item is added to ban file.""" + app = web.Application() + app["hass"] = hass + + async def unauth_handler(request): + """Return a mock web response.""" + raise HTTPUnauthorized + + app.router.add_get("/example", unauth_handler) + setup_bans(hass, app, 2) + mock_real_ip(app)("200.201.202.204") + + manager: IpBanManager = app[KEY_BAN_MANAGER] + m_open = mock_open() + + with patch("homeassistant.components.http.ban.open", m_open, create=True): + remote_ip = ip_address("200.201.202.204") + await manager.async_add_ban(remote_ip) + await manager.async_add_ban(remote_ip) + + assert m_open.call_count == 1 From b010c6b7938067cc726026a0a29d2e3cb153e60a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 14 Nov 2023 17:07:27 +0100 Subject: [PATCH 178/265] Fix openexchangerates form data description (#103974) --- homeassistant/components/openexchangerates/config_flow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openexchangerates/config_flow.py b/homeassistant/components/openexchangerates/config_flow.py index a61264dbf411..b78227ed1e51 100644 --- a/homeassistant/components/openexchangerates/config_flow.py +++ b/homeassistant/components/openexchangerates/config_flow.py @@ -66,7 +66,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._reauth_entry.data if self._reauth_entry else {} ) return self.async_show_form( - step_id="user", data_schema=get_data_schema(currencies, existing_data) + step_id="user", + data_schema=get_data_schema(currencies, existing_data), + description_placeholders={ + "signup": "https://openexchangerates.org/signup" + }, ) errors = {} From c241c2f79cd5ed294129b4c9ce880de5f0f8b33e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Nov 2023 03:27:50 -0600 Subject: [PATCH 179/265] Fix emulated_hue with None values (#104020) --- .../components/emulated_hue/hue_api.py | 25 ++++---- tests/components/emulated_hue/test_hue_api.py | 59 +++++++++++++++++++ 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 6dfd49c371c8..4dbe5aa315e8 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -676,19 +676,20 @@ def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]: @lru_cache(maxsize=512) def _build_entity_state_dict(entity: State) -> dict[str, Any]: """Build a state dict for an entity.""" + is_on = entity.state != STATE_OFF data: dict[str, Any] = { - STATE_ON: entity.state != STATE_OFF, + STATE_ON: is_on, STATE_BRIGHTNESS: None, STATE_HUE: None, STATE_SATURATION: None, STATE_COLOR_TEMP: None, } - if data[STATE_ON]: + attributes = entity.attributes + if is_on: data[STATE_BRIGHTNESS] = hass_to_hue_brightness( - entity.attributes.get(ATTR_BRIGHTNESS, 0) + attributes.get(ATTR_BRIGHTNESS) or 0 ) - hue_sat = entity.attributes.get(ATTR_HS_COLOR) - if hue_sat is not None: + if (hue_sat := attributes.get(ATTR_HS_COLOR)) is not None: hue = hue_sat[0] sat = hue_sat[1] # Convert hass hs values back to hue hs values @@ -697,7 +698,7 @@ def _build_entity_state_dict(entity: State) -> dict[str, Any]: 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) + data[STATE_COLOR_TEMP] = attributes.get(ATTR_COLOR_TEMP) or 0 else: data[STATE_BRIGHTNESS] = 0 @@ -706,25 +707,23 @@ def _build_entity_state_dict(entity: State) -> dict[str, Any]: data[STATE_COLOR_TEMP] = 0 if entity.domain == climate.DOMAIN: - temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) + temperature = 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) + humidity = 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 - ) + level = attributes.get(ATTR_MEDIA_VOLUME_LEVEL, 1.0 if is_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 + percentage = 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) + level = attributes.get(ATTR_CURRENT_POSITION, 0) data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX) _clamp_values(data) return data diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index fb5ff2654974..98f99349caca 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1694,3 +1694,62 @@ async def test_specificly_exposed_entities( result_json = await async_get_lights(client) assert "1" in result_json + + +async def test_get_light_state_when_none(hass_hue: HomeAssistant, hue_client) -> None: + """Test the getting of light state when brightness is None.""" + hass_hue.states.async_set( + "light.ceiling_lights", + STATE_ON, + { + light.ATTR_BRIGHTNESS: None, + light.ATTR_RGB_COLOR: None, + light.ATTR_HS_COLOR: None, + light.ATTR_COLOR_TEMP: None, + light.ATTR_XY_COLOR: None, + light.ATTR_SUPPORTED_COLOR_MODES: [ + light.COLOR_MODE_COLOR_TEMP, + light.COLOR_MODE_HS, + light.COLOR_MODE_XY, + ], + light.ATTR_COLOR_MODE: light.COLOR_MODE_XY, + }, + ) + + light_json = await perform_get_light_state( + hue_client, "light.ceiling_lights", HTTPStatus.OK + ) + state = light_json["state"] + assert state[HUE_API_STATE_ON] is True + assert state[HUE_API_STATE_BRI] == 1 + assert state[HUE_API_STATE_HUE] == 0 + assert state[HUE_API_STATE_SAT] == 0 + assert state[HUE_API_STATE_CT] == 153 + + hass_hue.states.async_set( + "light.ceiling_lights", + STATE_OFF, + { + light.ATTR_BRIGHTNESS: None, + light.ATTR_RGB_COLOR: None, + light.ATTR_HS_COLOR: None, + light.ATTR_COLOR_TEMP: None, + light.ATTR_XY_COLOR: None, + light.ATTR_SUPPORTED_COLOR_MODES: [ + light.COLOR_MODE_COLOR_TEMP, + light.COLOR_MODE_HS, + light.COLOR_MODE_XY, + ], + light.ATTR_COLOR_MODE: light.COLOR_MODE_XY, + }, + ) + + light_json = await perform_get_light_state( + hue_client, "light.ceiling_lights", HTTPStatus.OK + ) + state = light_json["state"] + assert state[HUE_API_STATE_ON] is False + assert state[HUE_API_STATE_BRI] == 1 + assert state[HUE_API_STATE_HUE] == 0 + assert state[HUE_API_STATE_SAT] == 0 + assert state[HUE_API_STATE_CT] == 153 From 399299c13c0888d51b94b847f78ec3cccb2684f2 Mon Sep 17 00:00:00 2001 From: deosrc Date: Wed, 15 Nov 2023 20:28:16 +0000 Subject: [PATCH 180/265] Fix netatmo authentication when using cloud authentication credentials (#104021) * Fix netatmo authentication loop * Update unit tests * Move logic to determine api scopes * Add unit tests for new method * Use pyatmo scope list (#1) * Exclude scopes not working with cloud * Fix linting error --------- Co-authored-by: Tobias Sauerwein --- homeassistant/components/netatmo/__init__.py | 19 +++++----------- homeassistant/components/netatmo/api.py | 18 +++++++++++++++ .../components/netatmo/config_flow.py | 10 ++------- homeassistant/components/netatmo/const.py | 7 ++++++ tests/components/netatmo/test_api.py | 22 +++++++++++++++++++ 5 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 tests/components/netatmo/test_api.py diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index ddd2fc61ed72..4535805915b1 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -8,7 +8,6 @@ from typing import Any import aiohttp import pyatmo -from pyatmo.const import ALL_SCOPES as NETATMO_SCOPES import voluptuous as vol from homeassistant.components import cloud @@ -143,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await session.async_ensure_token_valid() except aiohttp.ClientResponseError as ex: - _LOGGER.debug("API error: %s (%s)", ex.status, ex.message) + _LOGGER.warning("API error: %s (%s)", ex.status, ex.message) if ex.status in ( HTTPStatus.BAD_REQUEST, HTTPStatus.UNAUTHORIZED, @@ -152,19 +151,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex raise ConfigEntryNotReady from ex - if entry.data["auth_implementation"] == cloud.DOMAIN: - required_scopes = { - scope - for scope in NETATMO_SCOPES - if scope not in ("access_doorbell", "read_doorbell") - } - else: - required_scopes = set(NETATMO_SCOPES) - - if not (set(session.token["scope"]) & required_scopes): - _LOGGER.debug( + required_scopes = api.get_api_scopes(entry.data["auth_implementation"]) + if not (set(session.token["scope"]) & set(required_scopes)): + _LOGGER.warning( "Session is missing scopes: %s", - required_scopes - set(session.token["scope"]), + set(required_scopes) - set(session.token["scope"]), ) raise ConfigEntryAuthFailed("Token scope not valid, trigger renewal") diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index 0b36745338ef..7605689b3f5c 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -1,11 +1,29 @@ """API for Netatmo bound to HASS OAuth.""" +from collections.abc import Iterable from typing import cast from aiohttp import ClientSession import pyatmo +from homeassistant.components import cloud from homeassistant.helpers import config_entry_oauth2_flow +from .const import API_SCOPES_EXCLUDED_FROM_CLOUD + + +def get_api_scopes(auth_implementation: str) -> Iterable[str]: + """Return the Netatmo API scopes based on the auth implementation.""" + + if auth_implementation == cloud.DOMAIN: + return set( + { + scope + for scope in pyatmo.const.ALL_SCOPES + if scope not in API_SCOPES_EXCLUDED_FROM_CLOUD + } + ) + return sorted(pyatmo.const.ALL_SCOPES) + class AsyncConfigEntryNetatmoAuth(pyatmo.AbstractAsyncAuth): """Provide Netatmo authentication tied to an OAuth2 based config entry.""" diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index b4e6d8385375..bae81a7762f9 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -6,7 +6,6 @@ import logging from typing import Any import uuid -from pyatmo.const import ALL_SCOPES import voluptuous as vol from homeassistant import config_entries @@ -15,6 +14,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from .api import get_api_scopes from .const import ( CONF_AREA_NAME, CONF_LAT_NE, @@ -53,13 +53,7 @@ class NetatmoFlowHandler( @property def extra_authorize_data(self) -> dict: """Extra data that needs to be appended to the authorize url.""" - exclude = [] - if self.flow_impl.name == "Home Assistant Cloud": - exclude = ["access_doorbell", "read_doorbell"] - - scopes = [scope for scope in ALL_SCOPES if scope not in exclude] - scopes.sort() - + scopes = get_api_scopes(self.flow_impl.domain) return {"scope": " ".join(scopes)} async def async_step_user(self, user_input: dict | None = None) -> FlowResult: diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 9e7ac33c8b61..8a281d4d4a27 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -30,6 +30,13 @@ HOME_DATA = "netatmo_home_data" DATA_HANDLER = "netatmo_data_handler" SIGNAL_NAME = "signal_name" +API_SCOPES_EXCLUDED_FROM_CLOUD = [ + "access_doorbell", + "read_doorbell", + "read_mhs1", + "write_mhs1", +] + NETATMO_CREATE_BATTERY = "netatmo_create_battery" NETATMO_CREATE_CAMERA = "netatmo_create_camera" NETATMO_CREATE_CAMERA_LIGHT = "netatmo_create_camera_light" diff --git a/tests/components/netatmo/test_api.py b/tests/components/netatmo/test_api.py new file mode 100644 index 000000000000..e2d495555c6a --- /dev/null +++ b/tests/components/netatmo/test_api.py @@ -0,0 +1,22 @@ +"""The tests for the Netatmo api.""" + +from pyatmo.const import ALL_SCOPES + +from homeassistant.components import cloud +from homeassistant.components.netatmo import api +from homeassistant.components.netatmo.const import API_SCOPES_EXCLUDED_FROM_CLOUD + + +async def test_get_api_scopes_cloud() -> None: + """Test method to get API scopes when using cloud auth implementation.""" + result = api.get_api_scopes(cloud.DOMAIN) + + for scope in API_SCOPES_EXCLUDED_FROM_CLOUD: + assert scope not in result + + +async def test_get_api_scopes_other() -> None: + """Test method to get API scopes when using cloud auth implementation.""" + result = api.get_api_scopes("netatmo_239846i2f0j2") + + assert sorted(ALL_SCOPES) == result From 7ff1bdb0981a6ba13f11c26ffe928b13c3786351 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 15 Nov 2023 10:51:12 +0100 Subject: [PATCH 181/265] Fix device tracker see gps accuracy selector (#104022) --- homeassistant/components/device_tracker/services.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 08ccbcf0b5a9..3199dfd8af1e 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -25,9 +25,9 @@ see: gps_accuracy: selector: number: - min: 1 - max: 100 - unit_of_measurement: "%" + min: 0 + mode: box + unit_of_measurement: "m" battery: selector: number: From 885152df816df7d5e457ad04fd515f2018ec35f6 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Fri, 17 Nov 2023 14:16:53 +0100 Subject: [PATCH 182/265] Bump pyenphase to 1.14.3 (#104101) fix(101354):update pyenphase to 1.14.3 --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 718c33d28115..c8da6f74a405 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.14.2"], + "requirements": ["pyenphase==1.14.3"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index da17826e5222..f3758330e888 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1696,7 +1696,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.14.2 +pyenphase==1.14.3 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ec1f05ad262..f8ec9882920a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1278,7 +1278,7 @@ pyeconet==0.1.22 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.14.2 +pyenphase==1.14.3 # homeassistant.components.everlights pyeverlights==0.1.0 From d69d9863b5961eb6eefd2c721d907067d79b4daa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Nov 2023 03:08:44 -0600 Subject: [PATCH 183/265] Fix ESPHome BLE client raising confusing error when not connected (#104146) --- .../components/esphome/bluetooth/client.py | 134 ++++++------------ .../esphome/bluetooth/test_client.py | 62 ++++++++ 2 files changed, 107 insertions(+), 89 deletions(-) create mode 100644 tests/components/esphome/bluetooth/test_client.py diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 970e866b27bf..6cf1d6b53811 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -75,15 +75,13 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType: self: ESPHomeClient, *args: Any, **kwargs: Any ) -> Any: # pylint: disable=protected-access + if not self._is_connected: + raise BleakError(f"{self._description} is not connected") loop = self._loop disconnected_futures = self._disconnected_futures disconnected_future = loop.create_future() disconnected_futures.add(disconnected_future) - ble_device = self._ble_device - disconnect_message = ( - f"{self._source_name }: {ble_device.name} - {ble_device.address}: " - "Disconnected during operation" - ) + disconnect_message = f"{self._description}: Disconnected during operation" try: async with interrupt(disconnected_future, BleakError, disconnect_message): return await func(self, *args, **kwargs) @@ -115,10 +113,8 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType: if ex.error.error == -1: # pylint: disable=protected-access _LOGGER.debug( - "%s: %s - %s: BLE device disconnected during %s operation", - self._source_name, - self._ble_device.name, - self._ble_device.address, + "%s: BLE device disconnected during %s operation", + self._description, func.__name__, ) self._async_ble_device_disconnected() @@ -159,10 +155,11 @@ class ESPHomeClient(BaseBleakClient): assert isinstance(address_or_ble_device, BLEDevice) super().__init__(address_or_ble_device, *args, **kwargs) self._loop = asyncio.get_running_loop() - self._ble_device = address_or_ble_device - self._address_as_int = mac_to_int(self._ble_device.address) - assert self._ble_device.details is not None - self._source = self._ble_device.details["source"] + ble_device = address_or_ble_device + self._ble_device = ble_device + self._address_as_int = mac_to_int(ble_device.address) + assert ble_device.details is not None + self._source = ble_device.details["source"] self._cache = client_data.cache self._bluetooth_device = client_data.bluetooth_device self._client = client_data.client @@ -177,8 +174,11 @@ class ESPHomeClient(BaseBleakClient): self._feature_flags = device_info.bluetooth_proxy_feature_flags_compat( client_data.api_version ) - self._address_type = address_or_ble_device.details["address_type"] + self._address_type = ble_device.details["address_type"] self._source_name = f"{client_data.title} [{self._source}]" + self._description = ( + f"{self._source_name}: {ble_device.name} - {ble_device.address}" + ) scanner = client_data.scanner assert scanner is not None self._scanner = scanner @@ -196,12 +196,10 @@ class ESPHomeClient(BaseBleakClient): except (AssertionError, ValueError) as ex: _LOGGER.debug( ( - "%s: %s - %s: Failed to unsubscribe from connection state (likely" + "%s: Failed to unsubscribe from connection state (likely" " connection dropped): %s" ), - self._source_name, - self._ble_device.name, - self._ble_device.address, + self._description, ex, ) self._cancel_connection_state = None @@ -224,22 +222,12 @@ class ESPHomeClient(BaseBleakClient): was_connected = self._is_connected self._async_disconnected_cleanup() if was_connected: - _LOGGER.debug( - "%s: %s - %s: BLE device disconnected", - self._source_name, - self._ble_device.name, - self._ble_device.address, - ) + _LOGGER.debug("%s: BLE device disconnected", self._description) self._async_call_bleak_disconnected_callback() def _async_esp_disconnected(self) -> None: """Handle the esp32 client disconnecting from us.""" - _LOGGER.debug( - "%s: %s - %s: ESP device disconnected", - self._source_name, - self._ble_device.name, - self._ble_device.address, - ) + _LOGGER.debug("%s: ESP device disconnected", self._description) self._disconnect_callbacks.remove(self._async_esp_disconnected) self._async_ble_device_disconnected() @@ -258,10 +246,8 @@ class ESPHomeClient(BaseBleakClient): ) -> None: """Handle a connect or disconnect.""" _LOGGER.debug( - "%s: %s - %s: Connection state changed to connected=%s mtu=%s error=%s", - self._source_name, - self._ble_device.name, - self._ble_device.address, + "%s: Connection state changed to connected=%s mtu=%s error=%s", + self._description, connected, mtu, error, @@ -300,10 +286,8 @@ class ESPHomeClient(BaseBleakClient): return _LOGGER.debug( - "%s: %s - %s: connected, registering for disconnected callbacks", - self._source_name, - self._ble_device.name, - self._ble_device.address, + "%s: connected, registering for disconnected callbacks", + self._description, ) self._disconnect_callbacks.append(self._async_esp_disconnected) connected_future.set_result(connected) @@ -403,10 +387,8 @@ class ESPHomeClient(BaseBleakClient): if bluetooth_device.ble_connections_free: return _LOGGER.debug( - "%s: %s - %s: Out of connection slots, waiting for a free one", - self._source_name, - self._ble_device.name, - self._ble_device.address, + "%s: Out of connection slots, waiting for a free one", + self._description, ) async with asyncio.timeout(timeout): await bluetooth_device.wait_for_ble_connections_free() @@ -434,7 +416,7 @@ class ESPHomeClient(BaseBleakClient): if response.paired: return True _LOGGER.error( - "Pairing with %s failed due to error: %s", self.address, response.error + "%s: Pairing failed due to error: %s", self._description, response.error ) return False @@ -451,7 +433,7 @@ class ESPHomeClient(BaseBleakClient): if response.success: return True _LOGGER.error( - "Unpairing with %s failed due to error: %s", self.address, response.error + "%s: Unpairing failed due to error: %s", self._description, response.error ) return False @@ -486,30 +468,14 @@ class ESPHomeClient(BaseBleakClient): self._feature_flags & BluetoothProxyFeature.REMOTE_CACHING or dangerous_use_bleak_cache ) and (cached_services := cache.get_gatt_services_cache(address_as_int)): - _LOGGER.debug( - "%s: %s - %s: Cached services hit", - self._source_name, - self._ble_device.name, - self._ble_device.address, - ) + _LOGGER.debug("%s: Cached services hit", self._description) self.services = cached_services return self.services - _LOGGER.debug( - "%s: %s - %s: Cached services miss", - self._source_name, - self._ble_device.name, - self._ble_device.address, - ) + _LOGGER.debug("%s: Cached services miss", self._description) esphome_services = await self._client.bluetooth_gatt_get_services( address_as_int ) - _LOGGER.debug( - "%s: %s - %s: Got services: %s", - self._source_name, - self._ble_device.name, - self._ble_device.address, - esphome_services, - ) + _LOGGER.debug("%s: Got services: %s", self._description, esphome_services) max_write_without_response = self.mtu_size - GATT_HEADER_SIZE services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] for service in esphome_services.services: @@ -538,12 +504,7 @@ class ESPHomeClient(BaseBleakClient): raise BleakError("Failed to get services from remote esp") self.services = services - _LOGGER.debug( - "%s: %s - %s: Cached services saved", - self._source_name, - self._ble_device.name, - self._ble_device.address, - ) + _LOGGER.debug("%s: Cached services saved", self._description) cache.set_gatt_services_cache(address_as_int, services) return services @@ -552,13 +513,15 @@ class ESPHomeClient(BaseBleakClient): ) -> BleakGATTCharacteristic: """Resolve a characteristic specifier to a BleakGATTCharacteristic object.""" if (services := self.services) is None: - raise BleakError("Services have not been resolved") + raise BleakError(f"{self._description}: Services have not been resolved") if not isinstance(char_specifier, BleakGATTCharacteristic): characteristic = services.get_characteristic(char_specifier) else: characteristic = char_specifier if not characteristic: - raise BleakError(f"Characteristic {char_specifier} was not found!") + raise BleakError( + f"{self._description}: Characteristic {char_specifier} was not found!" + ) return characteristic @verify_connected @@ -579,8 +542,8 @@ class ESPHomeClient(BaseBleakClient): if response.success: return True _LOGGER.error( - "Clear cache failed with %s failed due to error: %s", - self.address, + "%s: Clear cache failed due to error: %s", + self._description, response.error, ) return False @@ -692,7 +655,7 @@ class ESPHomeClient(BaseBleakClient): ble_handle = characteristic.handle if ble_handle in self._notify_cancels: raise BleakError( - "Notifications are already enabled on " + f"{self._description}: Notifications are already enabled on " f"service:{characteristic.service_uuid} " f"characteristic:{characteristic.uuid} " f"handle:{ble_handle}" @@ -702,8 +665,8 @@ class ESPHomeClient(BaseBleakClient): and "indicate" not in characteristic.properties ): raise BleakError( - f"Characteristic {characteristic.uuid} does not have notify or indicate" - " property set." + f"{self._description}: Characteristic {characteristic.uuid} " + "does not have notify or indicate property set." ) self._notify_cancels[ @@ -725,18 +688,13 @@ class ESPHomeClient(BaseBleakClient): cccd_descriptor = characteristic.get_descriptor(CCCD_UUID) if not cccd_descriptor: raise BleakError( - f"Characteristic {characteristic.uuid} does not have a " - "characteristic client config descriptor." + f"{self._description}: Characteristic {characteristic.uuid} " + "does not have a characteristic client config descriptor." ) _LOGGER.debug( - ( - "%s: %s - %s: Writing to CCD descriptor %s for notifications with" - " properties=%s" - ), - self._source_name, - self._ble_device.name, - self._ble_device.address, + "%s: Writing to CCD descriptor %s for notifications with properties=%s", + self._description, cccd_descriptor.handle, characteristic.properties, ) @@ -774,12 +732,10 @@ class ESPHomeClient(BaseBleakClient): if self._cancel_connection_state: _LOGGER.warning( ( - "%s: %s - %s: ESPHomeClient bleak client was not properly" + "%s: ESPHomeClient bleak client was not properly" " disconnected before destruction" ), - self._source_name, - self._ble_device.name, - self._ble_device.address, + self._description, ) if not self._loop.is_closed(): self._loop.call_soon_threadsafe(self._async_disconnected_cleanup) diff --git a/tests/components/esphome/bluetooth/test_client.py b/tests/components/esphome/bluetooth/test_client.py new file mode 100644 index 000000000000..7ed1403041d4 --- /dev/null +++ b/tests/components/esphome/bluetooth/test_client.py @@ -0,0 +1,62 @@ +"""Tests for ESPHomeClient.""" +from __future__ import annotations + +from aioesphomeapi import APIClient, APIVersion, BluetoothProxyFeature, DeviceInfo +from bleak.exc import BleakError +import pytest + +from homeassistant.components.bluetooth import HaBluetoothConnector +from homeassistant.components.esphome.bluetooth.cache import ESPHomeBluetoothCache +from homeassistant.components.esphome.bluetooth.client import ( + ESPHomeClient, + ESPHomeClientData, +) +from homeassistant.components.esphome.bluetooth.device import ESPHomeBluetoothDevice +from homeassistant.components.esphome.bluetooth.scanner import ESPHomeScanner +from homeassistant.core import HomeAssistant + +from tests.components.bluetooth import generate_ble_device + +ESP_MAC_ADDRESS = "AA:BB:CC:DD:EE:FF" +ESP_NAME = "proxy" + + +@pytest.fixture(name="client_data") +async def client_data_fixture( + hass: HomeAssistant, mock_client: APIClient +) -> ESPHomeClientData: + """Return a client data fixture.""" + connector = HaBluetoothConnector(ESPHomeClientData, ESP_MAC_ADDRESS, lambda: True) + return ESPHomeClientData( + bluetooth_device=ESPHomeBluetoothDevice(ESP_NAME, ESP_MAC_ADDRESS), + cache=ESPHomeBluetoothCache(), + client=mock_client, + device_info=DeviceInfo( + mac_address=ESP_MAC_ADDRESS, + name=ESP_NAME, + bluetooth_proxy_feature_flags=BluetoothProxyFeature.PASSIVE_SCAN + & BluetoothProxyFeature.ACTIVE_CONNECTIONS + & BluetoothProxyFeature.REMOTE_CACHING + & BluetoothProxyFeature.PAIRING + & BluetoothProxyFeature.CACHE_CLEARING + & BluetoothProxyFeature.RAW_ADVERTISEMENTS, + ), + api_version=APIVersion(1, 9), + title=ESP_NAME, + scanner=ESPHomeScanner( + hass, ESP_MAC_ADDRESS, ESP_NAME, lambda info: None, connector, True + ), + ) + + +async def test_client_usage_while_not_connected(client_data: ESPHomeClientData) -> None: + """Test client usage while not connected.""" + ble_device = generate_ble_device( + "CC:BB:AA:DD:EE:FF", details={"source": ESP_MAC_ADDRESS, "address_type": 1} + ) + + client = ESPHomeClient(ble_device, client_data=client_data) + with pytest.raises( + BleakError, match=f"{ESP_NAME}.*{ESP_MAC_ADDRESS}.*not connected" + ): + await client.write_gatt_char("test", b"test") is False From fcc70209460b146ae5580d3169f3577593a118df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Nov 2023 07:58:22 +0100 Subject: [PATCH 184/265] Fix memory leak in ESPHome disconnect callbacks (#104149) --- .../components/esphome/bluetooth/client.py | 9 ++++++--- homeassistant/components/esphome/entry_data.py | 18 +++++++++++++++++- homeassistant/components/esphome/manager.py | 16 +++++----------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 6cf1d6b53811..22d4392ce31a 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -136,7 +136,7 @@ class ESPHomeClientData: api_version: APIVersion title: str scanner: ESPHomeScanner | None - disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) + disconnect_callbacks: set[Callable[[], None]] = field(default_factory=set) class ESPHomeClient(BaseBleakClient): @@ -215,6 +215,7 @@ class ESPHomeClient(BaseBleakClient): if not future.done(): future.set_result(None) self._disconnected_futures.clear() + self._disconnect_callbacks.discard(self._async_esp_disconnected) self._unsubscribe_connection_state() def _async_ble_device_disconnected(self) -> None: @@ -228,7 +229,9 @@ class ESPHomeClient(BaseBleakClient): def _async_esp_disconnected(self) -> None: """Handle the esp32 client disconnecting from us.""" _LOGGER.debug("%s: ESP device disconnected", self._description) - self._disconnect_callbacks.remove(self._async_esp_disconnected) + # Calling _async_ble_device_disconnected calls + # _async_disconnected_cleanup which will also remove + # the disconnect callbacks self._async_ble_device_disconnected() def _async_call_bleak_disconnected_callback(self) -> None: @@ -289,7 +292,7 @@ class ESPHomeClient(BaseBleakClient): "%s: connected, registering for disconnected callbacks", self._description, ) - self._disconnect_callbacks.append(self._async_esp_disconnected) + self._disconnect_callbacks.add(self._async_esp_disconnected) connected_future.set_result(connected) @api_error_as_bleak_error diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index e53200c2e90b..89629a65ea58 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -107,7 +107,7 @@ class RuntimeEntryData: bluetooth_device: ESPHomeBluetoothDevice | None = None api_version: APIVersion = field(default_factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) - disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) + disconnect_callbacks: set[Callable[[], None]] = field(default_factory=set) state_subscriptions: dict[ tuple[type[EntityState], int], Callable[[], None] ] = field(default_factory=dict) @@ -427,3 +427,19 @@ class RuntimeEntryData: if self.original_options == entry.options: return hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) + + @callback + def async_on_disconnect(self) -> None: + """Call when the entry has been disconnected. + + Safe to call multiple times. + """ + self.available = False + # Make a copy since calling the disconnect callbacks + # may also try to discard/remove themselves. + for disconnect_cb in self.disconnect_callbacks.copy(): + disconnect_cb() + # Make sure to clear the set to give up the reference + # to it and make sure all the callbacks can be GC'd. + self.disconnect_callbacks.clear() + self.disconnect_callbacks = set() diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index d2eca7d39f92..ad226e040613 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -294,7 +294,7 @@ class ESPHomeManager: event.data["entity_id"], attribute, new_state ) - self.entry_data.disconnect_callbacks.append( + self.entry_data.disconnect_callbacks.add( async_track_state_change_event( hass, [entity_id], send_home_assistant_state_event ) @@ -439,7 +439,7 @@ class ESPHomeManager: reconnect_logic.name = device_info.name if device_info.bluetooth_proxy_feature_flags_compat(cli.api_version): - entry_data.disconnect_callbacks.append( + entry_data.disconnect_callbacks.add( await async_connect_scanner( hass, entry, cli, entry_data, self.domain_data.bluetooth_cache ) @@ -459,7 +459,7 @@ class ESPHomeManager: await cli.subscribe_home_assistant_states(self.async_on_state_subscription) if device_info.voice_assistant_version: - entry_data.disconnect_callbacks.append( + entry_data.disconnect_callbacks.add( await cli.subscribe_voice_assistant( self._handle_pipeline_start, self._handle_pipeline_stop, @@ -487,10 +487,7 @@ class ESPHomeManager: host, expected_disconnect, ) - for disconnect_cb in entry_data.disconnect_callbacks: - disconnect_cb() - entry_data.disconnect_callbacks = [] - entry_data.available = False + entry_data.async_on_disconnect() entry_data.expected_disconnect = expected_disconnect # Mark state as stale so that we will always dispatch # the next state update of that type when the device reconnects @@ -755,10 +752,7 @@ async def cleanup_instance(hass: HomeAssistant, entry: ConfigEntry) -> RuntimeEn """Cleanup the esphome client if it exists.""" domain_data = DomainData.get(hass) data = domain_data.pop_entry_data(entry) - data.available = False - for disconnect_cb in data.disconnect_callbacks: - disconnect_cb() - data.disconnect_callbacks = [] + data.async_on_disconnect() for cleanup_callback in data.cleanup_callbacks: cleanup_callback() await data.async_cleanup() From 35b1051c677a041849b5de3eede5291e80546fc1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Nov 2023 01:35:51 -0500 Subject: [PATCH 185/265] Add debug logging for which adapter is used to connect bluetooth devices (#103264) Log which adapter is used to connect bluetooth devices This is a debug logging improvement to help users find problems with their setup --- homeassistant/components/bluetooth/wrappers.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index 97f253f88252..bfcee9d25df5 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -270,6 +270,8 @@ class HaBleakClientWrapper(BleakClient): """Connect to the specified GATT server.""" assert models.MANAGER is not None manager = models.MANAGER + if debug_logging := _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("%s: Looking for backend to connect", self.__address) wrapped_backend = self._async_get_best_available_backend_and_device(manager) device = wrapped_backend.device scanner = wrapped_backend.scanner @@ -281,12 +283,14 @@ class HaBleakClientWrapper(BleakClient): timeout=self.__timeout, hass=manager.hass, ) - if debug_logging := _LOGGER.isEnabledFor(logging.DEBUG): + if debug_logging: # Only lookup the description if we are going to log it description = ble_device_description(device) _, adv = scanner.discovered_devices_and_advertisement_data[device.address] rssi = adv.rssi - _LOGGER.debug("%s: Connecting (last rssi: %s)", description, rssi) + _LOGGER.debug( + "%s: Connecting via %s (last rssi: %s)", description, scanner.name, rssi + ) connected = None try: connected = await super().connect(**kwargs) @@ -301,7 +305,9 @@ class HaBleakClientWrapper(BleakClient): manager.async_release_connection_slot(device) if debug_logging: - _LOGGER.debug("%s: Connected (last rssi: %s)", description, rssi) + _LOGGER.debug( + "%s: Connected via %s (last rssi: %s)", description, scanner.name, rssi + ) return connected @hass_callback From 8b79d38497e7dbd7f1431bed29967c6fdd02148c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Nov 2023 08:22:26 -0600 Subject: [PATCH 186/265] Prevent Bluetooth reconnects from blocking shutdown (#104150) --- homeassistant/components/bluetooth/manager.py | 3 +++ .../components/bluetooth/wrappers.py | 2 ++ tests/components/bluetooth/test_wrappers.py | 23 +++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 34edccaf4ab3..ce047747a0c6 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -124,6 +124,7 @@ class BluetoothManager: "storage", "slot_manager", "_debug", + "shutdown", ) def __init__( @@ -165,6 +166,7 @@ class BluetoothManager: self.storage = storage self.slot_manager = slot_manager self._debug = _LOGGER.isEnabledFor(logging.DEBUG) + self.shutdown = False @property def supports_passive_scan(self) -> bool: @@ -259,6 +261,7 @@ class BluetoothManager: def async_stop(self, event: Event) -> None: """Stop the Bluetooth integration at shutdown.""" _LOGGER.debug("Stopping bluetooth manager") + self.shutdown = True if self._cancel_unavailable_tracking: self._cancel_unavailable_tracking() self._cancel_unavailable_tracking = None diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index bfcee9d25df5..9de020f163e4 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -270,6 +270,8 @@ class HaBleakClientWrapper(BleakClient): """Connect to the specified GATT server.""" assert models.MANAGER is not None manager = models.MANAGER + if manager.shutdown: + raise BleakError("Bluetooth is already shutdown") if debug_logging := _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug("%s: Looking for backend to connect", self.__address) wrapped_backend = self._async_get_best_available_backend_and_device(manager) diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index de646f8ef9c1..f69f8971479e 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -7,6 +7,7 @@ from unittest.mock import patch import bleak from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData +from bleak.exc import BleakError import pytest from homeassistant.components.bluetooth import ( @@ -366,3 +367,25 @@ async def test_we_switch_adapters_on_failure( assert await client.connect() is False cancel_hci0() cancel_hci1() + + +async def test_raise_after_shutdown( + hass: HomeAssistant, + two_adapters: None, + enable_bluetooth: None, + install_bleak_catcher, + mock_platform_client_that_raises_on_connect, +) -> None: + """Ensure the slot gets released on connection exception.""" + manager = _get_manager() + hci0_device_advs, cancel_hci0, cancel_hci1 = _generate_scanners_with_fake_devices( + hass + ) + # hci0 has 2 slots, hci1 has 1 slot + with patch.object(manager, "shutdown", True): + ble_device = hci0_device_advs["00:00:00:00:00:01"][0] + client = bleak.BleakClient(ble_device) + with pytest.raises(BleakError, match="shutdown"): + await client.connect() + cancel_hci0() + cancel_hci1() From 4680ac0cbf26c78e710a185c7e00d11c28cf8fb4 Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Sat, 18 Nov 2023 13:39:17 +0100 Subject: [PATCH 187/265] Bump boschshcpy to 0.2.75 (#104159) Bumped to boschshcpy==0.2.75 --- homeassistant/components/bosch_shc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index 9fd1055dd604..e29865153b3b 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/bosch_shc", "iot_class": "local_push", "loggers": ["boschshcpy"], - "requirements": ["boschshcpy==0.2.57"], + "requirements": ["boschshcpy==0.2.75"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index f3758330e888..fa65cf570620 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -568,7 +568,7 @@ bluetooth-data-tools==1.14.0 bond-async==0.2.1 # homeassistant.components.bosch_shc -boschshcpy==0.2.57 +boschshcpy==0.2.75 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8ec9882920a..e3693c5eb64d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -482,7 +482,7 @@ bluetooth-data-tools==1.14.0 bond-async==0.2.1 # homeassistant.components.bosch_shc -boschshcpy==0.2.57 +boschshcpy==0.2.75 # homeassistant.components.broadlink broadlink==0.18.3 From 83c59d41546dd7c18dcab28fb8ab5befe372aca2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 19 Nov 2023 11:26:58 -0800 Subject: [PATCH 188/265] Fix Local To-do list bug renaming items (#104182) * Fix Local To-do bug renaming items * Fix renaming --- homeassistant/components/local_todo/todo.py | 4 +- tests/components/local_todo/test_todo.py | 48 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/local_todo/todo.py b/homeassistant/components/local_todo/todo.py index 7e23d01ee465..b688d03253e8 100644 --- a/homeassistant/components/local_todo/todo.py +++ b/homeassistant/components/local_todo/todo.py @@ -63,9 +63,11 @@ def _todo_dict_factory(obj: Iterable[tuple[str, Any]]) -> dict[str, str]: """Convert TodoItem dataclass items to dictionary of attributes for ical consumption.""" result: dict[str, str] = {} for name, value in obj: + if value is None: + continue if name == "status": result[name] = ICS_TODO_STATUS_MAP_INV[value] - elif value is not None: + else: result[name] = value return result diff --git a/tests/components/local_todo/test_todo.py b/tests/components/local_todo/test_todo.py index 39e9264d45a0..5747e05ad05d 100644 --- a/tests/components/local_todo/test_todo.py +++ b/tests/components/local_todo/test_todo.py @@ -237,6 +237,54 @@ async def test_update_item( assert state.state == "0" +async def test_rename( + hass: HomeAssistant, + setup_integration: None, + ws_get_items: Callable[[], Awaitable[dict[str, str]]], +) -> None: + """Test renaming a todo item.""" + + # Create new item + await hass.services.async_call( + TODO_DOMAIN, + "add_item", + {"item": "soda"}, + target={"entity_id": TEST_ENTITY}, + blocking=True, + ) + + # Fetch item + items = await ws_get_items() + assert len(items) == 1 + item = items[0] + assert item["summary"] == "soda" + assert item["status"] == "needs_action" + + state = hass.states.get(TEST_ENTITY) + assert state + assert state.state == "1" + + # Rename item + await hass.services.async_call( + TODO_DOMAIN, + "update_item", + {"item": item["uid"], "rename": "water"}, + target={"entity_id": TEST_ENTITY}, + blocking=True, + ) + + # Verify item has been renamed + items = await ws_get_items() + assert len(items) == 1 + item = items[0] + assert item["summary"] == "water" + assert item["status"] == "needs_action" + + state = hass.states.get(TEST_ENTITY) + assert state + assert state.state == "1" + + @pytest.mark.parametrize( ("src_idx", "dst_idx", "expected_items"), [ From 5650df5cfbb8644976773392de177cd950b61f7c Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 19 Nov 2023 08:07:24 -0500 Subject: [PATCH 189/265] Bump aiosomecomfort to 0.0.22 (#104202) * Bump aiosomecomfort to 0.0.20 * Bump aiosomecomfort to 0.0.22 --- homeassistant/components/honeywell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index a53eaaab8ce1..47213476ad95 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/honeywell", "iot_class": "cloud_polling", "loggers": ["somecomfort"], - "requirements": ["AIOSomecomfort==0.0.17"] + "requirements": ["AIOSomecomfort==0.0.22"] } diff --git a/requirements_all.txt b/requirements_all.txt index fa65cf570620..dcef98e3fdaf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ AEMET-OpenData==0.4.5 AIOAladdinConnect==0.1.58 # homeassistant.components.honeywell -AIOSomecomfort==0.0.17 +AIOSomecomfort==0.0.22 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3693c5eb64d..2c2ead33d083 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ AEMET-OpenData==0.4.5 AIOAladdinConnect==0.1.58 # homeassistant.components.honeywell -AIOSomecomfort==0.0.17 +AIOSomecomfort==0.0.22 # homeassistant.components.adax Adax-local==0.1.5 From 64297aeb8f7de16a0f0f4ba23072fb2dcf028895 Mon Sep 17 00:00:00 2001 From: Rene Nemec <50780524+ertechdesign@users.noreply.github.com> Date: Sun, 19 Nov 2023 22:49:40 +0000 Subject: [PATCH 190/265] Increase Tomato request timeout (#104203) * tomato integration timeout fixed * update tests in tomato integration --- homeassistant/components/tomato/device_tracker.py | 4 ++-- tests/components/tomato/test_device_tracker.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index da64157dad86..d71dd45bcfe0 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -100,10 +100,10 @@ class TomatoDeviceScanner(DeviceScanner): try: if self.ssl: response = requests.Session().send( - self.req, timeout=3, verify=self.verify_ssl + self.req, timeout=60, verify=self.verify_ssl ) else: - response = requests.Session().send(self.req, timeout=3) + response = requests.Session().send(self.req, timeout=60) # Calling and parsing the Tomato api here. We only need the # wldev and dhcpd_lease values. diff --git a/tests/components/tomato/test_device_tracker.py b/tests/components/tomato/test_device_tracker.py index 7c187c7b4bbf..11e73b5695cc 100644 --- a/tests/components/tomato/test_device_tracker.py +++ b/tests/components/tomato/test_device_tracker.py @@ -157,7 +157,7 @@ def test_config_verify_ssl_but_no_ssl_enabled( assert "_http_id=1234567890" in result.req.body assert "exec=devlist" in result.req.body assert mock_session_send.call_count == 1 - assert mock_session_send.mock_calls[0] == mock.call(result.req, timeout=3) + assert mock_session_send.mock_calls[0] == mock.call(result.req, timeout=60) @mock.patch("os.access", return_value=True) @@ -192,7 +192,7 @@ def test_config_valid_verify_ssl_path(hass: HomeAssistant, mock_session_send) -> assert "exec=devlist" in result.req.body assert mock_session_send.call_count == 1 assert mock_session_send.mock_calls[0] == mock.call( - result.req, timeout=3, verify="/test/tomato.crt" + result.req, timeout=60, verify="/test/tomato.crt" ) @@ -223,7 +223,7 @@ def test_config_valid_verify_ssl_bool(hass: HomeAssistant, mock_session_send) -> assert "exec=devlist" in result.req.body assert mock_session_send.call_count == 1 assert mock_session_send.mock_calls[0] == mock.call( - result.req, timeout=3, verify=False + result.req, timeout=60, verify=False ) From 86beb9d13503008aaa4c2c5c1d667a631e60b11e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 19 Nov 2023 20:15:02 +0100 Subject: [PATCH 191/265] Fix imap does not decode text body correctly (#104217) --- homeassistant/components/imap/coordinator.py | 26 ++++- tests/components/imap/const.py | 99 ++++++++++++++++---- tests/components/imap/test_init.py | 51 +++++++++- 3 files changed, 153 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index 59c24b11e51e..d77f7fb05bb0 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -6,6 +6,7 @@ from collections.abc import Mapping from datetime import datetime, timedelta import email from email.header import decode_header, make_header +from email.message import Message from email.utils import parseaddr, parsedate_to_datetime import logging from typing import Any @@ -96,8 +97,9 @@ async def connect_to_server(data: Mapping[str, Any]) -> IMAP4_SSL: class ImapMessage: """Class to parse an RFC822 email message.""" - def __init__(self, raw_message: bytes) -> None: + def __init__(self, raw_message: bytes, charset: str = "utf-8") -> None: """Initialize IMAP message.""" + self._charset = charset self.email_message = email.message_from_bytes(raw_message) @property @@ -157,18 +159,30 @@ class ImapMessage: message_html: str | None = None message_untyped_text: str | None = None + def _decode_payload(part: Message) -> str: + """Try to decode text payloads. + + Common text encodings are quoted-printable or base64. + Falls back to the raw content part if decoding fails. + """ + try: + return str(part.get_payload(decode=True).decode(self._charset)) + except Exception: # pylint: disable=broad-except + return str(part.get_payload()) + + part: Message for part in self.email_message.walk(): if part.get_content_type() == CONTENT_TYPE_TEXT_PLAIN: if message_text is None: - message_text = part.get_payload() + message_text = _decode_payload(part) elif part.get_content_type() == "text/html": if message_html is None: - message_html = part.get_payload() + message_html = _decode_payload(part) elif ( part.get_content_type().startswith("text") and message_untyped_text is None ): - message_untyped_text = part.get_payload() + message_untyped_text = str(part.get_payload()) if message_text is not None: return message_text @@ -223,7 +237,9 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): """Send a event for the last message if the last message was changed.""" response = await self.imap_client.fetch(last_message_uid, "BODY.PEEK[]") if response.result == "OK": - message = ImapMessage(response.lines[1]) + message = ImapMessage( + response.lines[1], charset=self.config_entry.data[CONF_CHARSET] + ) # Set `initial` to `False` if the last message is triggered again initial: bool = True if (message_id := message.message_id) == self._last_message_id: diff --git a/tests/components/imap/const.py b/tests/components/imap/const.py index ec864fd4665a..713261936c75 100644 --- a/tests/components/imap/const.py +++ b/tests/components/imap/const.py @@ -18,16 +18,25 @@ TEST_MESSAGE_HEADERS1 = ( b"for ; Fri, 24 Mar 2023 13:52:01 +0100 (CET)\r\n" ) TEST_MESSAGE_HEADERS2 = ( - b"MIME-Version: 1.0\r\n" b"To: notify@example.com\r\n" b"From: John Doe \r\n" b"Subject: Test subject\r\n" - b"Message-ID: " + b"Message-ID: \r\n" + b"MIME-Version: 1.0\r\n" +) + +TEST_MULTIPART_HEADER = ( + b'Content-Type: multipart/related;\r\n\tboundary="Mark=_100584970350292485166"' ) TEST_MESSAGE_HEADERS3 = b"" TEST_MESSAGE = TEST_MESSAGE_HEADERS1 + DATE_HEADER1 + TEST_MESSAGE_HEADERS2 + +TEST_MESSAGE_MULTIPART = ( + TEST_MESSAGE_HEADERS1 + DATE_HEADER1 + TEST_MESSAGE_HEADERS2 + TEST_MULTIPART_HEADER +) + TEST_MESSAGE_NO_SUBJECT_TO_FROM = ( TEST_MESSAGE_HEADERS1 + DATE_HEADER1 + TEST_MESSAGE_HEADERS3 ) @@ -44,21 +53,27 @@ TEST_INVALID_DATE3 = ( TEST_CONTENT_TEXT_BARE = b"\r\nTest body\r\n\r\n" -TEST_CONTENT_BINARY = ( - b"Content-Type: application/binary\r\n" - b"Content-Transfer-Encoding: base64\r\n" - b"\r\n" - b"VGVzdCBib2R5\r\n" -) +TEST_CONTENT_BINARY = b"Content-Type: application/binary\r\n\r\nTest body\r\n" TEST_CONTENT_TEXT_PLAIN = ( - b"Content-Type: text/plain; charset=UTF-8; format=flowed\r\n" - b"Content-Transfer-Encoding: 7bit\r\n\r\nTest body\r\n\r\n" + b'Content-Type: text/plain; charset="utf-8"\r\n' + b"Content-Transfer-Encoding: 7bit\r\n\r\nTest body\r\n" ) +TEST_CONTENT_TEXT_BASE64 = ( + b'Content-Type: text/plain; charset="utf-8"\r\n' + b"Content-Transfer-Encoding: base64\r\n\r\nVGVzdCBib2R5\r\n" +) + +TEST_CONTENT_TEXT_BASE64_INVALID = ( + b'Content-Type: text/plain; charset="utf-8"\r\n' + b"Content-Transfer-Encoding: base64\r\n\r\nVGVzdCBib2R5invalid\r\n" +) +TEST_BADLY_ENCODED_CONTENT = "VGVzdCBib2R5invalid\r\n" + TEST_CONTENT_TEXT_OTHER = ( b"Content-Type: text/other; charset=UTF-8\r\n" - b"Content-Transfer-Encoding: 7bit\r\n\r\nTest body\r\n\r\n" + b"Content-Transfer-Encoding: 7bit\r\n\r\nTest body\r\n" ) TEST_CONTENT_HTML = ( @@ -76,14 +91,40 @@ TEST_CONTENT_HTML = ( b"\r\n" b"\r\n" ) +TEST_CONTENT_HTML_BASE64 = ( + b"Content-Type: text/html; charset=UTF-8\r\n" + b"Content-Transfer-Encoding: base64\r\n\r\n" + b"PGh0bWw+CiAgICA8aGVhZD48bWV0YSBodHRwLWVxdW" + b"l2PSJjb250ZW50LXR5cGUiIGNvbnRlbnQ9InRleHQvaHRtbDsgY2hhcnNldD1VVEYtOCI+PC9oZWFkPgog" + b"CAgPGJvZHk+CiAgICAgIDxwPlRlc3QgYm9keTxicj48L3A+CiAgICA8L2JvZHk+CjwvaHRtbD4=\r\n" +) + TEST_CONTENT_MULTIPART = ( b"\r\nThis is a multi-part message in MIME format.\r\n" - + b"--------------McwBciN2C0o3rWeF1tmFo2oI\r\n" + + b"\r\n--Mark=_100584970350292485166\r\n" + TEST_CONTENT_TEXT_PLAIN - + b"--------------McwBciN2C0o3rWeF1tmFo2oI\r\n" + + b"\r\n--Mark=_100584970350292485166\r\n" + TEST_CONTENT_HTML - + b"--------------McwBciN2C0o3rWeF1tmFo2oI--\r\n" + + b"\r\n--Mark=_100584970350292485166--\r\n" +) + +TEST_CONTENT_MULTIPART_BASE64 = ( + b"\r\nThis is a multi-part message in MIME format.\r\n" + + b"\r\n--Mark=_100584970350292485166\r\n" + + TEST_CONTENT_TEXT_BASE64 + + b"\r\n--Mark=_100584970350292485166\r\n" + + TEST_CONTENT_HTML_BASE64 + + b"\r\n--Mark=_100584970350292485166--\r\n" +) + +TEST_CONTENT_MULTIPART_BASE64_INVALID = ( + b"\r\nThis is a multi-part message in MIME format.\r\n" + + b"\r\n--Mark=_100584970350292485166\r\n" + + TEST_CONTENT_TEXT_BASE64_INVALID + + b"\r\n--Mark=_100584970350292485166\r\n" + + TEST_CONTENT_HTML_BASE64 + + b"\r\n--Mark=_100584970350292485166--\r\n" ) EMPTY_SEARCH_RESPONSE = ("OK", [b"", b"Search completed (0.0001 + 0.000 secs)."]) @@ -202,14 +243,40 @@ TEST_FETCH_RESPONSE_MULTIPART = ( "OK", [ b"1 FETCH (BODY[] {" - + str(len(TEST_MESSAGE + TEST_CONTENT_MULTIPART)).encode("utf-8") + + str(len(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART)).encode("utf-8") + b"}", - bytearray(TEST_MESSAGE + TEST_CONTENT_MULTIPART), + bytearray(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART), + b")", + b"Fetch completed (0.0001 + 0.000 secs).", + ], +) +TEST_FETCH_RESPONSE_MULTIPART_BASE64 = ( + "OK", + [ + b"1 FETCH (BODY[] {" + + str(len(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART_BASE64)).encode( + "utf-8" + ) + + b"}", + bytearray(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART_BASE64), b")", b"Fetch completed (0.0001 + 0.000 secs).", ], ) +TEST_FETCH_RESPONSE_MULTIPART_BASE64_INVALID = ( + "OK", + [ + b"1 FETCH (BODY[] {" + + str( + len(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART_BASE64_INVALID) + ).encode("utf-8") + + b"}", + bytearray(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART_BASE64_INVALID), + b")", + b"Fetch completed (0.0001 + 0.000 secs).", + ], +) TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM = ( "OK", diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index ceda841202c1..a00f9d9c25df 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -17,12 +17,15 @@ from homeassistant.util.dt import utcnow from .const import ( BAD_RESPONSE, EMPTY_SEARCH_RESPONSE, + TEST_BADLY_ENCODED_CONTENT, TEST_FETCH_RESPONSE_BINARY, TEST_FETCH_RESPONSE_HTML, TEST_FETCH_RESPONSE_INVALID_DATE1, TEST_FETCH_RESPONSE_INVALID_DATE2, TEST_FETCH_RESPONSE_INVALID_DATE3, TEST_FETCH_RESPONSE_MULTIPART, + TEST_FETCH_RESPONSE_MULTIPART_BASE64, + TEST_FETCH_RESPONSE_MULTIPART_BASE64_INVALID, TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM, TEST_FETCH_RESPONSE_TEXT_BARE, TEST_FETCH_RESPONSE_TEXT_OTHER, @@ -110,6 +113,7 @@ async def test_entry_startup_fails( (TEST_FETCH_RESPONSE_TEXT_OTHER, True), (TEST_FETCH_RESPONSE_HTML, True), (TEST_FETCH_RESPONSE_MULTIPART, True), + (TEST_FETCH_RESPONSE_MULTIPART_BASE64, True), (TEST_FETCH_RESPONSE_BINARY, True), ], ids=[ @@ -122,6 +126,7 @@ async def test_entry_startup_fails( "other", "html", "multipart", + "multipart_base64", "binary", ], ) @@ -154,7 +159,7 @@ async def test_receiving_message_successfully( assert data["folder"] == "INBOX" assert data["sender"] == "john.doe@example.com" assert data["subject"] == "Test subject" - assert data["text"] + assert "Test body" in data["text"] assert ( valid_date and isinstance(data["date"], datetime) @@ -163,6 +168,48 @@ async def test_receiving_message_successfully( ) +@pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE]) +@pytest.mark.parametrize( + ("imap_fetch"), + [ + TEST_FETCH_RESPONSE_MULTIPART_BASE64_INVALID, + ], + ids=[ + "multipart_base64_invalid", + ], +) +@pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) +async def test_receiving_message_with_invalid_encoding( + hass: HomeAssistant, mock_imap_protocol: MagicMock +) -> None: + """Test receiving a message successfully.""" + event_called = async_capture_events(hass, "imap_content") + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # Make sure we have had one update (when polling) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + state = hass.states.get("sensor.imap_email_email_com") + # we should have received one message + assert state is not None + assert state.state == "1" + assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT + + # we should have received one event + assert len(event_called) == 1 + data: dict[str, Any] = event_called[0].data + assert data["server"] == "imap.server.com" + assert data["username"] == "email@email.com" + assert data["search"] == "UnSeen UnDeleted" + assert data["folder"] == "INBOX" + assert data["sender"] == "john.doe@example.com" + assert data["subject"] == "Test subject" + assert data["text"] == TEST_BADLY_ENCODED_CONTENT + + @pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE]) @pytest.mark.parametrize("imap_fetch", [TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM]) @pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) @@ -196,7 +243,7 @@ async def test_receiving_message_no_subject_to_from( assert data["date"] == datetime( 2023, 3, 24, 13, 52, tzinfo=timezone(timedelta(seconds=3600)) ) - assert data["text"] == "Test body\r\n\r\n" + assert data["text"] == "Test body\r\n" assert data["headers"]["Return-Path"] == ("",) assert data["headers"]["Delivered-To"] == ("notify@example.com",) From b64ef24f203408e32d994aaa8146c1b4a455386a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 19 Nov 2023 19:50:25 +0100 Subject: [PATCH 192/265] Fix mqtt json light allows to set brightness value >255 (#104220) --- .../components/mqtt/light/schema_json.py | 11 +++++++---- tests/components/mqtt/test_light_json.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 6f70ff340519..2a2a262be36c 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -367,10 +367,13 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): if brightness_supported(self.supported_color_modes): try: if brightness := values["brightness"]: - self._attr_brightness = int( - brightness # type: ignore[operator] - / float(self._config[CONF_BRIGHTNESS_SCALE]) - * 255 + self._attr_brightness = min( + int( + brightness # type: ignore[operator] + / float(self._config[CONF_BRIGHTNESS_SCALE]) + * 255 + ), + 255, ) else: _LOGGER.debug( diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index e7471829856a..b3dd3a9a4e35 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -1785,6 +1785,24 @@ async def test_brightness_scale( assert state.state == STATE_ON assert state.attributes.get("brightness") == 255 + # Turn on the light with half brightness + async_fire_mqtt_message( + hass, "test_light_bright_scale", '{"state":"ON", "brightness": 50}' + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 128 + + # Test limmiting max brightness + async_fire_mqtt_message( + hass, "test_light_bright_scale", '{"state":"ON", "brightness": 103}' + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 255 + @pytest.mark.parametrize( "hass_config", From 669daabfdb9811bcddc566c0f0de3004a2cc36c6 Mon Sep 17 00:00:00 2001 From: Anton Tolchanov <1687799+knyar@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:53:25 +0000 Subject: [PATCH 193/265] Handle attributes set to None in prometheus (#104247) Better handle attributes set to None --- .../components/prometheus/__init__.py | 10 ++++++---- tests/components/prometheus/test_init.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index c96ed2e4ed35..561657dcffa8 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -19,6 +19,7 @@ from homeassistant.components.climate import ( from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY +from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -323,14 +324,14 @@ class PrometheusMetrics: } def _battery(self, state): - if "battery_level" in state.attributes: + if (battery_level := state.attributes.get(ATTR_BATTERY_LEVEL)) is not None: metric = self._metric( "battery_level_percent", self.prometheus_cli.Gauge, "Battery level as a percentage of its capacity", ) try: - value = float(state.attributes[ATTR_BATTERY_LEVEL]) + value = float(battery_level) metric.labels(**self._labels(state)).set(value) except ValueError: pass @@ -434,8 +435,9 @@ class PrometheusMetrics: ) try: - if "brightness" in state.attributes and state.state == STATE_ON: - value = state.attributes["brightness"] / 255.0 + brightness = state.attributes.get(ATTR_BRIGHTNESS) + if state.state == STATE_ON and brightness is not None: + value = brightness / 255.0 else: value = self.state_as_number(state) value = value * 100 diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index f24782b98d4d..f28c7b5081b5 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -466,6 +466,12 @@ async def test_light(client, light_entities) -> None: 'friendly_name="PC"} 70.58823529411765' in body ) + assert ( + 'light_brightness_percent{domain="light",' + 'entity="light.hallway",' + 'friendly_name="Hallway"} 100.0' in body + ) + @pytest.mark.parametrize("namespace", [""]) async def test_lock(client, lock_entities) -> None: @@ -1492,6 +1498,19 @@ async def light_fixture( data["light_4"] = light_4 data["light_4_attributes"] = light_4_attributes + light_5 = entity_registry.async_get_or_create( + domain=light.DOMAIN, + platform="test", + unique_id="light_5", + suggested_object_id="hallway", + original_name="Hallway", + ) + # Light is on, but brightness is unset; expect metrics to report + # brightness of 100%. + light_5_attributes = {light.ATTR_BRIGHTNESS: None} + set_state_with_entry(hass, light_5, STATE_ON, light_5_attributes) + data["light_5"] = light_5 + data["light_5_attributes"] = light_5_attributes await hass.async_block_till_done() return data From a5d48da07a9fa0ad207c5a27f8d57c1203a401cc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:58:55 +0100 Subject: [PATCH 194/265] Catch ClientOSError in renault integration (#104248) --- homeassistant/components/renault/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py index f69451290bcf..6b5679088a03 100644 --- a/homeassistant/components/renault/__init__.py +++ b/homeassistant/components/renault/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) try: await renault_hub.async_initialise(config_entry) - except aiohttp.ClientResponseError as exc: + except aiohttp.ClientError as exc: raise ConfigEntryNotReady() from exc hass.data[DOMAIN][config_entry.entry_id] = renault_hub From ae2ff926c1dbfed05b0b61421ab6434d68e7571e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 21 Nov 2023 07:59:39 +0100 Subject: [PATCH 195/265] Restore removed guard for non-string inputs in Alexa (#104263) --- homeassistant/components/alexa/capabilities.py | 6 ++++-- tests/components/alexa/test_capabilities.py | 3 ++- tests/components/alexa/test_smart_home.py | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index cde90e127f32..0856c39946b4 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -857,16 +857,18 @@ class AlexaInputController(AlexaCapability): def inputs(self) -> list[dict[str, str]] | None: """Return the list of valid supported inputs.""" - source_list: list[str] = self.entity.attributes.get( + source_list: list[Any] = self.entity.attributes.get( media_player.ATTR_INPUT_SOURCE_LIST, [] ) return AlexaInputController.get_valid_inputs(source_list) @staticmethod - def get_valid_inputs(source_list: list[str]) -> list[dict[str, str]]: + def get_valid_inputs(source_list: list[Any]) -> list[dict[str, str]]: """Return list of supported inputs.""" input_list: list[dict[str, str]] = [] for source in source_list: + if not isinstance(source, str): + continue formatted_source = ( source.lower().replace("-", "").replace("_", "").replace(" ", "") ) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index a6be57e9ed56..11e39c40cb1b 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -183,7 +183,7 @@ async def test_api_increase_color_temp( ("domain", "payload", "source_list", "idx"), [ ("media_player", "GAME CONSOLE", ["tv", "game console", 10000], 1), - ("media_player", "SATELLITE TV", ["satellite-tv", "game console"], 0), + ("media_player", "SATELLITE TV", ["satellite-tv", "game console", None], 0), ("media_player", "SATELLITE TV", ["satellite_tv", "game console"], 0), ("media_player", "BAD DEVICE", ["satellite_tv", "game console"], None), ], @@ -864,6 +864,7 @@ async def test_report_playback_state(hass: HomeAssistant) -> None: | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.STOP, "volume_level": 0.75, + "source_list": [None], }, ) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index e24ec4c950bc..7a1abe96110a 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1439,6 +1439,8 @@ async def test_media_player_inputs(hass: HomeAssistant) -> None: "aux", "input 1", "tv", + 0, + None, ], }, ) From da04c32893e477eb6bf57d0bf32f60704a54f3c4 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Tue, 21 Nov 2023 07:50:00 +0100 Subject: [PATCH 196/265] Bump bimmer_connected to 0.14.3 (#104282) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index b56526941204..911a998371e9 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected==0.14.2"] + "requirements": ["bimmer-connected==0.14.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index dcef98e3fdaf..9e8ff2d1b769 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -524,7 +524,7 @@ beautifulsoup4==4.12.2 bellows==0.36.8 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.14.2 +bimmer-connected==0.14.3 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c2ead33d083..0894d396b382 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ beautifulsoup4==4.12.2 bellows==0.36.8 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.14.2 +bimmer-connected==0.14.3 # homeassistant.components.bluetooth bleak-retry-connector==3.3.0 From 40326385ae91c721be4349dbd65233309aa0f5b9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 20 Nov 2023 22:57:31 -0800 Subject: [PATCH 197/265] Bump pyrainbird to 4.0.1 (#104293) --- 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 07a0bc0a5f69..b8cb86264f23 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rainbird", "iot_class": "local_polling", "loggers": ["pyrainbird"], - "requirements": ["pyrainbird==4.0.0"] + "requirements": ["pyrainbird==4.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9e8ff2d1b769..e48f37c55edc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1980,7 +1980,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==4.0.0 +pyrainbird==4.0.1 # homeassistant.components.recswitch pyrecswitch==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0894d396b382..ce32f0665a0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1496,7 +1496,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.rainbird -pyrainbird==4.0.0 +pyrainbird==4.0.1 # homeassistant.components.risco pyrisco==0.5.7 From da992e9f45824184635687c359880caf463bb536 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Nov 2023 17:41:04 +0100 Subject: [PATCH 198/265] Bump pychromecast to 13.0.8 (#104320) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 7cf318f12a64..5035b3c66202 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/cast", "iot_class": "local_polling", "loggers": ["casttube", "pychromecast"], - "requirements": ["PyChromecast==13.0.7"], + "requirements": ["PyChromecast==13.0.8"], "zeroconf": ["_googlecast._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index e48f37c55edc..ff8328e8e083 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -55,7 +55,7 @@ ProgettiHWSW==0.1.3 # PyBluez==0.22 # homeassistant.components.cast -PyChromecast==13.0.7 +PyChromecast==13.0.8 # homeassistant.components.flick_electric PyFlick==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce32f0665a0d..c6bd7472213f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -48,7 +48,7 @@ PlexAPI==4.15.4 ProgettiHWSW==0.1.3 # homeassistant.components.cast -PyChromecast==13.0.7 +PyChromecast==13.0.8 # homeassistant.components.flick_electric PyFlick==0.0.2 From 1200ded24c326c2ccfca203f19b293eeca4a208d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Nov 2023 09:12:24 +0100 Subject: [PATCH 199/265] Bumped version to 2023.11.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 479bfcbac6e2..c03087dc10f1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from typing import Final APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 11 -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, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 55dd7a81a37c..eb2ca031685d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.11.2" +version = "2023.11.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f5783cd3b5e8739c41815b82619161e945702aad Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Nov 2023 23:54:30 -0800 Subject: [PATCH 200/265] Bump ical to 6.0.0 (#103482) --- homeassistant/components/local_calendar/calendar.py | 6 +++--- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/local_todo/todo.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/local_calendar/calendar.py b/homeassistant/components/local_calendar/calendar.py index c8807d40cc13..2a90e3e9e199 100644 --- a/homeassistant/components/local_calendar/calendar.py +++ b/homeassistant/components/local_calendar/calendar.py @@ -9,9 +9,9 @@ from typing import Any from ical.calendar import Calendar from ical.calendar_stream import IcsCalendarStream from ical.event import Event +from ical.exceptions import CalendarParseError from ical.store import EventStore, EventStoreError from ical.types import Range, Recur -from pydantic import ValidationError import voluptuous as vol from homeassistant.components.calendar import ( @@ -178,8 +178,8 @@ def _parse_event(event: dict[str, Any]) -> Event: event[key] = dt_util.as_local(value).replace(tzinfo=None) try: - return Event.parse_obj(event) - except ValidationError as err: + return Event(**event) + except CalendarParseError as err: _LOGGER.debug("Error parsing event input fields: %s (%s)", event, str(err)) raise vol.Invalid("Error parsing event input fields") from err diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index ac95c6b0f0e1..d21048c191ca 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==5.1.0"] + "requirements": ["ical==6.0.0"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 049a18244959..cf2a49f65108 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==5.1.0"] + "requirements": ["ical==6.0.0"] } diff --git a/homeassistant/components/local_todo/todo.py b/homeassistant/components/local_todo/todo.py index b688d03253e8..cd30c2eeebe8 100644 --- a/homeassistant/components/local_todo/todo.py +++ b/homeassistant/components/local_todo/todo.py @@ -7,9 +7,9 @@ from typing import Any from ical.calendar import Calendar from ical.calendar_stream import IcsCalendarStream +from ical.exceptions import CalendarParseError from ical.store import TodoStore from ical.todo import Todo, TodoStatus -from pydantic import ValidationError from homeassistant.components.todo import ( TodoItem, @@ -76,7 +76,7 @@ def _convert_item(item: TodoItem) -> Todo: """Convert a HomeAssistant TodoItem to an ical Todo.""" try: return Todo(**dataclasses.asdict(item, dict_factory=_todo_dict_factory)) - except ValidationError as err: + except CalendarParseError as err: _LOGGER.debug("Error parsing todo input fields: %s (%s)", item, err) raise HomeAssistantError("Error parsing todo input fields") from err diff --git a/requirements_all.txt b/requirements_all.txt index ff8328e8e083..1dce7aa8bbc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1047,7 +1047,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==5.1.0 +ical==6.0.0 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6bd7472213f..4ac8f14118b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -827,7 +827,7 @@ ibeacon-ble==1.0.1 # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==5.1.0 +ical==6.0.0 # homeassistant.components.ping icmplib==3.0 From 9c4fd88a3d6b01eb6e09244fc7a5874ebcbbc3c9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 11 Nov 2023 03:57:05 -0800 Subject: [PATCH 201/265] Bump ical to 6.1.0 (#103759) --- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/xmpp/manifest.json | 2 +- requirements_all.txt | 5 ++++- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index d21048c191ca..d7b16ee3bef7 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==6.0.0"] + "requirements": ["ical==6.1.0"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index cf2a49f65108..4c3a8e10a621 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==6.0.0"] + "requirements": ["ical==6.1.0"] } diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 99b3ff126d3d..30dee6c842b5 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/xmpp", "iot_class": "cloud_push", "loggers": ["pyasn1", "slixmpp"], - "requirements": ["slixmpp==1.8.4"] + "requirements": ["slixmpp==1.8.4", "emoji==2.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1dce7aa8bbc5..cae2ddb153b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -742,6 +742,9 @@ elkm1-lib==2.2.6 # homeassistant.components.elmax elmax-api==0.0.4 +# homeassistant.components.xmpp +emoji==2.8.0 + # homeassistant.components.emulated_roku emulated-roku==0.2.1 @@ -1047,7 +1050,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==6.0.0 +ical==6.1.0 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ac8f14118b7..c910fa8e4bb9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -827,7 +827,7 @@ ibeacon-ble==1.0.1 # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==6.0.0 +ical==6.1.0 # homeassistant.components.ping icmplib==3.0 From a3bad5458320ee7b5d70bf758ed4e6b471790319 Mon Sep 17 00:00:00 2001 From: Sebastian YEPES Date: Wed, 29 Nov 2023 09:48:45 +0100 Subject: [PATCH 202/265] Add Tuya Smart Water Timer (#95053) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/const.py | 2 ++ homeassistant/components/tuya/select.py | 10 ++++++++++ homeassistant/components/tuya/sensor.py | 12 ++++++++++++ homeassistant/components/tuya/strings.json | 16 ++++++++++++++++ homeassistant/components/tuya/switch.py | 8 ++++++++ 5 files changed, 48 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index acf9f8bbd2ce..19faa76a191e 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -338,6 +338,7 @@ class DPCode(StrEnum): TEMP_VALUE_V2 = "temp_value_v2" TEMPER_ALARM = "temper_alarm" # Tamper alarm TIME_TOTAL = "time_total" + TIME_USE = "time_use" # Total seconds of irrigation TOTAL_CLEAN_AREA = "total_clean_area" TOTAL_CLEAN_COUNT = "total_clean_count" TOTAL_CLEAN_TIME = "total_clean_time" @@ -362,6 +363,7 @@ class DPCode(StrEnum): WATER_RESET = "water_reset" # Resetting of water usage days WATER_SET = "water_set" # Water level WATERSENSOR_STATE = "watersensor_state" + WEATHER_DELAY = "weather_delay" WET = "wet" # Humidification WINDOW_CHECK = "window_check" WINDOW_STATE = "window_state" diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 3cc8c72f5555..bc44ddf479cc 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -75,6 +75,16 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { icon="mdi:thermometer-lines", ), ), + # Smart Water Timer + "sfkzq": ( + # Irrigation will not be run within this set delay period + SelectEntityDescription( + key=DPCode.WEATHER_DELAY, + translation_key="weather_delay", + icon="mdi:weather-cloudy-clock", + entity_category=EntityCategory.CONFIG, + ), + ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 313900fab4ec..4bf8808f5f1a 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -517,6 +517,18 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), *BATTERY_SENSORS, ), + # Smart Water Timer + "sfkzq": ( + # Total seconds of irrigation. Read-write value; the device appears to ignore the write action (maybe firmware bug) + TuyaSensorEntityDescription( + key=DPCode.TIME_USE, + translation_key="total_watering_time", + icon="mdi:history", + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + ), + *BATTERY_SENSORS, + ), # Water Detector # https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli "sj": BATTERY_SENSORS, diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 9c8074195519..e9b13e10a957 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -421,6 +421,19 @@ "4": "Mood 4", "5": "Mood 5" } + }, + "weather_delay": { + "name": "Weather delay", + "state": { + "cancel": "Cancel", + "24h": "24h", + "48h": "48h", + "72h": "72h", + "96h": "96h", + "120h": "120h", + "144h": "144h", + "168h": "168h" + } } }, "sensor": { @@ -556,6 +569,9 @@ "water_level": { "name": "Water level" }, + "total_watering_time": { + "name": "Total watering time" + }, "filter_utilization": { "name": "Filter utilization" }, diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index a48d797555c4..ba304b4069e5 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -430,6 +430,14 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Smart Water Timer + "sfkzq": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + translation_key="switch", + icon="mdi:sprinkler-variant", + ), + ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( From 634785a2d85597b0a6b3cac49c887ceb753ecdd6 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Wed, 29 Nov 2023 10:02:49 +0100 Subject: [PATCH 203/265] Atag: add host field description (#104691) --- homeassistant/components/atag/strings.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/atag/strings.json b/homeassistant/components/atag/strings.json index 39ed972524df..82070c0209fe 100644 --- a/homeassistant/components/atag/strings.json +++ b/homeassistant/components/atag/strings.json @@ -2,10 +2,13 @@ "config": { "step": { "user": { - "title": "Connect to the device", + "description": "Connect to the device", "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "The hostname or IP address of the Atag device." } } }, From afc3f1d933621cd0c522ce6f2ea5e61bc1ec5af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 29 Nov 2023 11:07:54 +0200 Subject: [PATCH 204/265] Make huawei_lte operator search and preferred network modes translatable (#104673) --- homeassistant/components/huawei_lte/sensor.py | 20 +------------------ .../components/huawei_lte/strings.json | 19 +++++++++++++++--- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index ee7256340dff..ca3734bb3056 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -8,8 +8,6 @@ from datetime import datetime, timedelta import logging import re -from huawei_lte_api.enums.net import NetworkModeEnum - from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, @@ -575,10 +573,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "State": HuaweiSensorEntityDescription( key="State", translation_key="operator_search_mode", - format_fn=lambda x: ( - {"0": "Auto", "1": "Manual"}.get(x), - None, - ), entity_category=EntityCategory.DIAGNOSTIC, ), }, @@ -588,19 +582,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { descriptions={ "NetworkMode": HuaweiSensorEntityDescription( key="NetworkMode", - translation_key="preferred_mode", - format_fn=lambda x: ( - { - NetworkModeEnum.MODE_AUTO.value: "4G/3G/2G", - NetworkModeEnum.MODE_4G_3G_AUTO.value: "4G/3G", - NetworkModeEnum.MODE_4G_2G_AUTO.value: "4G/2G", - NetworkModeEnum.MODE_4G_ONLY.value: "4G", - NetworkModeEnum.MODE_3G_2G_AUTO.value: "3G/2G", - NetworkModeEnum.MODE_3G_ONLY.value: "3G", - NetworkModeEnum.MODE_2G_ONLY.value: "2G", - }.get(x), - None, - ), + translation_key="preferred_network_mode", entity_category=EntityCategory.DIAGNOSTIC, ), }, diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 9e46ca742b8d..754f192e57e3 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -231,10 +231,23 @@ "name": "Operator code" }, "operator_search_mode": { - "name": "Operator search mode" + "name": "Operator search mode", + "state": { + "0": "Auto", + "1": "Manual" + } }, - "preferred_mode": { - "name": "Preferred mode" + "preferred_network_mode": { + "name": "Preferred network mode", + "state": { + "00": "4G/3G/2G auto", + "0302": "4G/3G auto", + "0301": "4G/2G auto", + "03": "4G only", + "0201": "3G/2G auto", + "02": "3G only", + "01": "2G only" + } }, "sms_deleted_device": { "name": "SMS deleted (device)" From efd330f18222c463b9a61babbc7a4c58be7128c1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 29 Nov 2023 10:47:23 +0100 Subject: [PATCH 205/265] Send localization info on websocket_api script errors (#104638) * Send localization info on script errors * Use connection exception hander * Keep HomeAssistantError is unknown_error * Move specific exception handling --- .../components/websocket_api/__init__.py | 1 + .../components/websocket_api/commands.py | 20 ++++++- .../components/websocket_api/connection.py | 20 ++++++- tests/common.py | 3 + .../components/websocket_api/test_commands.py | 59 +++++++++++++++++++ .../websocket_api/test_connection.py | 6 ++ 6 files changed, 106 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 9c2645aec57c..f7086cc81db9 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -17,6 +17,7 @@ from .const import ( # noqa: F401 ERR_INVALID_FORMAT, ERR_NOT_FOUND, ERR_NOT_SUPPORTED, + ERR_SERVICE_VALIDATION_ERROR, ERR_TEMPLATE_ERROR, ERR_TIMEOUT, ERR_UNAUTHORIZED, diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 18688914e8b7..5edf50189380 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -778,7 +778,25 @@ async def handle_execute_script( context = connection.context(msg) script_obj = Script(hass, script_config, f"{const.DOMAIN} script", const.DOMAIN) - script_result = await script_obj.async_run(msg.get("variables"), context=context) + try: + script_result = await script_obj.async_run( + msg.get("variables"), context=context + ) + except ServiceValidationError as err: + connection.logger.error(err) + connection.logger.debug("", exc_info=err) + connection.send_error( + msg["id"], + const.ERR_SERVICE_VALIDATION_ERROR, + str(err), + translation_domain=err.translation_domain, + translation_key=err.translation_key, + translation_placeholders=err.translation_placeholders, + ) + return + except Exception as exc: # pylint: disable=broad-except + connection.async_handle_exception(msg, exc) + return connection.send_result( msg["id"], { diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 4581b3be773f..551d4b0d1fea 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -255,7 +255,10 @@ class ActiveConnection: log_handler = self.logger.error code = const.ERR_UNKNOWN_ERROR - err_message = None + err_message: str | None = None + translation_domain: str | None = None + translation_key: str | None = None + translation_placeholders: dict[str, Any] | None = None if isinstance(err, Unauthorized): code = const.ERR_UNAUTHORIZED @@ -268,6 +271,10 @@ class ActiveConnection: err_message = "Timeout" elif isinstance(err, HomeAssistantError): err_message = str(err) + code = const.ERR_UNKNOWN_ERROR + translation_domain = err.translation_domain + translation_key = err.translation_key + translation_placeholders = err.translation_placeholders # This if-check matches all other errors but also matches errors which # result in an empty message. In that case we will also log the stack @@ -276,7 +283,16 @@ class ActiveConnection: err_message = "Unknown error" log_handler = self.logger.exception - self.send_message(messages.error_message(msg["id"], code, err_message)) + self.send_message( + messages.error_message( + msg["id"], + code, + err_message, + translation_domain=translation_domain, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + ) if code: err_message += f" ({code})" diff --git a/tests/common.py b/tests/common.py index a4979c85853e..b2fa53d28fbc 100644 --- a/tests/common.py +++ b/tests/common.py @@ -297,6 +297,7 @@ def async_mock_service( schema: vol.Schema | None = None, response: ServiceResponse = None, supports_response: SupportsResponse | None = None, + raise_exception: Exception | None = None, ) -> list[ServiceCall]: """Set up a fake service & return a calls log list to this service.""" calls = [] @@ -305,6 +306,8 @@ def async_mock_service( def mock_service_log(call): # pylint: disable=unnecessary-lambda """Mock service call.""" calls.append(call) + if raise_exception is not None: + raise raise_exception return response if supports_response is None: diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index a9551310c2a1..c573fb85bb1b 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -2317,6 +2317,65 @@ async def test_execute_script( assert call.context.as_dict() == msg_var["result"]["context"] +@pytest.mark.parametrize( + ("raise_exception", "err_code"), + [ + ( + HomeAssistantError( + "Some error", + translation_domain="test", + translation_key="test_error", + translation_placeholders={"option": "bla"}, + ), + "unknown_error", + ), + ( + ServiceValidationError( + "Some error", + translation_domain="test", + translation_key="test_error", + translation_placeholders={"option": "bla"}, + ), + "service_validation_error", + ), + ], +) +async def test_execute_script_err_localization( + hass: HomeAssistant, + websocket_client: MockHAClientWebSocket, + raise_exception: HomeAssistantError, + err_code: str, +) -> None: + """Test testing a condition.""" + async_mock_service( + hass, "domain_test", "test_service", raise_exception=raise_exception + ) + + await websocket_client.send_json( + { + "id": 5, + "type": "execute_script", + "sequence": [ + { + "service": "domain_test.test_service", + "data": {"hello": "world"}, + }, + {"stop": "done", "response_variable": "service_result"}, + ], + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] is False + assert msg["error"]["code"] == err_code + assert msg["error"]["message"] == "Some error" + assert msg["error"]["translation_key"] == "test_error" + assert msg["error"]["translation_domain"] == "test" + assert msg["error"]["translation_placeholders"] == {"option": "bla"} + + async def test_execute_script_complex_response( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index da435d64d588..4bc736481c4c 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -43,6 +43,12 @@ from tests.common import MockUser "Failed to do X", "Error handling message: Failed to do X (unknown_error) Mock User from 127.0.0.42 (Browser)", ), + ( + exceptions.ServiceValidationError("Failed to do X"), + websocket_api.ERR_UNKNOWN_ERROR, + "Failed to do X", + "Error handling message: Failed to do X (unknown_error) Mock User from 127.0.0.42 (Browser)", + ), ( ValueError("Really bad"), websocket_api.ERR_UNKNOWN_ERROR, From bcfb5307f5865e15dc81ebf969acab2a4de5e33e Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:06:50 +0100 Subject: [PATCH 206/265] Balboa, Bond, Bosch: add host field description (#104695) --- homeassistant/components/balboa/strings.json | 5 ++++- homeassistant/components/bond/strings.json | 3 +++ homeassistant/components/bosch_shc/strings.json | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/balboa/strings.json b/homeassistant/components/balboa/strings.json index 238deb7d65d8..101436c0f317 100644 --- a/homeassistant/components/balboa/strings.json +++ b/homeassistant/components/balboa/strings.json @@ -2,9 +2,12 @@ "config": { "step": { "user": { - "title": "Connect to the Balboa Wi-Fi device", + "description": "Connect to the Balboa Wi-Fi device", "data": { "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "Hostname or IP address of your Balboa Spa Wifi Device. For example, 192.168.1.58." } } }, diff --git a/homeassistant/components/bond/strings.json b/homeassistant/components/bond/strings.json index 4c7c224bc44e..8986905c6eeb 100644 --- a/homeassistant/components/bond/strings.json +++ b/homeassistant/components/bond/strings.json @@ -12,6 +12,9 @@ "data": { "host": "[%key:common::config_flow::data::host%]", "access_token": "[%key:common::config_flow::data::access_token%]" + }, + "data_description": { + "host": "The IP address of your Bond hub." } } }, diff --git a/homeassistant/components/bosch_shc/strings.json b/homeassistant/components/bosch_shc/strings.json index 90688e1373ff..88eb817bbd95 100644 --- a/homeassistant/components/bosch_shc/strings.json +++ b/homeassistant/components/bosch_shc/strings.json @@ -6,6 +6,9 @@ "title": "SHC authentication parameters", "data": { "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The hostname or IP address of your Bosch Smart Home Controller." } }, "credentials": { From 999875d0e45c7b1414b78b0d0939bf0631abf6d6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Nov 2023 11:26:50 +0100 Subject: [PATCH 207/265] Autogenerate Dockerfile (#104669) --- Dockerfile | 3 ++ script/hassfest/__main__.py | 2 + script/hassfest/docker.py | 89 +++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 script/hassfest/docker.py diff --git a/Dockerfile b/Dockerfile index b61e1461c52b..97eeb5b0dfa2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,6 @@ +# Automatically generated by hassfest. +# +# To update, run python3 -m script.hassfest -p docker ARG BUILD_FROM FROM ${BUILD_FROM} diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 32803731ecd8..c454c69d1417 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -16,6 +16,7 @@ from . import ( coverage, dependencies, dhcp, + docker, json, manifest, metadata, @@ -50,6 +51,7 @@ INTEGRATION_PLUGINS = [ ] HASS_PLUGINS = [ coverage, + docker, mypy_config, metadata, ] diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py new file mode 100644 index 000000000000..1849e8e7ec85 --- /dev/null +++ b/script/hassfest/docker.py @@ -0,0 +1,89 @@ +"""Generate and validate the dockerfile.""" +from homeassistant import core +from homeassistant.util import executor, thread + +from .model import Config, Integration + +DOCKERFILE_TEMPLATE = """# Automatically generated by hassfest. +# +# To update, run python3 -m script.hassfest -p docker +ARG BUILD_FROM +FROM ${{BUILD_FROM}} + +# Synchronize with homeassistant/core.py:async_stop +ENV \\ + S6_SERVICES_GRACETIME={timeout} + +ARG QEMU_CPU + +WORKDIR /usr/src + +## Setup Home Assistant Core dependencies +COPY requirements.txt homeassistant/ +COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ +RUN \\ + pip3 install \\ + --only-binary=:all: \\ + -r homeassistant/requirements.txt + +COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/ +RUN \\ + if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \\ + pip3 install homeassistant/home_assistant_frontend-*.whl; \\ + fi \\ + && if ls homeassistant/home_assistant_intents*.whl 1> /dev/null 2>&1; then \\ + pip3 install homeassistant/home_assistant_intents-*.whl; \\ + fi \\ + && \\ + LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \\ + MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \\ + pip3 install \\ + --only-binary=:all: \\ + -r homeassistant/requirements_all.txt + +## Setup Home Assistant Core +COPY . homeassistant/ +RUN \\ + pip3 install \\ + --only-binary=:all: \\ + -e ./homeassistant \\ + && python3 -m compileall \\ + homeassistant/homeassistant + +# Home Assistant S6-Overlay +COPY rootfs / + +WORKDIR /config +""" + + +def _generate_dockerfile() -> str: + timeout = ( + core.STAGE_1_SHUTDOWN_TIMEOUT + + core.STAGE_2_SHUTDOWN_TIMEOUT + + core.STAGE_3_SHUTDOWN_TIMEOUT + + executor.EXECUTOR_SHUTDOWN_TIMEOUT + + thread.THREADING_SHUTDOWN_TIMEOUT + + 10 + ) + return DOCKERFILE_TEMPLATE.format(timeout=timeout * 1000) + + +def validate(integrations: dict[str, Integration], config: Config) -> None: + """Validate dockerfile.""" + dockerfile_content = _generate_dockerfile() + config.cache["dockerfile"] = dockerfile_content + + dockerfile_path = config.root / "Dockerfile" + if dockerfile_path.read_text() != dockerfile_content: + config.add_error( + "docker", + "File Dockerfile is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + + +def generate(integrations: dict[str, Integration], config: Config) -> None: + """Generate dockerfile.""" + dockerfile_path = config.root / "Dockerfile" + dockerfile_path.write_text(config.cache["dockerfile"]) From bd8f01bd35d95789b49769dbc40dc51870f8a339 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 29 Nov 2023 05:30:15 -0500 Subject: [PATCH 208/265] Bump ZHA dependencies (#104335) --- homeassistant/components/zha/__init__.py | 106 ++++++++++---- homeassistant/components/zha/config_flow.py | 26 ++-- homeassistant/components/zha/core/const.py | 2 +- homeassistant/components/zha/core/device.py | 2 +- homeassistant/components/zha/core/gateway.py | 96 ++++++------- homeassistant/components/zha/entity.py | 2 +- homeassistant/components/zha/manifest.json | 14 +- homeassistant/components/zha/radio_manager.py | 20 ++- requirements_all.txt | 14 +- requirements_test_all.txt | 14 +- .../test_silabs_multiprotocol_addon.py | 13 +- .../test_config_flow.py | 4 +- .../homeassistant_sky_connect/test_init.py | 8 +- .../homeassistant_yellow/test_config_flow.py | 4 +- .../homeassistant_yellow/test_init.py | 4 +- tests/components/zha/conftest.py | 6 +- tests/components/zha/test_config_flow.py | 97 +++++++------ tests/components/zha/test_gateway.py | 136 +++--------------- tests/components/zha/test_init.py | 79 ++++++++-- tests/components/zha/test_repairs.py | 6 + tests/components/zha/test_websocket_api.py | 18 ++- 21 files changed, 349 insertions(+), 322 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 222c7f1d4ef4..2046070d6a52 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -9,12 +9,12 @@ import re import voluptuous as vol from zhaquirks import setup as setup_quirks from zigpy.config import CONF_DATABASE, CONF_DEVICE, CONF_DEVICE_PATH -from zigpy.exceptions import NetworkSettingsInconsistent +from zigpy.exceptions import NetworkSettingsInconsistent, TransientConnectionError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TYPE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -29,6 +29,7 @@ from .core.const import ( CONF_CUSTOM_QUIRKS_PATH, CONF_DEVICE_CONFIG, CONF_ENABLE_QUIRKS, + CONF_FLOW_CONTROL, CONF_RADIO_TYPE, CONF_USB_PATH, CONF_ZIGPY, @@ -36,6 +37,8 @@ from .core.const import ( DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, + STARTUP_FAILURE_DELAY_S, + STARTUP_RETRIES, RadioType, ) from .core.device import get_device_automation_triggers @@ -158,42 +161,67 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("Trigger cache: %s", zha_data.device_trigger_cache) - zha_gateway = ZHAGateway(hass, zha_data.yaml_config, config_entry) + # Retry setup a few times before giving up to deal with missing serial ports in VMs + for attempt in range(STARTUP_RETRIES): + try: + zha_gateway = await ZHAGateway.async_from_config( + hass=hass, + config=zha_data.yaml_config, + config_entry=config_entry, + ) + break + except NetworkSettingsInconsistent as exc: + await warn_on_inconsistent_network_settings( + hass, + config_entry=config_entry, + old_state=exc.old_state, + new_state=exc.new_state, + ) + raise ConfigEntryError( + "Network settings do not match most recent backup" + ) from exc + except TransientConnectionError as exc: + raise ConfigEntryNotReady from exc + except Exception as exc: # pylint: disable=broad-except + _LOGGER.debug( + "Couldn't start coordinator (attempt %s of %s)", + attempt + 1, + STARTUP_RETRIES, + exc_info=exc, + ) - try: - await zha_gateway.async_initialize() - except NetworkSettingsInconsistent as exc: - await warn_on_inconsistent_network_settings( - hass, - config_entry=config_entry, - old_state=exc.old_state, - new_state=exc.new_state, - ) - raise HomeAssistantError( - "Network settings do not match most recent backup" - ) from exc - except Exception: - if RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp: - try: - await warn_on_wrong_silabs_firmware( - hass, config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] - ) - except AlreadyRunningEZSP as exc: - # If connecting fails but we somehow probe EZSP (e.g. stuck in the - # bootloader), reconnect, it should work - raise ConfigEntryNotReady from exc + if attempt < STARTUP_RETRIES - 1: + await asyncio.sleep(STARTUP_FAILURE_DELAY_S) + continue - raise + if RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp: + try: + # Ignore all exceptions during probing, they shouldn't halt setup + await warn_on_wrong_silabs_firmware( + hass, config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] + ) + except AlreadyRunningEZSP as ezsp_exc: + raise ConfigEntryNotReady from ezsp_exc + + raise repairs.async_delete_blocking_issues(hass) + manufacturer = zha_gateway.state.node_info.manufacturer + model = zha_gateway.state.node_info.model + + if manufacturer is None and model is None: + manufacturer = "Unknown" + model = "Unknown" + device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.coordinator_ieee))}, - identifiers={(DOMAIN, str(zha_gateway.coordinator_ieee))}, + connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.state.node_info.ieee))}, + identifiers={(DOMAIN, str(zha_gateway.state.node_info.ieee))}, name="Zigbee Coordinator", - manufacturer="ZHA", - model=zha_gateway.radio_description, + manufacturer=manufacturer, + model=model, + sw_version=zha_gateway.state.node_info.version, ) websocket_api.async_load_api(hass) @@ -267,5 +295,23 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> config_entry.version = 3 hass.config_entries.async_update_entry(config_entry, data=data) + if config_entry.version == 3: + data = {**config_entry.data} + + if not data[CONF_DEVICE].get(CONF_BAUDRATE): + data[CONF_DEVICE][CONF_BAUDRATE] = { + "deconz": 38400, + "xbee": 57600, + "ezsp": 57600, + "znp": 115200, + "zigate": 115200, + }[data[CONF_RADIO_TYPE]] + + if not data[CONF_DEVICE].get(CONF_FLOW_CONTROL): + data[CONF_DEVICE][CONF_FLOW_CONTROL] = None + + config_entry.version = 4 + hass.config_entries.async_update_entry(config_entry, data=data) + _LOGGER.info("Migration to version %s successful", config_entry.version) return True diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 1b6bbee5159d..60cf917d9f63 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -27,12 +27,13 @@ from homeassistant.util import dt as dt_util from .core.const import ( CONF_BAUDRATE, - CONF_FLOWCONTROL, + CONF_FLOW_CONTROL, CONF_RADIO_TYPE, DOMAIN, RadioType, ) from .radio_manager import ( + DEVICE_SCHEMA, HARDWARE_DISCOVERY_SCHEMA, RECOMMENDED_RADIOS, ProbeResult, @@ -42,7 +43,7 @@ from .radio_manager import ( CONF_MANUAL_PATH = "Enter Manually" SUPPORTED_PORT_SETTINGS = ( CONF_BAUDRATE, - CONF_FLOWCONTROL, + CONF_FLOW_CONTROL, ) DECONZ_DOMAIN = "deconz" @@ -160,7 +161,7 @@ class BaseZhaFlow(FlowHandler): return self.async_create_entry( title=self._title, data={ - CONF_DEVICE: device_settings, + CONF_DEVICE: DEVICE_SCHEMA(device_settings), CONF_RADIO_TYPE: self._radio_mgr.radio_type.name, }, ) @@ -281,7 +282,7 @@ class BaseZhaFlow(FlowHandler): for ( param, value, - ) in self._radio_mgr.radio_type.controller.SCHEMA_DEVICE.schema.items(): + ) in DEVICE_SCHEMA.schema.items(): if param not in SUPPORTED_PORT_SETTINGS: continue @@ -488,7 +489,7 @@ class BaseZhaFlow(FlowHandler): class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" - VERSION = 3 + VERSION = 4 async def _set_unique_id_or_update_path( self, unique_id: str, device_path: str @@ -646,22 +647,17 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN name = discovery_data["name"] radio_type = self._radio_mgr.parse_radio_type(discovery_data["radio_type"]) - - try: - device_settings = radio_type.controller.SCHEMA_DEVICE( - discovery_data["port"] - ) - except vol.Invalid: - return self.async_abort(reason="invalid_hardware_data") + device_settings = discovery_data["port"] + device_path = device_settings[CONF_DEVICE_PATH] await self._set_unique_id_or_update_path( - unique_id=f"{name}_{radio_type.name}_{device_settings[CONF_DEVICE_PATH]}", - device_path=device_settings[CONF_DEVICE_PATH], + unique_id=f"{name}_{radio_type.name}_{device_path}", + device_path=device_path, ) self._title = name self._radio_mgr.radio_type = radio_type - self._radio_mgr.device_path = device_settings[CONF_DEVICE_PATH] + self._radio_mgr.device_path = device_path self._radio_mgr.device_settings = device_settings self.context["title_placeholders"] = {CONF_NAME: name} diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 9874fddc5982..f89ed8d9a529 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -127,6 +127,7 @@ CONF_ALARM_FAILED_TRIES = "alarm_failed_tries" CONF_ALARM_ARM_REQUIRES_CODE = "alarm_arm_requires_code" CONF_BAUDRATE = "baudrate" +CONF_FLOW_CONTROL = "flow_control" CONF_CUSTOM_QUIRKS_PATH = "custom_quirks_path" CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEVICE_CONFIG = "device_config" @@ -136,7 +137,6 @@ CONF_ALWAYS_PREFER_XY_COLOR_MODE = "always_prefer_xy_color_mode" CONF_GROUP_MEMBERS_ASSUME_STATE = "group_members_assume_state" CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" -CONF_FLOWCONTROL = "flow_control" CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" CONF_USE_THREAD = "use_thread" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 44acbb172fcc..0ce6f47b61ed 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -285,7 +285,7 @@ class ZHADevice(LogMixin): if not self.is_coordinator: return False - return self.ieee == self.gateway.coordinator_ieee + return self.ieee == self.gateway.state.node_info.ieee @property def is_end_device(self) -> bool | None: diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index b4c02d330151..5c038a2d7f85 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -11,7 +11,7 @@ import itertools import logging import re import time -from typing import TYPE_CHECKING, Any, NamedTuple +from typing import TYPE_CHECKING, Any, NamedTuple, Self from zigpy.application import ControllerApplication from zigpy.config import ( @@ -24,15 +24,14 @@ from zigpy.config import ( ) import zigpy.device import zigpy.endpoint -from zigpy.exceptions import NetworkSettingsInconsistent, TransientConnectionError import zigpy.group +from zigpy.state import State from zigpy.types.named import EUI64 from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -66,8 +65,6 @@ from .const import ( SIGNAL_ADD_ENTITIES, SIGNAL_GROUP_MEMBERSHIP_CHANGE, SIGNAL_REMOVE, - STARTUP_FAILURE_DELAY_S, - STARTUP_RETRIES, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZHA_GW_MSG, @@ -123,10 +120,6 @@ class DevicePairingStatus(Enum): class ZHAGateway: """Gateway that handles events that happen on the ZHA Zigbee network.""" - # -- Set in async_initialize -- - application_controller: ControllerApplication - radio_description: str - def __init__( self, hass: HomeAssistant, config: ConfigType, config_entry: ConfigEntry ) -> None: @@ -135,7 +128,8 @@ class ZHAGateway: self._config = config self._devices: dict[EUI64, ZHADevice] = {} self._groups: dict[int, ZHAGroup] = {} - self.coordinator_zha_device: ZHADevice | None = None + self.application_controller: ControllerApplication = None + self.coordinator_zha_device: ZHADevice = None # type: ignore[assignment] self._device_registry: collections.defaultdict[ EUI64, list[EntityReference] ] = collections.defaultdict(list) @@ -147,13 +141,11 @@ class ZHAGateway: self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] + self.shutting_down = False def get_application_controller_data(self) -> tuple[ControllerApplication, dict]: """Get an uninitialized instance of a zigpy `ControllerApplication`.""" - radio_type = self.config_entry.data[CONF_RADIO_TYPE] - - app_controller_cls = RadioType[radio_type].controller - self.radio_description = RadioType[radio_type].description + radio_type = RadioType[self.config_entry.data[CONF_RADIO_TYPE]] app_config = self._config.get(CONF_ZIGPY, {}) database = self._config.get( @@ -170,7 +162,7 @@ class ZHAGateway: # event loop, when a connection to a TCP coordinator fails in a specific way if ( CONF_USE_THREAD not in app_config - and RadioType[radio_type] is RadioType.ezsp + and radio_type is RadioType.ezsp and app_config[CONF_DEVICE][CONF_DEVICE_PATH].startswith("socket://") ): app_config[CONF_USE_THREAD] = False @@ -189,48 +181,40 @@ class ZHAGateway: ): app_config.setdefault(CONF_NWK, {})[CONF_NWK_CHANNEL] = 15 - return app_controller_cls, app_controller_cls.SCHEMA(app_config) + return radio_type.controller, radio_type.controller.SCHEMA(app_config) + + @classmethod + async def async_from_config( + cls, hass: HomeAssistant, config: ConfigType, config_entry: ConfigEntry + ) -> Self: + """Create an instance of a gateway from config objects.""" + instance = cls(hass, config, config_entry) + await instance.async_initialize() + return instance async def async_initialize(self) -> None: """Initialize controller and connect radio.""" discovery.PROBE.initialize(self.hass) discovery.GROUP_PROBE.initialize(self.hass) + self.shutting_down = False + app_controller_cls, app_config = self.get_application_controller_data() - self.application_controller = await app_controller_cls.new( + app = await app_controller_cls.new( config=app_config, auto_form=False, start_radio=False, ) try: - for attempt in range(STARTUP_RETRIES): - try: - await self.application_controller.startup(auto_form=True) - except TransientConnectionError as exc: - raise ConfigEntryNotReady from exc - except NetworkSettingsInconsistent: - raise - except Exception as exc: # pylint: disable=broad-except - _LOGGER.debug( - "Couldn't start %s coordinator (attempt %s of %s)", - self.radio_description, - attempt + 1, - STARTUP_RETRIES, - exc_info=exc, - ) - - if attempt == STARTUP_RETRIES - 1: - raise exc - - await asyncio.sleep(STARTUP_FAILURE_DELAY_S) - else: - break + await app.startup(auto_form=True) except Exception: # Explicitly shut down the controller application on failure - await self.application_controller.shutdown() + await app.shutdown() raise + self.application_controller = app + zha_data = get_zha_data(self.hass) zha_data.gateway = self @@ -244,6 +228,17 @@ class ZHAGateway: self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) + def connection_lost(self, exc: Exception) -> None: + """Handle connection lost event.""" + if self.shutting_down: + return + + _LOGGER.debug("Connection to the radio was lost: %r", exc) + + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.config_entry.entry_id) + ) + def _find_coordinator_device(self) -> zigpy.device.Device: zigpy_coordinator = self.application_controller.get_device(nwk=0x0000) @@ -258,6 +253,7 @@ class ZHAGateway: @callback def async_load_devices(self) -> None: """Restore ZHA devices from zigpy application state.""" + for zigpy_device in self.application_controller.devices.values(): zha_device = self._async_get_or_create_device(zigpy_device, restored=True) delta_msg = "not known" @@ -280,6 +276,7 @@ class ZHAGateway: @callback def async_load_groups(self) -> None: """Initialize ZHA groups.""" + for group_id in self.application_controller.groups: group = self.application_controller.groups[group_id] zha_group = self._async_get_or_create_group(group) @@ -521,9 +518,9 @@ class ZHAGateway: entity_registry.async_remove(entry.entity_id) @property - def coordinator_ieee(self) -> EUI64: - """Return the active coordinator's IEEE address.""" - return self.application_controller.state.node_info.ieee + def state(self) -> State: + """Return the active coordinator's network state.""" + return self.application_controller.state @property def devices(self) -> dict[EUI64, ZHADevice]: @@ -711,6 +708,7 @@ class ZHAGateway: group_id: int | None = None, ) -> ZHAGroup | None: """Create a new Zigpy Zigbee group.""" + # we start with two to fill any gaps from a user removing existing groups if group_id is None: @@ -758,19 +756,13 @@ class ZHAGateway: async def shutdown(self) -> None: """Stop ZHA Controller Application.""" _LOGGER.debug("Shutting down ZHA ControllerApplication") + self.shutting_down = True + for unsubscribe in self._unsubs: unsubscribe() for device in self.devices.values(): device.async_cleanup_handles() - # shutdown is called when the config entry unloads are processed - # there are cases where unloads are processed because of a failure of - # some sort and the application controller may not have been - # created yet - if ( - hasattr(self, "application_controller") - and self.application_controller is not None - ): - await self.application_controller.shutdown() + await self.application_controller.shutdown() def handle_message( self, diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 05e1da7c570e..b92d077907fd 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -92,7 +92,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): manufacturer=zha_device_info[ATTR_MANUFACTURER], model=zha_device_info[ATTR_MODEL], name=zha_device_info[ATTR_NAME], - via_device=(DOMAIN, zha_gateway.coordinator_ieee), + via_device=(DOMAIN, zha_gateway.state.node_info.ieee), ) @callback diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 786caf1809c8..cd53772777a8 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,16 +21,16 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.36.8", + "bellows==0.37.1", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.107", - "zigpy-deconz==0.21.1", - "zigpy==0.59.0", - "zigpy-xbee==0.19.0", - "zigpy-zigate==0.11.0", - "zigpy-znp==0.11.6", - "universal-silabs-flasher==0.0.14", + "zigpy-deconz==0.22.0", + "zigpy==0.60.0", + "zigpy-xbee==0.20.0", + "zigpy-zigate==0.12.0", + "zigpy-znp==0.12.0", + "universal-silabs-flasher==0.0.15", "pyserial-asyncio-fast==0.11" ], "usb": [ diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index d20cf752a918..d3ca03de8d89 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -19,6 +19,7 @@ from zigpy.config import ( CONF_DEVICE, CONF_DEVICE_PATH, CONF_NWK_BACKUP_ENABLED, + SCHEMA_DEVICE, ) from zigpy.exceptions import NetworkNotFormed @@ -58,10 +59,21 @@ RETRY_DELAY_S = 1.0 BACKUP_RETRIES = 5 MIGRATION_RETRIES = 100 + +DEVICE_SCHEMA = vol.Schema( + { + vol.Required("path"): str, + vol.Optional("baudrate", default=115200): int, + vol.Optional("flow_control", default=None): vol.In( + ["hardware", "software", None] + ), + } +) + HARDWARE_DISCOVERY_SCHEMA = vol.Schema( { vol.Required("name"): str, - vol.Required("port"): dict, + vol.Required("port"): DEVICE_SCHEMA, vol.Required("radio_type"): str, } ) @@ -204,9 +216,7 @@ class ZhaRadioManager: for radio in AUTOPROBE_RADIOS: _LOGGER.debug("Attempting to probe radio type %s", radio) - dev_config = radio.controller.SCHEMA_DEVICE( - {CONF_DEVICE_PATH: self.device_path} - ) + dev_config = SCHEMA_DEVICE({CONF_DEVICE_PATH: self.device_path}) probe_result = await radio.controller.probe(dev_config) if not probe_result: @@ -357,7 +367,7 @@ class ZhaMultiPANMigrationHelper: migration_data["new_discovery_info"]["radio_type"] ) - new_device_settings = new_radio_type.controller.SCHEMA_DEVICE( + new_device_settings = SCHEMA_DEVICE( migration_data["new_discovery_info"]["port"] ) diff --git a/requirements_all.txt b/requirements_all.txt index bce0550bb449..756de3bd3061 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -523,7 +523,7 @@ beautifulsoup4==4.12.2 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.36.8 +bellows==0.37.1 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.3 @@ -2660,7 +2660,7 @@ unifi-discovery==1.1.7 unifiled==0.11 # homeassistant.components.zha -universal-silabs-flasher==0.0.14 +universal-silabs-flasher==0.0.15 # homeassistant.components.upb upb-lib==0.5.4 @@ -2828,19 +2828,19 @@ zhong-hong-hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.21.1 +zigpy-deconz==0.22.0 # homeassistant.components.zha -zigpy-xbee==0.19.0 +zigpy-xbee==0.20.0 # homeassistant.components.zha -zigpy-zigate==0.11.0 +zigpy-zigate==0.12.0 # homeassistant.components.zha -zigpy-znp==0.11.6 +zigpy-znp==0.12.0 # homeassistant.components.zha -zigpy==0.59.0 +zigpy==0.60.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a58be624388d..bbaaf1bcb16a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -445,7 +445,7 @@ base36==0.1.1 beautifulsoup4==4.12.2 # homeassistant.components.zha -bellows==0.36.8 +bellows==0.37.1 # homeassistant.components.bmw_connected_drive bimmer-connected==0.14.3 @@ -1979,7 +1979,7 @@ ultraheat-api==0.5.7 unifi-discovery==1.1.7 # homeassistant.components.zha -universal-silabs-flasher==0.0.14 +universal-silabs-flasher==0.0.15 # homeassistant.components.upb upb-lib==0.5.4 @@ -2117,19 +2117,19 @@ zeversolar==0.3.1 zha-quirks==0.0.107 # homeassistant.components.zha -zigpy-deconz==0.21.1 +zigpy-deconz==0.22.0 # homeassistant.components.zha -zigpy-xbee==0.19.0 +zigpy-xbee==0.20.0 # homeassistant.components.zha -zigpy-zigate==0.11.0 +zigpy-zigate==0.12.0 # homeassistant.components.zha -zigpy-znp==0.11.6 +zigpy-znp==0.12.0 # homeassistant.components.zha -zigpy==0.59.0 +zigpy==0.60.0 # homeassistant.components.zwave_js zwave-js-server-python==0.54.0 diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index fbc77cdee9ee..f58d561bfb33 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -293,7 +293,14 @@ async def test_option_flow_install_multi_pan_addon_zha( config_entry.add_to_hass(hass) zha_config_entry = MockConfigEntry( - data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, + data={ + "device": { + "path": "/dev/ttyTEST123", + "baudrate": 115200, + "flow_control": None, + }, + "radio_type": "ezsp", + }, domain=ZHA_DOMAIN, options={}, title="Test", @@ -348,8 +355,8 @@ async def test_option_flow_install_multi_pan_addon_zha( assert zha_config_entry.data == { "device": { "path": "socket://core-silabs-multiprotocol:9999", - "baudrate": 57600, # ZHA default - "flow_control": "software", # ZHA default + "baudrate": 115200, + "flow_control": None, }, "radio_type": "ezsp", } diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 4d43d29463a7..65636b27a16c 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -337,8 +337,8 @@ async def test_option_flow_install_multi_pan_addon_zha( assert zha_config_entry.data == { "device": { "path": "socket://core-silabs-multiprotocol:9999", - "baudrate": 57600, # ZHA default - "flow_control": "software", # ZHA default + "baudrate": 115200, + "flow_control": None, }, "radio_type": "ezsp", } diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index e00603dc8f78..11961c09a2de 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -147,7 +147,7 @@ async def test_setup_zha( assert config_entry.data == { "device": { "baudrate": 115200, - "flow_control": "software", + "flow_control": None, "path": CONFIG_ENTRY_DATA["device"], }, "radio_type": "ezsp", @@ -200,8 +200,8 @@ async def test_setup_zha_multipan( config_entry = hass.config_entries.async_entries("zha")[0] assert config_entry.data == { "device": { - "baudrate": 57600, # ZHA default - "flow_control": "software", # ZHA default + "baudrate": 115200, + "flow_control": None, "path": "socket://core-silabs-multiprotocol:9999", }, "radio_type": "ezsp", @@ -255,7 +255,7 @@ async def test_setup_zha_multipan_other_device( assert config_entry.data == { "device": { "baudrate": 115200, - "flow_control": "software", + "flow_control": None, "path": CONFIG_ENTRY_DATA["device"], }, "radio_type": "ezsp", diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 58d47c419874..242b316de669 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -249,8 +249,8 @@ async def test_option_flow_install_multi_pan_addon_zha( assert zha_config_entry.data == { "device": { "path": "socket://core-silabs-multiprotocol:9999", - "baudrate": 57600, # ZHA default - "flow_control": "software", # ZHA default + "baudrate": 115200, + "flow_control": None, }, "radio_type": "ezsp", } diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index addc519c8653..f8cdcd8a13bd 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -145,8 +145,8 @@ async def test_setup_zha_multipan( config_entry = hass.config_entries.async_entries("zha")[0] assert config_entry.data == { "device": { - "baudrate": 57600, # ZHA default - "flow_control": "software", # ZHA default + "baudrate": 115200, + "flow_control": None, "path": "socket://core-silabs-multiprotocol:9999", }, "radio_type": "ezsp", diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index a4ff5a3b2054..1b3a536007ad 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -46,7 +46,7 @@ def disable_request_retry_delay(): with patch( "homeassistant.components.zha.core.cluster_handlers.RETRYABLE_REQUEST_DECORATOR", zigpy.util.retryable_request(tries=3, delay=0), - ): + ), patch("homeassistant.components.zha.STARTUP_FAILURE_DELAY_S", 0.01): yield @@ -83,8 +83,8 @@ class _FakeApp(ControllerApplication): async def permit_ncp(self, time_s: int = 60): pass - async def permit_with_key( - self, node: zigpy.types.EUI64, code: bytes, time_s: int = 60 + async def permit_with_link_key( + self, node: zigpy.types.EUI64, link_key: zigpy.types.KeyData, time_s: int = 60 ): pass diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 9ec8048ea033..883df4aba94f 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -10,7 +10,7 @@ import pytest import serial.tools.list_ports from zigpy.backups import BackupManager import zigpy.config -from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH +from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH, SCHEMA_DEVICE import zigpy.device from zigpy.exceptions import NetworkNotFormed import zigpy.types @@ -22,7 +22,7 @@ from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_ from homeassistant.components.zha import config_flow, radio_manager from homeassistant.components.zha.core.const import ( CONF_BAUDRATE, - CONF_FLOWCONTROL, + CONF_FLOW_CONTROL, CONF_RADIO_TYPE, DOMAIN, EZSP_OVERWRITE_EUI64, @@ -118,9 +118,7 @@ def mock_detect_radio_type( async def detect(self): self.radio_type = radio_type - self.device_settings = radio_type.controller.SCHEMA_DEVICE( - {CONF_DEVICE_PATH: self.device_path} - ) + self.device_settings = SCHEMA_DEVICE({CONF_DEVICE_PATH: self.device_path}) return ret @@ -181,7 +179,7 @@ async def test_zeroconf_discovery_znp(hass: HomeAssistant) -> None: assert result3["data"] == { CONF_DEVICE: { CONF_BAUDRATE: 115200, - CONF_FLOWCONTROL: None, + CONF_FLOW_CONTROL: None, CONF_DEVICE_PATH: "socket://192.168.1.200:6638", }, CONF_RADIO_TYPE: "znp", @@ -238,6 +236,8 @@ async def test_zigate_via_zeroconf(setup_entry_mock, hass: HomeAssistant) -> Non assert result4["data"] == { CONF_DEVICE: { CONF_DEVICE_PATH: "socket://192.168.1.200:1234", + CONF_BAUDRATE: 115200, + CONF_FLOW_CONTROL: None, }, CONF_RADIO_TYPE: "zigate", } @@ -287,7 +287,7 @@ async def test_efr32_via_zeroconf(hass: HomeAssistant) -> None: CONF_DEVICE: { CONF_DEVICE_PATH: "socket://192.168.1.200:1234", CONF_BAUDRATE: 115200, - CONF_FLOWCONTROL: "software", + CONF_FLOW_CONTROL: None, }, CONF_RADIO_TYPE: "ezsp", } @@ -304,7 +304,7 @@ async def test_discovery_via_zeroconf_ip_change(hass: HomeAssistant) -> None: CONF_DEVICE: { CONF_DEVICE_PATH: "socket://192.168.1.5:6638", CONF_BAUDRATE: 115200, - CONF_FLOWCONTROL: None, + CONF_FLOW_CONTROL: None, } }, ) @@ -328,7 +328,7 @@ async def test_discovery_via_zeroconf_ip_change(hass: HomeAssistant) -> None: assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "socket://192.168.1.22:6638", CONF_BAUDRATE: 115200, - CONF_FLOWCONTROL: None, + CONF_FLOW_CONTROL: None, } @@ -483,6 +483,8 @@ async def test_zigate_discovery_via_usb(probe_mock, hass: HomeAssistant) -> None assert result4["data"] == { "device": { "path": "/dev/ttyZIGBEE", + "baudrate": 115200, + "flow_control": None, }, CONF_RADIO_TYPE: "zigate", } @@ -555,7 +557,7 @@ async def test_discovery_via_usb_path_changes(hass: HomeAssistant) -> None: CONF_DEVICE: { CONF_DEVICE_PATH: "/dev/ttyUSB1", CONF_BAUDRATE: 115200, - CONF_FLOWCONTROL: None, + CONF_FLOW_CONTROL: None, } }, ) @@ -579,7 +581,7 @@ async def test_discovery_via_usb_path_changes(hass: HomeAssistant) -> None: assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "/dev/ttyZIGBEE", CONF_BAUDRATE: 115200, - CONF_FLOWCONTROL: None, + CONF_FLOW_CONTROL: None, } @@ -754,6 +756,8 @@ async def test_user_flow(hass: HomeAssistant) -> None: assert result2["data"] == { "device": { "path": port.device, + CONF_BAUDRATE: 115200, + CONF_FLOW_CONTROL: None, }, CONF_RADIO_TYPE: "deconz", } @@ -773,7 +777,11 @@ async def test_user_flow_not_detected(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, - data={zigpy.config.CONF_DEVICE_PATH: port_select}, + data={ + zigpy.config.CONF_DEVICE_PATH: port_select, + CONF_BAUDRATE: 115200, + CONF_FLOW_CONTROL: None, + }, ) assert result["type"] == FlowResultType.FORM @@ -951,31 +959,6 @@ async def test_user_port_config(probe_mock, hass: HomeAssistant) -> None: assert probe_mock.await_count == 1 -@pytest.mark.parametrize( - ("old_type", "new_type"), - [ - ("ezsp", "ezsp"), - ("ti_cc", "znp"), # only one that should change - ("znp", "znp"), - ("deconz", "deconz"), - ], -) -async def test_migration_ti_cc_to_znp( - old_type, new_type, hass: HomeAssistant, config_entry: MockConfigEntry -) -> None: - """Test zigpy-cc to zigpy-znp config migration.""" - config_entry.data = {**config_entry.data, CONF_RADIO_TYPE: old_type} - config_entry.version = 2 - config_entry.add_to_hass(hass) - - with patch("homeassistant.components.zha.async_setup_entry", return_value=True): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.version > 2 - assert config_entry.data[CONF_RADIO_TYPE] == new_type - - @pytest.mark.parametrize("onboarded", [True, False]) @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_hardware(onboarded, hass: HomeAssistant) -> None: @@ -1022,7 +1005,7 @@ async def test_hardware(onboarded, hass: HomeAssistant) -> None: assert result3["data"] == { CONF_DEVICE: { CONF_BAUDRATE: 115200, - CONF_FLOWCONTROL: "hardware", + CONF_FLOW_CONTROL: "hardware", CONF_DEVICE_PATH: "/dev/ttyAMA1", }, CONF_RADIO_TYPE: "ezsp", @@ -1171,6 +1154,7 @@ async def test_formation_strategy_form_initial_network( @patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_onboarding_auto_formation_new_hardware( mock_app, hass: HomeAssistant ) -> None: @@ -1577,7 +1561,7 @@ async def test_options_flow_defaults( CONF_DEVICE: { CONF_DEVICE_PATH: "/dev/ttyUSB0", CONF_BAUDRATE: 12345, - CONF_FLOWCONTROL: None, + CONF_FLOW_CONTROL: None, }, CONF_RADIO_TYPE: "znp", }, @@ -1645,7 +1629,7 @@ async def test_options_flow_defaults( # Change everything CONF_DEVICE_PATH: "/dev/new_serial_port", CONF_BAUDRATE: 54321, - CONF_FLOWCONTROL: "software", + CONF_FLOW_CONTROL: "software", }, ) @@ -1668,7 +1652,7 @@ async def test_options_flow_defaults( CONF_DEVICE: { CONF_DEVICE_PATH: "/dev/new_serial_port", CONF_BAUDRATE: 54321, - CONF_FLOWCONTROL: "software", + CONF_FLOW_CONTROL: "software", }, CONF_RADIO_TYPE: "znp", } @@ -1697,7 +1681,7 @@ async def test_options_flow_defaults_socket(hass: HomeAssistant) -> None: CONF_DEVICE: { CONF_DEVICE_PATH: "socket://localhost:5678", CONF_BAUDRATE: 12345, - CONF_FLOWCONTROL: None, + CONF_FLOW_CONTROL: None, }, CONF_RADIO_TYPE: "znp", }, @@ -1766,7 +1750,7 @@ async def test_options_flow_restarts_running_zha_if_cancelled( CONF_DEVICE: { CONF_DEVICE_PATH: "socket://localhost:5678", CONF_BAUDRATE: 12345, - CONF_FLOWCONTROL: None, + CONF_FLOW_CONTROL: None, }, CONF_RADIO_TYPE: "znp", }, @@ -1821,7 +1805,7 @@ async def test_options_flow_migration_reset_old_adapter( CONF_DEVICE: { CONF_DEVICE_PATH: "/dev/serial/by-id/old_radio", CONF_BAUDRATE: 12345, - CONF_FLOWCONTROL: None, + CONF_FLOW_CONTROL: None, }, CONF_RADIO_TYPE: "znp", }, @@ -1954,3 +1938,28 @@ async def test_discovery_wrong_firmware_installed(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.ABORT assert result["reason"] == "wrong_firmware_installed" + + +@pytest.mark.parametrize( + ("old_type", "new_type"), + [ + ("ezsp", "ezsp"), + ("ti_cc", "znp"), # only one that should change + ("znp", "znp"), + ("deconz", "deconz"), + ], +) +async def test_migration_ti_cc_to_znp( + old_type: str, new_type: str, hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test zigpy-cc to zigpy-znp config migration.""" + config_entry.data = {**config_entry.data, CONF_RADIO_TYPE: old_type} + config_entry.version = 2 + config_entry.add_to_hass(hass) + + with patch("homeassistant.components.zha.async_setup_entry", return_value=True): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.version > 2 + assert config_entry.data[CONF_RADIO_TYPE] == new_type diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 2a0a241c8640..4f5209207046 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -4,22 +4,21 @@ from unittest.mock import MagicMock, patch import pytest from zigpy.application import ControllerApplication -import zigpy.exceptions import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting -from homeassistant.components.zha.core.const import RadioType -from homeassistant.components.zha.core.device import ZHADevice +from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.core.helpers import get_zha_gateway from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from .common import async_find_group_entity_id from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE +from tests.common import MockConfigEntry + IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" @@ -224,101 +223,6 @@ async def test_gateway_create_group_with_id( assert zha_group.group_id == 0x1234 -@patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_devices", - MagicMock(), -) -@patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_groups", - MagicMock(), -) -@patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) -@pytest.mark.parametrize( - "startup_effect", - [ - [asyncio.TimeoutError(), FileNotFoundError(), None], - [asyncio.TimeoutError(), None], - [None], - ], -) -async def test_gateway_initialize_success( - startup_effect: list[Exception | None], - hass: HomeAssistant, - device_light_1: ZHADevice, - coordinator: ZHADevice, - zigpy_app_controller: ControllerApplication, -) -> None: - """Test ZHA initializing the gateway successfully.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - - zigpy_app_controller.startup.side_effect = startup_effect - zigpy_app_controller.startup.reset_mock() - - with patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ): - await zha_gateway.async_initialize() - - assert zigpy_app_controller.startup.call_count == len(startup_effect) - device_light_1.async_cleanup_handles() - - -@patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) -async def test_gateway_initialize_failure( - hass: HomeAssistant, - device_light_1: ZHADevice, - coordinator: ZHADevice, - zigpy_app_controller: ControllerApplication, -) -> None: - """Test ZHA failing to initialize the gateway.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - - zigpy_app_controller.startup.side_effect = [ - asyncio.TimeoutError(), - RuntimeError(), - FileNotFoundError(), - ] - zigpy_app_controller.startup.reset_mock() - - with patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ), pytest.raises(FileNotFoundError): - await zha_gateway.async_initialize() - - assert zigpy_app_controller.startup.call_count == 3 - - -@patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01) -async def test_gateway_initialize_failure_transient( - hass: HomeAssistant, - device_light_1: ZHADevice, - coordinator: ZHADevice, - zigpy_app_controller: ControllerApplication, -) -> None: - """Test ZHA failing to initialize the gateway but with a transient error.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - - zigpy_app_controller.startup.side_effect = [ - RuntimeError(), - zigpy.exceptions.TransientConnectionError(), - ] - zigpy_app_controller.startup.reset_mock() - - with patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ), pytest.raises(ConfigEntryNotReady): - await zha_gateway.async_initialize() - - # Initialization immediately stops and is retried after TransientConnectionError - assert zigpy_app_controller.startup.call_count == 2 - - @patch( "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_devices", MagicMock(), @@ -340,22 +244,25 @@ async def test_gateway_initialize_bellows_thread( thread_state: bool, config_override: dict, hass: HomeAssistant, - coordinator: ZHADevice, zigpy_app_controller: ControllerApplication, + config_entry: MockConfigEntry, ) -> None: """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None + config_entry.data = dict(config_entry.data) + config_entry.data["device"]["path"] = device_path + config_entry.add_to_hass(hass) - zha_gateway.config_entry.data = dict(zha_gateway.config_entry.data) - zha_gateway.config_entry.data["device"]["path"] = device_path - zha_gateway._config.setdefault("zigpy_config", {}).update(config_override) + zha_gateway = ZHAGateway(hass, {"zigpy_config": config_override}, config_entry) - await zha_gateway.async_initialize() + with patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ) as mock_new: + await zha_gateway.async_initialize() - RadioType.ezsp.controller.new.mock_calls[-1].kwargs["config"][ - "use_thread" - ] is thread_state + mock_new.mock_calls[-1].kwargs["config"]["use_thread"] is thread_state + + await zha_gateway.shutdown() @pytest.mark.parametrize( @@ -373,15 +280,14 @@ async def test_gateway_force_multi_pan_channel( config_override: dict, expected_channel: int | None, hass: HomeAssistant, - coordinator, + config_entry: MockConfigEntry, ) -> None: """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None + config_entry.data = dict(config_entry.data) + config_entry.data["device"]["path"] = device_path + config_entry.add_to_hass(hass) - zha_gateway.config_entry.data = dict(zha_gateway.config_entry.data) - zha_gateway.config_entry.data["device"]["path"] = device_path - zha_gateway._config.setdefault("zigpy_config", {}).update(config_override) + zha_gateway = ZHAGateway(hass, {"zigpy_config": config_override}, config_entry) _, config = zha_gateway.get_application_controller_data() assert config["network"]["channel"] == expected_channel diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index ad6ab4e351e0..c2e9469c2396 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -1,5 +1,6 @@ """Tests for ZHA integration init.""" import asyncio +import typing from unittest.mock import AsyncMock, Mock, patch import pytest @@ -9,6 +10,7 @@ from zigpy.exceptions import TransientConnectionError from homeassistant.components.zha.core.const import ( CONF_BAUDRATE, + CONF_FLOW_CONTROL, CONF_RADIO_TYPE, CONF_USB_PATH, DOMAIN, @@ -61,9 +63,8 @@ async def test_migration_from_v1_no_baudrate( assert config_entry_v1.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE assert CONF_DEVICE in config_entry_v1.data assert config_entry_v1.data[CONF_DEVICE][CONF_DEVICE_PATH] == DATA_PORT_PATH - assert CONF_BAUDRATE not in config_entry_v1.data[CONF_DEVICE] assert CONF_USB_PATH not in config_entry_v1.data - assert config_entry_v1.version == 3 + assert config_entry_v1.version == 4 @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) @@ -80,7 +81,7 @@ async def test_migration_from_v1_with_baudrate( assert CONF_USB_PATH not in config_entry_v1.data assert CONF_BAUDRATE in config_entry_v1.data[CONF_DEVICE] assert config_entry_v1.data[CONF_DEVICE][CONF_BAUDRATE] == 115200 - assert config_entry_v1.version == 3 + assert config_entry_v1.version == 4 @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) @@ -95,8 +96,7 @@ async def test_migration_from_v1_wrong_baudrate( assert CONF_DEVICE in config_entry_v1.data assert config_entry_v1.data[CONF_DEVICE][CONF_DEVICE_PATH] == DATA_PORT_PATH assert CONF_USB_PATH not in config_entry_v1.data - assert CONF_BAUDRATE not in config_entry_v1.data[CONF_DEVICE] - assert config_entry_v1.version == 3 + assert config_entry_v1.version == 4 @pytest.mark.skipif( @@ -149,23 +149,74 @@ async def test_setup_with_v3_cleaning_uri( mock_zigpy_connect: ControllerApplication, ) -> None: """Test migration of config entry from v3, applying corrections to the port path.""" - config_entry_v3 = MockConfigEntry( + config_entry_v4 = MockConfigEntry( domain=DOMAIN, data={ CONF_RADIO_TYPE: DATA_RADIO_TYPE, - CONF_DEVICE: {CONF_DEVICE_PATH: path, CONF_BAUDRATE: 115200}, + CONF_DEVICE: { + CONF_DEVICE_PATH: path, + CONF_BAUDRATE: 115200, + CONF_FLOW_CONTROL: None, + }, }, - version=3, + version=4, ) - config_entry_v3.add_to_hass(hass) + config_entry_v4.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry_v3.entry_id) + await hass.config_entries.async_setup(config_entry_v4.entry_id) await hass.async_block_till_done() - await hass.config_entries.async_unload(config_entry_v3.entry_id) + await hass.config_entries.async_unload(config_entry_v4.entry_id) - assert config_entry_v3.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE - assert config_entry_v3.data[CONF_DEVICE][CONF_DEVICE_PATH] == cleaned_path - assert config_entry_v3.version == 3 + assert config_entry_v4.data[CONF_RADIO_TYPE] == DATA_RADIO_TYPE + assert config_entry_v4.data[CONF_DEVICE][CONF_DEVICE_PATH] == cleaned_path + assert config_entry_v4.version == 4 + + +@pytest.mark.parametrize( + ( + "radio_type", + "old_baudrate", + "old_flow_control", + "new_baudrate", + "new_flow_control", + ), + [ + ("znp", None, None, 115200, None), + ("znp", None, "software", 115200, "software"), + ("znp", 57600, "software", 57600, "software"), + ("deconz", None, None, 38400, None), + ("deconz", 115200, None, 115200, None), + ], +) +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_migration_baudrate_and_flow_control( + radio_type: str, + old_baudrate: int, + old_flow_control: typing.Literal["hardware", "software", None], + new_baudrate: int, + new_flow_control: typing.Literal["hardware", "software", None], + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test baudrate and flow control migration.""" + config_entry.data = { + **config_entry.data, + CONF_RADIO_TYPE: radio_type, + CONF_DEVICE: { + CONF_BAUDRATE: old_baudrate, + CONF_FLOW_CONTROL: old_flow_control, + CONF_DEVICE_PATH: "/dev/null", + }, + } + config_entry.version = 3 + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.version > 3 + assert config_entry.data[CONF_DEVICE][CONF_BAUDRATE] == new_baudrate + assert config_entry.data[CONF_DEVICE][CONF_FLOW_CONTROL] == new_flow_control @patch( diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index 9c79578843c9..d168e2e57b12 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -95,6 +95,7 @@ def test_detect_radio_hardware_failure(hass: HomeAssistant) -> None: assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.OTHER +@patch("homeassistant.components.zha.STARTUP_RETRIES", new=1) @pytest.mark.parametrize( ("detected_hardware", "expected_learn_more_url"), [ @@ -188,6 +189,7 @@ async def test_multipan_firmware_no_repair_on_probe_failure( assert issue is None +@patch("homeassistant.components.zha.STARTUP_RETRIES", new=1) async def test_multipan_firmware_retry_on_probe_ezsp( hass: HomeAssistant, config_entry: MockConfigEntry, @@ -312,6 +314,8 @@ async def test_inconsistent_settings_keep_new( data = await resp.json() assert data["type"] == "create_entry" + await hass.config_entries.async_unload(config_entry.entry_id) + assert ( issue_registry.async_get_issue( domain=DOMAIN, @@ -388,6 +392,8 @@ async def test_inconsistent_settings_restore_old( data = await resp.json() assert data["type"] == "create_entry" + await hass.config_entries.async_unload(config_entry.entry_id) + assert ( issue_registry.async_get_issue( domain=DOMAIN, diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index d914c88c0c29..44006ea6ca1d 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -62,7 +62,7 @@ from .conftest import ( ) from .data import BASE_CUSTOM_CONFIGURATION, CONFIG_WITH_ALARM_OPTIONS -from tests.common import MockUser +from tests.common import MockConfigEntry, MockUser IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7" IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" @@ -295,10 +295,12 @@ async def test_get_zha_config_with_alarm( async def test_update_zha_config( - zha_client, app_controller: ControllerApplication + hass: HomeAssistant, + config_entry: MockConfigEntry, + zha_client, + app_controller: ControllerApplication, ) -> None: """Test updating ZHA custom configuration.""" - configuration: dict = deepcopy(CONFIG_WITH_ALARM_OPTIONS) configuration["data"]["zha_options"]["default_light_transition"] = 10 @@ -312,10 +314,12 @@ async def test_update_zha_config( msg = await zha_client.receive_json() assert msg["success"] - await zha_client.send_json({ID: 6, TYPE: "zha/configuration"}) - msg = await zha_client.receive_json() - configuration = msg["result"] - assert configuration == configuration + await zha_client.send_json({ID: 6, TYPE: "zha/configuration"}) + msg = await zha_client.receive_json() + configuration = msg["result"] + assert configuration == configuration + + await hass.config_entries.async_unload(config_entry.entry_id) async def test_device_not_found(zha_client) -> None: From 6dc818b6822ce743eaf91d5f7fd2487c2047aa5a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Nov 2023 11:38:23 +0100 Subject: [PATCH 209/265] Add proj dependency to our wheels builder (#104699) --- .github/workflows/wheels.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3b23f1b5b057..be9d393d9fdd 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -199,7 +199,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -214,7 +214,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -228,7 +228,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -242,7 +242,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" From d9c0acc1d27e8991b06afdf0fcdbbc580519b658 Mon Sep 17 00:00:00 2001 From: Stefan Rado <628587+kroimon@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:45:15 +0100 Subject: [PATCH 210/265] Partially revert #103807: Remove deprecated aux heat support from ESPHome climate entities (#104694) --- homeassistant/components/esphome/climate.py | 16 ----- tests/components/esphome/test_climate.py | 66 +-------------------- 2 files changed, 1 insertion(+), 81 deletions(-) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 73b326204b56..08ed2f1109d9 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -173,8 +173,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti features |= ClimateEntityFeature.TARGET_TEMPERATURE if self._static_info.supports_target_humidity: features |= ClimateEntityFeature.TARGET_HUMIDITY - if self._static_info.supports_aux_heat: - features |= ClimateEntityFeature.AUX_HEAT if self.preset_modes: features |= ClimateEntityFeature.PRESET_MODE if self.fan_modes: @@ -272,12 +270,6 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti """Return the humidity we try to reach.""" return round(self._state.target_humidity) - @property - @esphome_state_property - def is_aux_heat(self) -> bool: - """Return the auxiliary heater state.""" - return self._state.aux_heat - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature (and operation mode if set).""" data: dict[str, Any] = {"key": self._key} @@ -326,11 +318,3 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti await self._client.climate_command( key=self._key, swing_mode=_SWING_MODES.from_hass(swing_mode) ) - - async def async_turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - await self._client.climate_command(key=self._key, aux_heat=True) - - async def async_turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - await self._client.climate_command(key=self._key, aux_heat=False) diff --git a/tests/components/esphome/test_climate.py b/tests/components/esphome/test_climate.py index 8f0b8f96c56b..065890fd623c 100644 --- a/tests/components/esphome/test_climate.py +++ b/tests/components/esphome/test_climate.py @@ -15,7 +15,6 @@ from aioesphomeapi import ( ) from homeassistant.components.climate import ( - ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_FAN_MODE, ATTR_HUMIDITY, @@ -29,7 +28,6 @@ from homeassistant.components.climate import ( ATTR_TEMPERATURE, DOMAIN as CLIMATE_DOMAIN, FAN_HIGH, - SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, @@ -39,7 +37,7 @@ from homeassistant.components.climate import ( SWING_BOTH, HVACMode, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant @@ -321,68 +319,6 @@ async def test_climate_entity_with_step_and_target_temp( mock_client.climate_command.reset_mock() -async def test_climate_entity_with_aux_heat( - hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry -) -> None: - """Test a generic climate entity with aux heat.""" - entity_info = [ - ClimateInfo( - object_id="myclimate", - key=1, - name="my climate", - unique_id="my_climate", - supports_current_temperature=True, - supports_two_point_target_temperature=True, - supports_action=True, - visual_min_temperature=10.0, - visual_max_temperature=30.0, - supports_aux_heat=True, - ) - ] - states = [ - ClimateState( - key=1, - mode=ClimateMode.HEAT, - action=ClimateAction.HEATING, - current_temperature=30, - target_temperature=20, - fan_mode=ClimateFanMode.AUTO, - swing_mode=ClimateSwingMode.BOTH, - aux_heat=True, - ) - ] - user_service = [] - await mock_generic_device_entry( - mock_client=mock_client, - entity_info=entity_info, - user_service=user_service, - states=states, - ) - state = hass.states.get("climate.test_myclimate") - assert state is not None - assert state.state == HVACMode.HEAT - attributes = state.attributes - assert attributes[ATTR_AUX_HEAT] == STATE_ON - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_AUX_HEAT, - {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_AUX_HEAT: False}, - blocking=True, - ) - mock_client.climate_command.assert_has_calls([call(key=1, aux_heat=False)]) - mock_client.climate_command.reset_mock() - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_AUX_HEAT, - {ATTR_ENTITY_ID: "climate.test_myclimate", ATTR_AUX_HEAT: True}, - blocking=True, - ) - mock_client.climate_command.assert_has_calls([call(key=1, aux_heat=True)]) - mock_client.climate_command.reset_mock() - - async def test_climate_entity_with_humidity( hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry ) -> None: From 31cab5803c36ce7650dd89510b2dbf0283111c0b Mon Sep 17 00:00:00 2001 From: schelv <13403863+schelv@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:25:06 +0100 Subject: [PATCH 211/265] Add Option For Kelvin Unit To Color Temperature Selector (#103799) --- homeassistant/components/light/services.yaml | 15 +++++------ homeassistant/helpers/selector.py | 28 ++++++++++++++++++-- tests/helpers/test_selector.py | 10 +++++++ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 433da53a5702..fb7a1539944e 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -252,8 +252,9 @@ turn_on: - light.ColorMode.RGBWW selector: color_temp: - min_mireds: 153 - max_mireds: 500 + unit: "mired" + min: 153 + max: 500 kelvin: filter: attribute: @@ -266,11 +267,10 @@ turn_on: - light.ColorMode.RGBWW advanced: true selector: - number: + color_temp: + unit: "kelvin" min: 2000 max: 6500 - step: 100 - unit_of_measurement: K brightness: filter: attribute: @@ -637,11 +637,10 @@ toggle: - light.ColorMode.RGBWW advanced: true selector: - number: + color_temp: + unit: "kelvin" min: 2000 max: 6500 - step: 100 - unit_of_measurement: K brightness: filter: attribute: diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index ac5166911ffa..bda2440cfb35 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -425,10 +425,20 @@ class ColorRGBSelector(Selector[ColorRGBSelectorConfig]): class ColorTempSelectorConfig(TypedDict, total=False): """Class to represent a color temp selector config.""" + unit: ColorTempSelectorUnit + min: int + max: int max_mireds: int min_mireds: int +class ColorTempSelectorUnit(StrEnum): + """Possible units for a color temperature selector.""" + + KELVIN = "kelvin" + MIRED = "mired" + + @SELECTORS.register("color_temp") class ColorTempSelector(Selector[ColorTempSelectorConfig]): """Selector of an color temperature.""" @@ -437,6 +447,11 @@ class ColorTempSelector(Selector[ColorTempSelectorConfig]): CONFIG_SCHEMA = vol.Schema( { + vol.Optional("unit", default=ColorTempSelectorUnit.MIRED): vol.All( + vol.Coerce(ColorTempSelectorUnit), lambda val: val.value + ), + vol.Optional("min"): vol.Coerce(int), + vol.Optional("max"): vol.Coerce(int), vol.Optional("max_mireds"): vol.Coerce(int), vol.Optional("min_mireds"): vol.Coerce(int), } @@ -448,11 +463,20 @@ class ColorTempSelector(Selector[ColorTempSelectorConfig]): def __call__(self, data: Any) -> int: """Validate the passed selection.""" + range_min = self.config.get("min") + range_max = self.config.get("max") + + if not range_min: + range_min = self.config.get("min_mireds") + + if not range_max: + range_max = self.config.get("max_mireds") + value: int = vol.All( vol.Coerce(float), vol.Range( - min=self.config.get("min_mireds"), - max=self.config.get("max_mireds"), + min=range_min, + max=range_max, ), )(data) return value diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 1e449fd103a4..93c342384fdc 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -907,6 +907,16 @@ def test_rgb_color_selector_schema( (100, 200), (99, 201), ), + ( + {"unit": "mired", "min": 100, "max": 200}, + (100, 200), + (99, 201), + ), + ( + {"unit": "kelvin", "min": 1000, "max": 2000}, + (1000, 2000), + (999, 2001), + ), ), ) def test_color_tempselector_schema( From fc7b17d35b42e640499067e2e00a27b503c25540 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 29 Nov 2023 12:33:25 +0100 Subject: [PATCH 212/265] Handle server disconnection for Comelit devices (#104583) --- homeassistant/components/comelit/coordinator.py | 4 ++-- homeassistant/components/comelit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py index d3bc973429b9..1573d5cb627c 100644 --- a/homeassistant/components/comelit/coordinator.py +++ b/homeassistant/components/comelit/coordinator.py @@ -68,13 +68,13 @@ class ComelitSerialBridge(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Update device data.""" _LOGGER.debug("Polling Comelit Serial Bridge host: %s", self._host) + try: await self.api.login() + return await self.api.get_all_devices() except exceptions.CannotConnect as err: _LOGGER.warning("Connection error for %s", self._host) await self.api.close() raise UpdateFailed(f"Error fetching data: {repr(err)}") from err except exceptions.CannotAuthenticate: raise ConfigEntryAuthFailed - - return await self.api.get_all_devices() diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index 77796ac7e7f0..89157b542554 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/comelit", "iot_class": "local_polling", "loggers": ["aiocomelit"], - "requirements": ["aiocomelit==0.5.2"] + "requirements": ["aiocomelit==0.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 756de3bd3061..fe298d256da6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -212,7 +212,7 @@ aiobafi6==0.9.0 aiobotocore==2.6.0 # homeassistant.components.comelit -aiocomelit==0.5.2 +aiocomelit==0.6.2 # homeassistant.components.dhcp aiodiscover==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bbaaf1bcb16a..6c8a9e7d10cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -191,7 +191,7 @@ aiobafi6==0.9.0 aiobotocore==2.6.0 # homeassistant.components.comelit -aiocomelit==0.5.2 +aiocomelit==0.6.2 # homeassistant.components.dhcp aiodiscover==1.5.1 From 9741380cc09592b74eb34da11f66e48f4706a05f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Nov 2023 12:41:47 +0100 Subject: [PATCH 213/265] Add proj-util dependency to our wheels builder (#104708) --- .github/workflows/wheels.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index be9d393d9fdd..01cdf0089177 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -199,7 +199,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-util;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -214,7 +214,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-util;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -228,7 +228,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-util;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -242,7 +242,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-util;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" From 861bb48ab6781436cbb214169f97bd4cdc666330 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 29 Nov 2023 13:07:52 +0100 Subject: [PATCH 214/265] Assign specific error code for HomeAssistantError on websocket_api connection exceptions (#104700) Assign specific error code for HomeAssistantError --- homeassistant/components/websocket_api/connection.py | 2 +- .../components/application_credentials/test_init.py | 2 +- tests/components/blueprint/test_websocket_api.py | 4 ++-- tests/components/config/test_device_registry.py | 12 ++++++------ tests/components/fritzbox/test_init.py | 2 +- tests/components/group/test_config_flow.py | 2 +- tests/components/input_select/test_init.py | 4 ++-- tests/components/tag/test_init.py | 2 +- tests/components/template/test_config_flow.py | 2 +- tests/components/websocket_api/test_commands.py | 2 +- tests/components/websocket_api/test_connection.py | 8 ++++---- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 551d4b0d1fea..25b6c90d1ba2 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -271,7 +271,7 @@ class ActiveConnection: err_message = "Timeout" elif isinstance(err, HomeAssistantError): err_message = str(err) - code = const.ERR_UNKNOWN_ERROR + code = const.ERR_HOME_ASSISTANT_ERROR translation_domain = err.translation_domain translation_key = err.translation_key translation_placeholders = err.translation_placeholders diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index cc56894cf0d1..807eff4ef8de 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -479,7 +479,7 @@ async def test_config_flow( resp = await client.cmd("delete", {"application_credentials_id": ID}) assert not resp.get("success") assert "error" in resp - assert resp["error"].get("code") == "unknown_error" + assert resp["error"].get("code") == "home_assistant_error" assert ( resp["error"].get("message") == "Cannot delete credential in use by integration fake_integration" diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 213dff89597d..b0439896c258 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -438,7 +438,7 @@ async def test_delete_blueprint_in_use_by_automation( assert msg["id"] == 9 assert not msg["success"] assert msg["error"] == { - "code": "unknown_error", + "code": "home_assistant_error", "message": "Blueprint in use", } @@ -484,6 +484,6 @@ async def test_delete_blueprint_in_use_by_script( assert msg["id"] == 9 assert not msg["success"] assert msg["error"] == { - "code": "unknown_error", + "code": "home_assistant_error", "message": "Blueprint in use", } diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 87bb9cc94090..4a784a6eff1f 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -242,7 +242,7 @@ async def test_remove_config_entry_from_device( response = await ws_client.receive_json() assert not response["success"] - assert response["error"]["code"] == "unknown_error" + assert response["error"]["code"] == "home_assistant_error" # Make async_remove_config_entry_device return True can_remove = True @@ -365,7 +365,7 @@ async def test_remove_config_entry_from_device_fails( response = await ws_client.receive_json() assert not response["success"] - assert response["error"]["code"] == "unknown_error" + assert response["error"]["code"] == "home_assistant_error" assert response["error"]["message"] == "Unknown config entry" # Try removing a config entry which does not support removal from the device @@ -380,7 +380,7 @@ async def test_remove_config_entry_from_device_fails( response = await ws_client.receive_json() assert not response["success"] - assert response["error"]["code"] == "unknown_error" + assert response["error"]["code"] == "home_assistant_error" assert ( response["error"]["message"] == "Config entry does not support device removal" ) @@ -397,7 +397,7 @@ async def test_remove_config_entry_from_device_fails( response = await ws_client.receive_json() assert not response["success"] - assert response["error"]["code"] == "unknown_error" + assert response["error"]["code"] == "home_assistant_error" assert response["error"]["message"] == "Unknown device" # Try removing a config entry from a device which it's not connected to @@ -428,7 +428,7 @@ async def test_remove_config_entry_from_device_fails( response = await ws_client.receive_json() assert not response["success"] - assert response["error"]["code"] == "unknown_error" + assert response["error"]["code"] == "home_assistant_error" assert response["error"]["message"] == "Config entry not in device" # Try removing a config entry which can't be loaded from a device - allowed @@ -443,5 +443,5 @@ async def test_remove_config_entry_from_device_fails( response = await ws_client.receive_json() assert not response["success"] - assert response["error"]["code"] == "unknown_error" + assert response["error"]["code"] == "home_assistant_error" assert response["error"]["message"] == "Integration not found" diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 5c8d30772f0b..b82732043250 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -296,7 +296,7 @@ async def test_remove_device( ) response = await ws_client.receive_json() assert not response["success"] - assert response["error"]["code"] == "unknown_error" + assert response["error"]["code"] == "home_assistant_error" await hass.async_block_till_done() # try to delete orphan_device diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 3189e344c62b..7b83ed9eb0d7 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -699,4 +699,4 @@ async def test_option_flow_sensor_preview_config_entry_removed( ) msg = await client.receive_json() assert not msg["success"] - assert msg["error"] == {"code": "unknown_error", "message": "Unknown error"} + assert msg["error"] == {"code": "home_assistant_error", "message": "Unknown error"} diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 03c503ae4948..3978d0cf1750 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -740,7 +740,7 @@ async def test_update_duplicates( ) resp = await client.receive_json() assert not resp["success"] - assert resp["error"]["code"] == "unknown_error" + assert resp["error"]["code"] == "home_assistant_error" assert resp["error"]["message"] == "Duplicate options are not allowed" state = hass.states.get(input_entity_id) @@ -812,7 +812,7 @@ async def test_ws_create_duplicates( ) resp = await client.receive_json() assert not resp["success"] - assert resp["error"]["code"] == "unknown_error" + assert resp["error"]["code"] == "home_assistant_error" assert resp["error"]["message"] == "Duplicate options are not allowed" assert not hass.states.get(input_entity_id) diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index 3e034d2b9f23..5d54f31b13a0 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -131,5 +131,5 @@ async def test_tag_id_exists( await client.send_json({"id": 2, "type": f"{DOMAIN}/create", "tag_id": "test tag"}) response = await client.receive_json() assert not response["success"] - assert response["error"]["code"] == "unknown_error" + assert response["error"]["code"] == "home_assistant_error" assert len(changes) == 0 diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index f4cfe90b9f03..b95a68afd858 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -845,4 +845,4 @@ async def test_option_flow_sensor_preview_config_entry_removed( ) msg = await client.receive_json() assert not msg["success"] - assert msg["error"] == {"code": "unknown_error", "message": "Unknown error"} + assert msg["error"] == {"code": "home_assistant_error", "message": "Unknown error"} diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index c573fb85bb1b..127b45484be3 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -2327,7 +2327,7 @@ async def test_execute_script( translation_key="test_error", translation_placeholders={"option": "bla"}, ), - "unknown_error", + "home_assistant_error", ), ( ServiceValidationError( diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index 4bc736481c4c..80936d30752c 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -39,15 +39,15 @@ from tests.common import MockUser ), ( exceptions.HomeAssistantError("Failed to do X"), - websocket_api.ERR_UNKNOWN_ERROR, + websocket_api.ERR_HOME_ASSISTANT_ERROR, "Failed to do X", - "Error handling message: Failed to do X (unknown_error) Mock User from 127.0.0.42 (Browser)", + "Error handling message: Failed to do X (home_assistant_error) Mock User from 127.0.0.42 (Browser)", ), ( exceptions.ServiceValidationError("Failed to do X"), - websocket_api.ERR_UNKNOWN_ERROR, + websocket_api.ERR_HOME_ASSISTANT_ERROR, "Failed to do X", - "Error handling message: Failed to do X (unknown_error) Mock User from 127.0.0.42 (Browser)", + "Error handling message: Failed to do X (home_assistant_error) Mock User from 127.0.0.42 (Browser)", ), ( ValueError("Really bad"), From 5f44dadb66068a04982340d8cce941e890f9536d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 29 Nov 2023 13:11:15 +0100 Subject: [PATCH 215/265] Rename todo due_date_time parameter to due_datetime (#104698) * Rename todo due_date_time parameter to due_datetime * Apply suggestions from code review --------- Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> --- homeassistant/components/todo/__init__.py | 8 +++----- homeassistant/components/todo/const.py | 2 +- homeassistant/components/todo/services.yaml | 4 ++-- homeassistant/components/todo/strings.json | 8 ++++---- tests/components/local_todo/test_todo.py | 6 +++--- tests/components/todo/test_init.py | 14 +++++++------- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index 814138dcb7f2..c0e0303d76ee 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -35,7 +35,7 @@ from .const import ( ATTR_DESCRIPTION, ATTR_DUE, ATTR_DUE_DATE, - ATTR_DUE_DATE_TIME, + ATTR_DUE_DATETIME, DOMAIN, TodoItemStatus, TodoListEntityFeature, @@ -73,7 +73,7 @@ TODO_ITEM_FIELDS = [ required_feature=TodoListEntityFeature.SET_DUE_DATE_ON_ITEM, ), TodoItemFieldDescription( - service_field=ATTR_DUE_DATE_TIME, + service_field=ATTR_DUE_DATETIME, validation=vol.All(cv.datetime, dt_util.as_local), todo_item_field=ATTR_DUE, required_feature=TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM, @@ -89,9 +89,7 @@ TODO_ITEM_FIELDS = [ TODO_ITEM_FIELD_SCHEMA = { vol.Optional(desc.service_field): desc.validation for desc in TODO_ITEM_FIELDS } -TODO_ITEM_FIELD_VALIDATIONS = [ - cv.has_at_most_one_key(ATTR_DUE_DATE, ATTR_DUE_DATE_TIME) -] +TODO_ITEM_FIELD_VALIDATIONS = [cv.has_at_most_one_key(ATTR_DUE_DATE, ATTR_DUE_DATETIME)] def _validate_supported_features( diff --git a/homeassistant/components/todo/const.py b/homeassistant/components/todo/const.py index 95e190cb3e37..a605f9fcba25 100644 --- a/homeassistant/components/todo/const.py +++ b/homeassistant/components/todo/const.py @@ -6,7 +6,7 @@ DOMAIN = "todo" ATTR_DUE = "due" ATTR_DUE_DATE = "due_date" -ATTR_DUE_DATE_TIME = "due_date_time" +ATTR_DUE_DATETIME = "due_datetime" ATTR_DESCRIPTION = "description" diff --git a/homeassistant/components/todo/services.yaml b/homeassistant/components/todo/services.yaml index 390aa82753aa..bc7da7db9413 100644 --- a/homeassistant/components/todo/services.yaml +++ b/homeassistant/components/todo/services.yaml @@ -29,7 +29,7 @@ add_item: example: "2023-11-17" selector: date: - due_date_time: + due_datetime: example: "2023-11-17 13:30:00" selector: datetime: @@ -65,7 +65,7 @@ update_item: example: "2023-11-17" selector: date: - due_date_time: + due_datetime: example: "2023-11-17 13:30:00" selector: datetime: diff --git a/homeassistant/components/todo/strings.json b/homeassistant/components/todo/strings.json index bca32f850ebc..3da921a8f477 100644 --- a/homeassistant/components/todo/strings.json +++ b/homeassistant/components/todo/strings.json @@ -28,8 +28,8 @@ "name": "Due date", "description": "The date the to-do item is expected to be completed." }, - "due_date_time": { - "name": "Due date time", + "due_datetime": { + "name": "Due date and time", "description": "The date and time the to-do item is expected to be completed." }, "description": { @@ -58,8 +58,8 @@ "name": "Due date", "description": "The date the to-do item is expected to be completed." }, - "due_date_time": { - "name": "Due date time", + "due_datetime": { + "name": "Due date and time", "description": "The date and time the to-do item is expected to be completed." }, "description": { diff --git a/tests/components/local_todo/test_todo.py b/tests/components/local_todo/test_todo.py index b2c79ef4bd10..67d0703ca7cb 100644 --- a/tests/components/local_todo/test_todo.py +++ b/tests/components/local_todo/test_todo.py @@ -71,7 +71,7 @@ def set_time_zone(hass: HomeAssistant) -> None: ({}, {}), ({"due_date": "2023-11-17"}, {"due": "2023-11-17"}), ( - {"due_date_time": "2023-11-17T11:30:00+00:00"}, + {"due_datetime": "2023-11-17T11:30:00+00:00"}, {"due": "2023-11-17T05:30:00-06:00"}, ), ({"description": "Additional detail"}, {"description": "Additional detail"}), @@ -118,7 +118,7 @@ async def test_add_item( ({}, {}), ({"due_date": "2023-11-17"}, {"due": "2023-11-17"}), ( - {"due_date_time": "2023-11-17T11:30:00+00:00"}, + {"due_datetime": "2023-11-17T11:30:00+00:00"}, {"due": "2023-11-17T05:30:00-06:00"}, ), ({"description": "Additional detail"}, {"description": "Additional detail"}), @@ -213,7 +213,7 @@ async def test_bulk_remove( ({"status": "completed"}, {"status": "completed"}, "0"), ({"due_date": "2023-11-17"}, {"due": "2023-11-17"}, "1"), ( - {"due_date_time": "2023-11-17T11:30:00+00:00"}, + {"due_datetime": "2023-11-17T11:30:00+00:00"}, {"due": "2023-11-17T05:30:00-06:00"}, "1", ), diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 0071d4ada867..90b06858e00f 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -354,10 +354,10 @@ async def test_add_item_service_raises( ( { "item": "Submit forms", - "due_date_time": f"2023-11-17T17:00:00{TEST_OFFSET}", + "due_datetime": f"2023-11-17T17:00:00{TEST_OFFSET}", }, ValueError, - "does not support setting field 'due_date_time'", + "does not support setting field 'due_datetime'", ), ], ) @@ -396,7 +396,7 @@ async def test_add_item_service_invalid_input( ), ( TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM, - {"item": "New item", "due_date_time": f"2023-11-13T17:00:00{TEST_OFFSET}"}, + {"item": "New item", "due_datetime": f"2023-11-13T17:00:00{TEST_OFFSET}"}, TodoItem( summary="New item", status=TodoItemStatus.NEEDS_ACTION, @@ -405,7 +405,7 @@ async def test_add_item_service_invalid_input( ), ( TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM, - {"item": "New item", "due_date_time": "2023-11-13T17:00:00+00:00"}, + {"item": "New item", "due_datetime": "2023-11-13T17:00:00+00:00"}, TodoItem( summary="New item", status=TodoItemStatus.NEEDS_ACTION, @@ -414,7 +414,7 @@ async def test_add_item_service_invalid_input( ), ( TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM, - {"item": "New item", "due_date_time": "2023-11-13"}, + {"item": "New item", "due_datetime": "2023-11-13"}, TodoItem( summary="New item", status=TodoItemStatus.NEEDS_ACTION, @@ -663,7 +663,7 @@ async def test_update_item_service_invalid_input( @pytest.mark.parametrize( ("update_data"), [ - ({"due_date_time": f"2023-11-13T17:00:00{TEST_OFFSET}"}), + ({"due_datetime": f"2023-11-13T17:00:00{TEST_OFFSET}"}), ({"due_date": "2023-11-13"}), ({"description": "Submit revised draft"}), ], @@ -697,7 +697,7 @@ async def test_update_todo_item_field_unsupported( ), ( TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM, - {"due_date_time": f"2023-11-13T17:00:00{TEST_OFFSET}"}, + {"due_datetime": f"2023-11-13T17:00:00{TEST_OFFSET}"}, TodoItem( uid="1", due=datetime.datetime(2023, 11, 13, 17, 0, 0, tzinfo=TEST_TIMEZONE), From cf23de1c488e3d7b89a76adf4cc26b5584a05ba6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Nov 2023 13:15:37 +0100 Subject: [PATCH 216/265] Add proj-dev dependency to our wheels builder (#104711) --- .github/workflows/wheels.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 01cdf0089177..9d16954cd097 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -199,7 +199,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-util;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-dev;proj-util;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -214,7 +214,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-util;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-dev;proj-util;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -228,7 +228,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-util;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-dev;proj-util;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -242,7 +242,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-util;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-dev;proj-util;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" From e5a7446afefb0d9dfd47b0fae803a7b7314f13e4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 29 Nov 2023 13:35:32 +0100 Subject: [PATCH 217/265] Use id as location data in Trafikverket Camera (#104473) --- .../trafikverket_camera/__init__.py | 43 +++++++++--- .../trafikverket_camera/config_flow.py | 18 ++--- .../trafikverket_camera/coordinator.py | 8 +-- .../trafikverket_camera/__init__.py | 7 +- .../trafikverket_camera/conftest.py | 6 +- .../trafikverket_camera/test_binary_sensor.py | 2 +- .../trafikverket_camera/test_camera.py | 8 +-- .../trafikverket_camera/test_config_flow.py | 20 +++--- .../trafikverket_camera/test_coordinator.py | 16 ++--- .../trafikverket_camera/test_init.py | 68 ++++++++++++++----- .../trafikverket_camera/test_recorder.py | 6 +- .../trafikverket_camera/test_sensor.py | 12 ++-- 12 files changed, 135 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/trafikverket_camera/__init__.py b/homeassistant/components/trafikverket_camera/__init__.py index d9d28cfe13b7..3ac3ce358824 100644 --- a/homeassistant/components/trafikverket_camera/__init__.py +++ b/homeassistant/components/trafikverket_camera/__init__.py @@ -6,7 +6,7 @@ import logging from pytrafikverket.trafikverket_camera import TrafikverketCamera from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -42,13 +42,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate old entry.""" + api_key = entry.data[CONF_API_KEY] + web_session = async_get_clientsession(hass) + camera_api = TrafikverketCamera(web_session, api_key) # Change entry unique id from location to camera id if entry.version == 1: location = entry.data[CONF_LOCATION] - api_key = entry.data[CONF_API_KEY] - - web_session = async_get_clientsession(hass) - camera_api = TrafikverketCamera(web_session, api_key) try: camera_info = await camera_api.async_get_camera(location) @@ -60,14 +59,40 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if camera_id := camera_info.camera_id: entry.version = 2 - _LOGGER.debug( - "Migrate Trafikverket Camera config entry unique id to %s", - camera_id, - ) hass.config_entries.async_update_entry( entry, unique_id=f"{DOMAIN}-{camera_id}", ) + _LOGGER.debug( + "Migrated Trafikverket Camera config entry unique id to %s", + camera_id, + ) + else: + _LOGGER.error("Could not migrate the config entry. Camera has no id") + return False + + # Change entry data from location to id + if entry.version == 2: + location = entry.data[CONF_LOCATION] + + try: + camera_info = await camera_api.async_get_camera(location) + except Exception: # pylint: disable=broad-except + _LOGGER.error( + "Could not migrate the config entry. No connection to the api" + ) + return False + + if camera_id := camera_info.camera_id: + entry.version = 3 + _LOGGER.debug( + "Migrate Trafikverket Camera config entry unique id to %s", + camera_id, + ) + new_data = entry.data.copy() + new_data.pop(CONF_LOCATION) + new_data[CONF_ID] = camera_id + hass.config_entries.async_update_entry(entry, data=new_data) return True _LOGGER.error("Could not migrate the config entry. Camera has no id") return False diff --git a/homeassistant/components/trafikverket_camera/config_flow.py b/homeassistant/components/trafikverket_camera/config_flow.py index e75bc0bfa300..7572855b7d44 100644 --- a/homeassistant/components/trafikverket_camera/config_flow.py +++ b/homeassistant/components/trafikverket_camera/config_flow.py @@ -14,7 +14,7 @@ from pytrafikverket.trafikverket_camera import CameraInfo, TrafikverketCamera import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_ID from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -25,7 +25,7 @@ from .const import CONF_LOCATION, DOMAIN class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Trafikverket Camera integration.""" - VERSION = 2 + VERSION = 3 entry: config_entries.ConfigEntry | None @@ -53,10 +53,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if camera_info: camera_id = camera_info.camera_id - if _location := camera_info.location: - camera_location = _location - else: - camera_location = camera_info.camera_name + camera_location = camera_info.camera_name or "Trafikverket Camera" return (errors, camera_location, camera_id) @@ -76,9 +73,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): api_key = user_input[CONF_API_KEY] assert self.entry is not None - errors, _, _ = await self.validate_input( - api_key, self.entry.data[CONF_LOCATION] - ) + errors, _, _ = await self.validate_input(api_key, self.entry.data[CONF_ID]) if not errors: self.hass.config_entries.async_update_entry( @@ -121,10 +116,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry( title=camera_location, - data={ - CONF_API_KEY: api_key, - CONF_LOCATION: camera_location, - }, + data={CONF_API_KEY: api_key, CONF_ID: camera_id}, ) return self.async_show_form( diff --git a/homeassistant/components/trafikverket_camera/coordinator.py b/homeassistant/components/trafikverket_camera/coordinator.py index eb5a047ca733..8270fecd487e 100644 --- a/homeassistant/components/trafikverket_camera/coordinator.py +++ b/homeassistant/components/trafikverket_camera/coordinator.py @@ -15,13 +15,13 @@ from pytrafikverket.exceptions import ( from pytrafikverket.trafikverket_camera import CameraInfo, TrafikverketCamera from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_ID from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_LOCATION, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) TIME_BETWEEN_UPDATES = timedelta(minutes=5) @@ -48,14 +48,14 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator[CameraData]): ) self.session = async_get_clientsession(hass) self._camera_api = TrafikverketCamera(self.session, entry.data[CONF_API_KEY]) - self._location = entry.data[CONF_LOCATION] + self._id = entry.data[CONF_ID] async def _async_update_data(self) -> CameraData: """Fetch data from Trafikverket.""" camera_data: CameraInfo image: bytes | None = None try: - camera_data = await self._camera_api.async_get_camera(self._location) + camera_data = await self._camera_api.async_get_camera(self._id) except (NoCameraFound, MultipleCamerasFound, UnknownError) as error: raise UpdateFailed from error except InvalidAuthentication as error: diff --git a/tests/components/trafikverket_camera/__init__.py b/tests/components/trafikverket_camera/__init__.py index 026c122fb57a..a9aa3ad70d10 100644 --- a/tests/components/trafikverket_camera/__init__.py +++ b/tests/components/trafikverket_camera/__init__.py @@ -2,9 +2,14 @@ from __future__ import annotations from homeassistant.components.trafikverket_camera.const import CONF_LOCATION -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_ID ENTRY_CONFIG = { + CONF_API_KEY: "1234567890", + CONF_ID: "1234", +} + +ENTRY_CONFIG_OLD_CONFIG = { CONF_API_KEY: "1234567890", CONF_LOCATION: "Test location", } diff --git a/tests/components/trafikverket_camera/conftest.py b/tests/components/trafikverket_camera/conftest.py index a4902ac2950b..a5eeb707b34a 100644 --- a/tests/components/trafikverket_camera/conftest.py +++ b/tests/components/trafikverket_camera/conftest.py @@ -32,9 +32,9 @@ async def load_integration_from_entry( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - version=2, + version=3, unique_id="trafikverket_camera-1234", - title="Test location", + title="Test Camera", ) config_entry.add_to_hass(hass) @@ -54,7 +54,7 @@ def fixture_get_camera() -> CameraInfo: """Construct Camera Mock.""" return CameraInfo( - camera_name="Test_camera", + camera_name="Test Camera", camera_id="1234", active=True, deleted=False, diff --git a/tests/components/trafikverket_camera/test_binary_sensor.py b/tests/components/trafikverket_camera/test_binary_sensor.py index 6f7eb5402891..87d0e6d58b76 100644 --- a/tests/components/trafikverket_camera/test_binary_sensor.py +++ b/tests/components/trafikverket_camera/test_binary_sensor.py @@ -16,5 +16,5 @@ async def test_sensor( ) -> None: """Test the Trafikverket Camera binary sensor.""" - state = hass.states.get("binary_sensor.test_location_active") + state = hass.states.get("binary_sensor.test_camera_active") assert state.state == STATE_ON diff --git a/tests/components/trafikverket_camera/test_camera.py b/tests/components/trafikverket_camera/test_camera.py index b3df7cfcdcb8..182924e9f0ed 100644 --- a/tests/components/trafikverket_camera/test_camera.py +++ b/tests/components/trafikverket_camera/test_camera.py @@ -26,7 +26,7 @@ async def test_camera( get_camera: CameraInfo, ) -> None: """Test the Trafikverket Camera sensor.""" - state1 = hass.states.get("camera.test_location") + state1 = hass.states.get("camera.test_camera") assert state1.state == "idle" assert state1.attributes["description"] == "Test Camera for testing" assert state1.attributes["location"] == "Test location" @@ -44,11 +44,11 @@ async def test_camera( async_fire_time_changed(hass) await hass.async_block_till_done() - state1 = hass.states.get("camera.test_location") + state1 = hass.states.get("camera.test_camera") assert state1.state == "idle" assert state1.attributes != {} - assert await async_get_image(hass, "camera.test_location") + assert await async_get_image(hass, "camera.test_camera") monkeypatch.setattr( get_camera, @@ -69,4 +69,4 @@ async def test_camera( await hass.async_block_till_done() with pytest.raises(HomeAssistantError): - await async_get_image(hass, "camera.test_location") + await async_get_image(hass, "camera.test_camera") diff --git a/tests/components/trafikverket_camera/test_config_flow.py b/tests/components/trafikverket_camera/test_config_flow.py index b53763c0ac79..305066832e5f 100644 --- a/tests/components/trafikverket_camera/test_config_flow.py +++ b/tests/components/trafikverket_camera/test_config_flow.py @@ -14,7 +14,7 @@ from pytrafikverket.trafikverket_camera import CameraInfo from homeassistant import config_entries from homeassistant.components.trafikverket_camera.const import CONF_LOCATION, DOMAIN -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_ID from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -47,10 +47,10 @@ async def test_form(hass: HomeAssistant, get_camera: CameraInfo) -> None: await hass.async_block_till_done() assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Test location" + assert result2["title"] == "Test Camera" assert result2["data"] == { "api_key": "1234567890", - "location": "Test location", + "id": "1234", } assert len(mock_setup_entry.mock_calls) == 1 assert result2["result"].unique_id == "trafikverket_camera-1234" @@ -87,7 +87,7 @@ async def test_form_no_location_data( assert result2["title"] == "Test Camera" assert result2["data"] == { "api_key": "1234567890", - "location": "Test Camera", + "id": "1234", } assert len(mock_setup_entry.mock_calls) == 1 assert result2["result"].unique_id == "trafikverket_camera-1234" @@ -150,10 +150,10 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: domain=DOMAIN, data={ CONF_API_KEY: "1234567890", - CONF_LOCATION: "Test location", + CONF_ID: "1234", }, unique_id="1234", - version=2, + version=3, ) entry.add_to_hass(hass) @@ -186,7 +186,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", - "location": "Test location", + "id": "1234", } @@ -223,10 +223,10 @@ async def test_reauth_flow_error( domain=DOMAIN, data={ CONF_API_KEY: "1234567890", - CONF_LOCATION: "Test location", + CONF_ID: "1234", }, unique_id="1234", - version=2, + version=3, ) entry.add_to_hass(hass) await hass.async_block_till_done() @@ -271,5 +271,5 @@ async def test_reauth_flow_error( assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", - "location": "Test location", + "id": "1234", } diff --git a/tests/components/trafikverket_camera/test_coordinator.py b/tests/components/trafikverket_camera/test_coordinator.py index 4183aa9fffa4..0f79307e0b6a 100644 --- a/tests/components/trafikverket_camera/test_coordinator.py +++ b/tests/components/trafikverket_camera/test_coordinator.py @@ -40,9 +40,9 @@ async def test_coordinator( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - version=2, + version=3, unique_id="trafikverket_camera-1234", - title="Test location", + title="Test Camera", ) entry.add_to_hass(hass) @@ -54,7 +54,7 @@ async def test_coordinator( await hass.async_block_till_done() mock_data.assert_called_once() - state1 = hass.states.get("camera.test_location") + state1 = hass.states.get("camera.test_camera") assert state1.state == "idle" @@ -101,9 +101,9 @@ async def test_coordinator_failed_update( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - version=2, + version=3, unique_id="trafikverket_camera-1234", - title="Test location", + title="Test Camera", ) entry.add_to_hass(hass) @@ -115,7 +115,7 @@ async def test_coordinator_failed_update( await hass.async_block_till_done() mock_data.assert_called_once() - state = hass.states.get("camera.test_location") + state = hass.states.get("camera.test_camera") assert state is None assert entry.state == entry_state @@ -135,7 +135,7 @@ async def test_coordinator_failed_get_image( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - version=2, + version=3, unique_id="trafikverket_camera-1234", title="Test location", ) @@ -149,6 +149,6 @@ async def test_coordinator_failed_get_image( await hass.async_block_till_done() mock_data.assert_called_once() - state = hass.states.get("camera.test_location") + state = hass.states.get("camera.test_camera") assert state is None assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY diff --git a/tests/components/trafikverket_camera/test_init.py b/tests/components/trafikverket_camera/test_init.py index 83a3fc1486a7..e10c6c16e331 100644 --- a/tests/components/trafikverket_camera/test_init.py +++ b/tests/components/trafikverket_camera/test_init.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import datetime from unittest.mock import patch +import pytest from pytrafikverket.exceptions import UnknownError from pytrafikverket.trafikverket_camera import CameraInfo @@ -14,7 +15,7 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from . import ENTRY_CONFIG +from . import ENTRY_CONFIG, ENTRY_CONFIG_OLD_CONFIG from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -35,9 +36,9 @@ async def test_setup_entry( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - version=2, + version=3, unique_id="trafikverket_camera-1234", - title="Test location", + title="Test Camera", ) entry.add_to_hass(hass) @@ -67,9 +68,9 @@ async def test_unload_entry( source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - version=2, + version=3, unique_id="trafikverket_camera-1234", - title="Test location", + title="Test Camera", ) entry.add_to_hass(hass) @@ -99,7 +100,7 @@ async def test_migrate_entry( entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_USER, - data=ENTRY_CONFIG, + data=ENTRY_CONFIG_OLD_CONFIG, entry_id="1", unique_id="trafikverket_camera-Test location", title="Test location", @@ -114,15 +115,31 @@ async def test_migrate_entry( await hass.async_block_till_done() assert entry.state is config_entries.ConfigEntryState.LOADED - assert entry.version == 2 + assert entry.version == 3 assert entry.unique_id == "trafikverket_camera-1234" - assert len(mock_tvt_camera.mock_calls) == 2 + assert entry.data == ENTRY_CONFIG + assert len(mock_tvt_camera.mock_calls) == 3 +@pytest.mark.parametrize( + ("version", "unique_id"), + [ + ( + 1, + "trafikverket_camera-Test location", + ), + ( + 2, + "trafikverket_camera-1234", + ), + ], +) async def test_migrate_entry_fails_with_error( hass: HomeAssistant, get_camera: CameraInfo, aioclient_mock: AiohttpClientMocker, + version: int, + unique_id: str, ) -> None: """Test migrate entry fails with api error.""" aioclient_mock.get( @@ -132,9 +149,10 @@ async def test_migrate_entry_fails_with_error( entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_USER, - data=ENTRY_CONFIG, + data=ENTRY_CONFIG_OLD_CONFIG, entry_id="1", - unique_id="trafikverket_camera-Test location", + version=version, + unique_id=unique_id, title="Test location", ) entry.add_to_hass(hass) @@ -147,14 +165,29 @@ async def test_migrate_entry_fails_with_error( await hass.async_block_till_done() assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR - assert entry.version == 1 - assert entry.unique_id == "trafikverket_camera-Test location" + assert entry.version == version + assert entry.unique_id == unique_id assert len(mock_tvt_camera.mock_calls) == 1 +@pytest.mark.parametrize( + ("version", "unique_id"), + [ + ( + 1, + "trafikverket_camera-Test location", + ), + ( + 2, + "trafikverket_camera-1234", + ), + ], +) async def test_migrate_entry_fails_no_id( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, + version: int, + unique_id: str, ) -> None: """Test migrate entry fails, camera returns no id.""" aioclient_mock.get( @@ -164,9 +197,10 @@ async def test_migrate_entry_fails_no_id( entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_USER, - data=ENTRY_CONFIG, + data=ENTRY_CONFIG_OLD_CONFIG, entry_id="1", - unique_id="trafikverket_camera-Test location", + version=version, + unique_id=unique_id, title="Test location", ) entry.add_to_hass(hass) @@ -195,8 +229,8 @@ async def test_migrate_entry_fails_no_id( await hass.async_block_till_done() assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR - assert entry.version == 1 - assert entry.unique_id == "trafikverket_camera-Test location" + assert entry.version == version + assert entry.unique_id == unique_id assert len(mock_tvt_camera.mock_calls) == 1 @@ -214,7 +248,7 @@ async def test_no_migration_needed( domain=DOMAIN, source=SOURCE_USER, data=ENTRY_CONFIG, - version=2, + version=3, entry_id="1234", unique_id="trafikverket_camera-1234", title="Test location", diff --git a/tests/components/trafikverket_camera/test_recorder.py b/tests/components/trafikverket_camera/test_recorder.py index b9add7ae4830..777c6ea26b32 100644 --- a/tests/components/trafikverket_camera/test_recorder.py +++ b/tests/components/trafikverket_camera/test_recorder.py @@ -24,7 +24,7 @@ async def test_exclude_attributes( get_camera: CameraInfo, ) -> None: """Test camera has description and location excluded from recording.""" - state1 = hass.states.get("camera.test_location") + state1 = hass.states.get("camera.test_camera") assert state1.state == "idle" assert state1.attributes["description"] == "Test Camera for testing" assert state1.attributes["location"] == "Test location" @@ -39,10 +39,10 @@ async def test_exclude_attributes( hass.states.async_entity_ids(), ) assert len(states) == 8 - assert states.get("camera.test_location") + assert states.get("camera.test_camera") for entity_states in states.values(): for state in entity_states: - if state.entity_id == "camera.test_location": + if state.entity_id == "camera.test_camera": assert "location" not in state.attributes assert "description" not in state.attributes assert "type" in state.attributes diff --git a/tests/components/trafikverket_camera/test_sensor.py b/tests/components/trafikverket_camera/test_sensor.py index 581fed1d2896..c1c98aed7973 100644 --- a/tests/components/trafikverket_camera/test_sensor.py +++ b/tests/components/trafikverket_camera/test_sensor.py @@ -15,15 +15,15 @@ async def test_sensor( ) -> None: """Test the Trafikverket Camera sensor.""" - state = hass.states.get("sensor.test_location_direction") + state = hass.states.get("sensor.test_camera_direction") assert state.state == "180" - state = hass.states.get("sensor.test_location_modified") + state = hass.states.get("sensor.test_camera_modified") assert state.state == "2022-04-04T04:04:04+00:00" - state = hass.states.get("sensor.test_location_photo_time") + state = hass.states.get("sensor.test_camera_photo_time") assert state.state == "2022-04-04T04:04:04+00:00" - state = hass.states.get("sensor.test_location_photo_url") + state = hass.states.get("sensor.test_camera_photo_url") assert state.state == "https://www.testurl.com/test_photo.jpg" - state = hass.states.get("sensor.test_location_status") + state = hass.states.get("sensor.test_camera_status") assert state.state == "Running" - state = hass.states.get("sensor.test_location_camera_type") + state = hass.states.get("sensor.test_camera_camera_type") assert state.state == "Road" From 49381cefa3d2e384bdbbb73f33cffadf30c53204 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 29 Nov 2023 13:37:23 +0100 Subject: [PATCH 218/265] Update frontend to 20231129.0 (#104710) --- 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 469deab23e1d..6a6c557c7b78 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20231030.2"] + "requirements": ["home-assistant-frontend==20231129.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9ea82cb3e75e..4aa2a4ea0131 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -27,7 +27,7 @@ ha-ffmpeg==3.1.0 hass-nabucasa==0.74.0 hassil==1.5.1 home-assistant-bluetooth==1.10.4 -home-assistant-frontend==20231030.2 +home-assistant-frontend==20231129.0 home-assistant-intents==2023.11.17 httpx==0.25.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index fe298d256da6..f76a27f0d8e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1014,7 +1014,7 @@ hole==0.8.0 holidays==0.36 # homeassistant.components.frontend -home-assistant-frontend==20231030.2 +home-assistant-frontend==20231129.0 # homeassistant.components.conversation home-assistant-intents==2023.11.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c8a9e7d10cb..64cd02ec6c45 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -801,7 +801,7 @@ hole==0.8.0 holidays==0.36 # homeassistant.components.frontend -home-assistant-frontend==20231030.2 +home-assistant-frontend==20231129.0 # homeassistant.components.conversation home-assistant-intents==2023.11.17 From 953a212dd697c5b916a335869bed8b36be466b92 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 29 Nov 2023 13:56:51 +0100 Subject: [PATCH 219/265] Use ServiceValidationError for invalid fan preset_mode and move check to fan entity component (#104560) * Use ServiceValidationError for fan preset_mode * Use _valid_preset_mode_or_raise to raise * Move preset_mode validation to entity component * Fix bond fan and comments * Fixes baf, fjaraskupan and template * More integration adjustments * Add custom components mock and test code * Make NotValidPresetModeError subclass * Update homeassistant/components/fan/strings.json Co-authored-by: Martin Hjelmare * Keep bond has_action validation * Move demo test asserts outside context block * Follow up comment * Update homeassistant/components/fan/strings.json Co-authored-by: G Johansson * Fix demo tests * Remove pylint disable * Remove unreachable code * Update homeassistant/components/fan/__init__.py Co-authored-by: G Johansson * Use NotValidPresetModeError, Final methods * Address comments * Correct docst * Follow up comments * Update homeassistant/components/fan/__init__.py Co-authored-by: Erik Montnemery --------- Co-authored-by: Martin Hjelmare Co-authored-by: G Johansson Co-authored-by: Erik Montnemery --- homeassistant/components/baf/fan.py | 2 - homeassistant/components/bond/fan.py | 4 -- homeassistant/components/demo/fan.py | 13 +--- homeassistant/components/fan/__init__.py | 49 ++++++++++++-- homeassistant/components/fan/strings.json | 5 ++ homeassistant/components/fjaraskupan/fan.py | 8 +-- homeassistant/components/mqtt/fan.py | 2 - homeassistant/components/template/fan.py | 9 --- homeassistant/components/tradfri/fan.py | 3 +- homeassistant/components/vallox/fan.py | 12 +--- homeassistant/components/xiaomi_miio/fan.py | 22 ------- homeassistant/components/zha/fan.py | 6 -- homeassistant/components/zwave_js/fan.py | 6 -- tests/components/bond/test_fan.py | 9 ++- tests/components/demo/test_fan.py | 24 +++++-- tests/components/fan/test_init.py | 65 ++++++++++++++++++- tests/components/mqtt/test_fan.py | 40 +++++++----- tests/components/template/test_fan.py | 16 +++-- tests/components/vallox/test_fan.py | 4 +- tests/components/zha/test_fan.py | 9 ++- tests/components/zwave_js/test_fan.py | 6 +- .../custom_components/test/fan.py | 64 ++++++++++++++++++ 22 files changed, 260 insertions(+), 118 deletions(-) create mode 100644 tests/testing_config/custom_components/test/fan.py diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index 059603fc5893..e2d1c5fcb3a0 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -93,8 +93,6 @@ class BAFFan(BAFEntity, FanEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" - if preset_mode != PRESET_MODE_AUTO: - raise ValueError(f"Invalid preset mode: {preset_mode}") self._device.fan_mode = OffOnAuto.AUTO async def async_set_direction(self, direction: str) -> None: diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index bc6235cb219c..3cb81ba40b40 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -199,10 +199,6 @@ class BondFan(BondEntity, FanEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" - if preset_mode != PRESET_MODE_BREEZE or not self._device.has_action( - Action.BREEZE_ON - ): - raise ValueError(f"Invalid preset mode: {preset_mode}") await self._hub.bond.action(self._device.device_id, Action(Action.BREEZE_ON)) async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 211389a54663..73cae4a64b16 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -161,12 +161,9 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if self.preset_modes and preset_mode in self.preset_modes: - self._preset_mode = preset_mode - self._percentage = None - self.schedule_update_ha_state() - else: - raise ValueError(f"Invalid preset mode: {preset_mode}") + self._preset_mode = preset_mode + self._percentage = None + self.schedule_update_ha_state() def turn_on( self, @@ -230,10 +227,6 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if self.preset_modes is None or preset_mode not in self.preset_modes: - raise ValueError( - f"{preset_mode} is not a valid preset_mode: {self.preset_modes}" - ) self._preset_mode = preset_mode self._percentage = None self.async_write_ha_state() diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index a149909e029f..21ffca35962a 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -18,7 +18,8 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -77,8 +78,19 @@ ATTR_PRESET_MODES = "preset_modes" # mypy: disallow-any-generics -class NotValidPresetModeError(ValueError): - """Exception class when the preset_mode in not in the preset_modes list.""" +class NotValidPresetModeError(ServiceValidationError): + """Raised when the preset_mode is not in the preset_modes list.""" + + def __init__( + self, *args: object, translation_placeholders: dict[str, str] | None = None + ) -> None: + """Initialize the exception.""" + super().__init__( + *args, + translation_domain=DOMAIN, + translation_key="not_valid_preset_mode", + translation_placeholders=translation_placeholders, + ) @bind_hass @@ -107,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ), vol.Optional(ATTR_PRESET_MODE): cv.string, }, - "async_turn_on", + "async_handle_turn_on_service", ) component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") @@ -156,7 +168,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_SET_PRESET_MODE, {vol.Required(ATTR_PRESET_MODE): cv.string}, - "async_set_preset_mode", + "async_handle_set_preset_mode_service", [FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE], ) @@ -237,17 +249,30 @@ class FanEntity(ToggleEntity): """Set new preset mode.""" raise NotImplementedError() + @final + async def async_handle_set_preset_mode_service(self, preset_mode: str) -> None: + """Validate and set new preset mode.""" + self._valid_preset_mode_or_raise(preset_mode) + await self.async_set_preset_mode(preset_mode) + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode) + @final + @callback def _valid_preset_mode_or_raise(self, preset_mode: str) -> None: """Raise NotValidPresetModeError on invalid preset_mode.""" preset_modes = self.preset_modes if not preset_modes or preset_mode not in preset_modes: + preset_modes_str: str = ", ".join(preset_modes or []) raise NotValidPresetModeError( f"The preset_mode {preset_mode} is not a valid preset_mode:" - f" {preset_modes}" + f" {preset_modes}", + translation_placeholders={ + "preset_mode": preset_mode, + "preset_modes": preset_modes_str, + }, ) def set_direction(self, direction: str) -> None: @@ -267,6 +292,18 @@ class FanEntity(ToggleEntity): """Turn on the fan.""" raise NotImplementedError() + @final + async def async_handle_turn_on_service( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Validate and turn on the fan.""" + if preset_mode is not None: + self._valid_preset_mode_or_raise(preset_mode) + await self.async_turn_on(percentage, preset_mode, **kwargs) + async def async_turn_on( self, percentage: int | None = None, diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index 674dcc2b92ef..aab714d3e072 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -144,5 +144,10 @@ "reverse": "Reverse" } } + }, + "exceptions": { + "not_valid_preset_mode": { + "message": "Preset mode {preset_mode} is not valid, valid preset modes are: {preset_modes}." + } } } diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index 142694a6bfb2..ee989bb2ee0b 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -131,11 +131,9 @@ class Fan(CoordinatorEntity[FjaraskupanCoordinator], FanEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if command := PRESET_TO_COMMAND.get(preset_mode): - async with self.coordinator.async_connect_and_update() as device: - await device.send_command(command) - else: - raise UnsupportedPreset(f"The preset {preset_mode} is unsupported") + command = PRESET_TO_COMMAND[preset_mode] + async with self.coordinator.async_connect_and_update() as device: + await device.send_command(command) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 0e9e7d708e98..e3dcf66c8b11 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -553,8 +553,6 @@ class MqttFan(MqttEntity, FanEntity): This method is a coroutine. """ - self._valid_preset_mode_or_raise(preset_mode) - mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode) await self.async_publish( diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index d39fa56775a6..8aeede425522 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -282,15 +282,6 @@ class TemplateFan(TemplateEntity, FanEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset_mode of the fan.""" - if self.preset_modes and preset_mode not in self.preset_modes: - _LOGGER.error( - "Received invalid preset_mode: %s for entity %s. Expected: %s", - preset_mode, - self.entity_id, - self.preset_modes, - ) - return - self._preset_mode = preset_mode if self._set_preset_mode_script: diff --git a/homeassistant/components/tradfri/fan.py b/homeassistant/components/tradfri/fan.py index c41b24a26473..5c0f05004bae 100644 --- a/homeassistant/components/tradfri/fan.py +++ b/homeassistant/components/tradfri/fan.py @@ -119,8 +119,7 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity): if not self._device_control: return - if not preset_mode == ATTR_AUTO: - raise ValueError("Preset must be 'Auto'.") + # Preset must be 'Auto' await self._api(self._device_control.turn_on_auto_mode()) diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 2f420096c748..e58c3ebd88dc 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -11,11 +11,7 @@ from vallox_websocket_api import ( ValloxInvalidInputException, ) -from homeassistant.components.fan import ( - FanEntity, - FanEntityFeature, - NotValidPresetModeError, -) +from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -200,12 +196,6 @@ class ValloxFanEntity(ValloxEntity, FanEntity): Returns true if the mode has been changed, false otherwise. """ - try: - self._valid_preset_mode_or_raise(preset_mode) - - except NotValidPresetModeError as err: - raise ValueError(f"Not valid preset mode: {preset_mode}") from err - if preset_mode == self.preset_mode: return False diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index a3bb28e7a8b7..9be019ed7244 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -530,9 +530,6 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): This method is a coroutine. """ - if preset_mode not in self.preset_modes: - _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) - return if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, @@ -623,9 +620,6 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" - if preset_mode not in self.preset_modes: - _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) - return if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, @@ -721,9 +715,6 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): This method is a coroutine. """ - if preset_mode not in self.preset_modes: - _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) - return if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, @@ -809,9 +800,6 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan. This method is a coroutine.""" - if preset_mode not in self.preset_modes: - _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) - return if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, @@ -958,10 +946,6 @@ class XiaomiFan(XiaomiGenericFan): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" - if preset_mode not in self.preset_modes: - _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) - return - if preset_mode == ATTR_MODE_NATURE: await self._try_command( "Setting natural fan speed percentage of the miio device failed.", @@ -1034,9 +1018,6 @@ class XiaomiFanP5(XiaomiGenericFan): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" - if preset_mode not in self.preset_modes: - _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) - return await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, @@ -1093,9 +1074,6 @@ class XiaomiFanMiot(XiaomiGenericFan): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" - if preset_mode not in self.preset_modes: - _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) - return await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 05bf3469c7b8..c6b9a1048855 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -13,7 +13,6 @@ from homeassistant.components.fan import ( ATTR_PRESET_MODE, FanEntity, FanEntityFeature, - NotValidPresetModeError, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNAVAILABLE, Platform @@ -131,11 +130,6 @@ class BaseFan(FanEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode for the fan.""" - if preset_mode not in self.preset_modes: - raise NotValidPresetModeError( - f"The preset_mode {preset_mode} is not a valid preset_mode:" - f" {self.preset_modes}" - ) await self._async_set_fan_mode(self.preset_name_to_mode[preset_mode]) @abstractmethod diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index d06306497657..d4247b65c8b7 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -18,7 +18,6 @@ from homeassistant.components.fan import ( DOMAIN as FAN_DOMAIN, FanEntity, FanEntityFeature, - NotValidPresetModeError, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -181,11 +180,6 @@ class ValueMappingZwaveFan(ZwaveFan): await self._async_set_value(self._target_value, zwave_value) return - raise NotValidPresetModeError( - f"The preset_mode {preset_mode} is not a valid preset_mode:" - f" {self.preset_modes}" - ) - @property def available(self) -> bool: """Return whether the entity is available.""" diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index db1c0fc787d5..e202433c8d6b 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -26,6 +26,7 @@ from homeassistant.components.fan import ( SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, FanEntityFeature, + NotValidPresetModeError, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -251,10 +252,14 @@ async def test_turn_on_fan_preset_mode_not_supported(hass: HomeAssistant) -> Non props={"max_speed": 6}, ) - with patch_bond_action(), patch_bond_device_state(), pytest.raises(ValueError): + with patch_bond_action(), patch_bond_device_state(), pytest.raises( + NotValidPresetModeError + ): await turn_fan_on(hass, "fan.name_1", preset_mode=PRESET_MODE_BREEZE) - with patch_bond_action(), patch_bond_device_state(), pytest.raises(ValueError): + with patch_bond_action(), patch_bond_device_state(), pytest.raises( + NotValidPresetModeError + ): await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PRESET_MODE, diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index 58a8c99ea3c2..a3f607aee769 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -182,7 +182,7 @@ async def test_turn_on_with_preset_mode_only( assert state.state == STATE_OFF assert state.attributes[fan.ATTR_PRESET_MODE] is None - with pytest.raises(ValueError): + with pytest.raises(fan.NotValidPresetModeError) as exc: await hass.services.async_call( fan.DOMAIN, SERVICE_TURN_ON, @@ -190,6 +190,12 @@ async def test_turn_on_with_preset_mode_only( blocking=True, ) await hass.async_block_till_done() + assert exc.value.translation_domain == fan.DOMAIN + assert exc.value.translation_key == "not_valid_preset_mode" + assert exc.value.translation_placeholders == { + "preset_mode": "invalid", + "preset_modes": "auto, smart, sleep, on", + } state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF @@ -250,7 +256,7 @@ async def test_turn_on_with_preset_mode_and_speed( assert state.attributes[fan.ATTR_PERCENTAGE] == 0 assert state.attributes[fan.ATTR_PRESET_MODE] is None - with pytest.raises(ValueError): + with pytest.raises(fan.NotValidPresetModeError) as exc: await hass.services.async_call( fan.DOMAIN, SERVICE_TURN_ON, @@ -258,6 +264,12 @@ async def test_turn_on_with_preset_mode_and_speed( blocking=True, ) await hass.async_block_till_done() + assert exc.value.translation_domain == fan.DOMAIN + assert exc.value.translation_key == "not_valid_preset_mode" + assert exc.value.translation_placeholders == { + "preset_mode": "invalid", + "preset_modes": "auto, smart, sleep, on", + } state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF @@ -343,7 +355,7 @@ async def test_set_preset_mode_invalid(hass: HomeAssistant, fan_entity_id) -> No state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF - with pytest.raises(ValueError): + with pytest.raises(fan.NotValidPresetModeError) as exc: await hass.services.async_call( fan.DOMAIN, fan.SERVICE_SET_PRESET_MODE, @@ -351,8 +363,10 @@ async def test_set_preset_mode_invalid(hass: HomeAssistant, fan_entity_id) -> No blocking=True, ) await hass.async_block_till_done() + assert exc.value.translation_domain == fan.DOMAIN + assert exc.value.translation_key == "not_valid_preset_mode" - with pytest.raises(ValueError): + with pytest.raises(fan.NotValidPresetModeError) as exc: await hass.services.async_call( fan.DOMAIN, SERVICE_TURN_ON, @@ -360,6 +374,8 @@ async def test_set_preset_mode_invalid(hass: HomeAssistant, fan_entity_id) -> No blocking=True, ) await hass.async_block_till_done() + assert exc.value.translation_domain == fan.DOMAIN + assert exc.value.translation_key == "not_valid_preset_mode" @pytest.mark.parametrize("fan_entity_id", FULL_FAN_ENTITY_IDS) diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 8338afc9c68f..ec421141768c 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -1,8 +1,19 @@ """Tests for fan platforms.""" import pytest -from homeassistant.components.fan import FanEntity +from homeassistant.components.fan import ( + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + DOMAIN, + SERVICE_SET_PRESET_MODE, + FanEntity, + NotValidPresetModeError, +) from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.testing_config.custom_components.test.fan import MockFan class BaseFan(FanEntity): @@ -82,3 +93,55 @@ def test_fanentity_attributes(attribute_name, attribute_value) -> None: fan = BaseFan() setattr(fan, f"_attr_{attribute_name}", attribute_value) assert getattr(fan, attribute_name) == attribute_value + + +async def test_preset_mode_validation( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + entity_registry: er.EntityRegistry, + enable_custom_integrations: None, +) -> None: + """Test preset mode validation.""" + + await hass.async_block_till_done() + + platform = getattr(hass.components, "test.fan") + platform.init(empty=False) + + assert await async_setup_component(hass, "fan", {"fan": {"platform": "test"}}) + await hass.async_block_till_done() + + test_fan: MockFan = platform.ENTITIES["support_preset_mode"] + await hass.async_block_till_done() + + state = hass.states.get("fan.support_fan_with_preset_mode_support") + assert state.attributes.get(ATTR_PRESET_MODES) == ["auto", "eco"] + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + { + "entity_id": "fan.support_fan_with_preset_mode_support", + "preset_mode": "eco", + }, + blocking=True, + ) + + state = hass.states.get("fan.support_fan_with_preset_mode_support") + assert state.attributes.get(ATTR_PRESET_MODE) == "eco" + + with pytest.raises(NotValidPresetModeError) as exc: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + { + "entity_id": "fan.support_fan_with_preset_mode_support", + "preset_mode": "invalid", + }, + blocking=True, + ) + assert exc.value.translation_key == "not_valid_preset_mode" + + with pytest.raises(NotValidPresetModeError) as exc: + await test_fan._valid_preset_mode_or_raise("invalid") + assert exc.value.translation_key == "not_valid_preset_mode" diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 21d3bcce3a97..e7c4eba54e21 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -705,8 +705,9 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get(ATTR_ASSUMED_STATE) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_set_preset_mode(hass, "fan.test", "low") + assert exc.value.translation_key == "not_valid_preset_mode" await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -916,11 +917,13 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy( assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get(ATTR_ASSUMED_STATE) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_set_preset_mode(hass, "fan.test", "low") + assert exc.value.translation_key == "not_valid_preset_mode" - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_set_preset_mode(hass, "fan.test", "auto") + assert exc.value.translation_key == "not_valid_preset_mode" await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -976,8 +979,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy( assert state.state == STATE_ON assert state.attributes.get(ATTR_ASSUMED_STATE) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_turn_on(hass, "fan.test", preset_mode="freaking-high") + assert exc.value.translation_key == "not_valid_preset_mode" @pytest.mark.parametrize( @@ -1078,11 +1082,13 @@ async def test_sending_mqtt_command_templates_( assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get(ATTR_ASSUMED_STATE) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_set_preset_mode(hass, "fan.test", "low") + assert exc.value.translation_key == "not_valid_preset_mode" - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_set_preset_mode(hass, "fan.test", "medium") + assert exc.value.translation_key == "not_valid_preset_mode" await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -1140,8 +1146,9 @@ async def test_sending_mqtt_command_templates_( assert state.state == STATE_ON assert state.attributes.get(ATTR_ASSUMED_STATE) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_turn_on(hass, "fan.test", preset_mode="low") + assert exc.value.translation_key == "not_valid_preset_mode" @pytest.mark.parametrize( @@ -1176,8 +1183,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_set_preset_mode(hass, "fan.test", "medium") + assert exc.value.translation_key == "not_valid_preset_mode" await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -1276,11 +1284,10 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_turn_on(hass, "fan.test", preset_mode="auto") - assert mqtt_mock.async_publish.call_count == 1 - # We can turn on, but the invalid preset mode will raise - mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) + assert exc.value.translation_key == "not_valid_preset_mode" + assert mqtt_mock.async_publish.call_count == 0 mqtt_mock.async_publish.reset_mock() await common.async_turn_on(hass, "fan.test", preset_mode="whoosh") @@ -1428,11 +1435,13 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( with pytest.raises(MultipleInvalid): await common.async_set_percentage(hass, "fan.test", 101) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_set_preset_mode(hass, "fan.test", "low") + assert exc.value.translation_key == "not_valid_preset_mode" - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_set_preset_mode(hass, "fan.test", "medium") + assert exc.value.translation_key == "not_valid_preset_mode" await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -1452,8 +1461,9 @@ async def test_sending_mqtt_commands_and_explicit_optimistic( assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await common.async_set_preset_mode(hass, "fan.test", "freaking-high") + assert exc.value.translation_key == "not_valid_preset_mode" mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index f9b0bddddcf2..ccdafebd8bb8 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -12,6 +12,7 @@ from homeassistant.components.fan import ( DIRECTION_REVERSE, DOMAIN, FanEntityFeature, + NotValidPresetModeError, ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant @@ -489,7 +490,11 @@ async def test_preset_modes(hass: HomeAssistant, calls) -> None: ("smart", "smart", 3), ("invalid", "smart", 3), ]: - await common.async_set_preset_mode(hass, _TEST_FAN, extra) + if extra != state: + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, _TEST_FAN, extra) + else: + await common.async_set_preset_mode(hass, _TEST_FAN, extra) assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == state assert len(calls) == expected_calls assert calls[-1].data["action"] == "set_preset_mode" @@ -550,6 +555,7 @@ async def test_no_value_template(hass: HomeAssistant, calls) -> None: with assert_setup_component(1, "fan"): test_fan_config = { "preset_mode_template": "{{ states('input_select.preset_mode') }}", + "preset_modes": ["auto"], "percentage_template": "{{ states('input_number.percentage') }}", "oscillating_template": "{{ states('input_select.osc') }}", "direction_template": "{{ states('input_select.direction') }}", @@ -625,18 +631,18 @@ async def test_no_value_template(hass: HomeAssistant, calls) -> None: await hass.async_block_till_done() await common.async_turn_on(hass, _TEST_FAN) - _verify(hass, STATE_ON, 0, None, None, None) + _verify(hass, STATE_ON, 0, None, None, "auto") await common.async_turn_off(hass, _TEST_FAN) - _verify(hass, STATE_OFF, 0, None, None, None) + _verify(hass, STATE_OFF, 0, None, None, "auto") percent = 100 await common.async_set_percentage(hass, _TEST_FAN, percent) assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == percent - _verify(hass, STATE_ON, percent, None, None, None) + _verify(hass, STATE_ON, percent, None, None, "auto") await common.async_turn_off(hass, _TEST_FAN) - _verify(hass, STATE_OFF, percent, None, None, None) + _verify(hass, STATE_OFF, percent, None, None, "auto") preset = "auto" await common.async_set_preset_mode(hass, _TEST_FAN, preset) diff --git a/tests/components/vallox/test_fan.py b/tests/components/vallox/test_fan.py index eb60a3d025db..12b24f46abaa 100644 --- a/tests/components/vallox/test_fan.py +++ b/tests/components/vallox/test_fan.py @@ -10,6 +10,7 @@ from homeassistant.components.fan import ( DOMAIN as FAN_DOMAIN, SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, + NotValidPresetModeError, ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant @@ -179,7 +180,7 @@ async def test_set_invalid_preset_mode( """Test set preset mode.""" await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - with pytest.raises(ValueError): + with pytest.raises(NotValidPresetModeError) as exc: await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -189,6 +190,7 @@ async def test_set_invalid_preset_mode( }, blocking=True, ) + assert exc.value.translation_key == "not_valid_preset_mode" async def test_set_preset_mode_exception( diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 737604482d88..7d45960d576a 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -222,10 +222,11 @@ async def test_fan( # set invalid preset_mode from HA cluster.write_attributes.reset_mock() - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await async_set_preset_mode( hass, entity_id, preset_mode="invalid does not exist" ) + assert exc.value.translation_key == "not_valid_preset_mode" assert len(cluster.write_attributes.mock_calls) == 0 # test adding new fan to the network and HA @@ -624,10 +625,11 @@ async def test_fan_ikea( # set invalid preset_mode from HA cluster.write_attributes.reset_mock() - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await async_set_preset_mode( hass, entity_id, preset_mode="invalid does not exist" ) + assert exc.value.translation_key == "not_valid_preset_mode" assert len(cluster.write_attributes.mock_calls) == 0 # test adding new fan to the network and HA @@ -813,8 +815,9 @@ async def test_fan_kof( # set invalid preset_mode from HA cluster.write_attributes.reset_mock() - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_AUTO) + assert exc.value.translation_key == "not_valid_preset_mode" assert len(cluster.write_attributes.mock_calls) == 0 # test adding new fan to the network and HA diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 92141eec3ff0..c26a5366d37f 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -536,13 +536,14 @@ async def test_inovelli_lzw36( assert args["value"] == 1 client.async_send_command.reset_mock() - with pytest.raises(NotValidPresetModeError): + with pytest.raises(NotValidPresetModeError) as exc: await hass.services.async_call( "fan", "turn_on", {"entity_id": entity_id, "preset_mode": "wheeze"}, blocking=True, ) + assert exc.value.translation_key == "not_valid_preset_mode" assert len(client.async_send_command.call_args_list) == 0 @@ -675,13 +676,14 @@ async def test_thermostat_fan( client.async_send_command.reset_mock() # Test setting unknown preset mode - with pytest.raises(ValueError): + with pytest.raises(NotValidPresetModeError) as exc: await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PRESET_MODE, {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "Turbo"}, blocking=True, ) + assert exc.value.translation_key == "not_valid_preset_mode" client.async_send_command.reset_mock() diff --git a/tests/testing_config/custom_components/test/fan.py b/tests/testing_config/custom_components/test/fan.py new file mode 100644 index 000000000000..133f372f4fa2 --- /dev/null +++ b/tests/testing_config/custom_components/test/fan.py @@ -0,0 +1,64 @@ +"""Provide a mock fan platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.fan import FanEntity, FanEntityFeature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from tests.common import MockEntity + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + "support_preset_mode": MockFan( + name="Support fan with preset_mode support", + supported_features=FanEntityFeature.PRESET_MODE, + unique_id="unique_support_preset_mode", + preset_modes=["auto", "eco"], + ) + } + ) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities_callback: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockFan(MockEntity, FanEntity): + """Mock Fan class.""" + + @property + def preset_mode(self) -> str | None: + """Return preset mode.""" + return self._handle("preset_mode") + + @property + def preset_modes(self) -> list[str] | None: + """Return preset mode.""" + return self._handle("preset_modes") + + @property + def supported_features(self): + """Return the class of this fan.""" + return self._handle("supported_features") + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set preset mode.""" + self._attr_preset_mode = preset_mode + await self.async_update_ha_state() From 0a1396820929cfc1ab445365ffdce2228fadf265 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:17:15 +0100 Subject: [PATCH 220/265] Improve devialet coordinator typing (#104707) --- homeassistant/components/devialet/coordinator.py | 4 ++-- homeassistant/components/devialet/media_player.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/devialet/coordinator.py b/homeassistant/components/devialet/coordinator.py index f0ee47150cce..9e1eada7183a 100644 --- a/homeassistant/components/devialet/coordinator.py +++ b/homeassistant/components/devialet/coordinator.py @@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=5) -class DevialetCoordinator(DataUpdateCoordinator): +class DevialetCoordinator(DataUpdateCoordinator[None]): """Devialet update coordinator.""" def __init__(self, hass: HomeAssistant, client: DevialetApi) -> None: @@ -27,6 +27,6 @@ class DevialetCoordinator(DataUpdateCoordinator): ) self.client = client - async def _async_update_data(self): + async def _async_update_data(self) -> None: """Fetch data from API endpoint.""" await self.client.async_update() diff --git a/homeassistant/components/devialet/media_player.py b/homeassistant/components/devialet/media_player.py index 75fc420fa87f..a79a82e6f607 100644 --- a/homeassistant/components/devialet/media_player.py +++ b/homeassistant/components/devialet/media_player.py @@ -46,13 +46,15 @@ async def async_setup_entry( async_add_entities([DevialetMediaPlayerEntity(coordinator, entry)]) -class DevialetMediaPlayerEntity(CoordinatorEntity, MediaPlayerEntity): +class DevialetMediaPlayerEntity( + CoordinatorEntity[DevialetCoordinator], MediaPlayerEntity +): """Devialet media player.""" _attr_has_entity_name = True _attr_name = None - def __init__(self, coordinator, entry: ConfigEntry) -> None: + def __init__(self, coordinator: DevialetCoordinator, entry: ConfigEntry) -> None: """Initialize the Devialet device.""" self.coordinator = coordinator super().__init__(coordinator) From 09d7679818bffeba4a999380722e281315afe3cf Mon Sep 17 00:00:00 2001 From: stegm Date: Wed, 29 Nov 2023 14:24:09 +0100 Subject: [PATCH 221/265] Add new sensors of Kostal Plenticore integration (#103802) --- .../components/kostal_plenticore/helper.py | 24 ++++-- .../kostal_plenticore/manifest.json | 2 +- .../components/kostal_plenticore/sensor.py | 79 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/kostal_plenticore/conftest.py | 24 +++--- .../kostal_plenticore/test_config_flow.py | 28 ++++++- .../kostal_plenticore/test_diagnostics.py | 22 +++--- .../kostal_plenticore/test_helper.py | 34 ++++++-- .../kostal_plenticore/test_number.py | 68 +++++++--------- .../kostal_plenticore/test_select.py | 20 ++++- 11 files changed, 221 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 1c495ac9db93..adb1bfb6f09b 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -3,13 +3,18 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Callable +from collections.abc import Callable, Mapping from datetime import datetime, timedelta import logging from typing import Any, TypeVar, cast from aiohttp.client_exceptions import ClientError -from pykoplenti import ApiClient, ApiException, AuthenticationException +from pykoplenti import ( + ApiClient, + ApiException, + AuthenticationException, + ExtendedApiClient, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, HomeAssistant @@ -51,7 +56,9 @@ class Plenticore: async def async_setup(self) -> bool: """Set up Plenticore API client.""" - self._client = ApiClient(async_get_clientsession(self.hass), host=self.host) + self._client = ExtendedApiClient( + async_get_clientsession(self.hass), host=self.host + ) try: await self._client.login(self.config_entry.data[CONF_PASSWORD]) except AuthenticationException as err: @@ -124,7 +131,7 @@ class DataUpdateCoordinatorMixin: async def async_read_data( self, module_id: str, data_id: str - ) -> dict[str, dict[str, str]] | None: + ) -> Mapping[str, Mapping[str, str]] | None: """Read data from Plenticore.""" if (client := self._plenticore.client) is None: return None @@ -190,7 +197,7 @@ class PlenticoreUpdateCoordinator(DataUpdateCoordinator[_DataT]): class ProcessDataUpdateCoordinator( - PlenticoreUpdateCoordinator[dict[str, dict[str, str]]] + PlenticoreUpdateCoordinator[Mapping[str, Mapping[str, str]]] ): """Implementation of PlenticoreUpdateCoordinator for process data.""" @@ -206,18 +213,19 @@ class ProcessDataUpdateCoordinator( return { module_id: { process_data.id: process_data.value - for process_data in fetched_data[module_id] + for process_data in fetched_data[module_id].values() } for module_id in fetched_data } class SettingDataUpdateCoordinator( - PlenticoreUpdateCoordinator[dict[str, dict[str, str]]], DataUpdateCoordinatorMixin + PlenticoreUpdateCoordinator[Mapping[str, Mapping[str, str]]], + DataUpdateCoordinatorMixin, ): """Implementation of PlenticoreUpdateCoordinator for settings data.""" - async def _async_update_data(self) -> dict[str, dict[str, str]]: + async def _async_update_data(self) -> Mapping[str, Mapping[str, str]]: client = self._plenticore.client if not self._fetch or client is None: diff --git a/homeassistant/components/kostal_plenticore/manifest.json b/homeassistant/components/kostal_plenticore/manifest.json index 95f4a1949775..d65368e7ee44 100644 --- a/homeassistant/components/kostal_plenticore/manifest.json +++ b/homeassistant/components/kostal_plenticore/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/kostal_plenticore", "iot_class": "local_polling", "loggers": ["kostal"], - "requirements": ["pykoplenti==1.0.0"] + "requirements": ["pykoplenti==1.2.2"] } diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index f7bad638df4f..ce18867511d9 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -649,6 +649,39 @@ SENSOR_PROCESS_DATA = [ state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischarge:Day", + name="Battery Discharge Day", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischarge:Month", + name="Battery Discharge Month", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischarge:Year", + name="Battery Discharge Year", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischarge:Total", + name="Battery Discharge Total", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), PlenticoreSensorEntityDescription( module_id="scb:statistic:EnergyFlow", key="Statistic:EnergyDischargeGrid:Day", @@ -682,6 +715,52 @@ SENSOR_PROCESS_DATA = [ state_class=SensorStateClass.TOTAL_INCREASING, formatter="format_energy", ), + PlenticoreSensorEntityDescription( + module_id="_virt_", + key="pv_P", + name="Sum power of all PV DC inputs", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=True, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="_virt_", + key="Statistic:EnergyGrid:Total", + name="Energy to Grid Total", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="_virt_", + key="Statistic:EnergyGrid:Year", + name="Energy to Grid Year", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="_virt_", + key="Statistic:EnergyGrid:Month", + name="Energy to Grid Month", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="_virt_", + key="Statistic:EnergyGrid:Day", + name="Energy to Grid Day", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), ] diff --git a/requirements_all.txt b/requirements_all.txt index f76a27f0d8e2..2f88508ce0bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1835,7 +1835,7 @@ pykmtronic==0.3.0 pykodi==0.2.7 # homeassistant.components.kostal_plenticore -pykoplenti==1.0.0 +pykoplenti==1.2.2 # homeassistant.components.kraken pykrakenapi==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64cd02ec6c45..6fb4dda42229 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1388,7 +1388,7 @@ pykmtronic==0.3.0 pykodi==0.2.7 # homeassistant.components.kostal_plenticore -pykoplenti==1.0.0 +pykoplenti==1.2.2 # homeassistant.components.kraken pykrakenapi==0.1.8 diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py index 814a46f4a254..a83d9fd5e177 100644 --- a/tests/components/kostal_plenticore/conftest.py +++ b/tests/components/kostal_plenticore/conftest.py @@ -49,24 +49,20 @@ def mock_plenticore() -> Generator[Plenticore, None, None]: plenticore.client.get_version = AsyncMock() plenticore.client.get_version.return_value = VersionData( - { - "api_version": "0.2.0", - "hostname": "scb", - "name": "PUCK RESTful API", - "sw_version": "01.16.05025", - } + api_version="0.2.0", + hostname="scb", + name="PUCK RESTful API", + sw_version="01.16.05025", ) plenticore.client.get_me = AsyncMock() plenticore.client.get_me.return_value = MeData( - { - "locked": False, - "active": True, - "authenticated": True, - "permissions": [], - "anonymous": False, - "role": "USER", - } + locked=False, + active=True, + authenticated=True, + permissions=[], + anonymous=False, + role="USER", ) plenticore.client.get_process_data = AsyncMock() diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 41facfe9c260..8bfe227bfdf4 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -54,7 +54,19 @@ async def test_form_g1( # mock of the context manager instance mock_apiclient.login = AsyncMock() mock_apiclient.get_settings = AsyncMock( - return_value={"scb:network": [SettingsData({"id": "Hostname"})]} + return_value={ + "scb:network": [ + SettingsData( + min="1", + max="63", + default=None, + access="readwrite", + unit=None, + id="Hostname", + type="string", + ), + ] + } ) mock_apiclient.get_setting_values = AsyncMock( # G1 model has the entry id "Hostname" @@ -108,7 +120,19 @@ async def test_form_g2( # mock of the context manager instance mock_apiclient.login = AsyncMock() mock_apiclient.get_settings = AsyncMock( - return_value={"scb:network": [SettingsData({"id": "Network:Hostname"})]} + return_value={ + "scb:network": [ + SettingsData( + min="1", + max="63", + default=None, + access="readwrite", + unit=None, + id="Network:Hostname", + type="string", + ), + ] + } ) mock_apiclient.get_setting_values = AsyncMock( # G1 model has the entry id "Hostname" diff --git a/tests/components/kostal_plenticore/test_diagnostics.py b/tests/components/kostal_plenticore/test_diagnostics.py index d6a576484006..87c8c0e26a88 100644 --- a/tests/components/kostal_plenticore/test_diagnostics.py +++ b/tests/components/kostal_plenticore/test_diagnostics.py @@ -26,15 +26,13 @@ async def test_entry_diagnostics( mock_plenticore.client.get_settings.return_value = { "devices:local": [ SettingsData( - { - "id": "Battery:MinSoc", - "unit": "%", - "default": "None", - "min": 5, - "max": 100, - "type": "byte", - "access": "readwrite", - } + min="5", + max="100", + default=None, + access="readwrite", + unit="%", + id="Battery:MinSoc", + type="byte", ) ] } @@ -56,12 +54,12 @@ async def test_entry_diagnostics( "disabled_by": None, }, "client": { - "version": "Version(api_version=0.2.0, hostname=scb, name=PUCK RESTful API, sw_version=01.16.05025)", - "me": "Me(locked=False, active=True, authenticated=True, permissions=[], anonymous=False, role=USER)", + "version": "api_version='0.2.0' hostname='scb' name='PUCK RESTful API' sw_version='01.16.05025'", + "me": "is_locked=False is_active=True is_authenticated=True permissions=[] is_anonymous=False role='USER'", "available_process_data": {"devices:local": ["HomeGrid_P", "HomePv_P"]}, "available_settings_data": { "devices:local": [ - "SettingsData(id=Battery:MinSoc, unit=%, default=None, min=5, max=100,type=byte, access=readwrite)" + "min='5' max='100' default=None access='readwrite' unit='%' id='Battery:MinSoc' type='byte'" ] }, }, diff --git a/tests/components/kostal_plenticore/test_helper.py b/tests/components/kostal_plenticore/test_helper.py index 61df222fd9e9..93550405897d 100644 --- a/tests/components/kostal_plenticore/test_helper.py +++ b/tests/components/kostal_plenticore/test_helper.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch -from pykoplenti import ApiClient, SettingsData +from pykoplenti import ApiClient, ExtendedApiClient, SettingsData import pytest from homeassistant.components.kostal_plenticore.const import DOMAIN @@ -17,10 +17,10 @@ from tests.common import MockConfigEntry def mock_apiclient() -> Generator[ApiClient, None, None]: """Return a mocked ApiClient class.""" with patch( - "homeassistant.components.kostal_plenticore.helper.ApiClient", + "homeassistant.components.kostal_plenticore.helper.ExtendedApiClient", autospec=True, ) as mock_api_class: - apiclient = MagicMock(spec=ApiClient) + apiclient = MagicMock(spec=ExtendedApiClient) apiclient.__aenter__.return_value = apiclient apiclient.__aexit__ = AsyncMock() mock_api_class.return_value = apiclient @@ -34,7 +34,19 @@ async def test_plenticore_async_setup_g1( ) -> None: """Tests the async_setup() method of the Plenticore class for G1 models.""" mock_apiclient.get_settings = AsyncMock( - return_value={"scb:network": [SettingsData({"id": "Hostname"})]} + return_value={ + "scb:network": [ + SettingsData( + min="1", + max="63", + default=None, + access="readwrite", + unit=None, + id="Hostname", + type="string", + ) + ] + } ) mock_apiclient.get_setting_values = AsyncMock( # G1 model has the entry id "Hostname" @@ -74,7 +86,19 @@ async def test_plenticore_async_setup_g2( ) -> None: """Tests the async_setup() method of the Plenticore class for G2 models.""" mock_apiclient.get_settings = AsyncMock( - return_value={"scb:network": [SettingsData({"id": "Network:Hostname"})]} + return_value={ + "scb:network": [ + SettingsData( + min="1", + max="63", + default=None, + access="readwrite", + unit=None, + id="Network:Hostname", + type="string", + ) + ] + } ) mock_apiclient.get_setting_values = AsyncMock( # G1 model has the entry id "Hostname" diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index dd5ba7127a83..fc7d9f213fe9 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -23,9 +23,9 @@ from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture def mock_plenticore_client() -> Generator[ApiClient, None, None]: - """Return a patched ApiClient.""" + """Return a patched ExtendedApiClient.""" with patch( - "homeassistant.components.kostal_plenticore.helper.ApiClient", + "homeassistant.components.kostal_plenticore.helper.ExtendedApiClient", autospec=True, ) as plenticore_client_class: yield plenticore_client_class.return_value @@ -41,39 +41,33 @@ def mock_get_setting_values(mock_plenticore_client: ApiClient) -> list: mock_plenticore_client.get_settings.return_value = { "devices:local": [ SettingsData( - { - "default": None, - "min": 5, - "max": 100, - "access": "readwrite", - "unit": "%", - "type": "byte", - "id": "Battery:MinSoc", - } + min="5", + max="100", + default=None, + access="readwrite", + unit="%", + id="Battery:MinSoc", + type="byte", ), SettingsData( - { - "default": None, - "min": 50, - "max": 38000, - "access": "readwrite", - "unit": "W", - "type": "byte", - "id": "Battery:MinHomeComsumption", - } + min="50", + max="38000", + default=None, + access="readwrite", + unit="W", + id="Battery:MinHomeComsumption", + type="byte", ), ], "scb:network": [ SettingsData( - { - "min": "1", - "default": None, - "access": "readwrite", - "unit": None, - "id": "Hostname", - "type": "string", - "max": "63", - } + min="1", + max="63", + default=None, + access="readwrite", + unit=None, + id="Hostname", + type="string", ) ], } @@ -129,15 +123,13 @@ async def test_setup_no_entries( mock_plenticore_client.get_settings.return_value = { "scb:network": [ SettingsData( - { - "min": "1", - "default": None, - "access": "readwrite", - "unit": None, - "id": "Hostname", - "type": "string", - "max": "63", - } + min="1", + max="63", + default=None, + access="readwrite", + unit=None, + id="Hostname", + type="string", ) ], } diff --git a/tests/components/kostal_plenticore/test_select.py b/tests/components/kostal_plenticore/test_select.py index 682e8f72ac87..9af2589af9b1 100644 --- a/tests/components/kostal_plenticore/test_select.py +++ b/tests/components/kostal_plenticore/test_select.py @@ -18,8 +18,24 @@ async def test_select_battery_charging_usage_available( mock_plenticore.client.get_settings.return_value = { "devices:local": [ - SettingsData({"id": "Battery:SmartBatteryControl:Enable"}), - SettingsData({"id": "Battery:TimeControl:Enable"}), + SettingsData( + min=None, + max=None, + default=None, + access="readwrite", + unit=None, + id="Battery:SmartBatteryControl:Enable", + type="string", + ), + SettingsData( + min=None, + max=None, + default=None, + access="readwrite", + unit=None, + id="Battery:TimeControl:Enable", + type="string", + ), ] } From e884933dbdd5088eb4bd91a4a6e8f16df6e87aca Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 29 Nov 2023 14:46:19 +0100 Subject: [PATCH 222/265] Remove rest api service call timeout (#104709) --- homeassistant/components/api/__init__.py | 13 ++++------ tests/components/api/test_init.py | 32 ++++++++++++++++++------ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 6bb3cc34050a..7e4966e2b0de 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -390,17 +390,14 @@ class APIDomainServicesView(HomeAssistantView): ) try: - async with timeout(SERVICE_WAIT_TIMEOUT): - # shield the service call from cancellation on connection drop - await shield( - hass.services.async_call( - domain, service, data, blocking=True, context=context - ) + # shield the service call from cancellation on connection drop + await shield( + hass.services.async_call( + domain, service, data, blocking=True, context=context ) + ) except (vol.Invalid, ServiceNotFound) as ex: raise HTTPBadRequest() from ex - except TimeoutError: - pass finally: cancel_listen() diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index f97b55c3ede5..08cb77b45591 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -1,9 +1,10 @@ """The tests for the Home Assistant API component.""" +import asyncio from http import HTTPStatus import json from unittest.mock import patch -from aiohttp import web +from aiohttp import ServerDisconnectedError, web from aiohttp.test_utils import TestClient import pytest import voluptuous as vol @@ -352,26 +353,41 @@ async def test_api_call_service_with_data( assert state["attributes"] == {"data": 1} -async def test_api_call_service_timeout( +async def test_api_call_service_client_closed( hass: HomeAssistant, mock_api_client: TestClient ) -> None: - """Test if the API does not fail on long running services.""" + """Test that services keep running if client is closed.""" test_value = [] fut = hass.loop.create_future() + service_call_started = asyncio.Event() async def listener(service_call): """Wait and return after mock_api_client.post finishes.""" + service_call_started.set() value = await fut test_value.append(value) hass.services.async_register("test_domain", "test_service", listener) - with patch("homeassistant.components.api.SERVICE_WAIT_TIMEOUT", 0): - await mock_api_client.post("/api/services/test_domain/test_service") - assert len(test_value) == 0 - fut.set_result(1) - await hass.async_block_till_done() + api_task = hass.async_create_task( + mock_api_client.post("/api/services/test_domain/test_service") + ) + + await service_call_started.wait() + + assert len(test_value) == 0 + + await mock_api_client.close() + + assert len(test_value) == 0 + assert api_task.done() + + with pytest.raises(ServerDisconnectedError): + await api_task + + fut.set_result(1) + await hass.async_block_till_done() assert len(test_value) == 1 assert test_value[0] == 1 From 8f2e69fdb7d586f5d1ef873a04c0945e8a4e85c5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Nov 2023 15:18:25 +0100 Subject: [PATCH 223/265] Revert "Update stookwijzer api to atlas leefomgeving (#103323)" (#104705) --- homeassistant/components/stookwijzer/const.py | 6 +++--- homeassistant/components/stookwijzer/diagnostics.py | 9 ++++----- homeassistant/components/stookwijzer/manifest.json | 2 +- homeassistant/components/stookwijzer/sensor.py | 6 +++--- homeassistant/components/stookwijzer/strings.json | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 16 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/stookwijzer/const.py b/homeassistant/components/stookwijzer/const.py index d1ac46148a77..1a125da6a6bf 100644 --- a/homeassistant/components/stookwijzer/const.py +++ b/homeassistant/components/stookwijzer/const.py @@ -10,6 +10,6 @@ LOGGER = logging.getLogger(__package__) class StookwijzerState(StrEnum): """Stookwijzer states for sensor entity.""" - CODE_YELLOW = "code_yellow" - CODE_ORANGE = "code_orange" - CODE_RED = "code_red" + BLUE = "blauw" + ORANGE = "oranje" + RED = "rood" diff --git a/homeassistant/components/stookwijzer/diagnostics.py b/homeassistant/components/stookwijzer/diagnostics.py index 85996bb63941..e29606cb191f 100644 --- a/homeassistant/components/stookwijzer/diagnostics.py +++ b/homeassistant/components/stookwijzer/diagnostics.py @@ -24,9 +24,8 @@ async def async_get_config_entry_diagnostics( return { "state": client.state, "last_updated": last_updated, - "alert": client.alert, - "air_quality_index": client.lki, - "windspeed_bft": client.windspeed_bft, - "windspeed_ms": client.windspeed_ms, - "forecast": client.forecast, + "lqi": client.lqi, + "windspeed": client.windspeed, + "weather": client.weather, + "concentrations": client.concentrations, } diff --git a/homeassistant/components/stookwijzer/manifest.json b/homeassistant/components/stookwijzer/manifest.json index 91504ef923fa..dbf902b1e1e6 100644 --- a/homeassistant/components/stookwijzer/manifest.json +++ b/homeassistant/components/stookwijzer/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/stookwijzer", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["stookwijzer==1.4.2"] + "requirements": ["stookwijzer==1.3.0"] } diff --git a/homeassistant/components/stookwijzer/sensor.py b/homeassistant/components/stookwijzer/sensor.py index e8d03499a8ee..312f8bdd02da 100644 --- a/homeassistant/components/stookwijzer/sensor.py +++ b/homeassistant/components/stookwijzer/sensor.py @@ -29,7 +29,7 @@ async def async_setup_entry( class StookwijzerSensor(SensorEntity): """Defines a Stookwijzer binary sensor.""" - _attr_attribution = "Data provided by atlasleefomgeving.nl" + _attr_attribution = "Data provided by stookwijzer.nu" _attr_device_class = SensorDeviceClass.ENUM _attr_has_entity_name = True _attr_name = None @@ -43,9 +43,9 @@ class StookwijzerSensor(SensorEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{entry.entry_id}")}, name="Stookwijzer", - manufacturer="Atlas Leefomgeving", + manufacturer="stookwijzer.nu", entry_type=DeviceEntryType.SERVICE, - configuration_url="https://www.atlasleefomgeving.nl/stookwijzer", + configuration_url="https://www.stookwijzer.nu", ) def update(self) -> None: diff --git a/homeassistant/components/stookwijzer/strings.json b/homeassistant/components/stookwijzer/strings.json index 62006f878c8c..549673165ec3 100644 --- a/homeassistant/components/stookwijzer/strings.json +++ b/homeassistant/components/stookwijzer/strings.json @@ -13,9 +13,9 @@ "sensor": { "stookwijzer": { "state": { - "code_yellow": "Code yellow", - "code_orange": "Code orange", - "code_red": "Code red" + "blauw": "Blue", + "oranje": "Orange", + "rood": "Red" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 2f88508ce0bb..1e570fecc986 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2522,7 +2522,7 @@ steamodd==4.21 stookalert==0.1.4 # homeassistant.components.stookwijzer -stookwijzer==1.4.2 +stookwijzer==1.3.0 # homeassistant.components.streamlabswater streamlabswater==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6fb4dda42229..2ff6f8942852 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1886,7 +1886,7 @@ steamodd==4.21 stookalert==0.1.4 # homeassistant.components.stookwijzer -stookwijzer==1.4.2 +stookwijzer==1.3.0 # homeassistant.components.huawei_lte # homeassistant.components.solaredge From 61d82ae9ab173e0be186b4b361f3144278eb217c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Nov 2023 15:20:21 +0100 Subject: [PATCH 224/265] Tweak dockerfile generation (#104717) --- script/hassfest/docker.py | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 1849e8e7ec85..3bd44736038e 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -4,14 +4,14 @@ from homeassistant.util import executor, thread from .model import Config, Integration -DOCKERFILE_TEMPLATE = """# Automatically generated by hassfest. +DOCKERFILE_TEMPLATE = r"""# Automatically generated by hassfest. # # To update, run python3 -m script.hassfest -p docker ARG BUILD_FROM FROM ${{BUILD_FROM}} # Synchronize with homeassistant/core.py:async_stop -ENV \\ +ENV \ S6_SERVICES_GRACETIME={timeout} ARG QEMU_CPU @@ -21,33 +21,33 @@ WORKDIR /usr/src ## Setup Home Assistant Core dependencies COPY requirements.txt homeassistant/ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ -RUN \\ - pip3 install \\ - --only-binary=:all: \\ +RUN \ + pip3 install \ + --only-binary=:all: \ -r homeassistant/requirements.txt COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/ -RUN \\ - if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \\ - pip3 install homeassistant/home_assistant_frontend-*.whl; \\ - fi \\ - && if ls homeassistant/home_assistant_intents*.whl 1> /dev/null 2>&1; then \\ - pip3 install homeassistant/home_assistant_intents-*.whl; \\ - fi \\ - && \\ - LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \\ - MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \\ - pip3 install \\ - --only-binary=:all: \\ +RUN \ + if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \ + pip3 install homeassistant/home_assistant_frontend-*.whl; \ + fi \ + && if ls homeassistant/home_assistant_intents*.whl 1> /dev/null 2>&1; then \ + pip3 install homeassistant/home_assistant_intents-*.whl; \ + fi \ + && \ + LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ + MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ + pip3 install \ + --only-binary=:all: \ -r homeassistant/requirements_all.txt ## Setup Home Assistant Core COPY . homeassistant/ -RUN \\ - pip3 install \\ - --only-binary=:all: \\ - -e ./homeassistant \\ - && python3 -m compileall \\ +RUN \ + pip3 install \ + --only-binary=:all: \ + -e ./homeassistant \ + && python3 -m compileall \ homeassistant/homeassistant # Home Assistant S6-Overlay From c6c8bb69700b7107b055de533c2b3a9dfd9a07eb Mon Sep 17 00:00:00 2001 From: Stefan Rado <628587+kroimon@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:20:40 +0100 Subject: [PATCH 225/265] Bump aioesphomeapi to 19.2.1 (#104703) --- 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 26d15da680b9..7eca285681d9 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==19.2.0", + "aioesphomeapi==19.2.1", "bluetooth-data-tools==1.15.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 1e570fecc986..a52aa448d7d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -236,7 +236,7 @@ aioelectricitymaps==0.1.5 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==19.2.0 +aioesphomeapi==19.2.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ff6f8942852..a49238e84d79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -215,7 +215,7 @@ aioelectricitymaps==0.1.5 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==19.2.0 +aioesphomeapi==19.2.1 # homeassistant.components.flo aioflo==2021.11.0 From 36eb858d0aecdf88c1f5c05b825d8d737b1a24ef Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 29 Nov 2023 15:22:21 +0100 Subject: [PATCH 226/265] Rename variable in Epson tests (#104722) --- tests/components/epson/test_media_player.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/epson/test_media_player.py b/tests/components/epson/test_media_player.py index 8d6af04c1749..874a12173d67 100644 --- a/tests/components/epson/test_media_player.py +++ b/tests/components/epson/test_media_player.py @@ -30,9 +30,9 @@ async def test_set_unique_id( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.unique_id is None - entity = entity_registry.async_get("media_player.epson") - assert entity - assert entity.unique_id == entry.entry_id + entity_entry = entity_registry.async_get("media_player.epson") + assert entity_entry + assert entity_entry.unique_id == entry.entry_id with patch( "homeassistant.components.epson.Projector.get_power", return_value="01" ), patch( @@ -43,7 +43,7 @@ async def test_set_unique_id( freezer.tick(timedelta(seconds=30)) async_fire_time_changed(hass) await hass.async_block_till_done() - entity = entity_registry.async_get("media_player.epson") - assert entity - assert entity.unique_id == "123" + entity_entry = entity_registry.async_get("media_player.epson") + assert entity_entry + assert entity_entry.unique_id == "123" assert entry.unique_id == "123" From ba481001c318d301e1a36cb65991de424b3fb251 Mon Sep 17 00:00:00 2001 From: dupondje Date: Wed, 29 Nov 2023 15:41:58 +0100 Subject: [PATCH 227/265] Add support for multiple mbus devices in dsmr (#84097) * Add support for multiple mbus devices in dsmr A dsmr meter can have 4 mbus devices. Support them all and also add support for a water meter on the mbus device. * Apply suggestions from code review Co-authored-by: Jan Bouwhuis * Rewrite old gas sensor to new mbus sensor * No force updates + fix mbus entity unique_id * Remove old gas device * Add additional tests * Fix remarks from last review + move migrated 5b gas meter to new device_id * Fix ruff error * Last fixes --------- Co-authored-by: Jan Bouwhuis --- homeassistant/components/dsmr/const.py | 1 + homeassistant/components/dsmr/sensor.py | 199 ++++++-- homeassistant/components/dsmr/strings.json | 3 + tests/components/dsmr/test_mbus_migration.py | 212 +++++++++ tests/components/dsmr/test_sensor.py | 451 ++++++++++++++----- 5 files changed, 720 insertions(+), 146 deletions(-) create mode 100644 tests/components/dsmr/test_mbus_migration.py diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 5e1a54aedc4f..ec0623a9ed6c 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -29,6 +29,7 @@ DATA_TASK = "task" DEVICE_NAME_ELECTRICITY = "Electricity Meter" DEVICE_NAME_GAS = "Gas Meter" +DEVICE_NAME_WATER = "Water Meter" DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"} diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 3dbd446001ff..487f996ac1f3 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -34,6 +34,7 @@ from homeassistant.const import ( UnitOfVolume, ) from homeassistant.core import CoreState, Event, HomeAssistant, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -57,6 +58,7 @@ from .const import ( DEFAULT_TIME_BETWEEN_UPDATE, DEVICE_NAME_ELECTRICITY, DEVICE_NAME_GAS, + DEVICE_NAME_WATER, DOMAIN, DSMR_PROTOCOL, LOGGER, @@ -73,6 +75,7 @@ class DSMRSensorEntityDescription(SensorEntityDescription): dsmr_versions: set[str] | None = None is_gas: bool = False + is_water: bool = False obis_reference: str @@ -374,28 +377,138 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ) -def add_gas_sensor_5B(telegram: dict[str, DSMRObject]) -> DSMRSensorEntityDescription: - """Return correct entity for 5B Gas meter.""" - ref = None - if obis_references.BELGIUM_MBUS1_METER_READING2 in telegram: - ref = obis_references.BELGIUM_MBUS1_METER_READING2 - elif obis_references.BELGIUM_MBUS2_METER_READING2 in telegram: - ref = obis_references.BELGIUM_MBUS2_METER_READING2 - elif obis_references.BELGIUM_MBUS3_METER_READING2 in telegram: - ref = obis_references.BELGIUM_MBUS3_METER_READING2 - elif obis_references.BELGIUM_MBUS4_METER_READING2 in telegram: - ref = obis_references.BELGIUM_MBUS4_METER_READING2 - elif ref is None: - ref = obis_references.BELGIUM_MBUS1_METER_READING2 - return DSMRSensorEntityDescription( - key="belgium_5min_gas_meter_reading", - translation_key="gas_meter_reading", - obis_reference=ref, - dsmr_versions={"5B"}, - is_gas=True, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ) +def create_mbus_entity( + mbus: int, mtype: int, telegram: dict[str, DSMRObject] +) -> DSMRSensorEntityDescription | None: + """Create a new MBUS Entity.""" + if ( + mtype == 3 + and ( + obis_reference := getattr( + obis_references, f"BELGIUM_MBUS{mbus}_METER_READING2" + ) + ) + in telegram + ): + return DSMRSensorEntityDescription( + key=f"mbus{mbus}_gas_reading", + translation_key="gas_meter_reading", + obis_reference=obis_reference, + is_gas=True, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ) + if ( + mtype == 7 + and ( + obis_reference := getattr( + obis_references, f"BELGIUM_MBUS{mbus}_METER_READING1" + ) + ) + in telegram + ): + return DSMRSensorEntityDescription( + key=f"mbus{mbus}_water_reading", + translation_key="water_meter_reading", + obis_reference=obis_reference, + is_water=True, + device_class=SensorDeviceClass.WATER, + state_class=SensorStateClass.TOTAL_INCREASING, + ) + return None + + +def device_class_and_uom( + telegram: dict[str, DSMRObject], + entity_description: DSMRSensorEntityDescription, +) -> tuple[SensorDeviceClass | None, str | None]: + """Get native unit of measurement from telegram,.""" + dsmr_object = telegram[entity_description.obis_reference] + uom: str | None = getattr(dsmr_object, "unit") or None + with suppress(ValueError): + if entity_description.device_class == SensorDeviceClass.GAS and ( + enery_uom := UnitOfEnergy(str(uom)) + ): + return (SensorDeviceClass.ENERGY, enery_uom) + if uom in UNIT_CONVERSION: + return (entity_description.device_class, UNIT_CONVERSION[uom]) + return (entity_description.device_class, uom) + + +def rename_old_gas_to_mbus( + hass: HomeAssistant, entry: ConfigEntry, mbus_device_id: str +) -> None: + """Rename old gas sensor to mbus variant.""" + dev_reg = dr.async_get(hass) + device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, entry.entry_id)}) + if device_entry_v1 is not None: + device_id = device_entry_v1.id + + ent_reg = er.async_get(hass) + entries = er.async_entries_for_device(ent_reg, device_id) + + for entity in entries: + if entity.unique_id.endswith("belgium_5min_gas_meter_reading"): + try: + ent_reg.async_update_entity( + entity.entity_id, + new_unique_id=mbus_device_id, + device_id=mbus_device_id, + ) + except ValueError: + LOGGER.warning( + "Skip migration of %s because it already exists", + entity.entity_id, + ) + else: + LOGGER.info( + "Migrated entity %s from unique id %s to %s", + entity.entity_id, + entity.unique_id, + mbus_device_id, + ) + # Cleanup old device + dev_entities = er.async_entries_for_device( + ent_reg, device_id, include_disabled_entities=True + ) + if not dev_entities: + dev_reg.async_remove_device(device_id) + + +def create_mbus_entities( + hass: HomeAssistant, telegram: dict[str, DSMRObject], entry: ConfigEntry +) -> list[DSMREntity]: + """Create MBUS Entities.""" + entities = [] + for idx in range(1, 5): + if ( + device_type := getattr(obis_references, f"BELGIUM_MBUS{idx}_DEVICE_TYPE") + ) not in telegram: + continue + if (type_ := int(telegram[device_type].value)) not in (3, 7): + continue + if ( + identifier := getattr( + obis_references, + f"BELGIUM_MBUS{idx}_EQUIPMENT_IDENTIFIER", + ) + ) in telegram: + serial_ = telegram[identifier].value + rename_old_gas_to_mbus(hass, entry, serial_) + else: + serial_ = "" + if description := create_mbus_entity(idx, type_, telegram): + entities.append( + DSMREntity( + description, + entry, + telegram, + *device_class_and_uom(telegram, description), # type: ignore[arg-type] + serial_, + idx, + ) + ) + return entities async def async_setup_entry( @@ -415,25 +528,10 @@ async def async_setup_entry( add_entities_handler() add_entities_handler = None - def device_class_and_uom( - telegram: dict[str, DSMRObject], - entity_description: DSMRSensorEntityDescription, - ) -> tuple[SensorDeviceClass | None, str | None]: - """Get native unit of measurement from telegram,.""" - dsmr_object = telegram[entity_description.obis_reference] - uom: str | None = getattr(dsmr_object, "unit") or None - with suppress(ValueError): - if entity_description.device_class == SensorDeviceClass.GAS and ( - enery_uom := UnitOfEnergy(str(uom)) - ): - return (SensorDeviceClass.ENERGY, enery_uom) - if uom in UNIT_CONVERSION: - return (entity_description.device_class, UNIT_CONVERSION[uom]) - return (entity_description.device_class, uom) - - all_sensors = SENSORS if dsmr_version == "5B": - all_sensors += (add_gas_sensor_5B(telegram),) + mbus_entities = create_mbus_entities(hass, telegram, entry) + for mbus_entity in mbus_entities: + entities.append(mbus_entity) entities.extend( [ @@ -443,7 +541,7 @@ async def async_setup_entry( telegram, *device_class_and_uom(telegram, description), # type: ignore[arg-type] ) - for description in all_sensors + for description in SENSORS if ( description.dsmr_versions is None or dsmr_version in description.dsmr_versions @@ -618,6 +716,8 @@ class DSMREntity(SensorEntity): telegram: dict[str, DSMRObject], device_class: SensorDeviceClass, native_unit_of_measurement: str | None, + serial_id: str = "", + mbus_id: int = 0, ) -> None: """Initialize entity.""" self.entity_description = entity_description @@ -629,8 +729,15 @@ class DSMREntity(SensorEntity): device_serial = entry.data[CONF_SERIAL_ID] device_name = DEVICE_NAME_ELECTRICITY if entity_description.is_gas: - device_serial = entry.data[CONF_SERIAL_ID_GAS] + if serial_id: + device_serial = serial_id + else: + device_serial = entry.data[CONF_SERIAL_ID_GAS] device_name = DEVICE_NAME_GAS + if entity_description.is_water: + if serial_id: + device_serial = serial_id + device_name = DEVICE_NAME_WATER if device_serial is None: device_serial = entry.entry_id @@ -638,7 +745,13 @@ class DSMREntity(SensorEntity): identifiers={(DOMAIN, device_serial)}, name=device_name, ) - self._attr_unique_id = f"{device_serial}_{entity_description.key}" + if mbus_id != 0: + if serial_id: + self._attr_unique_id = f"{device_serial}" + else: + self._attr_unique_id = f"{device_serial}_{mbus_id}" + else: + self._attr_unique_id = f"{device_serial}_{entity_description.key}" @callback def update_data(self, telegram: dict[str, DSMRObject] | None) -> None: diff --git a/homeassistant/components/dsmr/strings.json b/homeassistant/components/dsmr/strings.json index 5f0568e29059..055c0c412648 100644 --- a/homeassistant/components/dsmr/strings.json +++ b/homeassistant/components/dsmr/strings.json @@ -147,6 +147,9 @@ }, "voltage_swell_l3_count": { "name": "Voltage swells phase L3" + }, + "water_meter_reading": { + "name": "Water consumption" } } }, diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py new file mode 100644 index 000000000000..493fd93259ff --- /dev/null +++ b/tests/components/dsmr/test_mbus_migration.py @@ -0,0 +1,212 @@ +"""Tests for the DSMR integration.""" +import datetime +from decimal import Decimal + +from homeassistant.components.dsmr.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_migrate_gas_to_mbus( + hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture +) -> None: + """Test migration of unique_id.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.obis_references import ( + BELGIUM_MBUS1_DEVICE_TYPE, + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS1_METER_READING2, + ) + from dsmr_parser.objects import CosemObject, MBusObject + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="/dev/ttyUSB0", + data={ + "port": "/dev/ttyUSB0", + "dsmr_version": "5B", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "37464C4F32313139303333373331", + }, + options={ + "time_between_update": 0, + }, + ) + + mock_entry.add_to_hass(hass) + + old_unique_id = "37464C4F32313139303333373331_belgium_5min_gas_meter_reading" + + device_registry = hass.helpers.device_registry.async_get(hass) + device = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + identifiers={(DOMAIN, mock_entry.entry_id)}, + name="Gas Meter", + ) + await hass.async_block_till_done() + + entity: er.RegistryEntry = entity_registry.async_get_or_create( + suggested_object_id="gas_meter_reading", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + device_id=device.id, + unique_id=old_unique_id, + config_entry=mock_entry, + ) + assert entity.unique_id == old_unique_id + await hass.async_block_till_done() + + telegram = { + BELGIUM_MBUS1_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS1_DEVICE_TYPE, [{"value": "003", "unit": ""}] + ), + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373331", "unit": ""}], + ), + BELGIUM_MBUS1_METER_READING2: MBusObject( + BELGIUM_MBUS1_METER_READING2, + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ], + ), + } + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to be created + await hass.async_block_till_done() + + dev_entities = er.async_entries_for_device( + entity_registry, device.id, include_disabled_entities=True + ) + assert not dev_entities + + assert ( + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id) + is None + ) + assert ( + entity_registry.async_get_entity_id( + SENSOR_DOMAIN, DOMAIN, "37464C4F32313139303333373331" + ) + == "sensor.gas_meter_reading" + ) + + +async def test_migrate_gas_to_mbus_exists( + hass: HomeAssistant, entity_registry: er.EntityRegistry, dsmr_connection_fixture +) -> None: + """Test migration of unique_id.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.obis_references import ( + BELGIUM_MBUS1_DEVICE_TYPE, + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS1_METER_READING2, + ) + from dsmr_parser.objects import CosemObject, MBusObject + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="/dev/ttyUSB0", + data={ + "port": "/dev/ttyUSB0", + "dsmr_version": "5B", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "37464C4F32313139303333373331", + }, + options={ + "time_between_update": 0, + }, + ) + + mock_entry.add_to_hass(hass) + + old_unique_id = "37464C4F32313139303333373331_belgium_5min_gas_meter_reading" + + device_registry = hass.helpers.device_registry.async_get(hass) + device = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + identifiers={(DOMAIN, mock_entry.entry_id)}, + name="Gas Meter", + ) + await hass.async_block_till_done() + + entity: er.RegistryEntry = entity_registry.async_get_or_create( + suggested_object_id="gas_meter_reading", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + device_id=device.id, + unique_id=old_unique_id, + config_entry=mock_entry, + ) + assert entity.unique_id == old_unique_id + + device2 = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + identifiers={(DOMAIN, "37464C4F32313139303333373331")}, + name="Gas Meter", + ) + await hass.async_block_till_done() + + entity_registry.async_get_or_create( + suggested_object_id="gas_meter_reading_alt", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + device_id=device2.id, + unique_id="37464C4F32313139303333373331", + config_entry=mock_entry, + ) + await hass.async_block_till_done() + + telegram = { + BELGIUM_MBUS1_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS1_DEVICE_TYPE, [{"value": "003", "unit": ""}] + ), + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373331", "unit": ""}], + ), + BELGIUM_MBUS1_METER_READING2: MBusObject( + BELGIUM_MBUS1_METER_READING2, + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ], + ), + } + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to be created + await hass.async_block_till_done() + + assert ( + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id) + == "sensor.gas_meter_reading" + ) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 1895dd15dd17..1b7f8efb201a 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -8,21 +8,8 @@ import asyncio import datetime from decimal import Decimal from itertools import chain, repeat -from typing import Literal from unittest.mock import DEFAULT, MagicMock -from dsmr_parser.obis_references import ( - BELGIUM_MBUS1_METER_READING1, - BELGIUM_MBUS1_METER_READING2, - BELGIUM_MBUS2_METER_READING1, - BELGIUM_MBUS2_METER_READING2, - BELGIUM_MBUS3_METER_READING1, - BELGIUM_MBUS3_METER_READING2, - BELGIUM_MBUS4_METER_READING1, - BELGIUM_MBUS4_METER_READING2, -) -import pytest - from homeassistant import config_entries from homeassistant.components.sensor import ( ATTR_OPTIONS, @@ -145,8 +132,8 @@ async def test_default_setup( # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser telegram_callback(telegram) - # after receiving telegram entities need to have the chance to update - await asyncio.sleep(0) + # after receiving telegram entities need to have the chance to be created + await hass.async_block_till_done() # ensure entities have new state value after incoming telegram power_consumption = hass.states.get("sensor.electricity_meter_power_consumption") @@ -495,10 +482,18 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No from dsmr_parser.obis_references import ( BELGIUM_CURRENT_AVERAGE_DEMAND, BELGIUM_MAXIMUM_DEMAND_MONTH, + BELGIUM_MBUS1_DEVICE_TYPE, + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, BELGIUM_MBUS1_METER_READING2, - BELGIUM_MBUS2_METER_READING2, + BELGIUM_MBUS2_DEVICE_TYPE, + BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS2_METER_READING1, + BELGIUM_MBUS3_DEVICE_TYPE, + BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, BELGIUM_MBUS3_METER_READING2, - BELGIUM_MBUS4_METER_READING2, + BELGIUM_MBUS4_DEVICE_TYPE, + BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS4_METER_READING1, ELECTRICITY_ACTIVE_TARIFF, ) from dsmr_parser.objects import CosemObject, MBusObject @@ -509,41 +504,13 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No "precision": 4, "reconnect_interval": 30, "serial_id": "1234", - "serial_id_gas": "5678", + "serial_id_gas": None, } entry_options = { "time_between_update": 0, } telegram = { - BELGIUM_MBUS1_METER_READING2: MBusObject( - BELGIUM_MBUS1_METER_READING2, - [ - {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": "m3"}, - ], - ), - BELGIUM_MBUS2_METER_READING2: MBusObject( - BELGIUM_MBUS2_METER_READING2, - [ - {"value": datetime.datetime.fromtimestamp(1551642214)}, - {"value": Decimal(745.696), "unit": "m3"}, - ], - ), - BELGIUM_MBUS3_METER_READING2: MBusObject( - BELGIUM_MBUS3_METER_READING2, - [ - {"value": datetime.datetime.fromtimestamp(1551642215)}, - {"value": Decimal(745.697), "unit": "m3"}, - ], - ), - BELGIUM_MBUS4_METER_READING2: MBusObject( - BELGIUM_MBUS4_METER_READING2, - [ - {"value": datetime.datetime.fromtimestamp(1551642216)}, - {"value": Decimal(745.698), "unit": "m3"}, - ], - ), BELGIUM_CURRENT_AVERAGE_DEMAND: CosemObject( BELGIUM_CURRENT_AVERAGE_DEMAND, [{"value": Decimal(1.75), "unit": "kW"}], @@ -555,6 +522,62 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No {"value": Decimal(4.11), "unit": "kW"}, ], ), + BELGIUM_MBUS1_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS1_DEVICE_TYPE, [{"value": "003", "unit": ""}] + ), + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373331", "unit": ""}], + ), + BELGIUM_MBUS1_METER_READING2: MBusObject( + BELGIUM_MBUS1_METER_READING2, + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": "m3"}, + ], + ), + BELGIUM_MBUS2_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS2_DEVICE_TYPE, [{"value": "007", "unit": ""}] + ), + BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373332", "unit": ""}], + ), + BELGIUM_MBUS2_METER_READING1: MBusObject( + BELGIUM_MBUS2_METER_READING1, + [ + {"value": datetime.datetime.fromtimestamp(1551642214)}, + {"value": Decimal(678.695), "unit": "m3"}, + ], + ), + BELGIUM_MBUS3_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS3_DEVICE_TYPE, [{"value": "003", "unit": ""}] + ), + BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373333", "unit": ""}], + ), + BELGIUM_MBUS3_METER_READING2: MBusObject( + BELGIUM_MBUS3_METER_READING2, + [ + {"value": datetime.datetime.fromtimestamp(1551642215)}, + {"value": Decimal(12.12), "unit": "m3"}, + ], + ), + BELGIUM_MBUS4_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS4_DEVICE_TYPE, [{"value": "007", "unit": ""}] + ), + BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373334", "unit": ""}], + ), + BELGIUM_MBUS4_METER_READING1: MBusObject( + BELGIUM_MBUS4_METER_READING1, + [ + {"value": datetime.datetime.fromtimestamp(1551642216)}, + {"value": Decimal(13.13), "unit": "m3"}, + ], + ), ELECTRICITY_ACTIVE_TARIFF: CosemObject( ELECTRICITY_ACTIVE_TARIFF, [{"value": "0001", "unit": ""}] ), @@ -600,7 +623,7 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No assert max_demand.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.KILO_WATT assert max_demand.attributes.get(ATTR_STATE_CLASS) is None - # check if gas consumption is parsed correctly + # check if gas consumption mbus1 is parsed correctly gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS @@ -613,48 +636,69 @@ async def test_belgian_meter(hass: HomeAssistant, dsmr_connection_fixture) -> No == UnitOfVolume.CUBIC_METERS ) + # check if water usage mbus2 is parsed correctly + water_consumption = hass.states.get("sensor.water_meter_water_consumption") + assert water_consumption.state == "678.695" + assert ( + water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER + ) + assert ( + water_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + water_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS + ) -@pytest.mark.parametrize( - ("key1", "key2", "key3", "gas_value"), - [ - ( - BELGIUM_MBUS1_METER_READING1, - BELGIUM_MBUS2_METER_READING2, - BELGIUM_MBUS3_METER_READING1, - "745.696", - ), - ( - BELGIUM_MBUS1_METER_READING2, - BELGIUM_MBUS2_METER_READING1, - BELGIUM_MBUS3_METER_READING2, - "745.695", - ), - ( - BELGIUM_MBUS4_METER_READING2, - BELGIUM_MBUS2_METER_READING1, - BELGIUM_MBUS3_METER_READING1, - "745.695", - ), - ( - BELGIUM_MBUS4_METER_READING1, - BELGIUM_MBUS2_METER_READING1, - BELGIUM_MBUS3_METER_READING2, - "745.697", - ), - ], -) -async def test_belgian_meter_alt( - hass: HomeAssistant, - dsmr_connection_fixture, - key1: Literal, - key2: Literal, - key3: Literal, - gas_value: str, -) -> None: + # check if gas consumption mbus1 is parsed correctly + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption_2") + assert gas_consumption.state == "12.12" + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS + ) + + # check if water usage mbus2 is parsed correctly + water_consumption = hass.states.get("sensor.water_meter_water_consumption_2") + assert water_consumption.state == "13.13" + assert ( + water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER + ) + assert ( + water_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + water_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS + ) + + +async def test_belgian_meter_alt(hass: HomeAssistant, dsmr_connection_fixture) -> None: """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture - from dsmr_parser.objects import MBusObject + from dsmr_parser.obis_references import ( + BELGIUM_MBUS1_DEVICE_TYPE, + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS1_METER_READING1, + BELGIUM_MBUS2_DEVICE_TYPE, + BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS2_METER_READING2, + BELGIUM_MBUS3_DEVICE_TYPE, + BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS3_METER_READING1, + BELGIUM_MBUS4_DEVICE_TYPE, + BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS4_METER_READING2, + ) + from dsmr_parser.objects import CosemObject, MBusObject entry_data = { "port": "/dev/ttyUSB0", @@ -662,32 +706,67 @@ async def test_belgian_meter_alt( "precision": 4, "reconnect_interval": 30, "serial_id": "1234", - "serial_id_gas": "5678", + "serial_id_gas": None, } entry_options = { "time_between_update": 0, } telegram = { - key1: MBusObject( - key1, - [ - {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": "m3"}, - ], + BELGIUM_MBUS1_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS1_DEVICE_TYPE, [{"value": "007", "unit": ""}] ), - key2: MBusObject( - key2, - [ - {"value": datetime.datetime.fromtimestamp(1551642214)}, - {"value": Decimal(745.696), "unit": "m3"}, - ], + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373331", "unit": ""}], ), - key3: MBusObject( - key3, + BELGIUM_MBUS1_METER_READING1: MBusObject( + BELGIUM_MBUS1_METER_READING1, [ {"value": datetime.datetime.fromtimestamp(1551642215)}, - {"value": Decimal(745.697), "unit": "m3"}, + {"value": Decimal(123.456), "unit": "m3"}, + ], + ), + BELGIUM_MBUS2_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS2_DEVICE_TYPE, [{"value": "003", "unit": ""}] + ), + BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373332", "unit": ""}], + ), + BELGIUM_MBUS2_METER_READING2: MBusObject( + BELGIUM_MBUS2_METER_READING2, + [ + {"value": datetime.datetime.fromtimestamp(1551642216)}, + {"value": Decimal(678.901), "unit": "m3"}, + ], + ), + BELGIUM_MBUS3_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS3_DEVICE_TYPE, [{"value": "007", "unit": ""}] + ), + BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373333", "unit": ""}], + ), + BELGIUM_MBUS3_METER_READING1: MBusObject( + BELGIUM_MBUS3_METER_READING1, + [ + {"value": datetime.datetime.fromtimestamp(1551642217)}, + {"value": Decimal(12.12), "unit": "m3"}, + ], + ), + BELGIUM_MBUS4_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS4_DEVICE_TYPE, [{"value": "003", "unit": ""}] + ), + BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS4_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373334", "unit": ""}], + ), + BELGIUM_MBUS4_METER_READING2: MBusObject( + BELGIUM_MBUS4_METER_READING2, + [ + {"value": datetime.datetime.fromtimestamp(1551642218)}, + {"value": Decimal(13.13), "unit": "m3"}, ], ), } @@ -709,9 +788,24 @@ async def test_belgian_meter_alt( # after receiving telegram entities need to have the chance to be created await hass.async_block_till_done() - # check if gas consumption is parsed correctly + # check if water usage mbus1 is parsed correctly + water_consumption = hass.states.get("sensor.water_meter_water_consumption") + assert water_consumption.state == "123.456" + assert ( + water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER + ) + assert ( + water_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + water_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS + ) + + # check if gas consumption mbus2 is parsed correctly gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") - assert gas_consumption.state == gas_value + assert gas_consumption.state == "678.901" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) @@ -722,6 +816,157 @@ async def test_belgian_meter_alt( == UnitOfVolume.CUBIC_METERS ) + # check if water usage mbus3 is parsed correctly + water_consumption = hass.states.get("sensor.water_meter_water_consumption_2") + assert water_consumption.state == "12.12" + assert ( + water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER + ) + assert ( + water_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + water_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS + ) + + # check if gas consumption mbus4 is parsed correctly + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption_2") + assert gas_consumption.state == "13.13" + assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS + assert ( + gas_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS + ) + + +async def test_belgian_meter_mbus(hass: HomeAssistant, dsmr_connection_fixture) -> None: + """Test if Belgian meter is correctly parsed.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.obis_references import ( + BELGIUM_MBUS1_DEVICE_TYPE, + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS2_DEVICE_TYPE, + BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS3_DEVICE_TYPE, + BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, + BELGIUM_MBUS3_METER_READING2, + BELGIUM_MBUS4_DEVICE_TYPE, + BELGIUM_MBUS4_METER_READING1, + ELECTRICITY_ACTIVE_TARIFF, + ) + from dsmr_parser.objects import CosemObject, MBusObject + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5B", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": None, + } + entry_options = { + "time_between_update": 0, + } + + telegram = { + ELECTRICITY_ACTIVE_TARIFF: CosemObject( + ELECTRICITY_ACTIVE_TARIFF, [{"value": "0003", "unit": ""}] + ), + BELGIUM_MBUS1_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS1_DEVICE_TYPE, [{"value": "006", "unit": ""}] + ), + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS1_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373331", "unit": ""}], + ), + BELGIUM_MBUS2_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS2_DEVICE_TYPE, [{"value": "003", "unit": ""}] + ), + BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS2_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373332", "unit": ""}], + ), + BELGIUM_MBUS3_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS3_DEVICE_TYPE, [{"value": "007", "unit": ""}] + ), + BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER: CosemObject( + BELGIUM_MBUS3_EQUIPMENT_IDENTIFIER, + [{"value": "37464C4F32313139303333373333", "unit": ""}], + ), + BELGIUM_MBUS3_METER_READING2: MBusObject( + BELGIUM_MBUS3_METER_READING2, + [ + {"value": datetime.datetime.fromtimestamp(1551642217)}, + {"value": Decimal(12.12), "unit": "m3"}, + ], + ), + BELGIUM_MBUS4_DEVICE_TYPE: CosemObject( + BELGIUM_MBUS4_DEVICE_TYPE, [{"value": "007", "unit": ""}] + ), + BELGIUM_MBUS4_METER_READING1: MBusObject( + BELGIUM_MBUS4_METER_READING1, + [ + {"value": datetime.datetime.fromtimestamp(1551642218)}, + {"value": Decimal(13.13), "unit": "m3"}, + ], + ), + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to be created + await hass.async_block_till_done() + + # tariff should be translated in human readable and have no unit + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "unknown" + + # check if gas consumption mbus2 is parsed correctly + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") + assert gas_consumption is None + + # check if water usage mbus3 is parsed correctly + water_consumption = hass.states.get("sensor.water_meter_water_consumption_2") + assert water_consumption is None + + # check if gas consumption mbus4 is parsed correctly + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption_2") + assert gas_consumption is None + + # check if gas consumption mbus4 is parsed correctly + water_consumption = hass.states.get("sensor.water_meter_water_consumption") + assert water_consumption.state == "13.13" + assert ( + water_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER + ) + assert ( + water_consumption.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + ) + assert ( + water_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS + ) + async def test_belgian_meter_low(hass: HomeAssistant, dsmr_connection_fixture) -> None: """Test if Belgian meter is correctly parsed.""" From 608f4f7c5250ac0838358bd636552190290de64c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Nov 2023 09:13:54 -0600 Subject: [PATCH 228/265] Bump aiohttp to 3.9.1 (#104176) --- homeassistant/components/hassio/http.py | 4 ++++ homeassistant/components/hassio/ingress.py | 2 +- homeassistant/helpers/aiohttp_client.py | 2 +- homeassistant/package_constraints.txt | 3 +-- pyproject.toml | 3 +-- requirements.txt | 3 +-- tests/components/generic/test_camera.py | 16 ++++------------ tests/util/test_aiohttp.py | 22 +++++----------------- 8 files changed, 18 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 419d80484cf3..9d72d5842fdc 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -6,6 +6,7 @@ from http import HTTPStatus import logging import os import re +from typing import TYPE_CHECKING from urllib.parse import quote, unquote import aiohttp @@ -156,6 +157,9 @@ class HassIOView(HomeAssistantView): # _stored_content_type is only computed once `content_type` is accessed if path == "backups/new/upload": # We need to reuse the full content type that includes the boundary + if TYPE_CHECKING: + # pylint: disable-next=protected-access + assert isinstance(request._stored_content_type, str) # pylint: disable-next=protected-access headers[CONTENT_TYPE] = request._stored_content_type diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index b29f80ff2b38..7da6f044db09 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -172,7 +172,7 @@ class HassIOIngress(HomeAssistantView): content_length = result.headers.get(hdrs.CONTENT_LENGTH, UNDEFINED) # Avoid parsing content_type in simple cases for better performance if maybe_content_type := result.headers.get(hdrs.CONTENT_TYPE): - content_type = (maybe_content_type.partition(";"))[0].strip() + content_type: str = (maybe_content_type.partition(";"))[0].strip() else: content_type = result.content_type # Simple request diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index fba9bb647ddd..74527a5922f4 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -298,7 +298,7 @@ def _async_get_connector( return connectors[connector_key] if verify_ssl: - ssl_context: bool | SSLContext = ssl_util.get_default_context() + ssl_context: SSLContext = ssl_util.get_default_context() else: ssl_context = ssl_util.get_default_no_verify_context() diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4aa2a4ea0131..73f12f8db90a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -3,8 +3,7 @@ aiodiscover==1.5.1 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.1.1 -aiohttp==3.8.5;python_version<'3.12' -aiohttp==3.9.0;python_version>='3.12' +aiohttp==3.9.1 aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.36.2 diff --git a/pyproject.toml b/pyproject.toml index b05f5f5f9dce..71e58bf2177e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,8 +23,7 @@ classifiers = [ ] requires-python = ">=3.11.0" dependencies = [ - "aiohttp==3.9.0;python_version>='3.12'", - "aiohttp==3.8.5;python_version<'3.12'", + "aiohttp==3.9.1", "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-zlib-ng==0.1.1", diff --git a/requirements.txt b/requirements.txt index f26c1a84e810..aa9a0ab0e5ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.9.0;python_version>='3.12' -aiohttp==3.8.5;python_version<'3.12' +aiohttp==3.9.1 aiohttp_cors==0.7.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.1.1 diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index aecfcbc29c19..8bfd0a66dd5e 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -1,7 +1,6 @@ """The tests for generic camera component.""" import asyncio from http import HTTPStatus -import sys from unittest.mock import patch import aiohttp @@ -164,17 +163,10 @@ async def test_limit_refetch( hass.states.async_set("sensor.temp", "5") - # TODO: Remove version check with aiohttp 3.9.0 - if sys.version_info >= (3, 12): - with pytest.raises(aiohttp.ServerTimeoutError), patch( - "asyncio.timeout", side_effect=asyncio.TimeoutError() - ): - resp = await client.get("/api/camera_proxy/camera.config_test") - else: - with pytest.raises(aiohttp.ServerTimeoutError), patch( - "async_timeout.timeout", side_effect=asyncio.TimeoutError() - ): - resp = await client.get("/api/camera_proxy/camera.config_test") + with pytest.raises(aiohttp.ServerTimeoutError), patch( + "asyncio.timeout", side_effect=asyncio.TimeoutError() + ): + resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == 1 assert resp.status == HTTPStatus.OK diff --git a/tests/util/test_aiohttp.py b/tests/util/test_aiohttp.py index bfdc3c3e949c..ada0269ac0ec 100644 --- a/tests/util/test_aiohttp.py +++ b/tests/util/test_aiohttp.py @@ -1,5 +1,4 @@ """Test aiohttp request helper.""" -import sys from aiohttp import web @@ -50,22 +49,11 @@ def test_serialize_text() -> None: def test_serialize_body_str() -> None: """Test serializing a response with a str as body.""" response = web.Response(status=201, body="Hello") - # TODO: Remove version check with aiohttp 3.9.0 - if sys.version_info >= (3, 12): - assert aiohttp.serialize_response(response) == { - "status": 201, - "body": "Hello", - "headers": {"Content-Type": "text/plain; charset=utf-8"}, - } - else: - assert aiohttp.serialize_response(response) == { - "status": 201, - "body": "Hello", - "headers": { - "Content-Length": "5", - "Content-Type": "text/plain; charset=utf-8", - }, - } + assert aiohttp.serialize_response(response) == { + "status": 201, + "body": "Hello", + "headers": {"Content-Type": "text/plain; charset=utf-8"}, + } def test_serialize_body_None() -> None: From e2bab699b599a1198af787db14fdb8251304e90a Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 29 Nov 2023 16:24:30 +0100 Subject: [PATCH 229/265] Avoid double refresh when adding entities in bsblan (#104647) --- homeassistant/components/bsblan/climate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 39eab6e7e0a5..609d5ab6e83a 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -60,8 +60,7 @@ async def async_setup_entry( data.static, entry, ) - ], - True, + ] ) From 4628b036776649dbe1dacea3a908be817eb8d529 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 29 Nov 2023 16:29:20 +0100 Subject: [PATCH 230/265] Update frontend to 20231129.1 (#104723) --- 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 6a6c557c7b78..7a587d56d741 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20231129.0"] + "requirements": ["home-assistant-frontend==20231129.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 73f12f8db90a..acb65b6fce8a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ ha-ffmpeg==3.1.0 hass-nabucasa==0.74.0 hassil==1.5.1 home-assistant-bluetooth==1.10.4 -home-assistant-frontend==20231129.0 +home-assistant-frontend==20231129.1 home-assistant-intents==2023.11.17 httpx==0.25.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index a52aa448d7d1..b3c498a5f12c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1014,7 +1014,7 @@ hole==0.8.0 holidays==0.36 # homeassistant.components.frontend -home-assistant-frontend==20231129.0 +home-assistant-frontend==20231129.1 # homeassistant.components.conversation home-assistant-intents==2023.11.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a49238e84d79..8dc991f9a61e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -801,7 +801,7 @@ hole==0.8.0 holidays==0.36 # homeassistant.components.frontend -home-assistant-frontend==20231129.0 +home-assistant-frontend==20231129.1 # homeassistant.components.conversation home-assistant-intents==2023.11.17 From 82264a0d6b30426fd79ad8413f843241ce8a5f23 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 29 Nov 2023 16:29:42 +0100 Subject: [PATCH 231/265] Fix mqtt cover state is open after receiving stopped payload (#104726) --- homeassistant/components/mqtt/cover.py | 6 +++++- tests/components/mqtt/test_cover.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index c8da14e67e6f..4e8cf0f4129b 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -380,7 +380,11 @@ class MqttCover(MqttEntity, CoverEntity): else STATE_OPEN ) else: - state = STATE_CLOSED if self.state == STATE_CLOSING else STATE_OPEN + state = ( + STATE_CLOSED + if self.state in [STATE_CLOSED, STATE_CLOSING] + else STATE_OPEN + ) elif payload == self._config[CONF_STATE_OPENING]: state = STATE_OPENING elif payload == self._config[CONF_STATE_CLOSING]: diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index f3bf92951b09..8db1c89bc405 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -3347,6 +3347,11 @@ async def test_set_state_via_stopped_state_no_position_topic( state = hass.states.get("cover.test") assert state.state == STATE_CLOSED + async_fire_mqtt_message(hass, "state-topic", "STOPPED") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + @pytest.mark.parametrize( "hass_config", From b36ddaa15c5975fbae36e2b4eef9721ef45f1a35 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 29 Nov 2023 16:30:23 +0100 Subject: [PATCH 232/265] Change super class order in Withings Calendar (#104721) --- homeassistant/components/withings/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/withings/calendar.py b/homeassistant/components/withings/calendar.py index 19572682d1a8..132f00936f32 100644 --- a/homeassistant/components/withings/calendar.py +++ b/homeassistant/components/withings/calendar.py @@ -66,7 +66,7 @@ def get_event_name(category: WorkoutCategory) -> str: class WithingsWorkoutCalendarEntity( - CalendarEntity, WithingsEntity[WithingsWorkoutDataUpdateCoordinator] + WithingsEntity[WithingsWorkoutDataUpdateCoordinator], CalendarEntity ): """A calendar entity.""" From 4bf88b1690f9d720a012b98b49fbbd5b0dd40c13 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 29 Nov 2023 16:42:10 +0100 Subject: [PATCH 233/265] Improve MQTT json light brightness scaling (#104510) * Improve MQTT json light brightness scaling * Revert unrelated changes * Format --- homeassistant/components/mqtt/light/schema_json.py | 7 ++----- tests/components/mqtt/test_light_json.py | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 2a2a262be36c..3d2957f153d1 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -367,13 +367,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): if brightness_supported(self.supported_color_modes): try: if brightness := values["brightness"]: + scale = self._config[CONF_BRIGHTNESS_SCALE] self._attr_brightness = min( - int( - brightness # type: ignore[operator] - / float(self._config[CONF_BRIGHTNESS_SCALE]) - * 255 - ), 255, + round(brightness * 255 / scale), # type: ignore[operator] ) else: _LOGGER.debug( diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index b3dd3a9a4e35..82b0b3467f4c 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -1792,7 +1792,7 @@ async def test_brightness_scale( state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 128 + assert state.attributes.get("brightness") == 129 # Test limmiting max brightness async_fire_mqtt_message( @@ -1862,7 +1862,7 @@ async def test_white_scale( state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("brightness") == 128 + assert state.attributes.get("brightness") == 129 @pytest.mark.parametrize( From 47426a3ddca7b19e785ddaa6c151148f03f0a862 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 29 Nov 2023 16:56:26 +0100 Subject: [PATCH 234/265] Remove redundant websocket_api exception handler (#104727) --- homeassistant/components/websocket_api/commands.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 5edf50189380..cb90b46e182b 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -794,9 +794,6 @@ async def handle_execute_script( translation_placeholders=err.translation_placeholders, ) return - except Exception as exc: # pylint: disable=broad-except - connection.async_handle_exception(msg, exc) - return connection.send_result( msg["id"], { From a894146cee389078d5b04760eadbd0465bf8181c Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 29 Nov 2023 11:07:22 -0600 Subject: [PATCH 235/265] Fix TTS streaming for VoIP (#104620) * Use wav instead of raw tts audio in voip * More tests * Use mock TTS dir --- homeassistant/components/voip/voip.py | 29 +++- tests/components/voip/test_voip.py | 218 +++++++++++++++++++++++++- 2 files changed, 239 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 120f2d9559b6..74bc94e7dc52 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -5,10 +5,12 @@ import asyncio from collections import deque from collections.abc import AsyncIterable, MutableSequence, Sequence from functools import partial +import io import logging from pathlib import Path import time from typing import TYPE_CHECKING +import wave from voip_utils import ( CallInfo, @@ -285,7 +287,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): ), conversation_id=self._conversation_id, device_id=self.voip_device.device_id, - tts_audio_output="raw", + tts_audio_output="wav", ) if self._pipeline_error: @@ -402,11 +404,32 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): if self.transport is None: return - _extension, audio_bytes = await tts.async_get_media_source_audio( + extension, data = await tts.async_get_media_source_audio( self.hass, media_id, ) + if extension != "wav": + raise ValueError(f"Only WAV audio can be streamed, got {extension}") + + with io.BytesIO(data) as wav_io: + with wave.open(wav_io, "rb") as wav_file: + sample_rate = wav_file.getframerate() + sample_width = wav_file.getsampwidth() + sample_channels = wav_file.getnchannels() + + if ( + (sample_rate != 16000) + or (sample_width != 2) + or (sample_channels != 1) + ): + raise ValueError( + "Expected rate/width/channels as 16000/2/1," + " got {sample_rate}/{sample_width}/{sample_channels}}" + ) + + audio_bytes = wav_file.readframes(wav_file.getnframes()) + _LOGGER.debug("Sending %s byte(s) of audio", len(audio_bytes)) # Time out 1 second after TTS audio should be finished @@ -414,7 +437,7 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): tts_seconds = tts_samples / RATE async with asyncio.timeout(tts_seconds + self.tts_extra_timeout): - # Assume TTS audio is 16Khz 16-bit mono + # TTS audio is 16Khz 16-bit mono await self._async_send_audio(audio_bytes) except asyncio.TimeoutError as err: _LOGGER.warning("TTS timeout") diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index f82a00087c6d..692896c6dfaa 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -1,7 +1,9 @@ """Test VoIP protocol.""" import asyncio +import io import time from unittest.mock import AsyncMock, Mock, patch +import wave import pytest @@ -14,6 +16,24 @@ _ONE_SECOND = 16000 * 2 # 16Khz 16-bit _MEDIA_ID = "12345" +@pytest.fixture(autouse=True) +def mock_tts_cache_dir_autouse(mock_tts_cache_dir): + """Mock the TTS cache dir with empty dir.""" + return mock_tts_cache_dir + + +def _empty_wav() -> bytes: + """Return bytes of an empty WAV file.""" + with io.BytesIO() as wav_io: + wav_file: wave.Wave_write = wave.open(wav_io, "wb") + with wav_file: + wav_file.setframerate(16000) + wav_file.setsampwidth(2) + wav_file.setnchannels(1) + + return wav_io.getvalue() + + async def test_pipeline( hass: HomeAssistant, voip_device: VoIPDevice, @@ -72,8 +92,7 @@ async def test_pipeline( media_source_id: str, ) -> tuple[str, bytes]: assert media_source_id == _MEDIA_ID - - return ("mp3", b"") + return ("wav", _empty_wav()) with patch( "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", @@ -266,7 +285,7 @@ async def test_tts_timeout( media_source_id: str, ) -> tuple[str, bytes]: # Should time out immediately - return ("raw", bytes(0)) + return ("wav", _empty_wav()) with patch( "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", @@ -305,8 +324,197 @@ async def test_tts_timeout( done.set() - rtp_protocol._async_send_audio = AsyncMock(side_effect=async_send_audio) - rtp_protocol._send_tts = AsyncMock(side_effect=send_tts) + rtp_protocol._async_send_audio = AsyncMock(side_effect=async_send_audio) # type: ignore[method-assign] + rtp_protocol._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign] + + # silence + rtp_protocol.on_chunk(bytes(_ONE_SECOND)) + + # "speech" + rtp_protocol.on_chunk(bytes([255] * _ONE_SECOND * 2)) + + # silence (assumes relaxed VAD sensitivity) + rtp_protocol.on_chunk(bytes(_ONE_SECOND * 4)) + + # Wait for mock pipeline to exhaust the audio stream + async with asyncio.timeout(1): + await done.wait() + + +async def test_tts_wrong_extension( + hass: HomeAssistant, + voip_device: VoIPDevice, +) -> None: + """Test that TTS will only stream WAV audio.""" + assert await async_setup_component(hass, "voip", {}) + + def is_speech(self, chunk): + """Anything non-zero is speech.""" + return sum(chunk) > 0 + + done = asyncio.Event() + + async def async_pipeline_from_audio_stream(*args, **kwargs): + stt_stream = kwargs["stt_stream"] + event_callback = kwargs["event_callback"] + async for _chunk in stt_stream: + # Stream will end when VAD detects end of "speech" + pass + + # Fake intent result + event_callback( + assist_pipeline.PipelineEvent( + type=assist_pipeline.PipelineEventType.INTENT_END, + data={ + "intent_output": { + "conversation_id": "fake-conversation", + } + }, + ) + ) + + # Proceed with media output + event_callback( + assist_pipeline.PipelineEvent( + type=assist_pipeline.PipelineEventType.TTS_END, + data={"tts_output": {"media_id": _MEDIA_ID}}, + ) + ) + + async def async_get_media_source_audio( + hass: HomeAssistant, + media_source_id: str, + ) -> tuple[str, bytes]: + # Should fail because it's not "wav" + return ("mp3", b"") + + with patch( + "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", + new=is_speech, + ), patch( + "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), patch( + "homeassistant.components.voip.voip.tts.async_get_media_source_audio", + new=async_get_media_source_audio, + ): + rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( + hass, + hass.config.language, + voip_device, + Context(), + opus_payload_type=123, + ) + rtp_protocol.transport = Mock() + + original_send_tts = rtp_protocol._send_tts + + async def send_tts(*args, **kwargs): + # Call original then end test successfully + with pytest.raises(ValueError): + await original_send_tts(*args, **kwargs) + + done.set() + + rtp_protocol._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign] + + # silence + rtp_protocol.on_chunk(bytes(_ONE_SECOND)) + + # "speech" + rtp_protocol.on_chunk(bytes([255] * _ONE_SECOND * 2)) + + # silence (assumes relaxed VAD sensitivity) + rtp_protocol.on_chunk(bytes(_ONE_SECOND * 4)) + + # Wait for mock pipeline to exhaust the audio stream + async with asyncio.timeout(1): + await done.wait() + + +async def test_tts_wrong_wav_format( + hass: HomeAssistant, + voip_device: VoIPDevice, +) -> None: + """Test that TTS will only stream WAV audio with a specific format.""" + assert await async_setup_component(hass, "voip", {}) + + def is_speech(self, chunk): + """Anything non-zero is speech.""" + return sum(chunk) > 0 + + done = asyncio.Event() + + async def async_pipeline_from_audio_stream(*args, **kwargs): + stt_stream = kwargs["stt_stream"] + event_callback = kwargs["event_callback"] + async for _chunk in stt_stream: + # Stream will end when VAD detects end of "speech" + pass + + # Fake intent result + event_callback( + assist_pipeline.PipelineEvent( + type=assist_pipeline.PipelineEventType.INTENT_END, + data={ + "intent_output": { + "conversation_id": "fake-conversation", + } + }, + ) + ) + + # Proceed with media output + event_callback( + assist_pipeline.PipelineEvent( + type=assist_pipeline.PipelineEventType.TTS_END, + data={"tts_output": {"media_id": _MEDIA_ID}}, + ) + ) + + async def async_get_media_source_audio( + hass: HomeAssistant, + media_source_id: str, + ) -> tuple[str, bytes]: + # Should fail because it's not 16Khz, 16-bit mono + with io.BytesIO() as wav_io: + wav_file: wave.Wave_write = wave.open(wav_io, "wb") + with wav_file: + wav_file.setframerate(22050) + wav_file.setsampwidth(2) + wav_file.setnchannels(2) + + return ("wav", wav_io.getvalue()) + + with patch( + "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", + new=is_speech, + ), patch( + "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), patch( + "homeassistant.components.voip.voip.tts.async_get_media_source_audio", + new=async_get_media_source_audio, + ): + rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( + hass, + hass.config.language, + voip_device, + Context(), + opus_payload_type=123, + ) + rtp_protocol.transport = Mock() + + original_send_tts = rtp_protocol._send_tts + + async def send_tts(*args, **kwargs): + # Call original then end test successfully + with pytest.raises(ValueError): + await original_send_tts(*args, **kwargs) + + done.set() + + rtp_protocol._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign] # silence rtp_protocol.on_chunk(bytes(_ONE_SECOND)) From 2287c45afcef31e28aa51ef42f2392fda9651ad9 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:11:04 +0100 Subject: [PATCH 236/265] Bump bimmer-connected to 0.14.5 (#104715) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../bmw_connected_drive/snapshots/test_diagnostics.ambr | 5 ----- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 911a998371e9..1ebf52e52aec 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected==0.14.3"] + "requirements": ["bimmer-connected[china]==0.14.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index b3c498a5f12c..0ac2ddbecea6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -526,7 +526,7 @@ beautifulsoup4==4.12.2 bellows==0.37.1 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.14.3 +bimmer-connected[china]==0.14.5 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8dc991f9a61e..9307034b794f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -448,7 +448,7 @@ beautifulsoup4==4.12.2 bellows==0.37.1 # homeassistant.components.bmw_connected_drive -bimmer-connected==0.14.3 +bimmer-connected[china]==0.14.5 # homeassistant.components.bluetooth bleak-retry-connector==3.3.0 diff --git a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr index 32405d93e6ba..b3af5bc59b6f 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr @@ -413,7 +413,6 @@ 'servicePack': 'WAVE_01', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'is_metric': True, 'mappingInfo': dict({ 'isAssociated': False, 'isLmmEnabled': False, @@ -1288,7 +1287,6 @@ 'servicePack': 'WAVE_01', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'is_metric': True, 'mappingInfo': dict({ 'isAssociated': False, 'isLmmEnabled': False, @@ -1979,7 +1977,6 @@ 'charging_settings': dict({ }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'is_metric': True, 'mappingInfo': dict({ 'isAssociated': False, 'isLmmEnabled': False, @@ -2734,7 +2731,6 @@ 'servicePack': 'TCB1', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'is_metric': True, 'mappingInfo': dict({ 'isPrimaryUser': True, 'mappingStatus': 'CONFIRMED', @@ -5070,7 +5066,6 @@ 'servicePack': 'TCB1', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'is_metric': True, 'mappingInfo': dict({ 'isPrimaryUser': True, 'mappingStatus': 'CONFIRMED', From dfed10420caf4c3ba1823553196b5a0a601ac5c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Nov 2023 10:24:34 -0700 Subject: [PATCH 237/265] Remove aiohttp enable_compression helper (#104174) --- homeassistant/components/api/__init__.py | 7 ++++--- homeassistant/components/hassio/ingress.py | 4 ++-- homeassistant/components/http/view.py | 4 ++-- homeassistant/helpers/aiohttp_compat.py | 18 +----------------- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 7e4966e2b0de..057e85613fd9 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -41,7 +41,6 @@ from homeassistant.exceptions import ( Unauthorized, ) from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.aiohttp_compat import enable_compression from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.json import json_dumps from homeassistant.helpers.service import async_get_all_descriptions @@ -218,9 +217,11 @@ class APIStatesView(HomeAssistantView): if entity_perm(state.entity_id, "read") ) response = web.Response( - body=f'[{",".join(states)}]', content_type=CONTENT_TYPE_JSON + body=f'[{",".join(states)}]', + content_type=CONTENT_TYPE_JSON, + zlib_executor_size=32768, ) - enable_compression(response) + response.enable_compression() return response diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 7da6f044db09..751e90058095 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -17,7 +17,6 @@ from yarl import URL from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.aiohttp_compat import enable_compression from homeassistant.helpers.typing import UNDEFINED from .const import X_HASS_SOURCE, X_INGRESS_PATH @@ -188,11 +187,12 @@ class HassIOIngress(HomeAssistantView): status=result.status, content_type=content_type, body=body, + zlib_executor_size=32768, ) if content_length_int > MIN_COMPRESSED_SIZE and should_compress( content_type or simple_response.content_type ): - enable_compression(simple_response) + simple_response.enable_compression() await simple_response.prepare(request) return simple_response diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 7481381bbc80..1be3d761a3b2 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -20,7 +20,6 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import Context, HomeAssistant, is_callback -from homeassistant.helpers.aiohttp_compat import enable_compression from homeassistant.helpers.json import ( find_paths_unserializable_data, json_bytes, @@ -72,8 +71,9 @@ class HomeAssistantView: content_type=CONTENT_TYPE_JSON, status=int(status_code), headers=headers, + zlib_executor_size=32768, ) - enable_compression(response) + response.enable_compression() return response def json_message( diff --git a/homeassistant/helpers/aiohttp_compat.py b/homeassistant/helpers/aiohttp_compat.py index 6e281b659fe6..78aad44fa665 100644 --- a/homeassistant/helpers/aiohttp_compat.py +++ b/homeassistant/helpers/aiohttp_compat.py @@ -1,7 +1,7 @@ """Helper to restore old aiohttp behavior.""" from __future__ import annotations -from aiohttp import web, web_protocol, web_server +from aiohttp import web_protocol, web_server class CancelOnDisconnectRequestHandler(web_protocol.RequestHandler): @@ -23,19 +23,3 @@ def restore_original_aiohttp_cancel_behavior() -> None: """ web_protocol.RequestHandler = CancelOnDisconnectRequestHandler # type: ignore[misc] web_server.RequestHandler = CancelOnDisconnectRequestHandler # type: ignore[misc] - - -def enable_compression(response: web.Response) -> None: - """Enable compression on the response.""" - # - # Set _zlib_executor_size in the constructor once support for - # aiohttp < 3.9.0 is dropped - # - # We want large zlib payloads to be compressed in the executor - # to avoid blocking the event loop. - # - # 32KiB was chosen based on testing in production. - # aiohttp will generate a warning for payloads larger than 1MiB - # - response._zlib_executor_size = 32768 # pylint: disable=protected-access - response.enable_compression() From 38eda9f46e305cddd9bf6690dce50ef4ca975e02 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 29 Nov 2023 18:32:32 +0100 Subject: [PATCH 238/265] Add multiple option to text selector (#104635) Co-authored-by: Robert Resch --- homeassistant/helpers/selector.py | 12 +++++++++--- tests/helpers/test_config_validation.py | 1 + tests/helpers/test_selector.py | 5 +++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index bda2440cfb35..f7ceb4ab8121 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1206,6 +1206,7 @@ class TextSelectorConfig(TypedDict, total=False): suffix: str type: TextSelectorType autocomplete: str + multiple: bool class TextSelectorType(StrEnum): @@ -1243,6 +1244,7 @@ class TextSelector(Selector[TextSelectorConfig]): vol.Coerce(TextSelectorType), lambda val: val.value ), vol.Optional("autocomplete"): str, + vol.Optional("multiple", default=False): bool, } ) @@ -1250,10 +1252,14 @@ class TextSelector(Selector[TextSelectorConfig]): """Instantiate a selector.""" super().__init__(config) - def __call__(self, data: Any) -> str: + def __call__(self, data: Any) -> str | list[str]: """Validate the passed selection.""" - text: str = vol.Schema(str)(data) - return text + if not self.config["multiple"]: + text: str = vol.Schema(str)(data) + return text + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] class ThemeSelectorConfig(TypedDict): diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index a9ddd89a0b36..6d1945f2d5fc 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -832,6 +832,7 @@ def test_selector_in_serializer() -> None: "selector": { "text": { "multiline": False, + "multiple": False, } } } diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 93c342384fdc..c4ad244620b0 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -602,6 +602,11 @@ def test_object_selector_schema(schema, valid_selections, invalid_selections) -> ({"multiline": True}, (), ()), ({"multiline": False, "type": "email"}, (), ()), ({"prefix": "before", "suffix": "after"}, (), ()), + ( + {"multiple": True}, + (["abc123", "def456"],), + ("abc123", None, ["abc123", None]), + ), ), ) def test_text_selector_schema(schema, valid_selections, invalid_selections) -> None: From 1727c19e0deb1af006dbfe966aaeb208dfeed680 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:35:55 +0100 Subject: [PATCH 239/265] Address review comments for Picnic (#104732) --- homeassistant/components/picnic/todo.py | 7 +++---- tests/components/picnic/conftest.py | 20 ++------------------ tests/components/picnic/test_todo.py | 3 ++- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/picnic/todo.py b/homeassistant/components/picnic/todo.py index 47b9685c9ec6..fea99f7403dd 100644 --- a/homeassistant/components/picnic/todo.py +++ b/homeassistant/components/picnic/todo.py @@ -12,6 +12,7 @@ from homeassistant.components.todo import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -31,7 +32,7 @@ async def async_setup_entry( """Set up the Picnic shopping cart todo platform config entry.""" picnic_coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR] - async_add_entities([PicnicCart(hass, picnic_coordinator, config_entry)]) + async_add_entities([PicnicCart(picnic_coordinator, config_entry)]) class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]): @@ -44,7 +45,6 @@ class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]): def __init__( self, - hass: HomeAssistant, coordinator: PicnicUpdateCoordinator, config_entry: ConfigEntry, ) -> None: @@ -56,7 +56,6 @@ class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]): manufacturer="Picnic", model=config_entry.unique_id, ) - self.hass = hass self._attr_unique_id = f"{config_entry.unique_id}-cart" @property @@ -87,7 +86,7 @@ class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]): ) if not product_id: - raise ValueError("No product found or no product ID given") + raise ServiceValidationError("No product found or no product ID given") await self.hass.async_add_executor_job( self.coordinator.picnic_api_client.add_product, product_id, 1 diff --git a/tests/components/picnic/conftest.py b/tests/components/picnic/conftest.py index fb6c99f35e9c..1ca6413fc42b 100644 --- a/tests/components/picnic/conftest.py +++ b/tests/components/picnic/conftest.py @@ -56,31 +56,16 @@ async def init_integration( return mock_config_entry -@pytest.fixture -def ws_req_id() -> Callable[[], int]: - """Fixture for incremental websocket requests.""" - - id = 0 - - def next_id() -> int: - nonlocal id - id += 1 - return id - - return next_id - - @pytest.fixture async def get_items( - hass_ws_client: WebSocketGenerator, ws_req_id: Callable[[], int] + hass_ws_client: WebSocketGenerator ) -> Callable[[], Awaitable[dict[str, str]]]: """Fixture to fetch items from the todo websocket.""" async def get() -> list[dict[str, str]]: # Fetch items using To-do platform client = await hass_ws_client() - id = ws_req_id() - await client.send_json( + await client.send_json_auto_id( { "id": id, "type": "todo/item/list", @@ -88,7 +73,6 @@ async def get_items( } ) resp = await client.receive_json() - assert resp.get("id") == id assert resp.get("success") return resp.get("result", {}).get("items", []) diff --git a/tests/components/picnic/test_todo.py b/tests/components/picnic/test_todo.py index a65fb83ca952..cdd30967058e 100644 --- a/tests/components/picnic/test_todo.py +++ b/tests/components/picnic/test_todo.py @@ -7,6 +7,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.todo import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from .conftest import ENTITY_ID @@ -115,7 +116,7 @@ async def test_create_todo_list_item_not_found( mock_picnic_api.search = Mock() mock_picnic_api.search.return_value = [{"items": []}] - with pytest.raises(ValueError): + with pytest.raises(ServiceValidationError): await hass.services.async_call( DOMAIN, "add_item", From 1b048ff388c047a0741bd3a3d1a5677a788943e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Nov 2023 10:40:19 -0700 Subject: [PATCH 240/265] Remove HomeAssistantAccessLogger (#104173) --- .../components/emulated_hue/__init__.py | 3 +-- homeassistant/components/http/__init__.py | 24 +------------------ tests/components/http/test_init.py | 23 +----------------- 3 files changed, 3 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index a98d2c08a488..1ba93da716c5 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -6,7 +6,6 @@ import logging from aiohttp import web import voluptuous as vol -from homeassistant.components.http import HomeAssistantAccessLogger from homeassistant.components.network import async_get_source_ip from homeassistant.const import ( CONF_ENTITIES, @@ -101,7 +100,7 @@ async def start_emulated_hue_bridge( config.advertise_port or config.listen_port, ) - runner = web.AppRunner(app, access_log_class=HomeAssistantAccessLogger) + runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, config.host_ip_addr, config.listen_port) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 5a1d182e80ce..3ccfa18ef120 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -16,7 +16,6 @@ from aiohttp.http_parser import RawRequestMessage from aiohttp.streams import StreamReader from aiohttp.typedefs import JSONDecoder, StrOrURL from aiohttp.web_exceptions import HTTPMovedPermanently, HTTPRedirection -from aiohttp.web_log import AccessLogger from aiohttp.web_protocol import RequestHandler from aiohttp_fast_url_dispatcher import FastUrlDispatcher, attach_fast_url_dispatcher from aiohttp_zlib_ng import enable_zlib_ng @@ -238,25 +237,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class HomeAssistantAccessLogger(AccessLogger): - """Access logger for Home Assistant that does not log when disabled.""" - - def log( - self, request: web.BaseRequest, response: web.StreamResponse, time: float - ) -> None: - """Log the request. - - The default implementation logs the request to the logger - with the INFO level and than throws it away if the logger - is not enabled for the INFO level. This implementation - does not log the request if the logger is not enabled for - the INFO level. - """ - if not self.logger.isEnabledFor(logging.INFO): - return - super().log(request, response, time) - - class HomeAssistantRequest(web.Request): """Home Assistant request object.""" @@ -540,9 +520,7 @@ class HomeAssistantHTTP: # pylint: disable-next=protected-access self.app._router.freeze = lambda: None # type: ignore[method-assign] - self.runner = web.AppRunner( - self.app, access_log_class=HomeAssistantAccessLogger - ) + self.runner = web.AppRunner(self.app) await self.runner.setup() self.site = HomeAssistantTCPSite( diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 5a5bffe67483..97e39811cd8e 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -5,8 +5,7 @@ from http import HTTPStatus from ipaddress import ip_network import logging from pathlib import Path -import time -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import Mock, patch import pytest @@ -21,7 +20,6 @@ from homeassistant.util import dt as dt_util from homeassistant.util.ssl import server_context_intermediate, server_context_modern from tests.common import async_fire_time_changed -from tests.test_util.aiohttp import AiohttpClientMockResponse from tests.typing import ClientSessionGenerator @@ -501,22 +499,3 @@ async def test_logging( response = await client.get("/api/states/logging.entity") assert response.status == HTTPStatus.OK assert "GET /api/states/logging.entity" not in caplog.text - - -async def test_hass_access_logger_at_info_level( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test that logging happens at info level.""" - test_logger = logging.getLogger("test.aiohttp.logger") - logger = http.HomeAssistantAccessLogger(test_logger) - mock_request = MagicMock() - response = AiohttpClientMockResponse( - "POST", "http://127.0.0.1", status=HTTPStatus.OK - ) - setattr(response, "body_length", 42) - logger.log(mock_request, response, time.time()) - assert "42" in caplog.text - caplog.clear() - test_logger.setLevel(logging.WARNING) - logger.log(mock_request, response, time.time()) - assert "42" not in caplog.text From e10d58ef3e8d1362fbeb7858bf6cf2b62753a8c9 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 29 Nov 2023 11:52:27 -0600 Subject: [PATCH 241/265] Bump intents to 2023.11.29 (#104738) --- 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 e99334b5c37b..2a069d5d92b8 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.5.1", "home-assistant-intents==2023.11.17"] + "requirements": ["hassil==1.5.1", "home-assistant-intents==2023.11.29"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index acb65b6fce8a..75209de59964 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -27,7 +27,7 @@ hass-nabucasa==0.74.0 hassil==1.5.1 home-assistant-bluetooth==1.10.4 home-assistant-frontend==20231129.1 -home-assistant-intents==2023.11.17 +home-assistant-intents==2023.11.29 httpx==0.25.0 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0ac2ddbecea6..ec0e5f232e2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1017,7 +1017,7 @@ holidays==0.36 home-assistant-frontend==20231129.1 # homeassistant.components.conversation -home-assistant-intents==2023.11.17 +home-assistant-intents==2023.11.29 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9307034b794f..75947854c7f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -804,7 +804,7 @@ holidays==0.36 home-assistant-frontend==20231129.1 # homeassistant.components.conversation -home-assistant-intents==2023.11.17 +home-assistant-intents==2023.11.29 # homeassistant.components.home_connect homeconnect==0.7.2 From 1fefa936484d80ca7a5eafda411b0bd944039137 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 29 Nov 2023 19:03:58 +0100 Subject: [PATCH 242/265] Use config entry callbacks in Gree (#104740) --- homeassistant/components/gree/__init__.py | 15 ++++----------- homeassistant/components/gree/climate.py | 5 ++--- homeassistant/components/gree/const.py | 1 - homeassistant/components/gree/switch.py | 6 +++--- 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index ff3438ed53fb..13e93d780b26 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -11,7 +11,6 @@ from homeassistant.helpers.event import async_track_time_interval from .bridge import DiscoveryService from .const import ( COORDINATORS, - DATA_DISCOVERY_INTERVAL, DATA_DISCOVERY_SERVICE, DISCOVERY_SCAN_INTERVAL, DISPATCHERS, @@ -29,7 +28,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gree_discovery = DiscoveryService(hass) hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery - hass.data[DOMAIN].setdefault(DISPATCHERS, []) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def _async_scan_update(_=None): @@ -39,8 +37,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Scanning network for Gree devices") await _async_scan_update() - hass.data[DOMAIN][DATA_DISCOVERY_INTERVAL] = async_track_time_interval( - hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL) + entry.async_on_unload( + async_track_time_interval( + hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL) + ) ) return True @@ -48,13 +48,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].get(DISPATCHERS) is not None: - for cleanup in hass.data[DOMAIN][DISPATCHERS]: - cleanup() - - if hass.data[DOMAIN].get(DATA_DISCOVERY_INTERVAL) is not None: - hass.data[DOMAIN].pop(DATA_DISCOVERY_INTERVAL)() - if hass.data.get(DATA_DISCOVERY_SERVICE) is not None: hass.data.pop(DATA_DISCOVERY_SERVICE) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index b14b9cfaba48..ba1621737245 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -47,7 +47,6 @@ from .bridge import DeviceDataUpdateCoordinator from .const import ( COORDINATORS, DISPATCH_DEVICE_DISCOVERED, - DISPATCHERS, DOMAIN, FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW, @@ -88,7 +87,7 @@ SWING_MODES = [SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH] async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Gree HVAC device from a config entry.""" @@ -101,7 +100,7 @@ async def async_setup_entry( for coordinator in hass.data[DOMAIN][COORDINATORS]: init_device(coordinator) - hass.data[DOMAIN][DISPATCHERS].append( + entry.async_on_unload( async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device) ) diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index b4df7a1acde5..464792109214 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -3,7 +3,6 @@ COORDINATORS = "coordinators" DATA_DISCOVERY_SERVICE = "gree_discovery" -DATA_DISCOVERY_INTERVAL = "gree_discovery_interval" DISCOVERY_SCAN_INTERVAL = 300 DISCOVERY_TIMEOUT = 8 diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index 68c11ad6e1f4..7916df18abc2 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DISPATCHERS, DOMAIN +from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DOMAIN from .entity import GreeEntity @@ -102,7 +102,7 @@ GREE_SWITCHES: tuple[GreeSwitchEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Gree HVAC device from a config entry.""" @@ -119,7 +119,7 @@ async def async_setup_entry( for coordinator in hass.data[DOMAIN][COORDINATORS]: init_device(coordinator) - hass.data[DOMAIN][DISPATCHERS].append( + entry.async_on_unload( async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device) ) From 50f2c411456b8f53b199d7782328da0c8e8138be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Nov 2023 11:20:22 -0700 Subject: [PATCH 243/265] Avoid db hit and executor job for impossible history queries (#104724) --- homeassistant/components/history/__init__.py | 5 +++-- homeassistant/components/history/helpers.py | 9 +++++++++ homeassistant/components/history/websocket_api.py | 5 +++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index f5b97a7fb13d..9eab92dce5cb 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -21,7 +21,7 @@ import homeassistant.util.dt as dt_util from . import websocket_api from .const import DOMAIN -from .helpers import entities_may_have_state_changes_after +from .helpers import entities_may_have_state_changes_after, has_recorder_run_after CONF_ORDER = "use_include_order" @@ -106,7 +106,8 @@ class HistoryPeriodView(HomeAssistantView): no_attributes = "no_attributes" in request.query if ( - not include_start_time_state + (end_time and not has_recorder_run_after(hass, end_time)) + or not include_start_time_state and entity_ids and not entities_may_have_state_changes_after( hass, entity_ids, start_time, no_attributes diff --git a/homeassistant/components/history/helpers.py b/homeassistant/components/history/helpers.py index 523b1fafb7fd..7e28e69e5f97 100644 --- a/homeassistant/components/history/helpers.py +++ b/homeassistant/components/history/helpers.py @@ -4,6 +4,8 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.models import process_timestamp from homeassistant.core import HomeAssistant @@ -21,3 +23,10 @@ def entities_may_have_state_changes_after( return True return False + + +def has_recorder_run_after(hass: HomeAssistant, run_time: dt) -> bool: + """Check if the recorder has any runs after a specific time.""" + return run_time >= process_timestamp( + get_instance(hass).recorder_runs_manager.first.start + ) diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index 24ec07b6a878..4be63f29c027 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -39,7 +39,7 @@ from homeassistant.helpers.typing import EventType import homeassistant.util.dt as dt_util from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES -from .helpers import entities_may_have_state_changes_after +from .helpers import entities_may_have_state_changes_after, has_recorder_run_after _LOGGER = logging.getLogger(__name__) @@ -142,7 +142,8 @@ async def ws_get_history_during_period( no_attributes = msg["no_attributes"] if ( - not include_start_time_state + (end_time and not has_recorder_run_after(hass, end_time)) + or not include_start_time_state and entity_ids and not entities_may_have_state_changes_after( hass, entity_ids, start_time, no_attributes From 1522118453972b9396435dd695987f9dace47aab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Nov 2023 11:20:36 -0700 Subject: [PATCH 244/265] Remove aiohttp cancel on disconnect workaround (#104175) --- homeassistant/components/http/__init__.py | 2 +- homeassistant/core.py | 2 - homeassistant/helpers/aiohttp_compat.py | 25 ----------- tests/helpers/test_aiohttp_compat.py | 55 ----------------------- 4 files changed, 1 insertion(+), 83 deletions(-) delete mode 100644 homeassistant/helpers/aiohttp_compat.py delete mode 100644 tests/helpers/test_aiohttp_compat.py diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 3ccfa18ef120..449f00fb335c 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -520,7 +520,7 @@ class HomeAssistantHTTP: # pylint: disable-next=protected-access self.app._router.freeze = lambda: None # type: ignore[method-assign] - self.runner = web.AppRunner(self.app) + self.runner = web.AppRunner(self.app, handler_cancellation=True) await self.runner.setup() self.site = HomeAssistantTCPSite( diff --git a/homeassistant/core.py b/homeassistant/core.py index 972e0e618e66..7d9d8d19b49c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -80,7 +80,6 @@ from .exceptions import ( ServiceNotFound, Unauthorized, ) -from .helpers.aiohttp_compat import restore_original_aiohttp_cancel_behavior from .helpers.json import json_dumps from .util import dt as dt_util, location from .util.async_ import ( @@ -113,7 +112,6 @@ STAGE_2_SHUTDOWN_TIMEOUT = 60 STAGE_3_SHUTDOWN_TIMEOUT = 30 block_async_io.enable() -restore_original_aiohttp_cancel_behavior() _T = TypeVar("_T") _R = TypeVar("_R") diff --git a/homeassistant/helpers/aiohttp_compat.py b/homeassistant/helpers/aiohttp_compat.py deleted file mode 100644 index 78aad44fa665..000000000000 --- a/homeassistant/helpers/aiohttp_compat.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Helper to restore old aiohttp behavior.""" -from __future__ import annotations - -from aiohttp import web_protocol, web_server - - -class CancelOnDisconnectRequestHandler(web_protocol.RequestHandler): - """Request handler that cancels tasks on disconnect.""" - - def connection_lost(self, exc: BaseException | None) -> None: - """Handle connection lost.""" - task_handler = self._task_handler - super().connection_lost(exc) - if task_handler is not None: - task_handler.cancel("aiohttp connection lost") - - -def restore_original_aiohttp_cancel_behavior() -> None: - """Patch aiohttp to restore cancel behavior. - - Remove this once aiohttp 3.9 is released as we can use - https://github.com/aio-libs/aiohttp/pull/7128 - """ - web_protocol.RequestHandler = CancelOnDisconnectRequestHandler # type: ignore[misc] - web_server.RequestHandler = CancelOnDisconnectRequestHandler # type: ignore[misc] diff --git a/tests/helpers/test_aiohttp_compat.py b/tests/helpers/test_aiohttp_compat.py deleted file mode 100644 index 749984dbc2ed..000000000000 --- a/tests/helpers/test_aiohttp_compat.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Test the aiohttp compatibility shim.""" - -import asyncio -from contextlib import suppress - -from aiohttp import client, web, web_protocol, web_server -import pytest - -from homeassistant.helpers.aiohttp_compat import CancelOnDisconnectRequestHandler - - -@pytest.mark.allow_hosts(["127.0.0.1"]) -async def test_handler_cancellation(socket_enabled, unused_tcp_port_factory) -> None: - """Test that handler cancels the request on disconnect. - - From aiohttp tests/test_web_server.py - """ - assert web_protocol.RequestHandler is CancelOnDisconnectRequestHandler - assert web_server.RequestHandler is CancelOnDisconnectRequestHandler - - event = asyncio.Event() - port = unused_tcp_port_factory() - - async def on_request(_: web.Request) -> web.Response: - nonlocal event - try: - await asyncio.sleep(10) - except asyncio.CancelledError: - event.set() - raise - else: - raise web.HTTPInternalServerError() - - app = web.Application() - app.router.add_route("GET", "/", on_request) - - runner = web.AppRunner(app) - await runner.setup() - - site = web.TCPSite(runner, host="127.0.0.1", port=port) - - await site.start() - - try: - async with client.ClientSession( - timeout=client.ClientTimeout(total=0.1) - ) as sess: - with pytest.raises(asyncio.TimeoutError): - await sess.get(f"http://127.0.0.1:{port}/") - - with suppress(asyncio.TimeoutError): - await asyncio.wait_for(event.wait(), timeout=1) - assert event.is_set(), "Request handler hasn't been cancelled" - finally: - await asyncio.gather(runner.shutdown(), site.stop()) From af2f8699b7436bf74cfe803b9b460939b82f43ec Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 29 Nov 2023 10:35:36 -0800 Subject: [PATCH 245/265] Add due date and description to CalDAV To-do (#104656) Co-authored-by: Martin Hjelmare Co-authored-by: Robert Resch --- homeassistant/components/caldav/todo.py | 46 ++++++--- tests/components/caldav/test_todo.py | 120 +++++++++++++++++++++--- 2 files changed, 142 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/caldav/todo.py b/homeassistant/components/caldav/todo.py index eddfe4101001..1bd24dc542af 100644 --- a/homeassistant/components/caldav/todo.py +++ b/homeassistant/components/caldav/todo.py @@ -2,10 +2,10 @@ from __future__ import annotations import asyncio -from datetime import timedelta +from datetime import date, datetime, timedelta from functools import partial import logging -from typing import cast +from typing import Any, cast import caldav from caldav.lib.error import DAVError, NotFoundError @@ -21,6 +21,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import dt as dt_util from .api import async_get_calendars, get_attr_value from .const import DOMAIN @@ -71,6 +72,12 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None: or (summary := get_attr_value(todo, "summary")) is None ): return None + due: date | datetime | None = None + if due_value := get_attr_value(todo, "due"): + if isinstance(due_value, datetime): + due = dt_util.as_local(due_value) + elif isinstance(due_value, date): + due = due_value return TodoItem( uid=uid, summary=summary, @@ -78,9 +85,28 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None: get_attr_value(todo, "status") or "", TodoItemStatus.NEEDS_ACTION, ), + due=due, + description=get_attr_value(todo, "description"), ) +def _to_ics_fields(item: TodoItem) -> dict[str, Any]: + """Convert a TodoItem to the set of add or update arguments.""" + item_data: dict[str, Any] = {} + if summary := item.summary: + item_data["summary"] = summary + if status := item.status: + item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION") + if due := item.due: + if isinstance(due, datetime): + item_data["due"] = dt_util.as_utc(due).strftime("%Y%m%dT%H%M%SZ") + else: + item_data["due"] = due.strftime("%Y%m%d") + if description := item.description: + item_data["description"] = description + return item_data + + class WebDavTodoListEntity(TodoListEntity): """CalDAV To-do list entity.""" @@ -89,6 +115,9 @@ class WebDavTodoListEntity(TodoListEntity): TodoListEntityFeature.CREATE_TODO_ITEM | TodoListEntityFeature.UPDATE_TODO_ITEM | TodoListEntityFeature.DELETE_TODO_ITEM + | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM + | TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM + | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM ) def __init__(self, calendar: caldav.Calendar, config_entry_id: str) -> None: @@ -116,13 +145,7 @@ class WebDavTodoListEntity(TodoListEntity): """Add an item to the To-do list.""" try: await self.hass.async_add_executor_job( - partial( - self._calendar.save_todo, - summary=item.summary, - status=TODO_STATUS_MAP_INV.get( - item.status or TodoItemStatus.NEEDS_ACTION, "NEEDS-ACTION" - ), - ), + partial(self._calendar.save_todo, **_to_ics_fields(item)), ) except (requests.ConnectionError, DAVError) as err: raise HomeAssistantError(f"CalDAV save error: {err}") from err @@ -139,10 +162,7 @@ class WebDavTodoListEntity(TodoListEntity): except (requests.ConnectionError, DAVError) as err: raise HomeAssistantError(f"CalDAV lookup error: {err}") from err vtodo = todo.icalendar_component # type: ignore[attr-defined] - if item.summary: - vtodo["summary"] = item.summary - if item.status: - vtodo["status"] = TODO_STATUS_MAP_INV.get(item.status, "NEEDS-ACTION") + vtodo.update(**_to_ics_fields(item)) try: await self.hass.async_add_executor_job( partial( diff --git a/tests/components/caldav/test_todo.py b/tests/components/caldav/test_todo.py index 55ae0d564d06..6e92f2114635 100644 --- a/tests/components/caldav/test_todo.py +++ b/tests/components/caldav/test_todo.py @@ -17,7 +17,7 @@ from tests.typing import WebSocketGenerator CALENDAR_NAME = "My Tasks" ENTITY_NAME = "My tasks" TEST_ENTITY = "todo.my_tasks" -SUPPORTED_FEATURES = 7 +SUPPORTED_FEATURES = 119 TODO_NO_STATUS = """BEGIN:VCALENDAR VERSION:2.0 @@ -40,6 +40,12 @@ STATUS:NEEDS-ACTION END:VTODO END:VCALENDAR""" +RESULT_ITEM = { + "uid": "2", + "summary": "Cheese", + "status": "needs_action", +} + TODO_COMPLETED = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//E-Corp.//CalDAV Client//EN @@ -69,6 +75,12 @@ def platforms() -> list[Platform]: return [Platform.TODO] +@pytest.fixture(autouse=True) +def set_tz(hass: HomeAssistant) -> None: + """Fixture to set timezone with fixed offset year round.""" + hass.config.set_time_zone("America/Regina") + + @pytest.fixture(name="todos") def mock_todos() -> list[str]: """Fixture to return VTODO objects for the calendar.""" @@ -178,10 +190,49 @@ async def test_supported_components( assert (state is not None) == has_entity +@pytest.mark.parametrize( + ("item_data", "expcted_save_args", "expected_item"), + [ + ( + {}, + {"status": "NEEDS-ACTION", "summary": "Cheese"}, + RESULT_ITEM, + ), + ( + {"due_date": "2023-11-18"}, + {"status": "NEEDS-ACTION", "summary": "Cheese", "due": "20231118"}, + {**RESULT_ITEM, "due": "2023-11-18"}, + ), + ( + {"due_datetime": "2023-11-18T08:30:00-06:00"}, + {"status": "NEEDS-ACTION", "summary": "Cheese", "due": "20231118T143000Z"}, + {**RESULT_ITEM, "due": "2023-11-18T08:30:00-06:00"}, + ), + ( + {"description": "Make sure to get Swiss"}, + { + "status": "NEEDS-ACTION", + "summary": "Cheese", + "description": "Make sure to get Swiss", + }, + {**RESULT_ITEM, "description": "Make sure to get Swiss"}, + ), + ], + ids=[ + "summary", + "due_date", + "due_datetime", + "description", + ], +) async def test_add_item( hass: HomeAssistant, config_entry: MockConfigEntry, + dav_client: Mock, calendar: Mock, + item_data: dict[str, Any], + expcted_save_args: dict[str, Any], + expected_item: dict[str, Any], ) -> None: """Test adding an item to the list.""" calendar.search.return_value = [] @@ -197,16 +248,13 @@ async def test_add_item( await hass.services.async_call( TODO_DOMAIN, "add_item", - {"item": "Cheese"}, + {"item": "Cheese", **item_data}, target={"entity_id": TEST_ENTITY}, blocking=True, ) assert calendar.save_todo.call_args - assert calendar.save_todo.call_args.kwargs == { - "status": "NEEDS-ACTION", - "summary": "Cheese", - } + assert calendar.save_todo.call_args.kwargs == expcted_save_args # Verify state was updated state = hass.states.get(TEST_ENTITY) @@ -235,20 +283,59 @@ async def test_add_item_failure( @pytest.mark.parametrize( - ("update_data", "expected_ics", "expected_state"), + ("update_data", "expected_ics", "expected_state", "expected_item"), [ ( {"rename": "Swiss Cheese"}, ["SUMMARY:Swiss Cheese", "STATUS:NEEDS-ACTION"], "1", + {**RESULT_ITEM, "summary": "Swiss Cheese"}, + ), + ( + {"status": "needs_action"}, + ["SUMMARY:Cheese", "STATUS:NEEDS-ACTION"], + "1", + RESULT_ITEM, + ), + ( + {"status": "completed"}, + ["SUMMARY:Cheese", "STATUS:COMPLETED"], + "0", + {**RESULT_ITEM, "status": "completed"}, ), - ({"status": "needs_action"}, ["SUMMARY:Cheese", "STATUS:NEEDS-ACTION"], "1"), - ({"status": "completed"}, ["SUMMARY:Cheese", "STATUS:COMPLETED"], "0"), ( {"rename": "Swiss Cheese", "status": "needs_action"}, ["SUMMARY:Swiss Cheese", "STATUS:NEEDS-ACTION"], "1", + {**RESULT_ITEM, "summary": "Swiss Cheese"}, ), + ( + {"due_date": "2023-11-18"}, + ["SUMMARY:Cheese", "DUE:20231118"], + "1", + {**RESULT_ITEM, "due": "2023-11-18"}, + ), + ( + {"due_datetime": "2023-11-18T08:30:00-06:00"}, + ["SUMMARY:Cheese", "DUE:20231118T143000Z"], + "1", + {**RESULT_ITEM, "due": "2023-11-18T08:30:00-06:00"}, + ), + ( + {"description": "Make sure to get Swiss"}, + ["SUMMARY:Cheese", "DESCRIPTION:Make sure to get Swiss"], + "1", + {**RESULT_ITEM, "description": "Make sure to get Swiss"}, + ), + ], + ids=[ + "rename", + "status_needs_action", + "status_completed", + "rename_status", + "due_date", + "due_datetime", + "description", ], ) async def test_update_item( @@ -259,8 +346,9 @@ async def test_update_item( update_data: dict[str, Any], expected_ics: list[str], expected_state: str, + expected_item: dict[str, Any], ) -> None: - """Test creating a an item on the list.""" + """Test updating an item on the list.""" item = Todo(dav_client, None, TODO_NEEDS_ACTION, calendar, "2") calendar.search = MagicMock(return_value=[item]) @@ -295,6 +383,16 @@ async def test_update_item( assert state assert state.state == expected_state + result = await hass.services.async_call( + TODO_DOMAIN, + "get_items", + {}, + target={"entity_id": TEST_ENTITY}, + blocking=True, + return_response=True, + ) + assert result == {TEST_ENTITY: {"items": [expected_item]}} + async def test_update_item_failure( hass: HomeAssistant, @@ -506,7 +604,7 @@ async def test_subscribe( calendar: Mock, hass_ws_client: WebSocketGenerator, ) -> None: - """Test creating a an item on the list.""" + """Test subscription to item updates.""" item = Todo(dav_client, None, TODO_NEEDS_ACTION, calendar, "2") calendar.search = MagicMock(return_value=[item]) From 0a98a648bb185c48a459b30242afe15e2fc1eb5b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Nov 2023 21:29:11 +0100 Subject: [PATCH 246/265] Bump version to 2024.1.0dev0 (#104746) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 4 ++-- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 71030e50074b..b9b9c8babb9b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,7 +36,7 @@ env: CACHE_VERSION: 5 PIP_CACHE_VERSION: 4 MYPY_CACHE_VERSION: 6 - HA_SHORT_VERSION: "2023.12" + HA_SHORT_VERSION: "2024.1" DEFAULT_PYTHON: "3.11" ALL_PYTHON_VERSIONS: "['3.11', '3.12']" # 10.3 is the oldest supported version diff --git a/homeassistant/const.py b/homeassistant/const.py index c6655ba39007..8da1c251b4ef 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,8 +5,8 @@ from enum import StrEnum from typing import Final APPLICATION_NAME: Final = "HomeAssistant" -MAJOR_VERSION: Final = 2023 -MINOR_VERSION: Final = 12 +MAJOR_VERSION: Final = 2024 +MINOR_VERSION: Final = 1 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index 71e58bf2177e..844fac7142fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.12.0.dev0" +version = "2024.1.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bdf4c61a052a2e2b970b95e759f4c3e3e04737ff Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 29 Nov 2023 21:41:32 +0100 Subject: [PATCH 247/265] Add faa_delays to strict typing (#104720) --- .strict-typing | 1 + .../components/faa_delays/binary_sensor.py | 56 +++++++++++-------- .../components/faa_delays/config_flow.py | 6 +- .../components/faa_delays/coordinator.py | 8 +-- mypy.ini | 10 ++++ 5 files changed, 54 insertions(+), 27 deletions(-) diff --git a/.strict-typing b/.strict-typing index 3c18a1988f36..2c8717596c67 100644 --- a/.strict-typing +++ b/.strict-typing @@ -120,6 +120,7 @@ homeassistant.components.energy.* homeassistant.components.esphome.* homeassistant.components.event.* homeassistant.components.evil_genius_labs.* +homeassistant.components.faa_delays.* homeassistant.components.fan.* homeassistant.components.fastdotcom.* homeassistant.components.feedreader.* diff --git a/homeassistant/components/faa_delays/binary_sensor.py b/homeassistant/components/faa_delays/binary_sensor.py index 5cbb206f2231..5a209a41de16 100644 --- a/homeassistant/components/faa_delays/binary_sensor.py +++ b/homeassistant/components/faa_delays/binary_sensor.py @@ -1,8 +1,12 @@ """Platform for FAA Delays sensor component.""" from __future__ import annotations +from collections.abc import Callable, Mapping +from dataclasses import dataclass from typing import Any +from faadelays import Airport + from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, @@ -12,33 +16,47 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import FAADataUpdateCoordinator from .const import DOMAIN -FAA_BINARY_SENSORS: tuple[BinarySensorEntityDescription, ...] = ( - BinarySensorEntityDescription( + +@dataclass(kw_only=True) +class FaaDelaysBinarySensorEntityDescription(BinarySensorEntityDescription): + """Mixin for required keys.""" + + is_on_fn: Callable[[Airport], bool | None] + + +FAA_BINARY_SENSORS: tuple[FaaDelaysBinarySensorEntityDescription, ...] = ( + FaaDelaysBinarySensorEntityDescription( key="GROUND_DELAY", name="Ground Delay", icon="mdi:airport", + is_on_fn=lambda airport: airport.ground_delay.status, ), - BinarySensorEntityDescription( + FaaDelaysBinarySensorEntityDescription( key="GROUND_STOP", name="Ground Stop", icon="mdi:airport", + is_on_fn=lambda airport: airport.ground_stop.status, ), - BinarySensorEntityDescription( + FaaDelaysBinarySensorEntityDescription( key="DEPART_DELAY", name="Departure Delay", icon="mdi:airplane-takeoff", + is_on_fn=lambda airport: airport.depart_delay.status, ), - BinarySensorEntityDescription( + FaaDelaysBinarySensorEntityDescription( key="ARRIVE_DELAY", name="Arrival Delay", icon="mdi:airplane-landing", + is_on_fn=lambda airport: airport.arrive_delay.status, ), - BinarySensorEntityDescription( + FaaDelaysBinarySensorEntityDescription( key="CLOSURE", name="Closure", icon="mdi:airplane:off", + is_on_fn=lambda airport: airport.closure.status, ), ) @@ -57,11 +75,16 @@ async def async_setup_entry( async_add_entities(entities) -class FAABinarySensor(CoordinatorEntity, BinarySensorEntity): +class FAABinarySensor(CoordinatorEntity[FAADataUpdateCoordinator], BinarySensorEntity): """Define a binary sensor for FAA Delays.""" + entity_description: FaaDelaysBinarySensorEntityDescription + def __init__( - self, coordinator, entry_id, description: BinarySensorEntityDescription + self, + coordinator: FAADataUpdateCoordinator, + entry_id: str, + description: FaaDelaysBinarySensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -75,23 +98,12 @@ class FAABinarySensor(CoordinatorEntity, BinarySensorEntity): self._attr_unique_id = f"{_id}_{description.key}" @property - def is_on(self): + def is_on(self) -> bool | None: """Return the status of the sensor.""" - sensor_type = self.entity_description.key - if sensor_type == "GROUND_DELAY": - return self.coordinator.data.ground_delay.status - if sensor_type == "GROUND_STOP": - return self.coordinator.data.ground_stop.status - if sensor_type == "DEPART_DELAY": - return self.coordinator.data.depart_delay.status - if sensor_type == "ARRIVE_DELAY": - return self.coordinator.data.arrive_delay.status - if sensor_type == "CLOSURE": - return self.coordinator.data.closure.status - return None + return self.entity_description.is_on_fn(self.coordinator.data) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any]: """Return attributes for sensor.""" sensor_type = self.entity_description.key if sensor_type == "GROUND_DELAY": diff --git a/homeassistant/components/faa_delays/config_flow.py b/homeassistant/components/faa_delays/config_flow.py index b2f7f69dd49f..2f91ce9f797c 100644 --- a/homeassistant/components/faa_delays/config_flow.py +++ b/homeassistant/components/faa_delays/config_flow.py @@ -1,5 +1,6 @@ """Config flow for FAA Delays integration.""" import logging +from typing import Any from aiohttp import ClientConnectionError import faadelays @@ -7,6 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ID +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -21,7 +23,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/faa_delays/coordinator.py b/homeassistant/components/faa_delays/coordinator.py index f2aefdada66c..2f110cf7730f 100644 --- a/homeassistant/components/faa_delays/coordinator.py +++ b/homeassistant/components/faa_delays/coordinator.py @@ -6,6 +6,7 @@ import logging from aiohttp import ClientConnectionError from faadelays import Airport +from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -14,19 +15,18 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class FAADataUpdateCoordinator(DataUpdateCoordinator): +class FAADataUpdateCoordinator(DataUpdateCoordinator[Airport]): """Class to manage fetching FAA API data from a single endpoint.""" - def __init__(self, hass, code): + def __init__(self, hass: HomeAssistant, code: str) -> None: """Initialize the coordinator.""" super().__init__( hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=1) ) self.session = aiohttp_client.async_get_clientsession(hass) self.data = Airport(code, self.session) - self.code = code - async def _async_update_data(self): + async def _async_update_data(self) -> Airport: try: async with asyncio.timeout(10): await self.data.update() diff --git a/mypy.ini b/mypy.ini index 0ed06edaa1dd..e01be53db3d0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -961,6 +961,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.faa_delays.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.fan.*] check_untyped_defs = true disallow_incomplete_defs = true From 7638b9bba1ad57f6d1f4e88e9f09bcec577ad490 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 29 Nov 2023 21:54:05 +0100 Subject: [PATCH 248/265] Fix Philips TV none recordings_list (#104665) Correct for missing recordings list in api client. --------- Co-authored-by: Joakim Plate --- .../components/philips_js/binary_sensor.py | 2 ++ .../philips_js/test_binary_sensor.py | 31 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/philips_js/binary_sensor.py b/homeassistant/components/philips_js/binary_sensor.py index 78aa9f17b050..1e6c1241aea7 100644 --- a/homeassistant/components/philips_js/binary_sensor.py +++ b/homeassistant/components/philips_js/binary_sensor.py @@ -66,6 +66,8 @@ async def async_setup_entry( def _check_for_recording_entry(api: PhilipsTV, entry: str, value: str) -> bool: """Return True if at least one specified value is available within entry of list.""" + if api.recordings_list is None: + return False for rec in api.recordings_list["recordings"]: if rec.get(entry) == value: return True diff --git a/tests/components/philips_js/test_binary_sensor.py b/tests/components/philips_js/test_binary_sensor.py index d11f3fe22f18..01233706d07b 100644 --- a/tests/components/philips_js/test_binary_sensor.py +++ b/tests/components/philips_js/test_binary_sensor.py @@ -1,7 +1,7 @@ """The tests for philips_js binary_sensor.""" import pytest -from homeassistant.const import STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from . import MOCK_NAME, MOCK_RECORDINGS_LIST @@ -32,7 +32,16 @@ async def mock_tv_api_valid(mock_tv): return mock_tv -async def test_recordings_list_invalid( +@pytest.fixture +async def mock_tv_recordings_list_unavailable(mock_tv): + """Set up a valid mock_tv with should create sensors.""" + mock_tv.secured_transport = True + mock_tv.api_version = 6 + mock_tv.recordings_list = None + return mock_tv + + +async def test_recordings_list_api_invalid( mock_tv_api_invalid, mock_config_entry, hass: HomeAssistant ) -> None: """Test if sensors are not created if mock_tv is invalid.""" @@ -54,7 +63,21 @@ async def test_recordings_list_valid( assert await hass.config_entries.async_setup(mock_config_entry.entry_id) state = hass.states.get(ID_RECORDING_AVAILABLE) - assert state.state is STATE_ON + assert state.state == STATE_ON state = hass.states.get(ID_RECORDING_ONGOING) - assert state.state is STATE_ON + assert state.state == STATE_ON + + +async def test_recordings_list_unavailable( + mock_tv_recordings_list_unavailable, mock_config_entry, hass: HomeAssistant +) -> None: + """Test if sensors are created correctly.""" + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + + state = hass.states.get(ID_RECORDING_AVAILABLE) + assert state.state == STATE_OFF + + state = hass.states.get(ID_RECORDING_ONGOING) + assert state.state == STATE_OFF From 9126b00dfe3fedc5a98d6e61064f08a73a9f6959 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 29 Nov 2023 22:01:03 +0100 Subject: [PATCH 249/265] Move Faa delays state attributes to entity description (#104748) --- .../components/faa_delays/binary_sensor.py | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/faa_delays/binary_sensor.py b/homeassistant/components/faa_delays/binary_sensor.py index 5a209a41de16..e8d7a03752ff 100644 --- a/homeassistant/components/faa_delays/binary_sensor.py +++ b/homeassistant/components/faa_delays/binary_sensor.py @@ -25,6 +25,7 @@ class FaaDelaysBinarySensorEntityDescription(BinarySensorEntityDescription): """Mixin for required keys.""" is_on_fn: Callable[[Airport], bool | None] + extra_state_attributes_fn: Callable[[Airport], Mapping[str, Any]] FAA_BINARY_SENSORS: tuple[FaaDelaysBinarySensorEntityDescription, ...] = ( @@ -33,30 +34,54 @@ FAA_BINARY_SENSORS: tuple[FaaDelaysBinarySensorEntityDescription, ...] = ( name="Ground Delay", icon="mdi:airport", is_on_fn=lambda airport: airport.ground_delay.status, + extra_state_attributes_fn=lambda airport: { + "average": airport.ground_delay.average, + "reason": airport.ground_delay.reason, + }, ), FaaDelaysBinarySensorEntityDescription( key="GROUND_STOP", name="Ground Stop", icon="mdi:airport", is_on_fn=lambda airport: airport.ground_stop.status, + extra_state_attributes_fn=lambda airport: { + "endtime": airport.ground_stop.endtime, + "reason": airport.ground_stop.reason, + }, ), FaaDelaysBinarySensorEntityDescription( key="DEPART_DELAY", name="Departure Delay", icon="mdi:airplane-takeoff", is_on_fn=lambda airport: airport.depart_delay.status, + extra_state_attributes_fn=lambda airport: { + "minimum": airport.depart_delay.minimum, + "maximum": airport.depart_delay.maximum, + "trend": airport.depart_delay.trend, + "reason": airport.depart_delay.reason, + }, ), FaaDelaysBinarySensorEntityDescription( key="ARRIVE_DELAY", name="Arrival Delay", icon="mdi:airplane-landing", is_on_fn=lambda airport: airport.arrive_delay.status, + extra_state_attributes_fn=lambda airport: { + "minimum": airport.arrive_delay.minimum, + "maximum": airport.arrive_delay.maximum, + "trend": airport.arrive_delay.trend, + "reason": airport.arrive_delay.reason, + }, ), FaaDelaysBinarySensorEntityDescription( key="CLOSURE", name="Closure", icon="mdi:airplane:off", is_on_fn=lambda airport: airport.closure.status, + extra_state_attributes_fn=lambda airport: { + "begin": airport.closure.start, + "end": airport.closure.end, + }, ), ) @@ -89,10 +114,6 @@ class FAABinarySensor(CoordinatorEntity[FAADataUpdateCoordinator], BinarySensorE """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - - self.coordinator = coordinator - self._entry_id = entry_id - self._attrs: dict[str, Any] = {} _id = coordinator.data.code self._attr_name = f"{_id} {description.name}" self._attr_unique_id = f"{_id}_{description.key}" @@ -105,24 +126,4 @@ class FAABinarySensor(CoordinatorEntity[FAADataUpdateCoordinator], BinarySensorE @property def extra_state_attributes(self) -> Mapping[str, Any]: """Return attributes for sensor.""" - sensor_type = self.entity_description.key - if sensor_type == "GROUND_DELAY": - self._attrs["average"] = self.coordinator.data.ground_delay.average - self._attrs["reason"] = self.coordinator.data.ground_delay.reason - elif sensor_type == "GROUND_STOP": - self._attrs["endtime"] = self.coordinator.data.ground_stop.endtime - self._attrs["reason"] = self.coordinator.data.ground_stop.reason - elif sensor_type == "DEPART_DELAY": - self._attrs["minimum"] = self.coordinator.data.depart_delay.minimum - self._attrs["maximum"] = self.coordinator.data.depart_delay.maximum - self._attrs["trend"] = self.coordinator.data.depart_delay.trend - self._attrs["reason"] = self.coordinator.data.depart_delay.reason - elif sensor_type == "ARRIVE_DELAY": - self._attrs["minimum"] = self.coordinator.data.arrive_delay.minimum - self._attrs["maximum"] = self.coordinator.data.arrive_delay.maximum - self._attrs["trend"] = self.coordinator.data.arrive_delay.trend - self._attrs["reason"] = self.coordinator.data.arrive_delay.reason - elif sensor_type == "CLOSURE": - self._attrs["begin"] = self.coordinator.data.closure.start - self._attrs["end"] = self.coordinator.data.closure.end - return self._attrs + return self.entity_description.extra_state_attributes_fn(self.coordinator.data) From 8e64eff62620bc3744fab55b031ece11706205be Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 29 Nov 2023 22:23:46 +0100 Subject: [PATCH 250/265] Add entity translations to faa_delays (#104749) --- .../components/faa_delays/binary_sensor.py | 19 +++-- .../components/faa_delays/strings.json | 71 +++++++++++++++++++ 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/faa_delays/binary_sensor.py b/homeassistant/components/faa_delays/binary_sensor.py index e8d7a03752ff..c72fedaf59a5 100644 --- a/homeassistant/components/faa_delays/binary_sensor.py +++ b/homeassistant/components/faa_delays/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -31,7 +32,7 @@ class FaaDelaysBinarySensorEntityDescription(BinarySensorEntityDescription): FAA_BINARY_SENSORS: tuple[FaaDelaysBinarySensorEntityDescription, ...] = ( FaaDelaysBinarySensorEntityDescription( key="GROUND_DELAY", - name="Ground Delay", + translation_key="ground_delay", icon="mdi:airport", is_on_fn=lambda airport: airport.ground_delay.status, extra_state_attributes_fn=lambda airport: { @@ -41,7 +42,7 @@ FAA_BINARY_SENSORS: tuple[FaaDelaysBinarySensorEntityDescription, ...] = ( ), FaaDelaysBinarySensorEntityDescription( key="GROUND_STOP", - name="Ground Stop", + translation_key="ground_stop", icon="mdi:airport", is_on_fn=lambda airport: airport.ground_stop.status, extra_state_attributes_fn=lambda airport: { @@ -51,7 +52,7 @@ FAA_BINARY_SENSORS: tuple[FaaDelaysBinarySensorEntityDescription, ...] = ( ), FaaDelaysBinarySensorEntityDescription( key="DEPART_DELAY", - name="Departure Delay", + translation_key="depart_delay", icon="mdi:airplane-takeoff", is_on_fn=lambda airport: airport.depart_delay.status, extra_state_attributes_fn=lambda airport: { @@ -63,7 +64,7 @@ FAA_BINARY_SENSORS: tuple[FaaDelaysBinarySensorEntityDescription, ...] = ( ), FaaDelaysBinarySensorEntityDescription( key="ARRIVE_DELAY", - name="Arrival Delay", + translation_key="arrive_delay", icon="mdi:airplane-landing", is_on_fn=lambda airport: airport.arrive_delay.status, extra_state_attributes_fn=lambda airport: { @@ -75,7 +76,7 @@ FAA_BINARY_SENSORS: tuple[FaaDelaysBinarySensorEntityDescription, ...] = ( ), FaaDelaysBinarySensorEntityDescription( key="CLOSURE", - name="Closure", + translation_key="closure", icon="mdi:airplane:off", is_on_fn=lambda airport: airport.closure.status, extra_state_attributes_fn=lambda airport: { @@ -103,6 +104,8 @@ async def async_setup_entry( class FAABinarySensor(CoordinatorEntity[FAADataUpdateCoordinator], BinarySensorEntity): """Define a binary sensor for FAA Delays.""" + _attr_has_entity_name = True + entity_description: FaaDelaysBinarySensorEntityDescription def __init__( @@ -117,6 +120,12 @@ class FAABinarySensor(CoordinatorEntity[FAADataUpdateCoordinator], BinarySensorE _id = coordinator.data.code self._attr_name = f"{_id} {description.name}" self._attr_unique_id = f"{_id}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, _id)}, + name=_id, + manufacturer="Federal Aviation Administration", + entry_type=DeviceEntryType.SERVICE, + ) @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/faa_delays/strings.json b/homeassistant/components/faa_delays/strings.json index 92a9dafb4da9..145c9e3ab349 100644 --- a/homeassistant/components/faa_delays/strings.json +++ b/homeassistant/components/faa_delays/strings.json @@ -17,5 +17,76 @@ "abort": { "already_configured": "This airport is already configured." } + }, + "entity": { + "binary_sensor": { + "ground_delay": { + "name": "Ground delay", + "state_attributes": { + "average": { + "name": "Average" + }, + "reason": { + "name": "Reason" + } + } + }, + "ground_stop": { + "name": "Ground stop", + "state_attributes": { + "endtime": { + "name": "End time" + }, + "reason": { + "name": "[%key:component::faa_delays::entity::binary_sensor::ground_delay::state_attributes::reason::name%]" + } + } + }, + "depart_delay": { + "name": "Departure delay", + "state_attributes": { + "minimum": { + "name": "Minimum" + }, + "maximum": { + "name": "Maximum" + }, + "trend": { + "name": "Trend" + }, + "reason": { + "name": "[%key:component::faa_delays::entity::binary_sensor::ground_delay::state_attributes::reason::name%]" + } + } + }, + "arrive_delay": { + "name": "Arrival delay", + "state_attributes": { + "minimum": { + "name": "[%key:component::faa_delays::entity::binary_sensor::depart_delay::state_attributes::minimum::name%]" + }, + "maximum": { + "name": "[%key:component::faa_delays::entity::binary_sensor::depart_delay::state_attributes::maximum::name%]" + }, + "trend": { + "name": "[%key:component::faa_delays::entity::binary_sensor::depart_delay::state_attributes::trend::name%]" + }, + "reason": { + "name": "[%key:component::faa_delays::entity::binary_sensor::ground_delay::state_attributes::reason::name%]" + } + } + }, + "closure": { + "name": "Closure", + "state_attributes": { + "begin": { + "name": "Begin" + }, + "end": { + "name": "End" + } + } + } + } } } From c8aed064384d335be2ab33da57137515083742b0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 29 Nov 2023 13:37:43 -0800 Subject: [PATCH 251/265] Add due date and description to Google Tasks (#104654) * Add tests for config validation function * Add Google Tasks due date and description * Revert test timezone * Update changes after upstream * Update homeassistant/components/google_tasks/todo.py Co-authored-by: Martin Hjelmare * Add google tasks tests for creating --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/google_tasks/todo.py | 39 ++++++--- .../google_tasks/snapshots/test_todo.ambr | 58 ++++++++++--- tests/components/google_tasks/test_todo.py | 85 +++++++++---------- 3 files changed, 114 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/google_tasks/todo.py b/homeassistant/components/google_tasks/todo.py index d3c4dfa69361..130c0d2cc01a 100644 --- a/homeassistant/components/google_tasks/todo.py +++ b/homeassistant/components/google_tasks/todo.py @@ -1,7 +1,7 @@ """Google Tasks todo platform.""" from __future__ import annotations -from datetime import timedelta +from datetime import date, datetime, timedelta from typing import Any, cast from homeassistant.components.todo import ( @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util from .api import AsyncConfigEntryAuth from .const import DOMAIN @@ -35,9 +36,31 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str]: result["title"] = item.summary if item.status is not None: result["status"] = TODO_STATUS_MAP_INV[item.status] + if (due := item.due) is not None: + # due API field is a timestamp string, but with only date resolution + result["due"] = dt_util.start_of_local_day(due).isoformat() + if (description := item.description) is not None: + result["notes"] = description return result +def _convert_api_item(item: dict[str, str]) -> TodoItem: + """Convert tasks API items into a TodoItem.""" + due: date | None = None + if (due_str := item.get("due")) is not None: + due = datetime.fromisoformat(due_str).date() + return TodoItem( + summary=item["title"], + uid=item["id"], + status=TODO_STATUS_MAP.get( + item.get("status", ""), + TodoItemStatus.NEEDS_ACTION, + ), + due=due, + description=item.get("notes"), + ) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -68,6 +91,8 @@ class GoogleTaskTodoListEntity( TodoListEntityFeature.CREATE_TODO_ITEM | TodoListEntityFeature.UPDATE_TODO_ITEM | TodoListEntityFeature.DELETE_TODO_ITEM + | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM + | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM ) def __init__( @@ -88,17 +113,7 @@ class GoogleTaskTodoListEntity( """Get the current set of To-do items.""" if self.coordinator.data is None: return None - return [ - TodoItem( - summary=item["title"], - uid=item["id"], - status=TODO_STATUS_MAP.get( - item.get("status"), # type: ignore[arg-type] - TodoItemStatus.NEEDS_ACTION, - ), - ) - for item in _order_tasks(self.coordinator.data) - ] + return [_convert_api_item(item) for item in _order_tasks(self.coordinator.data)] async def async_create_todo_item(self, item: TodoItem) -> None: """Add an item to the To-do list.""" diff --git a/tests/components/google_tasks/snapshots/test_todo.ambr b/tests/components/google_tasks/snapshots/test_todo.ambr index 73289b313d9d..7d6eb920593f 100644 --- a/tests/components/google_tasks/snapshots/test_todo.ambr +++ b/tests/components/google_tasks/snapshots/test_todo.ambr @@ -1,11 +1,29 @@ # serializer version: 1 -# name: test_create_todo_list_item[api_responses0] +# name: test_create_todo_list_item[description] tuple( 'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks?alt=json', 'POST', ) # --- -# name: test_create_todo_list_item[api_responses0].1 +# name: test_create_todo_list_item[description].1 + '{"title": "Soda", "status": "needsAction", "notes": "6-pack"}' +# --- +# name: test_create_todo_list_item[due] + tuple( + 'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks?alt=json', + 'POST', + ) +# --- +# name: test_create_todo_list_item[due].1 + '{"title": "Soda", "status": "needsAction", "due": "2023-11-18T00:00:00-08:00"}' +# --- +# name: test_create_todo_list_item[summary] + tuple( + 'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks?alt=json', + 'POST', + ) +# --- +# name: test_create_todo_list_item[summary].1 '{"title": "Soda", "status": "needsAction"}' # --- # name: test_delete_todo_list_item[_handler] @@ -38,6 +56,33 @@ }), ]) # --- +# name: test_partial_update[description] + tuple( + 'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json', + 'PATCH', + ) +# --- +# name: test_partial_update[description].1 + '{"notes": "6-pack"}' +# --- +# name: test_partial_update[due_date] + tuple( + 'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json', + 'PATCH', + ) +# --- +# name: test_partial_update[due_date].1 + '{"due": "2023-11-18T00:00:00-08:00"}' +# --- +# name: test_partial_update[rename] + tuple( + 'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json', + 'PATCH', + ) +# --- +# name: test_partial_update[rename].1 + '{"title": "Soda"}' +# --- # name: test_partial_update_status[api_responses0] tuple( 'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json', @@ -47,15 +92,6 @@ # name: test_partial_update_status[api_responses0].1 '{"status": "needsAction"}' # --- -# name: test_partial_update_title[api_responses0] - tuple( - 'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json', - 'PATCH', - ) -# --- -# name: test_partial_update_title[api_responses0].1 - '{"title": "Soda"}' -# --- # name: test_update_todo_list_item[api_responses0] tuple( 'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json', diff --git a/tests/components/google_tasks/test_todo.py b/tests/components/google_tasks/test_todo.py index 0b82815b33a2..3329f89c1cad 100644 --- a/tests/components/google_tasks/test_todo.py +++ b/tests/components/google_tasks/test_todo.py @@ -19,13 +19,12 @@ from homeassistant.exceptions import HomeAssistantError from tests.typing import WebSocketGenerator ENTITY_ID = "todo.my_tasks" +ITEM = { + "id": "task-list-id-1", + "title": "My tasks", +} LIST_TASK_LIST_RESPONSE = { - "items": [ - { - "id": "task-list-id-1", - "title": "My tasks", - }, - ] + "items": [ITEM], } EMPTY_RESPONSE = {} LIST_TASKS_RESPONSE = { @@ -76,6 +75,20 @@ LIST_TASKS_RESPONSE_MULTIPLE = { ], } +# API responses when testing update methods +UPDATE_API_RESPONSES = [ + LIST_TASK_LIST_RESPONSE, + LIST_TASKS_RESPONSE_WATER, + EMPTY_RESPONSE, # update + LIST_TASKS_RESPONSE, # refresh after update +] +CREATE_API_RESPONSES = [ + LIST_TASK_LIST_RESPONSE, + LIST_TASKS_RESPONSE, + EMPTY_RESPONSE, # create + LIST_TASKS_RESPONSE, # refresh +] + @pytest.fixture def platforms() -> list[str]: @@ -207,12 +220,14 @@ def mock_http_response(response_handler: list | Callable) -> Mock: "title": "Task 1", "status": "needsAction", "position": "0000000000000001", + "due": "2023-11-18T00:00:00+00:00", }, { "id": "task-2", "title": "Task 2", "status": "completed", "position": "0000000000000002", + "notes": "long description", }, ], }, @@ -238,11 +253,13 @@ async def test_get_items( "uid": "task-1", "summary": "Task 1", "status": "needs_action", + "due": "2023-11-18", }, { "uid": "task-2", "summary": "Task 2", "status": "completed", + "description": "long description", }, ] @@ -333,21 +350,20 @@ async def test_task_items_error_response( @pytest.mark.parametrize( - "api_responses", + ("api_responses", "item_data"), [ - [ - LIST_TASK_LIST_RESPONSE, - LIST_TASKS_RESPONSE, - EMPTY_RESPONSE, # create - LIST_TASKS_RESPONSE, # refresh after delete - ] + (CREATE_API_RESPONSES, {}), + (CREATE_API_RESPONSES, {"due_date": "2023-11-18"}), + (CREATE_API_RESPONSES, {"description": "6-pack"}), ], + ids=["summary", "due", "description"], ) async def test_create_todo_list_item( hass: HomeAssistant, setup_credentials: None, integration_setup: Callable[[], Awaitable[bool]], mock_http_response: Mock, + item_data: dict[str, Any], snapshot: SnapshotAssertion, ) -> None: """Test for creating a To-do Item.""" @@ -361,7 +377,7 @@ async def test_create_todo_list_item( await hass.services.async_call( TODO_DOMAIN, "add_item", - {"item": "Soda"}, + {"item": "Soda", **item_data}, target={"entity_id": "todo.my_tasks"}, blocking=True, ) @@ -407,17 +423,7 @@ async def test_create_todo_list_item_error( ) -@pytest.mark.parametrize( - "api_responses", - [ - [ - LIST_TASK_LIST_RESPONSE, - LIST_TASKS_RESPONSE_WATER, - EMPTY_RESPONSE, # update - LIST_TASKS_RESPONSE, # refresh after update - ] - ], -) +@pytest.mark.parametrize("api_responses", [UPDATE_API_RESPONSES]) async def test_update_todo_list_item( hass: HomeAssistant, setup_credentials: None, @@ -483,21 +489,20 @@ async def test_update_todo_list_item_error( @pytest.mark.parametrize( - "api_responses", + ("api_responses", "item_data"), [ - [ - LIST_TASK_LIST_RESPONSE, - LIST_TASKS_RESPONSE_WATER, - EMPTY_RESPONSE, # update - LIST_TASKS_RESPONSE, # refresh after update - ] + (UPDATE_API_RESPONSES, {"rename": "Soda"}), + (UPDATE_API_RESPONSES, {"due_date": "2023-11-18"}), + (UPDATE_API_RESPONSES, {"description": "6-pack"}), ], + ids=("rename", "due_date", "description"), ) -async def test_partial_update_title( +async def test_partial_update( hass: HomeAssistant, setup_credentials: None, integration_setup: Callable[[], Awaitable[bool]], mock_http_response: Any, + item_data: dict[str, Any], snapshot: SnapshotAssertion, ) -> None: """Test for partial update with title only.""" @@ -511,7 +516,7 @@ async def test_partial_update_title( await hass.services.async_call( TODO_DOMAIN, "update_item", - {"item": "some-task-id", "rename": "Soda"}, + {"item": "some-task-id", **item_data}, target={"entity_id": "todo.my_tasks"}, blocking=True, ) @@ -522,17 +527,7 @@ async def test_partial_update_title( assert call.kwargs.get("body") == snapshot -@pytest.mark.parametrize( - "api_responses", - [ - [ - LIST_TASK_LIST_RESPONSE, - LIST_TASKS_RESPONSE_WATER, - EMPTY_RESPONSE, # update - LIST_TASKS_RESPONSE, # refresh after update - ] - ], -) +@pytest.mark.parametrize("api_responses", [UPDATE_API_RESPONSES]) async def test_partial_update_status( hass: HomeAssistant, setup_credentials: None, From 2437fb314f80d75da85750824473cbbfc83a2a0d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 30 Nov 2023 00:05:10 +0200 Subject: [PATCH 252/265] Add strict typing to push bullet (#99538) Co-authored-by: Franck Nijhof --- .strict-typing | 1 + homeassistant/components/pushbullet/api.py | 3 ++- homeassistant/components/pushbullet/notify.py | 9 ++++++--- mypy.ini | 10 ++++++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.strict-typing b/.strict-typing index 2c8717596c67..a4969bcc8104 100644 --- a/.strict-typing +++ b/.strict-typing @@ -265,6 +265,7 @@ homeassistant.components.proximity.* homeassistant.components.prusalink.* homeassistant.components.pure_energie.* homeassistant.components.purpleair.* +homeassistant.components.pushbullet.* homeassistant.components.pvoutput.* homeassistant.components.qnap_qsw.* homeassistant.components.radarr.* diff --git a/homeassistant/components/pushbullet/api.py b/homeassistant/components/pushbullet/api.py index ff6a57aa9319..691ef7413c36 100644 --- a/homeassistant/components/pushbullet/api.py +++ b/homeassistant/components/pushbullet/api.py @@ -1,4 +1,5 @@ """Pushbullet Notification provider.""" +from __future__ import annotations from typing import Any @@ -10,7 +11,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send from .const import DATA_UPDATED -class PushBulletNotificationProvider(Listener): +class PushBulletNotificationProvider(Listener): # type: ignore[misc] """Provider for an account, leading to one or more sensors.""" def __init__(self, hass: HomeAssistant, pushbullet: PushBullet) -> None: diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py index 1cc851bdb991..662240d0bf5e 100644 --- a/homeassistant/components/pushbullet/notify.py +++ b/homeassistant/components/pushbullet/notify.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .api import PushBulletNotificationProvider from .const import ATTR_FILE, ATTR_FILE_URL, ATTR_URL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -34,8 +35,10 @@ async def async_get_service( """Get the Pushbullet notification service.""" if TYPE_CHECKING: assert discovery_info is not None - pushbullet: PushBullet = hass.data[DOMAIN][discovery_info["entry_id"]].pushbullet - return PushBulletNotificationService(hass, pushbullet) + pb_provider: PushBulletNotificationProvider = hass.data[DOMAIN][ + discovery_info["entry_id"] + ] + return PushBulletNotificationService(hass, pb_provider.pushbullet) class PushBulletNotificationService(BaseNotificationService): @@ -120,7 +123,7 @@ class PushBulletNotificationService(BaseNotificationService): pusher: PushBullet, email: str | None = None, phonenumber: str | None = None, - ): + ) -> None: """Create the message content.""" kwargs = {"body": message, "title": title} if email: diff --git a/mypy.ini b/mypy.ini index e01be53db3d0..a27282fc667e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2411,6 +2411,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.pushbullet.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.pvoutput.*] check_untyped_defs = true disallow_incomplete_defs = true From e8091a47db337f06ee10846ac272069566546fe7 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 29 Nov 2023 17:13:05 -0500 Subject: [PATCH 253/265] Bump pynws to 1.6.0 (#104679) --- homeassistant/components/nws/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index 05194d85a26d..4006a145db46 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["metar", "pynws"], "quality_scale": "platinum", - "requirements": ["pynws==1.5.1"] + "requirements": ["pynws==1.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ec0e5f232e2d..d19df8c1c9ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1922,7 +1922,7 @@ pynuki==1.6.2 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.5.1 +pynws==1.6.0 # homeassistant.components.nx584 pynx584==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75947854c7f4..5bf8f26c8740 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1451,7 +1451,7 @@ pynuki==1.6.2 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.5.1 +pynws==1.6.0 # homeassistant.components.nx584 pynx584==0.5 From ec3795ab974a7f3853dce3c8798965aea8b61aab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Nov 2023 23:16:58 +0100 Subject: [PATCH 254/265] Revert "Add proj dependency to our wheels builder (#104699)" (#104704) --- .github/workflows/wheels.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 9d16954cd097..3b23f1b5b057 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -199,7 +199,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-dev;proj-util;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -214,7 +214,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-dev;proj-util;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -228,7 +228,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-dev;proj-util;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" @@ -242,7 +242,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;proj-dev;proj-util;uchardet-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" From 04f1b11ef723b468517b68530474e95ff382d8f6 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 30 Nov 2023 00:32:12 +0100 Subject: [PATCH 255/265] Debug level logging for DSMR migration code (#104757) --- homeassistant/components/dsmr/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 487f996ac1f3..f56e2c3ed336 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -456,12 +456,12 @@ def rename_old_gas_to_mbus( device_id=mbus_device_id, ) except ValueError: - LOGGER.warning( + LOGGER.debug( "Skip migration of %s because it already exists", entity.entity_id, ) else: - LOGGER.info( + LOGGER.debug( "Migrated entity %s from unique id %s to %s", entity.entity_id, entity.unique_id, From d7932031632d3f2bb6935a1605403a15d825b5c6 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 29 Nov 2023 18:31:27 -0600 Subject: [PATCH 256/265] Skip TTS when text is empty (#104741) Co-authored-by: Paulus Schoutsen --- .../components/assist_pipeline/pipeline.py | 59 ++++++++------- .../components/esphome/voice_assistant.py | 22 +++--- homeassistant/components/voip/voip.py | 15 ++-- .../snapshots/test_websocket.ambr | 27 +++++++ .../assist_pipeline/test_websocket.py | 51 +++++++++++++ .../esphome/test_voice_assistant.py | 22 ++++++ tests/components/voip/test_voip.py | 72 +++++++++++++++++++ 7 files changed, 225 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 1eb32a9dc3f1..4f2a9a8d99b0 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1024,39 +1024,38 @@ class PipelineRun: ) ) - try: - # Synthesize audio and get URL - tts_media_id = tts_generate_media_source_id( - self.hass, - tts_input, - engine=self.tts_engine, - language=self.pipeline.tts_language, - options=self.tts_options, - ) - tts_media = await media_source.async_resolve_media( - self.hass, - tts_media_id, - None, - ) - except Exception as src_error: - _LOGGER.exception("Unexpected error during text-to-speech") - raise TextToSpeechError( - code="tts-failed", - message="Unexpected error during text-to-speech", - ) from src_error + if tts_input := tts_input.strip(): + try: + # Synthesize audio and get URL + tts_media_id = tts_generate_media_source_id( + self.hass, + tts_input, + engine=self.tts_engine, + language=self.pipeline.tts_language, + options=self.tts_options, + ) + tts_media = await media_source.async_resolve_media( + self.hass, + tts_media_id, + None, + ) + except Exception as src_error: + _LOGGER.exception("Unexpected error during text-to-speech") + raise TextToSpeechError( + code="tts-failed", + message="Unexpected error during text-to-speech", + ) from src_error - _LOGGER.debug("TTS result %s", tts_media) + _LOGGER.debug("TTS result %s", tts_media) + tts_output = { + "media_id": tts_media_id, + **asdict(tts_media), + } + else: + tts_output = {} self.process_event( - PipelineEvent( - PipelineEventType.TTS_END, - { - "tts_output": { - "media_id": tts_media_id, - **asdict(tts_media), - } - }, - ) + PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output}) ) return tts_media.url diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index 68ed98aa7893..de6b521d9808 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -186,16 +186,22 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): data_to_send = {"text": event.data["tts_input"]} elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: assert event.data is not None - path = event.data["tts_output"]["url"] - url = async_process_play_media_url(self.hass, path) - data_to_send = {"url": url} + tts_output = event.data["tts_output"] + if tts_output: + path = tts_output["url"] + url = async_process_play_media_url(self.hass, path) + data_to_send = {"url": url} - if self.device_info.voice_assistant_version >= 2: - media_id = event.data["tts_output"]["media_id"] - self._tts_task = self.hass.async_create_background_task( - self._send_tts(media_id), "esphome_voice_assistant_tts" - ) + if self.device_info.voice_assistant_version >= 2: + media_id = tts_output["media_id"] + self._tts_task = self.hass.async_create_background_task( + self._send_tts(media_id), "esphome_voice_assistant_tts" + ) + else: + self._tts_done.set() else: + # Empty TTS response + data_to_send = {} self._tts_done.set() elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END: assert event.data is not None diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index 74bc94e7dc52..11f70c631f1f 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -389,11 +389,16 @@ class PipelineRtpDatagramProtocol(RtpDatagramProtocol): self._conversation_id = event.data["intent_output"]["conversation_id"] elif event.type == PipelineEventType.TTS_END: # Send TTS audio to caller over RTP - media_id = event.data["tts_output"]["media_id"] - self.hass.async_create_background_task( - self._send_tts(media_id), - "voip_pipeline_tts", - ) + tts_output = event.data["tts_output"] + if tts_output: + media_id = tts_output["media_id"] + self.hass.async_create_background_task( + self._send_tts(media_id), + "voip_pipeline_tts", + ) + else: + # Empty TTS response + self._tts_done.set() elif event.type == PipelineEventType.ERROR: # Play error tone instead of wait for TTS self._pipeline_error = True diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 1f6255288063..072b1ff730ac 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -650,6 +650,33 @@ 'message': 'Timeout running pipeline', }) # --- +# name: test_pipeline_empty_tts_output + dict({ + 'language': 'en', + 'pipeline': , + 'runner_data': dict({ + 'stt_binary_handler_id': None, + 'timeout': 300, + }), + }) +# --- +# name: test_pipeline_empty_tts_output.1 + dict({ + 'engine': 'test', + 'language': 'en-US', + 'tts_input': '', + 'voice': 'james_earl_jones', + }) +# --- +# name: test_pipeline_empty_tts_output.2 + dict({ + 'tts_output': dict({ + }), + }) +# --- +# name: test_pipeline_empty_tts_output.3 + None +# --- # name: test_stt_provider_missing dict({ 'language': 'en', diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 931b31dd77bc..0e2a3ad538c7 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -2452,3 +2452,54 @@ async def test_device_capture_queue_full( assert msg["event"] == snapshot assert msg["event"]["type"] == "end" assert msg["event"]["overflow"] + + +async def test_pipeline_empty_tts_output( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + init_components, + snapshot: SnapshotAssertion, +) -> None: + """Test events from a pipeline run with a empty text-to-speech text.""" + events = [] + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "tts", + "end_stage": "tts", + "input": { + "text": "", + }, + } + ) + + # result + msg = await client.receive_json() + assert msg["success"] + + # run start + msg = await client.receive_json() + assert msg["event"]["type"] == "run-start" + msg["event"]["data"]["pipeline"] = ANY + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + # text-to-speech + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) + + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-end" + assert msg["event"]["data"] == snapshot + assert not msg["event"]["data"]["tts_output"] + events.append(msg["event"]) + + # run end + msg = await client.receive_json() + assert msg["event"]["type"] == "run-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) diff --git a/tests/components/esphome/test_voice_assistant.py b/tests/components/esphome/test_voice_assistant.py index ca74c99f0cd8..38a33bfdec24 100644 --- a/tests/components/esphome/test_voice_assistant.py +++ b/tests/components/esphome/test_voice_assistant.py @@ -337,6 +337,28 @@ async def test_send_tts_called( mock_send_tts.assert_called_with(_TEST_MEDIA_ID) +async def test_send_tts_not_called_when_empty( + hass: HomeAssistant, + voice_assistant_udp_server_v1: VoiceAssistantUDPServer, + voice_assistant_udp_server_v2: VoiceAssistantUDPServer, +) -> None: + """Test the UDP server with a v1/v2 device doesn't call _send_tts when the output is empty.""" + with patch( + "homeassistant.components.esphome.voice_assistant.VoiceAssistantUDPServer._send_tts" + ) as mock_send_tts: + voice_assistant_udp_server_v1._event_callback( + PipelineEvent(type=PipelineEventType.TTS_END, data={"tts_output": {}}) + ) + + mock_send_tts.assert_not_called() + + voice_assistant_udp_server_v2._event_callback( + PipelineEvent(type=PipelineEventType.TTS_END, data={"tts_output": {}}) + ) + + mock_send_tts.assert_not_called() + + async def test_send_tts( hass: HomeAssistant, voice_assistant_udp_server_v2: VoiceAssistantUDPServer, diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index 692896c6dfaa..dbb848f3b9dc 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -528,3 +528,75 @@ async def test_tts_wrong_wav_format( # Wait for mock pipeline to exhaust the audio stream async with asyncio.timeout(1): await done.wait() + + +async def test_empty_tts_output( + hass: HomeAssistant, + voip_device: VoIPDevice, +) -> None: + """Test that TTS will not stream when output is empty.""" + assert await async_setup_component(hass, "voip", {}) + + def is_speech(self, chunk): + """Anything non-zero is speech.""" + return sum(chunk) > 0 + + async def async_pipeline_from_audio_stream(*args, **kwargs): + stt_stream = kwargs["stt_stream"] + event_callback = kwargs["event_callback"] + async for _chunk in stt_stream: + # Stream will end when VAD detects end of "speech" + pass + + # Fake intent result + event_callback( + assist_pipeline.PipelineEvent( + type=assist_pipeline.PipelineEventType.INTENT_END, + data={ + "intent_output": { + "conversation_id": "fake-conversation", + } + }, + ) + ) + + # Empty TTS output + event_callback( + assist_pipeline.PipelineEvent( + type=assist_pipeline.PipelineEventType.TTS_END, + data={"tts_output": {}}, + ) + ) + + with patch( + "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", + new=is_speech, + ), patch( + "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), patch( + "homeassistant.components.voip.voip.PipelineRtpDatagramProtocol._send_tts", + ) as mock_send_tts: + rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( + hass, + hass.config.language, + voip_device, + Context(), + opus_payload_type=123, + ) + rtp_protocol.transport = Mock() + + # silence + rtp_protocol.on_chunk(bytes(_ONE_SECOND)) + + # "speech" + rtp_protocol.on_chunk(bytes([255] * _ONE_SECOND * 2)) + + # silence (assumes relaxed VAD sensitivity) + rtp_protocol.on_chunk(bytes(_ONE_SECOND * 4)) + + # Wait for mock pipeline to finish + async with asyncio.timeout(1): + await rtp_protocol._tts_done.wait() + + mock_send_tts.assert_not_called() From b957c4e3ee4f022742fdf7066741fdd0137bebf8 Mon Sep 17 00:00:00 2001 From: Daniel Gangl <31815106+killer0071234@users.noreply.github.com> Date: Thu, 30 Nov 2023 01:31:39 +0100 Subject: [PATCH 257/265] Bump zamg to 0.3.3 (#104756) --- homeassistant/components/zamg/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zamg/manifest.json b/homeassistant/components/zamg/manifest.json index df17672231e1..f83e38002b8f 100644 --- a/homeassistant/components/zamg/manifest.json +++ b/homeassistant/components/zamg/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zamg", "iot_class": "cloud_polling", - "requirements": ["zamg==0.3.0"] + "requirements": ["zamg==0.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index d19df8c1c9ae..de84d8fb4209 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2807,7 +2807,7 @@ youtubeaio==1.1.5 yt-dlp==2023.11.16 # homeassistant.components.zamg -zamg==0.3.0 +zamg==0.3.3 # homeassistant.components.zengge zengge==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bf8f26c8740..f2241b1c5d4d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2105,7 +2105,7 @@ youtubeaio==1.1.5 yt-dlp==2023.11.16 # homeassistant.components.zamg -zamg==0.3.0 +zamg==0.3.3 # homeassistant.components.zeroconf zeroconf==0.127.0 From ec647677e98a8b627481c8b6534c51c193203368 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Thu, 30 Nov 2023 01:38:33 +0100 Subject: [PATCH 258/265] Update initial translation for ViCare water heater entity (#104696) --- homeassistant/components/vicare/strings.json | 4 ++-- homeassistant/components/vicare/water_heater.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index e9ee272edd86..47ee60b2ea8e 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -283,8 +283,8 @@ } }, "water_heater": { - "water": { - "name": "Water" + "domestic_hot_water": { + "name": "Domestic hot water" } } }, diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 036ced5ee550..66a90ca065bc 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -64,13 +64,13 @@ def _build_entities( api: PyViCareDevice, device_config: PyViCareDeviceConfig, ) -> list[ViCareWater]: - """Create ViCare water entities for a device.""" + """Create ViCare domestic hot water entities for a device.""" return [ ViCareWater( api, circuit, device_config, - "water", + "domestic_hot_water", ) for circuit in get_circuits(api) ] @@ -81,7 +81,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the ViCare climate platform.""" + """Set up the ViCare water heater platform.""" api = hass.data[DOMAIN][config_entry.entry_id][VICARE_API] device_config = hass.data[DOMAIN][config_entry.entry_id][VICARE_DEVICE_CONFIG] From 9fa163c1076285b97988cb924110bf1b6ea1e842 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 30 Nov 2023 01:50:37 +0100 Subject: [PATCH 259/265] Reolink cleanup when CAM disconnected from NVR (#103888) Co-authored-by: Franck Nijhof --- homeassistant/components/reolink/__init__.py | 56 +++++++++++++++++ tests/components/reolink/conftest.py | 6 +- .../reolink/snapshots/test_diagnostics.ambr | 2 +- tests/components/reolink/test_init.py | 61 ++++++++++++++++++- 4 files changed, 120 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 46761beae001..eca5a5aa853c 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -148,6 +149,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b firmware_coordinator=firmware_coordinator, ) + cleanup_disconnected_cams(hass, config_entry.entry_id, host) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) config_entry.async_on_unload( @@ -175,3 +178,56 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok + + +def cleanup_disconnected_cams( + hass: HomeAssistant, config_entry_id: str, host: ReolinkHost +) -> None: + """Clean-up disconnected camera channels or channels where a different model camera is connected.""" + if not host.api.is_nvr: + return + + device_reg = dr.async_get(hass) + devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) + for device in devices: + device_id = [ + dev_id[1].split("_ch") + for dev_id in device.identifiers + if dev_id[0] == DOMAIN + ][0] + + if len(device_id) < 2: + # Do not consider the NVR itself + continue + + ch = int(device_id[1]) + ch_model = host.api.camera_model(ch) + remove = False + if ch not in host.api.channels: + remove = True + _LOGGER.debug( + "Removing Reolink device %s, since no camera is connected to NVR channel %s anymore", + device.name, + ch, + ) + if ch_model not in [device.model, "Unknown"]: + remove = True + _LOGGER.debug( + "Removing Reolink device %s, since the camera model connected to channel %s changed from %s to %s", + device.name, + ch, + device.model, + ch_model, + ) + if not remove: + continue + + # clean entity and device registry + entity_reg = er.async_get(hass) + entities = er.async_entries_for_device( + entity_reg, device.id, include_disabled_entities=True + ) + for entity in entities: + entity_reg.async_remove(entity.entity_id) + + device_reg.async_remove_device(device.id) diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 75d2dc0c6615..464d4120c657 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -25,6 +25,8 @@ TEST_PORT = 1234 TEST_NVR_NAME = "test_reolink_name" TEST_NVR_NAME2 = "test2_reolink_name" TEST_USE_HTTPS = True +TEST_HOST_MODEL = "RLN8-410" +TEST_CAM_MODEL = "RLC-123" @pytest.fixture @@ -70,8 +72,8 @@ def reolink_connect_class( host_mock.hardware_version = "IPC_00000" host_mock.sw_version = "v1.0.0.0.0.0000" host_mock.manufacturer = "Reolink" - host_mock.model = "RLC-123" - host_mock.camera_model.return_value = "RLC-123" + host_mock.model = TEST_HOST_MODEL + host_mock.camera_model.return_value = TEST_CAM_MODEL host_mock.camera_name.return_value = TEST_NVR_NAME host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000" host_mock.session_active = True diff --git a/tests/components/reolink/snapshots/test_diagnostics.ambr b/tests/components/reolink/snapshots/test_diagnostics.ambr index 604a93643207..9f70673695ca 100644 --- a/tests/components/reolink/snapshots/test_diagnostics.ambr +++ b/tests/components/reolink/snapshots/test_diagnostics.ambr @@ -41,7 +41,7 @@ 'event connection': 'Fast polling', 'firmware version': 'v1.0.0.0.0.0000', 'hardware version': 'IPC_00000', - 'model': 'RLC-123', + 'model': 'RLN8-410', 'stream channels': list([ 0, ]), diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index e2bd622bb43a..6a9a8b957db7 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -11,11 +11,15 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir +from homeassistant.helpers import ( + device_registry as dr, + entity_registry as er, + issue_registry as ir, +) from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .conftest import TEST_NVR_NAME +from .conftest import TEST_CAM_MODEL, TEST_HOST_MODEL, TEST_NVR_NAME from tests.common import MockConfigEntry, async_fire_time_changed @@ -102,6 +106,7 @@ async def test_entry_reloading( reolink_connect: MagicMock, ) -> None: """Test the entry is reloaded correctly when settings change.""" + reolink_connect.is_nvr = False assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -115,6 +120,58 @@ async def test_entry_reloading( assert config_entry.title == "New Name" +@pytest.mark.parametrize( + ("attr", "value", "expected_models"), + [ + ( + None, + None, + [TEST_HOST_MODEL, TEST_CAM_MODEL], + ), + ("channels", [], [TEST_HOST_MODEL]), + ( + "camera_model", + Mock(return_value="RLC-567"), + [TEST_HOST_MODEL, "RLC-567"], + ), + ], +) +async def test_cleanup_disconnected_cams( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + attr: str | None, + value: Any, + expected_models: list[str], +) -> None: + """Test device and entity registry are cleaned up when camera is disconnected from NVR.""" + reolink_connect.channels = [0] + # setup CH 0 and NVR switch entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + device_models = [device.model for device in device_entries] + assert sorted(device_models) == sorted([TEST_HOST_MODEL, TEST_CAM_MODEL]) + + # reload integration after 'disconnecting' a camera. + if attr is not None: + setattr(reolink_connect, attr, value) + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_reload(config_entry.entry_id) + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + device_models = [device.model for device in device_entries] + assert sorted(device_models) == sorted(expected_models) + + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: From c72e4e8b5cd6c5872d6190a4f8040429b31c2804 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 29 Nov 2023 21:11:38 -0800 Subject: [PATCH 260/265] Fix bug in rainbird device ids that are int serial numbers (#104768) --- homeassistant/components/rainbird/__init__.py | 2 +- tests/components/rainbird/test_init.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index c149c993acb3..e5731dc08fe0 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -183,7 +183,7 @@ def _async_fix_device_id( device_entry_map = {} migrations = {} for device_entry in device_entries: - unique_id = next(iter(device_entry.identifiers))[1] + unique_id = str(next(iter(device_entry.identifiers))[1]) device_entry_map[unique_id] = device_entry if (suffix := unique_id.removeprefix(str(serial_number))) != unique_id: migrations[unique_id] = f"{mac_address}{suffix}" diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index 7048e1d63f45..00cbefc65561 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -239,6 +239,14 @@ async def test_fix_unique_id_duplicate( f"{MAC_ADDRESS_UNIQUE_ID}-rain-delay", f"{MAC_ADDRESS_UNIQUE_ID}-1", ), + ( + SERIAL_NUMBER, + SERIAL_NUMBER, + SERIAL_NUMBER, + SERIAL_NUMBER, + MAC_ADDRESS_UNIQUE_ID, + MAC_ADDRESS_UNIQUE_ID, + ), ("0", 0, "0", "0", MAC_ADDRESS_UNIQUE_ID, MAC_ADDRESS_UNIQUE_ID), ( "0", @@ -268,6 +276,7 @@ async def test_fix_unique_id_duplicate( ids=( "serial-number", "serial-number-with-suffix", + "serial-number-int", "zero-serial", "zero-serial-suffix", "new-format", From 64a6a6a7786f15583135d7f3b88ab4734d642883 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 29 Nov 2023 22:01:57 -0800 Subject: [PATCH 261/265] Add due date and description fields to Todoist To-do entity (#104655) * Add Todoist Due date and description fields * Update entity features with new names * Make items into walrus * Update due_datetime field * Add additional tests for adding new fields to items * Fix call args in todoist test --- homeassistant/components/todoist/todo.py | 41 ++++- tests/components/todoist/conftest.py | 5 +- tests/components/todoist/test_todo.py | 211 +++++++++++++++++++++-- 3 files changed, 236 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/todoist/todo.py b/homeassistant/components/todoist/todo.py index c0d3ec6e2ce8..64e83b8cc6ed 100644 --- a/homeassistant/components/todoist/todo.py +++ b/homeassistant/components/todoist/todo.py @@ -1,7 +1,8 @@ """A todo platform for Todoist.""" import asyncio -from typing import cast +import datetime +from typing import Any, cast from homeassistant.components.todo import ( TodoItem, @@ -13,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util from .const import DOMAIN from .coordinator import TodoistCoordinator @@ -30,6 +32,24 @@ async def async_setup_entry( ) +def _task_api_data(item: TodoItem) -> dict[str, Any]: + """Convert a TodoItem to the set of add or update arguments.""" + item_data: dict[str, Any] = {} + if summary := item.summary: + item_data["content"] = summary + if due := item.due: + if isinstance(due, datetime.datetime): + item_data["due"] = { + "date": due.date().isoformat(), + "datetime": due.isoformat(), + } + else: + item_data["due"] = {"date": due.isoformat()} + if description := item.description: + item_data["description"] = description + return item_data + + class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntity): """A Todoist TodoListEntity.""" @@ -37,6 +57,9 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit TodoListEntityFeature.CREATE_TODO_ITEM | TodoListEntityFeature.UPDATE_TODO_ITEM | TodoListEntityFeature.DELETE_TODO_ITEM + | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM + | TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM + | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM ) def __init__( @@ -66,11 +89,21 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit status = TodoItemStatus.COMPLETED else: status = TodoItemStatus.NEEDS_ACTION + due: datetime.date | datetime.datetime | None = None + if task_due := task.due: + if task_due.datetime: + due = dt_util.as_local( + datetime.datetime.fromisoformat(task_due.datetime) + ) + elif task_due.date: + due = datetime.date.fromisoformat(task_due.date) items.append( TodoItem( summary=task.content, uid=task.id, status=status, + due=due, + description=task.description or None, # Don't use empty string ) ) self._attr_todo_items = items @@ -81,7 +114,7 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit if item.status != TodoItemStatus.NEEDS_ACTION: raise ValueError("Only active tasks may be created.") await self.coordinator.api.add_task( - content=item.summary or "", + **_task_api_data(item), project_id=self._project_id, ) await self.coordinator.async_refresh() @@ -89,8 +122,8 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit async def async_update_todo_item(self, item: TodoItem) -> None: """Update a To-do item.""" uid: str = cast(str, item.uid) - if item.summary: - await self.coordinator.api.update_task(task_id=uid, content=item.summary) + if update_data := _task_api_data(item): + await self.coordinator.api.update_task(task_id=uid, **update_data) if item.status is not None: if item.status == TodoItemStatus.COMPLETED: await self.coordinator.api.close_task(task_id=uid) diff --git a/tests/components/todoist/conftest.py b/tests/components/todoist/conftest.py index 28f22e1061a1..4e4d41b6914e 100644 --- a/tests/components/todoist/conftest.py +++ b/tests/components/todoist/conftest.py @@ -45,6 +45,7 @@ def make_api_task( is_completed: bool = False, due: Due | None = None, project_id: str | None = None, + description: str | None = None, ) -> Task: """Mock a todoist Task instance.""" return Task( @@ -55,8 +56,8 @@ def make_api_task( content=content or SUMMARY, created_at="2021-10-01T00:00:00", creator_id="1", - description="A task", - due=due or Due(is_recurring=False, date=TODAY, string="today"), + description=description, + due=due, id=id or "1", labels=["Label1"], order=1, diff --git a/tests/components/todoist/test_todo.py b/tests/components/todoist/test_todo.py index fb6f707be477..aa00e2c2ff4f 100644 --- a/tests/components/todoist/test_todo.py +++ b/tests/components/todoist/test_todo.py @@ -1,7 +1,9 @@ """Unit tests for the Todoist todo platform.""" +from typing import Any from unittest.mock import AsyncMock import pytest +from todoist_api_python.models import Due, Task from homeassistant.components.todo import DOMAIN as TODO_DOMAIN from homeassistant.const import Platform @@ -19,6 +21,12 @@ def platforms() -> list[Platform]: return [Platform.TODO] +@pytest.fixture(autouse=True) +def set_time_zone(hass: HomeAssistant) -> None: + """Set the time zone for the tests that keesp UTC-6 all year round.""" + hass.config.set_time_zone("America/Regina") + + @pytest.mark.parametrize( ("tasks", "expected_state"), [ @@ -57,11 +65,91 @@ async def test_todo_item_state( assert state.state == expected_state -@pytest.mark.parametrize(("tasks"), [[]]) +@pytest.mark.parametrize( + ("tasks", "item_data", "tasks_after_update", "add_kwargs", "expected_item"), + [ + ( + [], + {}, + [make_api_task(id="task-id-1", content="Soda", is_completed=False)], + {"content": "Soda"}, + {"uid": "task-id-1", "summary": "Soda", "status": "needs_action"}, + ), + ( + [], + {"due_date": "2023-11-18"}, + [ + make_api_task( + id="task-id-1", + content="Soda", + is_completed=False, + due=Due(is_recurring=False, date="2023-11-18", string="today"), + ) + ], + {"due": {"date": "2023-11-18"}}, + { + "uid": "task-id-1", + "summary": "Soda", + "status": "needs_action", + "due": "2023-11-18", + }, + ), + ( + [], + {"due_datetime": "2023-11-18T06:30:00"}, + [ + make_api_task( + id="task-id-1", + content="Soda", + is_completed=False, + due=Due( + date="2023-11-18", + is_recurring=False, + datetime="2023-11-18T12:30:00.000000Z", + string="today", + ), + ) + ], + { + "due": {"date": "2023-11-18", "datetime": "2023-11-18T06:30:00-06:00"}, + }, + { + "uid": "task-id-1", + "summary": "Soda", + "status": "needs_action", + "due": "2023-11-18T06:30:00-06:00", + }, + ), + ( + [], + {"description": "6-pack"}, + [ + make_api_task( + id="task-id-1", + content="Soda", + description="6-pack", + is_completed=False, + ) + ], + {"description": "6-pack"}, + { + "uid": "task-id-1", + "summary": "Soda", + "status": "needs_action", + "description": "6-pack", + }, + ), + ], + ids=["summary", "due_date", "due_datetime", "description"], +) async def test_add_todo_list_item( hass: HomeAssistant, setup_integration: None, api: AsyncMock, + item_data: dict[str, Any], + tasks_after_update: list[Task], + add_kwargs: dict[str, Any], + expected_item: dict[str, Any], ) -> None: """Test for adding a To-do Item.""" @@ -71,28 +159,35 @@ async def test_add_todo_list_item( api.add_task = AsyncMock() # Fake API response when state is refreshed after create - api.get_tasks.return_value = [ - make_api_task(id="task-id-1", content="Soda", is_completed=False) - ] + api.get_tasks.return_value = tasks_after_update await hass.services.async_call( TODO_DOMAIN, "add_item", - {"item": "Soda"}, + {"item": "Soda", **item_data}, target={"entity_id": "todo.name"}, blocking=True, ) args = api.add_task.call_args assert args - assert args.kwargs.get("content") == "Soda" - assert args.kwargs.get("project_id") == PROJECT_ID + assert args.kwargs == {"project_id": PROJECT_ID, "content": "Soda", **add_kwargs} # Verify state is refreshed state = hass.states.get("todo.name") assert state assert state.state == "1" + result = await hass.services.async_call( + TODO_DOMAIN, + "get_items", + {}, + target={"entity_id": "todo.name"}, + blocking=True, + return_response=True, + ) + assert result == {"todo.name": {"items": [expected_item]}} + @pytest.mark.parametrize( ("tasks"), [[make_api_task(id="task-id-1", content="Soda", is_completed=False)]] @@ -158,12 +253,91 @@ async def test_update_todo_item_status( @pytest.mark.parametrize( - ("tasks"), [[make_api_task(id="task-id-1", content="Soda", is_completed=False)]] + ("tasks", "update_data", "tasks_after_update", "update_kwargs", "expected_item"), + [ + ( + [make_api_task(id="task-id-1", content="Soda", is_completed=False)], + {"rename": "Milk"}, + [make_api_task(id="task-id-1", content="Milk", is_completed=False)], + {"task_id": "task-id-1", "content": "Milk"}, + {"uid": "task-id-1", "summary": "Milk", "status": "needs_action"}, + ), + ( + [make_api_task(id="task-id-1", content="Soda", is_completed=False)], + {"due_date": "2023-11-18"}, + [ + make_api_task( + id="task-id-1", + content="Soda", + is_completed=False, + due=Due(is_recurring=False, date="2023-11-18", string="today"), + ) + ], + {"task_id": "task-id-1", "due": {"date": "2023-11-18"}}, + { + "uid": "task-id-1", + "summary": "Soda", + "status": "needs_action", + "due": "2023-11-18", + }, + ), + ( + [make_api_task(id="task-id-1", content="Soda", is_completed=False)], + {"due_datetime": "2023-11-18T06:30:00"}, + [ + make_api_task( + id="task-id-1", + content="Soda", + is_completed=False, + due=Due( + date="2023-11-18", + is_recurring=False, + datetime="2023-11-18T12:30:00.000000Z", + string="today", + ), + ) + ], + { + "task_id": "task-id-1", + "due": {"date": "2023-11-18", "datetime": "2023-11-18T06:30:00-06:00"}, + }, + { + "uid": "task-id-1", + "summary": "Soda", + "status": "needs_action", + "due": "2023-11-18T06:30:00-06:00", + }, + ), + ( + [make_api_task(id="task-id-1", content="Soda", is_completed=False)], + {"description": "6-pack"}, + [ + make_api_task( + id="task-id-1", + content="Soda", + description="6-pack", + is_completed=False, + ) + ], + {"task_id": "task-id-1", "description": "6-pack"}, + { + "uid": "task-id-1", + "summary": "Soda", + "status": "needs_action", + "description": "6-pack", + }, + ), + ], + ids=["rename", "due_date", "due_datetime", "description"], ) -async def test_update_todo_item_summary( +async def test_update_todo_items( hass: HomeAssistant, setup_integration: None, api: AsyncMock, + update_data: dict[str, Any], + tasks_after_update: list[Task], + update_kwargs: dict[str, Any], + expected_item: dict[str, Any], ) -> None: """Test for updating a To-do Item that changes the summary.""" @@ -174,22 +348,29 @@ async def test_update_todo_item_summary( api.update_task = AsyncMock() # Fake API response when state is refreshed after close - api.get_tasks.return_value = [ - make_api_task(id="task-id-1", content="Soda", is_completed=True) - ] + api.get_tasks.return_value = tasks_after_update await hass.services.async_call( TODO_DOMAIN, "update_item", - {"item": "task-id-1", "rename": "Milk"}, + {"item": "task-id-1", **update_data}, target={"entity_id": "todo.name"}, blocking=True, ) assert api.update_task.called args = api.update_task.call_args assert args - assert args.kwargs.get("task_id") == "task-id-1" - assert args.kwargs.get("content") == "Milk" + assert args.kwargs == update_kwargs + + result = await hass.services.async_call( + TODO_DOMAIN, + "get_items", + {}, + target={"entity_id": "todo.name"}, + blocking=True, + return_response=True, + ) + assert result == {"todo.name": {"items": [expected_item]}} @pytest.mark.parametrize( From 2f0846bd878ef5f53841ac45bb476632a58e8b5a Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Thu, 30 Nov 2023 08:12:39 +0100 Subject: [PATCH 262/265] Axis: add host and user name field description (#104693) --- homeassistant/components/axis/strings.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 47a25b542a72..8c302dba2012 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -3,12 +3,16 @@ "flow_title": "{name} ({host})", "step": { "user": { - "title": "Set up Axis device", + "description": "Set up an Axis device", "data": { "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "The hostname or IP address of the Axis device.", + "username": "The user name you set up on your Axis device. It is recommended to create a user specifically for Home Assistant." } } }, From 6ac9a1d1f276a76ce1293e0dd4c4657190a30fd8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 30 Nov 2023 08:22:16 +0100 Subject: [PATCH 263/265] Migrate GDACS to has entity name (#104733) --- homeassistant/components/gdacs/sensor.py | 10 +++++++++- tests/components/gdacs/test_sensor.py | 8 ++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py index 5d5589c54d67..8a0a0113ceda 100644 --- a/homeassistant/components/gdacs/sensor.py +++ b/homeassistant/components/gdacs/sensor.py @@ -7,6 +7,7 @@ import logging from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util @@ -44,12 +45,14 @@ class GdacsSensor(SensorEntity): _attr_should_poll = False _attr_icon = DEFAULT_ICON _attr_native_unit_of_measurement = DEFAULT_UNIT_OF_MEASUREMENT + _attr_has_entity_name = True + _attr_name = None def __init__(self, config_entry: ConfigEntry, manager) -> None: """Initialize entity.""" + assert config_entry.unique_id self._config_entry_id = config_entry.entry_id self._attr_unique_id = config_entry.unique_id - self._attr_name = f"GDACS ({config_entry.title})" self._manager = manager self._status = None self._last_update = None @@ -60,6 +63,11 @@ class GdacsSensor(SensorEntity): self._updated = None self._removed = None self._remove_signal_status: Callable[[], None] | None = None + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, config_entry.unique_id)}, + entry_type=DeviceEntryType.SERVICE, + manufacturer="GDACS", + ) async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py index da318b1a94da..670d3efce51c 100644 --- a/tests/components/gdacs/test_sensor.py +++ b/tests/components/gdacs/test_sensor.py @@ -72,10 +72,10 @@ async def test_setup(hass: HomeAssistant) -> None: == 4 ) - state = hass.states.get("sensor.gdacs_32_87336_117_22743") + state = hass.states.get("sensor.32_87336_117_22743") assert state is not None assert int(state.state) == 3 - assert state.name == "GDACS (32.87336, -117.22743)" + assert state.name == "32.87336, -117.22743" attributes = state.attributes assert attributes[ATTR_STATUS] == "OK" assert attributes[ATTR_CREATED] == 3 @@ -96,7 +96,7 @@ async def test_setup(hass: HomeAssistant) -> None: == 4 ) - state = hass.states.get("sensor.gdacs_32_87336_117_22743") + state = hass.states.get("sensor.32_87336_117_22743") attributes = state.attributes assert attributes[ATTR_CREATED] == 1 assert attributes[ATTR_UPDATED] == 2 @@ -125,6 +125,6 @@ async def test_setup(hass: HomeAssistant) -> None: == 1 ) - state = hass.states.get("sensor.gdacs_32_87336_117_22743") + state = hass.states.get("sensor.32_87336_117_22743") attributes = state.attributes assert attributes[ATTR_REMOVED] == 3 From 8bc1f9d03d401f8ac210ff95b2ef14e68b193466 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Thu, 30 Nov 2023 08:47:17 +0100 Subject: [PATCH 264/265] Comelit, Coolmaster: add host field description (#104771) --- homeassistant/components/comelit/strings.json | 3 +++ homeassistant/components/coolmaster/strings.json | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json index 730674e913a0..73c2c7d00c66 100644 --- a/homeassistant/components/comelit/strings.json +++ b/homeassistant/components/comelit/strings.json @@ -13,6 +13,9 @@ "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", "pin": "[%key:common::config_flow::data::pin%]" + }, + "data_description": { + "host": "The hostname or IP address of your Comelit device." } } }, diff --git a/homeassistant/components/coolmaster/strings.json b/homeassistant/components/coolmaster/strings.json index 7baa6444c1db..17deab306df6 100644 --- a/homeassistant/components/coolmaster/strings.json +++ b/homeassistant/components/coolmaster/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Set up your CoolMasterNet connection details.", + "description": "Set up your CoolMasterNet connection details.", "data": { "host": "[%key:common::config_flow::data::host%]", "off": "Can be turned off", @@ -12,6 +12,9 @@ "dry": "Support dry mode", "fan_only": "Support fan only mode", "swing_support": "Control swing mode" + }, + "data_description": { + "host": "The hostname or IP address of your CoolMasterNet device." } } }, From 56350d1c0a756f05ce633bff45118f6c29e2e9d1 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Thu, 30 Nov 2023 08:47:37 +0100 Subject: [PATCH 265/265] Broadlink, BSB-Lan: add host field description (#104770) --- homeassistant/components/broadlink/strings.json | 5 ++++- homeassistant/components/bsblan/strings.json | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/broadlink/strings.json b/homeassistant/components/broadlink/strings.json index 87567bcb7b17..335984d1ebe4 100644 --- a/homeassistant/components/broadlink/strings.json +++ b/homeassistant/components/broadlink/strings.json @@ -3,10 +3,13 @@ "flow_title": "{name} ({model} at {host})", "step": { "user": { - "title": "Connect to the device", + "description": "Connect to the device", "data": { "host": "[%key:common::config_flow::data::host%]", "timeout": "Timeout" + }, + "data_description": { + "host": "The hostname or IP address of your Broadlink device." } }, "auth": { diff --git a/homeassistant/components/bsblan/strings.json b/homeassistant/components/bsblan/strings.json index 0693f3fb8ea0..689d1f893d3c 100644 --- a/homeassistant/components/bsblan/strings.json +++ b/homeassistant/components/bsblan/strings.json @@ -11,6 +11,9 @@ "passkey": "Passkey string", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "host": "The hostname or IP address of your BSB-Lan device." } } },