From 05fd64fe802b82a04888244e65e012611d8eb912 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Oct 2023 17:41:53 +0200 Subject: [PATCH 001/201] 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 77c5582464e..5b8e9f43b21 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 82bb7d08e26..8068b7e55b7 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/201] 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 f3be6977891..bab3421c58d 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 00000000000..db2a4964f6c --- /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/201] 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 38fc80ac3af..a2cddcf9a65 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 628c25b987e..2a1814c83d0 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 7d5f24f0b82..aa93dd46f26 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 e46c62e8976..dc8a40bc325 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/201] 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 83c599bc65d..a00455293f6 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 3a7105684f4..457b7ccbf9b 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/201] 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 5c35303f859..1888a177895 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/201] 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 14d14316faf..7e23d01ee46 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 f2de59b10af..e2f04b5d880 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 53c9e6b6d74..d89f376d662 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 a6660b0231a..12eac858f75 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 845b70b72ba..f43dd9b6672 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 6d06649a6ba..8a7e38c9773 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 15f1e50bdb9..ab28c6cbe6d 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 833a4ea266b..f4d671ad352 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/201] 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 55bd862349b..fc7f1ddce1f 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 3b179bc158c..b8756d9ace5 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/201] 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 89438c9c7c1..c6413dd4372 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/201] 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 4f6cc24edc8..17d1237e579 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 aa93dd46f26..ab9941778cc 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 dc8a40bc325..f40240690eb 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/201] 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 3472ca231e9..26b04929a45 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 ab9941778cc..5986c17e07b 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 f40240690eb..bf76fe945ef 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/201] 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 240610e4868..8eacd3e291a 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 fc870f2bfe3..31d90a6e93d 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/201] 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 ad35a2cfbdd..2976886881d 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/201] 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 960a86637ae..06e7d34e68d 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 def5f0c9afa..3aff4601d45 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 5986c17e07b..befc5e905cb 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 bf76fe945ef..34d7a6a4207 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/201] 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 e61ca04374f..69bed1af700 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 befc5e905cb..63f4f31ac5e 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 34d7a6a4207..c32f38b1cf1 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/201] 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 8ad0e1ad43f..803669c806d 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 c63b1713b8d..4f2cdc22ba9 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/201] 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 a1df31ceecc..d43ae7da50c 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 63f4f31ac5e..61d80f9654e 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 c32f38b1cf1..c6dca20ecd3 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/201] 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 8138587b9b5..7dd39c14039 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 9e3f954a0c5..8cc76e01d8c 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/201] 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 064777b4921..31f4dc14559 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 3aff4601d45..e99afb6330b 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 61d80f9654e..3a85a24589a 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 c6dca20ecd3..678a2eefcf7 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/201] 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 423ba3117ea..c9b7cb10386 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 1e2a121d6fb..f7830f03ed6 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/201] 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 1f8e5bbf2e7..ac229cb677f 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 4eb3a3f5171..cb0e840604e 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/201] 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 3218c1e839b..4aa4e72b0bb 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 6af03136760..a3fd02686ac 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/201] 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 4aa4e72b0bb..a5e68cb877d 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/201] 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 8201cbc5b7a..2ec991750f0 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/201] 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 2edf3309e32..8efc93ad4e7 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 cf5f3da2b3a..c31a7e88808 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 4a5a33e94e5..623c46375f0 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/201] 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 5b8e9f43b21..39f957ef77c 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 8068b7e55b7..1303d1cd7fe 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/201] 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 aa1907a8a23..336d06e182c 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 711f66ea033..6a083232079 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 3a85a24589a..173a7f8894f 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/201] 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 45b8ea21b0e..4885c9fa16d 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 e6ab39aff59..152439ec19a 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 b54f154d406..5421a652125 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/201] 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 a3c0f5e7dc0..eb959342122 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 173a7f8894f..3ece034b571 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 678a2eefcf7..c89d160b4ce 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 412f0df1337..76349d06481 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/201] 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 31f4dc14559..a47ef38264e 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 e99afb6330b..5d68cead747 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 3ece034b571..2b78d878adf 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 c89d160b4ce..ae7c3ea23cd 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/201] 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 229ee8458c6..3c773e39e33 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 2b78d878adf..a3d43bb2a9b 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 ae7c3ea23cd..9a27453bf95 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/201] 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 82c808a0f13..b24b49da965 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 7c6fb0bdb0d..770496b5612 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/201] 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 96fe1b157f8..19ffb1d6042 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/201] 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 51dbe9fcad3..61bf20dbbc4 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/201] 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 d51d180edb1..07f6bb9b55f 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/201] 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 a8f89b67ce9..b8103f7a4cb 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 00000000000..5e1025a8d31 --- /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 64e8b0c6dfd..9e9797439b3 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/201] 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 b5c98c7203a..a233ca38705 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 a3d43bb2a9b..b69dbf4c258 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 9a27453bf95..5ec473c9ef1 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/201] 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 33bba375fd3..d89e30311e9 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/201] 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 6098df40ea0..b8c79210dfb 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 68c2305d073..442114eb118 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 4175902da31..141f12269de 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/201] 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 a4520435161..07b5f931f79 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 34f87b5c261..0ec8765e604 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/201] 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 39f957ef77c..cbc21687110 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 1303d1cd7fe..92c11a19a12 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/201] 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 9366a18b6fe..d6ae50c33c1 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/201] 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 63d05135d5d..ab23c878c15 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 d5153a69f65..32846563c44 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 7bd76cb8522..53cb70475c9 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/201] 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 bc3c62cfb9f..ddff1954eb3 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 30096a9bf97..1953e413672 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 c3f295e1c4d..38fde322673 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 ef98ce99bfb..369e952c1be 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 d1ea0233145..caaefd7aa26 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 ab88c5fb750..c60f4392f1e 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 87c158ec0b9..79cea94d479 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 b7c60f333f4..c6d3fecab65 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 615a336ee5f..39d789f4cf9 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/201] 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 15c4192ccf5..96639e4a547 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 ffa33deae14..10b2fec7467 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 a069ae0807b..4882fd10e80 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 b9fefa805e6..caa0d887dec 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/201] 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 6e3f5eaee33..b82b2b34a4b 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 484dc8bac48..3f0bdae8e53 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/201] 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 88e03842504..a27d6f6f680 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 b69dbf4c258..c2c34d359e3 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 5ec473c9ef1..f6005b77f3c 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/201] 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 2aa0cd42fe1..4b79ef3df1b 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/201] 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 d89e30311e9..f12b2ad72bc 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/201] 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 07b5f931f79..23a8fc3bf64 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 0ec8765e604..cd96d0d7b81 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/201] 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 b8c020e6e1e..2d0b062c750 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 c2c34d359e3..b248c8f1dab 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 f6005b77f3c..cf6c995b317 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/201] 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 cd581d8c37f..794a65db03a 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 43d68d87362..2c64338c4f3 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/201] 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 0b9c68e9664..e7e110e7db6 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 6de6b0f4e45..7df88f7dca4 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 210339e22d7..6df5881107a 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/201] 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 28084eecfa6..d511a030fb1 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 534f2fd2457..5a424b38c5b 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 a5e86b9c013..00000000000 --- 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 cdc1e9959a7..e9040dfd397 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/201] 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 4885c9fa16d..d0d939ce67e 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 155e5499543..682fb0edd3b 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 152439ec19a..cf2d5d17f22 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/201] 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 9fae6ca9f73..0c4367c77c8 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 b248c8f1dab..74749f3eafe 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 cf6c995b317..1c2a04d2b51 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/201] 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 bf24fc4a4e9..89244642207 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 74749f3eafe..dc5ed080b1b 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 1c2a04d2b51..c0d89daaa42 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/201] 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 68fa39bfdc9..6197e580b1d 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/201] 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 c719afa968d..b8733dd2435 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 dc5ed080b1b..f9fa0cdc2b6 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 c0d89daaa42..2c944ecf02c 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/201] 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 911ccb0ff5b..1c76f480529 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/201] 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 5ce35e949af..239f23e7523 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/201] 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 418172975d8..e8bc4178073 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 f9fa0cdc2b6..dcbe0859038 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 2c944ecf02c..4715e175534 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/201] 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 3568692c6ca..f474f739904 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 dcbe0859038..34d110f4a3d 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 4715e175534..1328f70970e 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/201] 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 1bef72c48ec..707059a2930 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 0bf6b323146..5d42ace495b 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/201] 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 7ed9f6ce2c9..31c9ffef569 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 3b6a5390bd6..f9b4a1d9bba 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/201] 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 cbc21687110..4587f1d37a1 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 92c11a19a12..d6070c019ae 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/201] 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 4029023bb07..28079293821 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 34d110f4a3d..51697865b8a 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/201] 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 7d019935e6c..df78c5d96aa 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 d16981add87..a987233fef0 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 9df509b9783..91597d0e43d 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 81d94b9aac4..77ffd6a8b2a 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/201] 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 12eac858f75..968256ce3d9 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 c31a7e88808..4d6237760ca 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 623c46375f0..6ba8aaba1a5 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 f4d671ad352..3e84049efa8 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/201] 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 16bfc93f715..53764252043 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 def06634f42..8a5c405c1c1 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 d9085f8251f..652763947df 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/201] 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 a47ef38264e..b1eaaaf77e1 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 5d68cead747..cd1623c7d0d 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 51697865b8a..4adfc711309 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 1328f70970e..aa37475486d 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/201] 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 15141b6d428..f055f02ebda 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 f36b8180560..816bde430e7 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/201] 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 5dc7f10fea0..e19ac1272cd 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/201] 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 ab28c6cbe6d..681ccea60ac 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/201] 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 4d6237760ca..1bdb8aca779 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/201] 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 0700d5d6f0e..056a4df7920 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/201] 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 77570f0377f..b8e5e26f42c 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 e7dbbc2b625..f15c31f42d4 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 00000000000..75ecfddab20 --- /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 b05e1eb108d..e92da605697 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/201] 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 4587f1d37a1..a21d7a8f647 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 d6070c019ae..b479c69aa54 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/201] 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 8a7e38c9773..39e9264d45a 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/201] 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 bbfaf6c493b..a14f362ea5b 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/201] 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 b3e854de04b..f5ffdee9142 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/201] 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 a0feb8f38ea..5b2d3afa427 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 0774848ef11..7f70c13f0b0 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/201] 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 9d9d8d59e88..1c1d8dd96b1 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 4adfc711309..dff43a67450 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 aa37475486d..6799e4782c3 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/201] 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 c9b7cb10386..b756c2765e1 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/201] 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 1d0eb9cdd83..ef806cb52bc 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 ff918396640..91fd199e17c 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 dff43a67450..751b7af1598 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 6799e4782c3..065a0a44d73 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/201] 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 02a9dd9dade..2b8f1ec4065 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 0e88f3ed7c7..929ad687e11 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 d17c724cb2a..eb771b7e6a6 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/201] 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 a21d7a8f647..8022275b824 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 b479c69aa54..61c18f5d4ba 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/201] 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 d13adb8ec47..e0ac41bdec6 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 751b7af1598..f664cb7e7df 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 065a0a44d73..c8c884e92b2 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 4206c0de6ad..c40560d2a89 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/201] 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 e66b9ca9014..caf0384eca2 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 ee2340e7587..dd7e79e2c65 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 2d74408a73f..889b56f1bbd 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 cf2d5d17f22..d51379c9adc 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/201] 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 d50da96a39f..aec23331664 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 a29bee86116..2dfa48c28fe 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/201] 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 ac229cb677f..be283271dee 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 b071252ea64..2aa8de388b1 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/201] 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 b1eaaaf77e1..6fffc0e8acd 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 cd1623c7d0d..a70bcf4524a 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 f664cb7e7df..77633056811 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 c8c884e92b2..666c3ea4dc6 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/201] 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 8022275b824..1ec9532e11f 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 61c18f5d4ba..aa2a3c66b0e 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/201] 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 1ec9532e11f..28241ef15f4 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 aa2a3c66b0e..7efa6915a46 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/201] 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 698ef17902f..1c9a533d998 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 77633056811..17de99f535a 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 666c3ea4dc6..3c23ab27a0d 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/201] 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 1cac5be375b..f5731da2a7e 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 17de99f535a..7545800d5dc 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 3c23ab27a0d..6ed2583ec3c 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/201] 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 ab23c878c15..e9af4b2fd95 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 53cb70475c9..45ce862dba8 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/201] 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 3b2b34af67b..7d103111301 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/201] 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 ffa4e2d8292..13e78ced379 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/201] 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 7ab2e9ebf90..a89fb8a22fc 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 dae768a1359..358fa6eb675 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 02192676784..0e9e7d708e9 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 77a74b15197..75a74a0dcaa 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 908e3c768b8..91a5511001b 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 93151c51542..e1c7ba64aba 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 f6aeac3be7c..da93a6b619e 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 0d5c9ee2e8d..40049431edb 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 89eaf87fb3a..6d6c7475366 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 6642d778f53..21d3bcce3a9 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 2aa8de388b1..93d73094885 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 80f38dffcf9..a602f1e3065 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/201] 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 793f381d52f..c05f18107a0 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 dfc76ae1415..f11855ce7e2 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/201] 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 0700bd4e71a..4cffcce2d5c 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 7545800d5dc..328d0817705 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 6ed2583ec3c..3e5689ea266 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/201] 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 396d8469ffc..495a1fcb1eb 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/201] 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 6646cce3369..138e5fe3b59 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 7ec5559a608..9df20a5ffc8 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/201] 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 50df1f4bd1d..aacebb4bb5c 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 8d15fbb9a9f..be388ec563c 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 328d0817705..354f72252a3 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 3e5689ea266..00b5b61e9d1 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/201] 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 518f4d97a04..99b63fade5f 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/201] 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 a5f625f9e73..8491f58e387 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 6a14148585a..03443e4c4b9 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 00000000000..6cbd75ab09c --- /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 221ae891d67..a9753824edc 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/201] 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 6efae29e46e..e49eb8a2017 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/201] 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 a27d6f6f680..1022ab07e2c 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 354f72252a3..374f5ee062e 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 00b5b61e9d1..b19fe7fffcf 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/201] 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 28241ef15f4..99c1312dc98 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 7efa6915a46..616dd219baf 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/201] 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 fd62f8451fb..8425f29fbe8 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 49e964e2b3f..7f2ff3e0053 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 d470711267d..0075bbac4e6 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 4ac8166410f..938093df4a3 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 1c1d8dd96b1..9189de89efa 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 24e5d1bd72b..6be0cef1670 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 6282f29e442..b9e8ddb8e73 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 57efe1d9e92..1c10671550d 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 374f5ee062e..14b75e6cd6d 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 b19fe7fffcf..bf601b3156b 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/201] 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 9d1490a4ae6..d8f4018ada2 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 14b75e6cd6d..b5386244ee0 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 bf601b3156b..d0dc4588022 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/201] 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 f82f421f752..da49e67c60a 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 00000000000..fda5d0f5c39 --- /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 00000000000..1e8d295227f --- /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 00000000000..a206cb8353c --- /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 00000000000..a2fb5421fb7 --- /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 00000000000..96d81d993e9 --- /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 00000000000..d1e12e7abb4 --- /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/201] 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 442114eb118..0f81702a4d0 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/201] 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 c428ce7a5b1..5dffd629e80 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 ccae59932de..1ac7adfb549 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/201] 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 8a5c405c1c1..97b99e826cd 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/201] 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 f1fc4777467..ed8649896dd 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 b5386244ee0..c005a86cac3 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 d0dc4588022..b41909be078 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/201] 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 ee3c5d9071d..45270863f01 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/201] 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 9a8cb203c05..364eafd8caf 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 1e8d295227f..826f3eebe0c 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 f0da41e4b6f..cf04e885d79 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 a2fb5421fb7..982708aaa11 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 d1e12e7abb4..ee03d57f4c7 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 fc593de883b..54be2b43765 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/201] 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 f2853e0032c..300d7ebafc7 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/201] 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 eb959342122..bbc8a84a3dc 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 c005a86cac3..0e9c7378b8d 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 b41909be078..ce384b2e248 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/201] 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 bbc8a84a3dc..ea22487f4a2 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 0e9c7378b8d..1e388942667 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 ce384b2e248..62ac01261a0 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/201] 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 bc5d38e99e5..f3e5b8744e6 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/201] 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 3b8b727015b..e8db096f31d 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 a72555962eb..c362e1ea8f0 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 1e388942667..7baa4592aec 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 62ac01261a0..bfcae4f5396 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 5c5359df381..5e476dcaaa5 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/201] 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 d27def55251..c7d51c7ea1f 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 7baa4592aec..7568d76b55e 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 bfcae4f5396..06fd96dfe82 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/201] 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 0fd69a12825..687cee7741f 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 088389060f5..14f4206f44f 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 7568d76b55e..b1927de3b45 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 06fd96dfe82..a21b1a9cd70 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 12f11a5656d..ec3b6afa139 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 00000000000..eba8d9bd145 --- /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 a6b892dbc86..4f900db7401 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/201] 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 7fe1f9b8c04..d031632ed75 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 b1927de3b45..0e7821f82d9 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 a21b1a9cd70..d0bc7b85dcd 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 228fc7563e0..bd9005bd389 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/201] 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 99e86d4b6b5..bcc52f512a1 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/201] 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 bef58b3fa56..5fa314d589c 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 0f79a125c86..72aebbd396f 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/201] 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 d7a6b4cca0f..52aa37535d6 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/201] 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 e14ee501d7b..d5c871d59ba 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 ca3fc5c7f58..4359c54164a 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/201] 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 35eeb6e62b6..7dec48a3489 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 1bcfcfcc1d2..4e7f05142d3 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/201] 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 c7b9ab4e380..3b32d354766 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 67e8b724a97..01aff5ae48e 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 8ad7c7214a3..c4bbe9a7086 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/201] 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 6532f2a3bc7..53f254008e7 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/201] 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 a89fb8a22fc..7ab2e9ebf90 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 93d73094885..8112a289e62 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/201] 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 812cf430d09..d2eca7d39f9 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 702f75b166e..8968fa7da4f 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 0e7821f82d9..74339e7a29b 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 d0bc7b85dcd..f92b787a6ef 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/201] 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 8968fa7da4f..4619ffef4c5 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 74339e7a29b..930ade42cc4 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 f92b787a6ef..0b45c9579b8 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/201] 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 06e7d34e68d..813bc900900 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 4619ffef4c5..4e8d3c8dde4 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 f82b2fff62b..7996376b6ac 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 a0f7685a2ec..21543ad6788 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 91ef843a864..663461ceaa1 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 a70bcf4524a..12b1c6a4d0a 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 930ade42cc4..e02ddb809e2 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 0b45c9579b8..28f515c826d 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/201] 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 54f36ec6e2e..bb8fd4a5a51 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 e02ddb809e2..c6c465e7f9d 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 28f515c826d..3c22bbb9a05 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/201] 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 7bc0247aea6..5e1a54aedc4 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 99af30b8111..fa58bd8c5a6 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 9c8c4e6fc70..e7f0e715f59 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/201] 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 b7d400ce831..2ced868ada7 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 124dab73004..f20a79cc9e6 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 a70a0042fcd..0eacd63b42d 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/201] 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 4e8d3c8dde4..cb1a741c447 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 c6c465e7f9d..df1e8835288 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 3c22bbb9a05..7d343781c60 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 4ff6b503b3c..48b0868e406 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/201] 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 4cffcce2d5c..718c33d2811 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 df1e8835288..74d9af9953a 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 7d343781c60..4c356309f24 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/201] 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 b3f59a15b80..90fd2d6cdce 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 74d9af9953a..8a6230bb9d7 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 4c356309f24..77ab2e7e38f 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/201] 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 b4294216003..981a2a8633a 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/201] 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 cb9c6173694..6e60c2ec4f1 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/201] 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 37a8a0d6773..d16439800a9 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 8a6230bb9d7..f48915d5629 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 77ab2e7e38f..ad95793bfee 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/201] 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 59fbdc22747..a27c84b9593 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 25719c4cff7..3efc1e481df 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 1a4bf999cce..9b449d4b851 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/201] 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 d8285cbed70..f2a1e682304 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 816251ae3bb..9006a058c57 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/201] 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 7dec48a3489..2639ccccf7d 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/201] 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 6fffc0e8acd..469deab23e1 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 12b1c6a4d0a..67050e43eeb 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 f48915d5629..ab4afc4dfa9 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 ad95793bfee..798a6851b62 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/201] 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 99c1312dc98..479bfcbac6e 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 616dd219baf..55dd7a81a37 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/201] 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 5a5a1de2a01..307d68c4b7b 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 ab4afc4dfa9..870fcc94897 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 798a6851b62..96a13584578 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/201] 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 f25bdfb1d86..6e8d58219c1 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 24cd1750ed4..7b9fb41ed1f 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 2e486226a7b..d6c14834342 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 cfc4ff3b5cb..f93da8d9839 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 f548d3aacda..7ec22b88867 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 b3cfd56832d..0beae1f5a95 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 d8fb053c0ff..00d778335c5 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 31b64dded99..e2b6a99d01a 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/201] 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 496aba290ba..701f7f444cf 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/201] 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 307d68c4b7b..b74711ccbe6 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 870fcc94897..48df0873e15 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 96a13584578..4f2ca912a17 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/201] 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 6f494153a97..174ebb1cab9 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 48df0873e15..0be089e8784 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 4f2ca912a17..994d04dee98 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 3c5b82ad5b8..8a67ef0fb63 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 4b834cd9090..3abecbdf66f 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/201] 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 0c4367c77c8..0cffe2576d1 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 0be089e8784..85be51deb9b 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 994d04dee98..1c4a21c0a2a 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/201] 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 52b8e905b4b..2831ebe9a38 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 e1fb4464b83..61922e8e8c9 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 00000000000..ceed22d2524 --- /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 6fbe5d58f28..ac03d731ee1 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/201] 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 358fa6eb675..3fa3ebfd30c 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/201] 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 102e0c83b7b..7e7b7a688df 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/201] 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 81334c12379..20679ed09b2 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/201] 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 a97af14f449..e7a7c1200b9 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 bf6682e7a6f..f90e13d37f3 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 6e8d58219c1..52b98e5c6b6 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 7b9fb41ed1f..afe18337377 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 d6c14834342..04e423a399c 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 f93da8d9839..6c0e13fef39 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 7ec22b88867..db9c4c8739e 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 0beae1f5a95..79b8fd5ec37 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 00d778335c5..2a0195f8d97 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 e2b6a99d01a..f9c03f63dd3 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/201] 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 3b3afddc489..f57e351a282 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 85be51deb9b..64ad1b8cc63 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 1c4a21c0a2a..d819b2005eb 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/201] 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 f57e351a282..cc9a410392a 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 64ad1b8cc63..b1eca178fc4 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 d819b2005eb..7b054c4eda8 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/201] 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 5978f17cfc4..77796ac7e7f 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 b1eca178fc4..d5fa9a91e5c 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 7b054c4eda8..de51a342b44 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/201] 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 3c773e39e33..1f0dd001853 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 d5fa9a91e5c..799a7e88059 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 de51a342b44..63d7557c6ed 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/201] 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 b63700720e6..ee6f6d05548 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 799a7e88059..4332a347cb7 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 63d7557c6ed..f7f24dc8ccf 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/201] 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 2c6e7b2fff8..c98e634dcb3 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 6fa6c55de2e..e73feef55a1 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 981a2a8633a..37a3b2ec969 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 4332a347cb7..b1f7d464621 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/201] 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 509100a5174..fc9107bb8d2 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 b1f7d464621..728d79fe1e0 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 f7f24dc8ccf..06286df206c 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/201] 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 ceb619c4385..49e51a0fd98 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 dd7e79e2c65..7ef6ecbfa28 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 38b1d0bb786..cd8ece163a4 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 682fb0edd3b..a076be7f63d 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 d51379c9adc..78d20b0fb58 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/201] 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 bd0fe18912e..3e34a7234a4 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 3a9673441c0..83544087104 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/201] 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 d0d939ce67e..1bac147306a 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 5421a652125..d14c7ae78da 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/201] 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 4a3c8eed63c..72bbf56e08a 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/201] 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 5dc60f535d7..38053295411 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 c5fdec4d511..3fc8158f970 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 47fd3b91129..1442a626f74 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 6ab33f3bc7c..0f8930dbeff 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 2b437e15478..138b941c62c 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 c214330d5f9..38224a9992f 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/201] 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 364eafd8caf..27919a17614 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 f0c1dcec6b5..f2d32d499c9 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 728d79fe1e0..1f95654e7cd 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 06286df206c..10e69388532 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/201] 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 e8db096f31d..f2514063a40 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 1f95654e7cd..da17826e522 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 10e69388532..2ec1f05ad26 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/201] 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 85feb19a24b..0fa3e95eaf2 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 e6e237a7b67..c5fb56a28fc 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/201] 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 a61264dbf41..b78227ed1e5 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/201] 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 6dfd49c371c..4dbe5aa315e 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 fb5ff265497..98f99349cac 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/201] 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 ddd2fc61ed7..4535805915b 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 0b36745338e..7605689b3f5 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 b4e6d838537..bae81a7762f 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 9e7ac33c8b6..8a281d4d4a2 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 00000000000..e2d495555c6 --- /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/201] 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 08ccbcf0b5a..3199dfd8af1 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/201] 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 718c33d2811..c8da6f74a40 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 da17826e522..f3758330e88 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 2ec1f05ad26..f8ec9882920 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/201] 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 970e866b27b..6cf1d6b5381 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 00000000000..7ed1403041d --- /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/201] 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 6cf1d6b5381..22d4392ce31 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 e53200c2e90..89629a65ea5 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 d2eca7d39f9..ad226e04061 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/201] 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 97f253f8825..bfcee9d25df 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/201] 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 34edccaf4ab..ce047747a0c 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 bfcee9d25df..9de020f163e 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 de646f8ef9c..f69f8971479 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/201] 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 9fd1055dd60..e29865153b3 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 f3758330e88..fa65cf57062 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 f8ec9882920..e3693c5eb64 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/201] 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 7e23d01ee46..b688d03253e 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 39e9264d45a..5747e05ad05 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/201] 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 a53eaaab8ce..47213476ad9 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 fa65cf57062..dcef98e3fda 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 e3693c5eb64..2c2ead33d08 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/201] 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 da64157dad8..d71dd45bcfe 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 7c187c7b4bb..11e73b5695c 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/201] 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 59c24b11e51..d77f7fb05bb 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 ec864fd4665..713261936c7 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 ceda841202c..a00f9d9c25d 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/201] 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 6f70ff34051..2a2a262be36 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 e7471829856..b3dd3a9a4e3 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/201] 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 c96ed2e4ed3..561657dcffa 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 f24782b98d4..f28c7b5081b 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/201] 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 f69451290bc..6b5679088a0 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/201] 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 cde90e127f3..0856c39946b 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 a6be57e9ed5..11e39c40cb1 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 e24ec4c950b..7a1abe96110 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/201] 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 b5652694120..911a998371e 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 dcef98e3fda..9e8ff2d1b76 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 2c2ead33d08..0894d396b38 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/201] 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 07a0bc0a5f6..b8cb86264f2 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 9e8ff2d1b76..e48f37c55ed 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 0894d396b38..ce32f0665a0 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/201] 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 7cf318f12a6..5035b3c6620 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 e48f37c55ed..ff8328e8e08 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 ce32f0665a0..c6bd7472213 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/201] 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 479bfcbac6e..c03087dc10f 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 55dd7a81a37..eb2ca031685 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/201] 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 c8807d40cc1..2a90e3e9e19 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 ac95c6b0f0e..d21048c191c 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 049a1824495..cf2a49f6510 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 b688d03253e..cd30c2eeebe 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 ff8328e8e08..1dce7aa8bbc 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 c6bd7472213..4ac8f14118b 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/201] 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 d21048c191c..d7b16ee3bef 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 cf2a49f6510..4c3a8e10a62 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 99b3ff126d3..30dee6c842b 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 1dce7aa8bbc..cae2ddb153b 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 4ac8f14118b..c910fa8e4bb 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