From 95528f875e41ff9d5ee16bfefd446b76639f1bbb Mon Sep 17 00:00:00 2001 From: FFT Date: Tue, 13 Jun 2023 14:46:58 +0800 Subject: [PATCH 01/31] Change pyoppleio to pyoppleio-legacy (#88050) * Change pyoppleio to pyoppleio-310 (#75268) * [m] change opple component's dependency to a new working one --- homeassistant/components/opple/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opple/manifest.json b/homeassistant/components/opple/manifest.json index 9d87114c2d0..174907dfd0f 100644 --- a/homeassistant/components/opple/manifest.json +++ b/homeassistant/components/opple/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/opple", "iot_class": "local_polling", "loggers": ["pyoppleio"], - "requirements": ["pyoppleio==1.0.5"] + "requirements": ["pyoppleio-legacy==1.0.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index eca5516f7ec..2afad933fcd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1861,7 +1861,7 @@ pyopenuv==2023.02.0 pyopnsense==0.2.0 # homeassistant.components.opple -pyoppleio==1.0.5 +pyoppleio-legacy==1.0.8 # homeassistant.components.opentherm_gw pyotgw==2.1.3 From 427f0f4beed21c3995fd560c97355c6a18476f6d Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sun, 11 Jun 2023 10:21:33 -0400 Subject: [PATCH 02/31] Fix issue with Insteon linked devices maintaining current state (#94286) * Bump pyinsteon * Update tests --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/insteon/mock_devices.py | 4 ++-- tests/components/insteon/test_api_properties.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index cc8495384b1..ad3fb7bfbe8 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["pyinsteon", "pypubsub"], "requirements": [ - "pyinsteon==1.4.2", + "pyinsteon==1.4.3", "insteon-frontend-home-assistant==0.3.5" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2afad933fcd..b8348d1c005 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1699,7 +1699,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.4.2 +pyinsteon==1.4.3 # homeassistant.components.intesishome pyintesishome==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f48debc62b..dcd24f6f751 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1248,7 +1248,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.4.2 +pyinsteon==1.4.3 # homeassistant.components.ipma pyipma==3.0.6 diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index dd0ab0b56a0..dea9fb4e34f 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -151,11 +151,11 @@ class MockDevices: for flag in operating_flags: value = operating_flags[flag] if device.operating_flags.get(flag): - device.operating_flags[flag].load(value) + device.operating_flags[flag].set_value(value) for flag in properties: value = properties[flag] if device.properties.get(flag): - device.properties[flag].load(value) + device.properties[flag].set_value(value) async def async_add_device(self, address=None, multiple=False): """Mock the async_add_device method.""" diff --git a/tests/components/insteon/test_api_properties.py b/tests/components/insteon/test_api_properties.py index a667e2144d0..850ccc85411 100644 --- a/tests/components/insteon/test_api_properties.py +++ b/tests/components/insteon/test_api_properties.py @@ -119,7 +119,7 @@ async def test_get_read_only_properties( mock_read_only = ExtendedProperty( "44.44.44", "mock_read_only", bool, is_read_only=True ) - mock_read_only.load(False) + mock_read_only.set_value(False) ws_client, devices = await _setup( hass, hass_ws_client, "44.44.44", iolinc_properties_data @@ -368,7 +368,7 @@ async def test_change_float_property( ) device = devices["44.44.44"] delay_prop = device.configuration[MOMENTARY_DELAY] - delay_prop.load(0) + delay_prop.set_value(0) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( { From 9c8444da0e77d551002f39c77e58a4859b1dbe8b Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Sun, 11 Jun 2023 21:25:01 -0400 Subject: [PATCH 03/31] Bump elkm1-lib to 2.2.5 (#94296) Co-authored-by: J. Nick Koston --- homeassistant/components/elkm1/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index d7094a2e60b..ccac1593fa0 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -15,5 +15,5 @@ "documentation": "https://www.home-assistant.io/integrations/elkm1", "iot_class": "local_push", "loggers": ["elkm1_lib"], - "requirements": ["elkm1-lib==2.2.2"] + "requirements": ["elkm1-lib==2.2.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index b8348d1c005..0d88b5127ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -650,7 +650,7 @@ elgato==4.0.1 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==2.2.2 +elkm1-lib==2.2.5 # homeassistant.components.elmax elmax_api==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dcd24f6f751..7e44c97b171 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -515,7 +515,7 @@ easyenergy==0.3.0 elgato==4.0.1 # homeassistant.components.elkm1 -elkm1-lib==2.2.2 +elkm1-lib==2.2.5 # homeassistant.components.elmax elmax_api==0.0.4 From e091793b6cefd4bd58840d4f374ca1ae28a9a649 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Tue, 13 Jun 2023 03:51:46 -0600 Subject: [PATCH 04/31] Bump pylitterbot to 2023.4.2 (#94301) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index d3dcf77f324..2a4a3447eb6 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["pylitterbot"], - "requirements": ["pylitterbot==2023.4.0"] + "requirements": ["pylitterbot==2023.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0d88b5127ac..ede0ea411bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1771,7 +1771,7 @@ pylibrespot-java==0.1.1 pylitejet==0.5.0 # homeassistant.components.litterrobot -pylitterbot==2023.4.0 +pylitterbot==2023.4.2 # homeassistant.components.lutron_caseta pylutron-caseta==0.18.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e44c97b171..741d5897a14 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1302,7 +1302,7 @@ pylibrespot-java==0.1.1 pylitejet==0.5.0 # homeassistant.components.litterrobot -pylitterbot==2023.4.0 +pylitterbot==2023.4.2 # homeassistant.components.lutron_caseta pylutron-caseta==0.18.1 From 3434d749937d14f748bcaab95cb4e4a8ec7798ce Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Fri, 9 Jun 2023 00:39:14 -0700 Subject: [PATCH 05/31] Upgrade sisyphus-control to 3.1.3 (#94310) --- homeassistant/components/sisyphus/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index 1a8d9e2e16b..dbb40344d66 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/sisyphus", "iot_class": "local_push", "loggers": ["sisyphus_control"], - "requirements": ["sisyphus-control==3.1.2"] + "requirements": ["sisyphus-control==3.1.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index ede0ea411bc..f6ec7496d6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2367,7 +2367,7 @@ simplepush==2.1.1 simplisafe-python==2023.05.0 # homeassistant.components.sisyphus -sisyphus-control==3.1.2 +sisyphus-control==3.1.3 # homeassistant.components.slack slackclient==2.5.0 From 6029e23ab770319cf61037ad58839a437bed7b54 Mon Sep 17 00:00:00 2001 From: Jafar Atili Date: Fri, 9 Jun 2023 21:54:11 +0300 Subject: [PATCH 06/31] fix: electrasmart - cast temperature to int in set_temperature (#94368) fix: cast temperature to int --- homeassistant/components/electrasmart/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/electrasmart/climate.py b/homeassistant/components/electrasmart/climate.py index 361f906133d..a9688939048 100644 --- a/homeassistant/components/electrasmart/climate.py +++ b/homeassistant/components/electrasmart/climate.py @@ -250,7 +250,7 @@ class ElectraClimateEntity(ClimateEntity): if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: raise ValueError("No target temperature provided") - self._electra_ac_device.set_temperature(temperature) + self._electra_ac_device.set_temperature(int(temperature)) await self._async_operate_electra_ac() def _update_device_attrs(self) -> None: From 96cb5ff8b0d29d98ad2e386026d3baa22146b831 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 11 Jun 2023 16:21:06 +0200 Subject: [PATCH 07/31] Fix dep noaa-coops for noaa_tides (#94370) Bump noaa-coops to 0.1.9 --- homeassistant/components/noaa_tides/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/noaa_tides/manifest.json b/homeassistant/components/noaa_tides/manifest.json index 7b954153cf1..85c6fbcb788 100644 --- a/homeassistant/components/noaa_tides/manifest.json +++ b/homeassistant/components/noaa_tides/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/noaa_tides", "iot_class": "cloud_polling", "loggers": ["noaa_coops"], - "requirements": ["noaa-coops==0.1.8"] + "requirements": ["noaa-coops==0.1.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index f6ec7496d6a..74a8df65c03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1218,7 +1218,7 @@ niko-home-control==0.2.1 niluclient==0.1.2 # homeassistant.components.noaa_tides -noaa-coops==0.1.8 +noaa-coops==0.1.9 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 From 582fd11a7037b09fb3fb30ab49d839a964fae3bb Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 11 Jun 2023 20:01:41 +0200 Subject: [PATCH 08/31] Fix deprecated asyncio.wait use with coroutines (#94371) --- homeassistant/components/xiaomi_miio/fan.py | 4 +--- homeassistant/components/xiaomi_miio/light.py | 4 +++- homeassistant/components/xiaomi_miio/switch.py | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index aaf471518d8..247b91d1b06 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -275,9 +275,7 @@ async def async_setup_entry( if not entity_method: continue await entity_method(**params) - update_tasks.append( - hass.async_create_task(entity.async_update_ha_state(True)) - ) + update_tasks.append(asyncio.create_task(entity.async_update_ha_state(True))) if update_tasks: await asyncio.wait(update_tasks) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index ed1bdef9e33..9b8357a534f 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -229,7 +229,9 @@ async def async_setup_entry( if not hasattr(target_device, method["method"]): continue await getattr(target_device, method["method"])(**params) - update_tasks.append(target_device.async_update_ha_state(True)) + update_tasks.append( + asyncio.create_task(target_device.async_update_ha_state(True)) + ) if update_tasks: await asyncio.wait(update_tasks) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 08b15f58217..9bba9f61123 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -500,7 +500,9 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): if not hasattr(device, method["method"]): continue await getattr(device, method["method"])(**params) - update_tasks.append(device.async_update_ha_state(True)) + update_tasks.append( + asyncio.create_task(device.async_update_ha_state(True)) + ) if update_tasks: await asyncio.wait(update_tasks) From 4dbc40869639acb141f1397d1ac35faf194f80e1 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 10 Jun 2023 10:41:51 +0200 Subject: [PATCH 09/31] Update xknxproject to 3.1.1 (#94375) --- 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 ba706c756cb..61defa64e22 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -12,7 +12,7 @@ "quality_scale": "platinum", "requirements": [ "xknx==2.10.0", - "xknxproject==3.1.0", + "xknxproject==3.1.1", "knx_frontend==2023.5.31.141540" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 74a8df65c03..2798c50cc4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2680,7 +2680,7 @@ xiaomi-ble==0.17.2 xknx==2.10.0 # homeassistant.components.knx -xknxproject==3.1.0 +xknxproject==3.1.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 741d5897a14..7a3b9058373 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1953,7 +1953,7 @@ xiaomi-ble==0.17.2 xknx==2.10.0 # homeassistant.components.knx -xknxproject==3.1.0 +xknxproject==3.1.1 # homeassistant.components.bluesound # homeassistant.components.fritz From 2c7a1765806fd226d0276144f6f370761cbc6cf8 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 11 Jun 2023 02:35:52 -0400 Subject: [PATCH 10/31] Re-add event listeners after Z-Wave server disconnection (#94383) * Re-add event listeners after Z-Wave server disconnection * switch order * Add tests --- homeassistant/components/zwave_js/__init__.py | 3 + .../components/zwave_js/triggers/event.py | 67 ++++++++----- .../zwave_js/triggers/value_updated.py | 61 ++++++++---- tests/components/zwave_js/test_trigger.py | 98 +++++++++++++++++++ 4 files changed, 187 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index a89d20d8384..814e3e86a7a 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -209,6 +209,9 @@ async def start_client( LOGGER.info("Connection to Zwave JS Server initialized") assert client.driver + async_dispatcher_send( + hass, f"{DOMAIN}_{client.driver.controller.home_id}_connected_to_server" + ) await driver_events.setup(client.driver) diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 12c9d267ca6..32bd3130e03 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -1,18 +1,20 @@ """Offer Z-Wave JS event listening automation trigger.""" from __future__ import annotations +from collections.abc import Callable import functools from pydantic import ValidationError import voluptuous as vol from zwave_js_server.client import Client from zwave_js_server.model.controller import CONTROLLER_EVENT_MODEL_MAP -from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP +from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP, Driver from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType @@ -150,7 +152,7 @@ async def async_attach_trigger( event_name = config[ATTR_EVENT] event_data_filter = config.get(ATTR_EVENT_DATA, {}) - unsubs = [] + unsubs: list[Callable] = [] job = HassJob(action) trigger_data = trigger_info["trigger_data"] @@ -199,26 +201,6 @@ async def async_attach_trigger( hass.async_run_hass_job(job, {"trigger": payload}) - if not nodes: - entry_id = config[ATTR_CONFIG_ENTRY_ID] - client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT] - assert client.driver - if event_source == "controller": - unsubs.append(client.driver.controller.on(event_name, async_on_event)) - else: - unsubs.append(client.driver.on(event_name, async_on_event)) - - for node in nodes: - driver = node.client.driver - assert driver is not None # The node comes from the driver. - device_identifier = get_device_id(driver, node) - device = dev_reg.async_get_device({device_identifier}) - assert device - # We need to store the device for the callback - unsubs.append( - node.on(event_name, functools.partial(async_on_event, device=device)) - ) - @callback def async_remove() -> None: """Remove state listeners async.""" @@ -226,4 +208,45 @@ async def async_attach_trigger( unsub() unsubs.clear() + @callback + def _create_zwave_listeners() -> None: + """Create Z-Wave JS listeners.""" + async_remove() + # Nodes list can come from different drivers and we will need to listen to + # server connections for all of them. + drivers: set[Driver] = set() + if not nodes: + entry_id = config[ATTR_CONFIG_ENTRY_ID] + client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + driver = client.driver + assert driver + drivers.add(driver) + if event_source == "controller": + unsubs.append(driver.controller.on(event_name, async_on_event)) + else: + unsubs.append(driver.on(event_name, async_on_event)) + + for node in nodes: + driver = node.client.driver + assert driver is not None # The node comes from the driver. + drivers.add(driver) + device_identifier = get_device_id(driver, node) + device = dev_reg.async_get_device({device_identifier}) + assert device + # We need to store the device for the callback + unsubs.append( + node.on(event_name, functools.partial(async_on_event, device=device)) + ) + + for driver in drivers: + unsubs.append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{driver.controller.home_id}_connected_to_server", + _create_zwave_listeners, + ) + ) + + _create_zwave_listeners() + return async_remove diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 655d1f9070e..4e21774c98f 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -1,15 +1,18 @@ """Offer Z-Wave JS value updated listening automation trigger.""" from __future__ import annotations +from collections.abc import Callable import functools import voluptuous as vol from zwave_js_server.const import CommandClass +from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value, get_value_id_str from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType @@ -99,7 +102,7 @@ async def async_attach_trigger( property_ = config[ATTR_PROPERTY] endpoint = config.get(ATTR_ENDPOINT) property_key = config.get(ATTR_PROPERTY_KEY) - unsubs = [] + unsubs: list[Callable] = [] job = HassJob(action) trigger_data = trigger_info["trigger_data"] @@ -153,29 +156,11 @@ async def async_attach_trigger( ATTR_PREVIOUS_VALUE_RAW: prev_value_raw, ATTR_CURRENT_VALUE: curr_value, ATTR_CURRENT_VALUE_RAW: curr_value_raw, - "description": f"Z-Wave value {value_id} updated on {device_name}", + "description": f"Z-Wave value {value.value_id} updated on {device_name}", } hass.async_run_hass_job(job, {"trigger": payload}) - for node in nodes: - driver = node.client.driver - assert driver is not None # The node comes from the driver. - device_identifier = get_device_id(driver, node) - device = dev_reg.async_get_device({device_identifier}) - assert device - value_id = get_value_id_str( - node, command_class, property_, endpoint, property_key - ) - value = node.values[value_id] - # We need to store the current value and device for the callback - unsubs.append( - node.on( - "value updated", - functools.partial(async_on_value_updated, value, device), - ) - ) - @callback def async_remove() -> None: """Remove state listeners async.""" @@ -183,4 +168,40 @@ async def async_attach_trigger( unsub() unsubs.clear() + def _create_zwave_listeners() -> None: + """Create Z-Wave JS listeners.""" + async_remove() + # Nodes list can come from different drivers and we will need to listen to + # server connections for all of them. + drivers: set[Driver] = set() + for node in nodes: + driver = node.client.driver + assert driver is not None # The node comes from the driver. + drivers.add(driver) + device_identifier = get_device_id(driver, node) + device = dev_reg.async_get_device({device_identifier}) + assert device + value_id = get_value_id_str( + node, command_class, property_, endpoint, property_key + ) + value = node.values[value_id] + # We need to store the current value and device for the callback + unsubs.append( + node.on( + "value updated", + functools.partial(async_on_value_updated, value, device), + ) + ) + + for driver in drivers: + unsubs.append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{driver.controller.home_id}_connected_to_server", + _create_zwave_listeners, + ) + ) + + _create_zwave_listeners() + return async_remove diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index 9df8aa75f43..0fb3b829d9a 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -1109,3 +1109,101 @@ def test_get_trigger_platform_failure() -> None: """Test _get_trigger_platform.""" with pytest.raises(ValueError): _get_trigger_platform({CONF_PLATFORM: "zwave_js.invalid"}) + + +async def test_server_reconnect_event( + hass: HomeAssistant, client, lock_schlage_be469, integration +) -> None: + """Test that when we reconnect to server, event triggers reattach.""" + trigger_type = f"{DOMAIN}.event" + node: Node = lock_schlage_be469 + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device + + event_name = "interview stage completed" + + original_len = len(node._listeners.get(event_name, [])) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": trigger_type, + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "event_source": "node", + "event": event_name, + }, + "action": { + "event": "blah", + }, + }, + ] + }, + ) + + assert len(node._listeners.get(event_name, [])) == original_len + 1 + old_listener = node._listeners.get(event_name, [])[original_len] + + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + # Make sure there is still a listener added for the trigger + assert len(node._listeners.get(event_name, [])) == original_len + 1 + + # Make sure the old listener was removed + assert old_listener not in node._listeners.get(event_name, []) + + +async def test_server_reconnect_value_updated( + hass: HomeAssistant, client, lock_schlage_be469, integration +) -> None: + """Test that when we reconnect to server, value_updated triggers reattach.""" + trigger_type = f"{DOMAIN}.value_updated" + node: Node = lock_schlage_be469 + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device( + {get_device_id(client.driver, lock_schlage_be469)} + ) + assert device + + event_name = "value updated" + + original_len = len(node._listeners.get(event_name, [])) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": trigger_type, + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + }, + "action": { + "event": "no_value_filter", + }, + }, + ] + }, + ) + + assert len(node._listeners.get(event_name, [])) == original_len + 1 + old_listener = node._listeners.get(event_name, [])[original_len] + + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + # Make sure there is still a listener added for the trigger + assert len(node._listeners.get(event_name, [])) == original_len + 1 + + # Make sure the old listener was removed + assert old_listener not in node._listeners.get(event_name, []) From b2db84979814fd32cf01bb710fcbb133413d636c Mon Sep 17 00:00:00 2001 From: jasonkuster Date: Mon, 12 Jun 2023 10:07:42 -0700 Subject: [PATCH 11/31] Fix ZHA binding api to actually return responses (#94388) --- homeassistant/components/zha/websocket_api.py | 13 ++- tests/components/zha/test_websocket_api.py | 94 ++++++++++++++++++- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index 28e115c0ec4..97862bd36f0 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -907,6 +907,7 @@ async def websocket_bind_devices( ATTR_TARGET_IEEE, target_ieee, ) + connection.send_result(msg[ID]) @websocket_api.require_admin @@ -935,6 +936,7 @@ async def websocket_unbind_devices( ATTR_TARGET_IEEE, target_ieee, ) + connection.send_result(msg[ID]) @websocket_api.require_admin @@ -951,13 +953,14 @@ async def websocket_bind_group( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Directly bind a device to a group.""" - zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = get_gateway(hass) source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE] group_id: int = msg[GROUP_ID] bindings: list[ClusterBinding] = msg[BINDINGS] source_device = zha_gateway.get_device(source_ieee) assert source_device await source_device.async_bind_to_group(group_id, bindings) + connection.send_result(msg[ID]) @websocket_api.require_admin @@ -974,13 +977,19 @@ async def websocket_unbind_group( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Unbind a device from a group.""" - zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = get_gateway(hass) source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE] group_id: int = msg[GROUP_ID] bindings: list[ClusterBinding] = msg[BINDINGS] source_device = zha_gateway.get_device(source_ieee) assert source_device await source_device.async_unbind_from_group(group_id, bindings) + connection.send_result(msg[ID]) + + +def get_gateway(hass: HomeAssistant) -> ZHAGateway: + """Return Gateway, mainly as fixture for mocking during testing.""" + return hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] async def async_binding_operation( diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index 5250b62a9b0..0904fc1f685 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -4,15 +4,17 @@ from __future__ import annotations from binascii import unhexlify from copy import deepcopy from typing import TYPE_CHECKING -from unittest.mock import ANY, AsyncMock, call, patch +from unittest.mock import ANY, AsyncMock, MagicMock, call, patch import pytest import voluptuous as vol import zigpy.backups import zigpy.profiles.zha import zigpy.types +from zigpy.types.named import EUI64 import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.security as security +import zigpy.zdo.types as zdo_types from homeassistant.components.websocket_api import const from homeassistant.components.zha import DOMAIN @@ -26,6 +28,8 @@ from homeassistant.components.zha.core.const import ( ATTR_MODEL, ATTR_NEIGHBORS, ATTR_QUIRK_APPLIED, + ATTR_TYPE, + BINDINGS, CLUSTER_TYPE_IN, EZSP_OVERWRITE_EUI64, GROUP_ID, @@ -37,6 +41,7 @@ from homeassistant.components.zha.websocket_api import ( ATTR_INSTALL_CODE, ATTR_QR_CODE, ATTR_SOURCE_IEEE, + ATTR_TARGET_IEEE, ID, SERVICE_PERMIT, TYPE, @@ -884,3 +889,90 @@ async def test_websocket_change_channel( assert msg["success"] change_channel_mock.mock_calls == [call(ANY, new_channel)] + + +@pytest.mark.parametrize( + "operation", + [("bind", zdo_types.ZDOCmd.Bind_req), ("unbind", zdo_types.ZDOCmd.Unbind_req)], +) +async def test_websocket_bind_unbind_devices( + operation: tuple[str, zdo_types.ZDOCmd], + app_controller: ControllerApplication, + zha_client, +) -> None: + """Test websocket API for binding and unbinding devices to devices.""" + + command_type, req = operation + with patch( + "homeassistant.components.zha.websocket_api.async_binding_operation", + autospec=True, + ) as binding_operation_mock: + await zha_client.send_json( + { + ID: 27, + TYPE: f"zha/devices/{command_type}", + ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE, + ATTR_TARGET_IEEE: IEEE_GROUPABLE_DEVICE, + } + ) + msg = await zha_client.receive_json() + + assert msg["id"] == 27 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert binding_operation_mock.mock_calls == [ + call( + ANY, + EUI64.convert(IEEE_SWITCH_DEVICE), + EUI64.convert(IEEE_GROUPABLE_DEVICE), + req, + ) + ] + + +@pytest.mark.parametrize("command_type", ["bind", "unbind"]) +async def test_websocket_bind_unbind_group( + command_type: str, + app_controller: ControllerApplication, + zha_client, +) -> None: + """Test websocket API for binding and unbinding devices to groups.""" + + test_group_id = 0x0001 + gateway_mock = MagicMock() + with patch( + "homeassistant.components.zha.websocket_api.get_gateway", + return_value=gateway_mock, + ): + device_mock = MagicMock() + bind_mock = AsyncMock() + unbind_mock = AsyncMock() + device_mock.async_bind_to_group = bind_mock + device_mock.async_unbind_from_group = unbind_mock + gateway_mock.get_device = MagicMock() + gateway_mock.get_device.return_value = device_mock + await zha_client.send_json( + { + ID: 27, + TYPE: f"zha/groups/{command_type}", + ATTR_SOURCE_IEEE: IEEE_SWITCH_DEVICE, + GROUP_ID: test_group_id, + BINDINGS: [ + { + ATTR_ENDPOINT_ID: 1, + ID: 6, + ATTR_NAME: "OnOff", + ATTR_TYPE: "out", + }, + ], + } + ) + msg = await zha_client.receive_json() + + assert msg["id"] == 27 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + if command_type == "bind": + assert bind_mock.mock_calls == [call(test_group_id, ANY)] + elif command_type == "unbind": + assert unbind_mock.mock_calls == [call(test_group_id, ANY)] From ac00977e57665b056c2ac6a2465a212d11e96dd4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 11 Jun 2023 12:30:38 +0200 Subject: [PATCH 12/31] Abort youtube configuration if user has no channel (#94402) * Abort configuration if user has no channel * Clean up --------- Co-authored-by: Martin Hjelmare --- .../components/youtube/config_flow.py | 13 +++++- homeassistant/components/youtube/const.py | 1 + homeassistant/components/youtube/strings.json | 1 + .../youtube/fixtures/get_no_channel.json | 9 +++++ tests/components/youtube/test_config_flow.py | 40 +++++++++++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/components/youtube/fixtures/get_no_channel.json diff --git a/homeassistant/components/youtube/config_flow.py b/homeassistant/components/youtube/config_flow.py index a2adebc84af..92695f80a2e 100644 --- a/homeassistant/components/youtube/config_flow.py +++ b/homeassistant/components/youtube/config_flow.py @@ -22,7 +22,13 @@ from homeassistant.helpers.selector import ( SelectSelectorConfig, ) -from .const import CONF_CHANNELS, DEFAULT_ACCESS, DOMAIN, LOGGER +from .const import ( + CHANNEL_CREATION_HELP_URL, + CONF_CHANNELS, + DEFAULT_ACCESS, + DOMAIN, + LOGGER, +) async def get_resource(hass: HomeAssistant, token: str) -> Resource: @@ -99,6 +105,11 @@ class OAuth2FlowHandler( response = await self.hass.async_add_executor_job( own_channel_request.execute ) + if not response["items"]: + return self.async_abort( + reason="no_channel", + description_placeholders={"support_url": CHANNEL_CREATION_HELP_URL}, + ) own_channel = response["items"][0] except HttpError as ex: error = ex.reason diff --git a/homeassistant/components/youtube/const.py b/homeassistant/components/youtube/const.py index e2757e3856d..7404cd04665 100644 --- a/homeassistant/components/youtube/const.py +++ b/homeassistant/components/youtube/const.py @@ -4,6 +4,7 @@ import logging DEFAULT_ACCESS = ["https://www.googleapis.com/auth/youtube.readonly"] DOMAIN = "youtube" MANUFACTURER = "Google, Inc." +CHANNEL_CREATION_HELP_URL = "https://support.google.com/youtube/answer/1646861" CONF_CHANNELS = "channels" CONF_ID = "id" diff --git a/homeassistant/components/youtube/strings.json b/homeassistant/components/youtube/strings.json index 24369ab26f9..eb89738708e 100644 --- a/homeassistant/components/youtube/strings.json +++ b/homeassistant/components/youtube/strings.json @@ -2,6 +2,7 @@ "config": { "abort": { "access_not_configured": "Please read the below message we got from Google:\n\n{message}", + "no_channel": "Please create a YouTube channel to be able to use the integration. Instructions can be found at {support_url}.", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/tests/components/youtube/fixtures/get_no_channel.json b/tests/components/youtube/fixtures/get_no_channel.json new file mode 100644 index 00000000000..7ec03c0461a --- /dev/null +++ b/tests/components/youtube/fixtures/get_no_channel.json @@ -0,0 +1,9 @@ +{ + "kind": "youtube#channelListResponse", + "etag": "8HTiiXpKCq-GJvDVOd88e5o_KGc", + "pageInfo": { + "totalResults": 0, + "resultsPerPage": 5 + }, + "items": [] +} diff --git a/tests/components/youtube/test_config_flow.py b/tests/components/youtube/test_config_flow.py index ed33947b593..5b91ff958f8 100644 --- a/tests/components/youtube/test_config_flow.py +++ b/tests/components/youtube/test_config_flow.py @@ -83,6 +83,46 @@ async def test_full_flow( assert result["options"] == {CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]} +async def test_flow_abort_without_channel( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + current_request_with_host: None, +) -> None: + """Check abort flow if user has no channel.""" + result = await hass.config_entries.flow.async_init( + "youtube", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope={'+'.join(SCOPES)}" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + service = MockService(channel_fixture="youtube/get_no_channel.json") + with patch( + "homeassistant.components.youtube.async_setup_entry", return_value=True + ), patch("homeassistant.components.youtube.api.build", return_value=service), patch( + "homeassistant.components.youtube.config_flow.build", return_value=service + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_channel" + + async def test_flow_http_error( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, From 4cb30e69ace562063c0e598ed24683bfc69622a0 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 10 Jun 2023 20:48:14 +0200 Subject: [PATCH 13/31] Update knx-frontend to 2023.6.9.195839 (#94404) --- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/websocket.py | 12 ++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 61defa64e22..1f0a6d3cc5e 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -13,6 +13,6 @@ "requirements": [ "xknx==2.10.0", "xknxproject==3.1.1", - "knx_frontend==2023.5.31.141540" + "knx-frontend==2023.6.9.195839" ] } diff --git a/homeassistant/components/knx/websocket.py b/homeassistant/components/knx/websocket.py index d63ba89fbcc..a9da5036857 100644 --- a/homeassistant/components/knx/websocket.py +++ b/homeassistant/components/knx/websocket.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Final -from knx_frontend import get_build_id, locate_dir +from knx_frontend import entrypoint_js, is_dev_build, locate_dir import voluptuous as vol from xknx.telegram import TelegramDirection from xknxproject.exceptions import XknxProjectException @@ -31,9 +31,10 @@ async def register_panel(hass: HomeAssistant) -> None: if DOMAIN not in hass.data.get("frontend_panels", {}): path = locate_dir() - build_id = get_build_id() hass.http.register_static_path( - URL_BASE, path, cache_headers=(build_id != "dev") + URL_BASE, + path, + cache_headers=not is_dev_build, ) await panel_custom.async_register_panel( hass=hass, @@ -41,12 +42,13 @@ async def register_panel(hass: HomeAssistant) -> None: webcomponent_name="knx-frontend", sidebar_title=DOMAIN.upper(), sidebar_icon="mdi:bus-electric", - module_url=f"{URL_BASE}/entrypoint-{build_id}.js", + module_url=f"{URL_BASE}/{entrypoint_js()}", embed_iframe=True, require_admin=True, ) +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required("type"): "knx/info", @@ -129,6 +131,7 @@ async def ws_project_file_remove( connection.send_result(msg["id"]) +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required("type"): "knx/group_monitor_info", @@ -155,6 +158,7 @@ def ws_group_monitor_info( ) +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required("type"): "knx/subscribe_telegrams", diff --git a/requirements_all.txt b/requirements_all.txt index 2798c50cc4b..79fa1d989b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1032,7 +1032,7 @@ kegtron-ble==0.4.0 kiwiki-client==0.1.1 # homeassistant.components.knx -knx_frontend==2023.5.31.141540 +knx-frontend==2023.6.9.195839 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a3b9058373..05f638f3e32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -791,7 +791,7 @@ justnimbus==0.6.0 kegtron-ble==0.4.0 # homeassistant.components.knx -knx_frontend==2023.5.31.141540 +knx-frontend==2023.6.9.195839 # homeassistant.components.konnected konnected==1.2.0 From 238eebb0b6dc14b9cc3705604e1f811d45509403 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 11 Jun 2023 10:22:12 -0400 Subject: [PATCH 14/31] Bump unifiprotect to 4.10.3 (#94416) * Bump unifiprotect to 4.10.3 * Reqs --- 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 a414c03a0d4..cfa90664f36 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.10.2", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.10.3", "unifi-discovery==1.1.7"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 79fa1d989b7..22784bd8952 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2168,7 +2168,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.10.2 +pyunifiprotect==4.10.3 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05f638f3e32..33d1ed2ebf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1579,7 +1579,7 @@ pytrafikverket==0.3.3 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.10.2 +pyunifiprotect==4.10.3 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 2505de35c9bb52a899bef343c055906fb71e3fea Mon Sep 17 00:00:00 2001 From: Sander Date: Mon, 12 Jun 2023 08:25:09 +0200 Subject: [PATCH 15/31] Fix: Xiaomi Miio Fan, delay off countdown unit conversion (#94428) --- homeassistant/components/xiaomi_miio/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 732710f7129..a8346caa894 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -441,7 +441,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): return await self._try_command( "Setting delay off miio device failed.", self._device.delay_off, - delay_off_countdown * 60, + delay_off_countdown, ) async def async_set_led_brightness_level(self, level: int) -> bool: From 0083649e43fca29dc769af1d133a52c881398268 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Tue, 13 Jun 2023 10:38:56 -0400 Subject: [PATCH 16/31] Add unit inference for Amps and VA in APCUPSD integration (#94431) * Add unit inference for Amps and VA * Rename `init_integration` to `async_init_integration` for better consistency with HA naming style --- homeassistant/components/apcupsd/sensor.py | 2 ++ tests/components/apcupsd/__init__.py | 6 ++++-- tests/components/apcupsd/test_binary_sensor.py | 6 +++--- tests/components/apcupsd/test_init.py | 12 ++++++------ tests/components/apcupsd/test_sensor.py | 6 +++--- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 17168700f66..8b7034357df 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -430,7 +430,9 @@ INFERRED_UNITS = { " Percent": PERCENTAGE, " Volts": UnitOfElectricPotential.VOLT, " Ampere": UnitOfElectricCurrent.AMPERE, + " Amps": UnitOfElectricCurrent.AMPERE, " Volt-Ampere": UnitOfApparentPower.VOLT_AMPERE, + " VA": UnitOfApparentPower.VOLT_AMPERE, " Watts": UnitOfPower.WATT, " Hz": UnitOfFrequency.HERTZ, " C": UnitOfTemperature.CELSIUS, diff --git a/tests/components/apcupsd/__init__.py b/tests/components/apcupsd/__init__.py index f99b29c7bb7..f5c3f573030 100644 --- a/tests/components/apcupsd/__init__.py +++ b/tests/components/apcupsd/__init__.py @@ -26,6 +26,7 @@ MOCK_STATUS: Final = OrderedDict( ("LOADPCT", "14.0 Percent"), ("BCHARGE", "100.0 Percent"), ("TIMELEFT", "51.0 Minutes"), + ("NOMAPNT", "60.0 VA"), ("ITEMP", "34.6 C Internal"), ("MBATTCHG", "5 Percent"), ("MINTIMEL", "3 Minutes"), @@ -35,6 +36,7 @@ MOCK_STATUS: Final = OrderedDict( ("HITRANS", "139.0 Volts"), ("ALARMDEL", "30 Seconds"), ("BATTV", "13.7 Volts"), + ("OUTCURNT", "0.88 Amps"), ("LASTXFER", "Automatic or explicit self test"), ("NUMXFERS", "1"), ("XONBATT", "1970-01-01 00:00:00 0000"), @@ -74,7 +76,7 @@ MOCK_MINIMAL_STATUS: Final = OrderedDict( ) -async def init_integration( +async def async_init_integration( hass: HomeAssistant, host: str = "test", status=None ) -> MockConfigEntry: """Set up the APC UPS Daemon integration in HomeAssistant.""" @@ -95,7 +97,7 @@ async def init_integration( with patch("apcaccess.status.parse", return_value=status), patch( "apcaccess.status.get", return_value=b"" ): - await hass.config_entries.async_setup(entry.entry_id) + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() return entry diff --git a/tests/components/apcupsd/test_binary_sensor.py b/tests/components/apcupsd/test_binary_sensor.py index c00707b7ff1..6ba9a09f837 100644 --- a/tests/components/apcupsd/test_binary_sensor.py +++ b/tests/components/apcupsd/test_binary_sensor.py @@ -2,12 +2,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import MOCK_STATUS, init_integration +from . import MOCK_STATUS, async_init_integration async def test_binary_sensor(hass: HomeAssistant) -> None: """Test states of binary sensor.""" - await init_integration(hass, status=MOCK_STATUS) + await async_init_integration(hass, status=MOCK_STATUS) registry = er.async_get(hass) state = hass.states.get("binary_sensor.ups_online_status") @@ -22,7 +22,7 @@ async def test_no_binary_sensor(hass: HomeAssistant) -> None: """Test binary sensor when STATFLAG is not available.""" status = MOCK_STATUS.copy() status.pop("STATFLAG") - await init_integration(hass, status=status) + await async_init_integration(hass, status=status) state = hass.states.get("binary_sensor.ups_online_status") assert state is None diff --git a/tests/components/apcupsd/test_init.py b/tests/components/apcupsd/test_init.py index eae5df9b0c1..6e00a382e79 100644 --- a/tests/components/apcupsd/test_init.py +++ b/tests/components/apcupsd/test_init.py @@ -9,7 +9,7 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS, init_integration +from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_async_setup_entry(hass: HomeAssistant, status: OrderedDict) -> No """Test a successful setup entry.""" # Minimal status does not contain "SERIALNO" field, which is used to determine the # unique ID of this integration. But, the integration should work fine without it. - await init_integration(hass, status=status) + await async_init_integration(hass, status=status) # Verify successful setup by querying the status sensor. state = hass.states.get("binary_sensor.ups_online_status") @@ -34,8 +34,8 @@ async def test_multiple_integrations(hass: HomeAssistant) -> None: status1 = MOCK_STATUS | {"LOADPCT": "15.0 Percent", "SERIALNO": "XXXXX1"} status2 = MOCK_STATUS | {"LOADPCT": "16.0 Percent", "SERIALNO": "XXXXX2"} entries = ( - await init_integration(hass, host="test1", status=status1), - await init_integration(hass, host="test2", status=status2), + await async_init_integration(hass, host="test1", status=status1), + await async_init_integration(hass, host="test2", status=status2), ) assert len(hass.config_entries.async_entries(DOMAIN)) == 2 @@ -70,8 +70,8 @@ async def test_unload_remove(hass: HomeAssistant) -> None: """Test successful unload of entry.""" # Load two integrations from two mock hosts. entries = ( - await init_integration(hass, host="test1", status=MOCK_STATUS), - await init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS), + await async_init_integration(hass, host="test1", status=MOCK_STATUS), + await async_init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS), ) # Assert they are loaded. diff --git a/tests/components/apcupsd/test_sensor.py b/tests/components/apcupsd/test_sensor.py index a9f6820faa0..1b09e107682 100644 --- a/tests/components/apcupsd/test_sensor.py +++ b/tests/components/apcupsd/test_sensor.py @@ -16,12 +16,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import MOCK_STATUS, init_integration +from . import MOCK_STATUS, async_init_integration async def test_sensor(hass: HomeAssistant) -> None: """Test states of sensor.""" - await init_integration(hass, status=MOCK_STATUS) + await async_init_integration(hass, status=MOCK_STATUS) registry = er.async_get(hass) # Test a representative string sensor. @@ -89,7 +89,7 @@ async def test_sensor(hass: HomeAssistant) -> None: async def test_sensor_disabled(hass: HomeAssistant) -> None: """Test sensor disabled by default.""" - await init_integration(hass) + await async_init_integration(hass) registry = er.async_get(hass) # Test a representative integration-disabled sensor. From f5aa4f5866bea5c047ccb71a973e7ab41cd5ed00 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Jun 2023 21:50:23 +0200 Subject: [PATCH 17/31] Fix manual update for Command Line (#94433) Manual update command line --- .../components/command_line/binary_sensor.py | 8 +++ .../components/command_line/cover.py | 9 ++- .../components/command_line/sensor.py | 8 +++ .../components/command_line/switch.py | 9 ++- .../command_line/test_binary_sensor.py | 59 ++++++++++++++++++- tests/components/command_line/test_cover.py | 58 ++++++++++++++++++ tests/components/command_line/test_sensor.py | 56 ++++++++++++++++++ tests/components/command_line/test_switch.py | 59 +++++++++++++++++++ 8 files changed, 263 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 8abe401ec9c..e7007b24592 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -30,6 +30,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import dt as dt_util from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER from .sensor import CommandSensorData @@ -183,3 +184,10 @@ class CommandBinarySensor(BinarySensorEntity): self._attr_is_on = False self.async_write_ha_state() + + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + await self._update_entity_state(dt_util.now()) diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 2d2dc8c5fc2..041fa122f37 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -31,7 +31,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import slugify +from homeassistant.util import dt as dt_util, slugify from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER from .utils import call_shell_with_timeout, check_output_or_log @@ -220,6 +220,13 @@ class CommandCover(CoverEntity): self._state = int(payload) await self.async_update_ha_state(True) + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + await self._update_entity_state(dt_util.now()) + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self.hass.async_add_executor_job(self._move_cover, self._command_open) diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index f42ac062081..8137173d613 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -33,6 +33,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import dt as dt_util from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER from .utils import check_output_or_log @@ -200,6 +201,13 @@ class CommandSensor(SensorEntity): self.async_write_ha_state() + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + await self._update_entity_state(dt_util.now()) + class CommandSensorData: """The class for handling the data retrieval.""" diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 1a3dd39a342..4a33d8072d7 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -34,7 +34,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss from homeassistant.helpers.template import Template from homeassistant.helpers.template_entity import ManualTriggerEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import slugify +from homeassistant.util import dt as dt_util, slugify from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, LOGGER from .utils import call_shell_with_timeout, check_output_or_log @@ -240,6 +240,13 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): self._process_manual_data(payload) await self.async_update_ha_state(True) + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + await self._update_entity_state(dt_util.now()) + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if await self._switch(self._command_on) and not self._command_state: diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index eb6b52a66be..9e97f053e07 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -12,7 +12,11 @@ from homeassistant import setup from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.command_line.binary_sensor import CommandBinarySensor from homeassistant.components.command_line.const import DOMAIN -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.helpers.issue_registry as ir @@ -252,3 +256,56 @@ async def test_updating_to_often( ) await asyncio.sleep(0.2) + + +async def test_updating_manually( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling manual updating using homeassistant udate_entity service.""" + await setup.async_setup_component(hass, HA_DOMAIN, {}) + called = [] + + class MockCommandBinarySensor(CommandBinarySensor): + """Mock entity that updates slow.""" + + async def _async_update(self) -> None: + """Update slow.""" + called.append(1) + # Add waiting time + await asyncio.sleep(1) + + with patch( + "homeassistant.components.command_line.binary_sensor.CommandBinarySensor", + side_effect=MockCommandBinarySensor, + ): + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "binary_sensor": { + "name": "Test", + "command": "echo 1", + "payload_on": "1", + "payload_off": "0", + "scan_interval": 10, + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert len(called) == 1 + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ["binary_sensor.test"]}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(called) == 2 + + await asyncio.sleep(0.2) diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index d977c202b04..bb41ed16e82 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -13,6 +13,10 @@ from homeassistant import config as hass_config, setup from homeassistant.components.command_line import DOMAIN from homeassistant.components.command_line.cover import CommandCover from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, SCAN_INTERVAL +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, @@ -378,3 +382,57 @@ async def test_updating_to_often( ) await asyncio.sleep(0.2) + + +async def test_updating_manually( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling manual updating using homeassistant udate_entity service.""" + await setup.async_setup_component(hass, HA_DOMAIN, {}) + called = [] + + class MockCommandCover(CommandCover): + """Mock entity that updates slow.""" + + async def _async_update(self) -> None: + """Update slow.""" + called.append(1) + # Add waiting time + await asyncio.sleep(1) + + with patch( + "homeassistant.components.command_line.cover.CommandCover", + side_effect=MockCommandCover, + ): + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "cover": { + "command_state": "echo 1", + "value_template": "{{ value }}", + "name": "Test", + "scan_interval": 10, + } + } + ] + }, + ) + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(called) == 1 + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ["cover.test"]}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(called) == 2 + + await asyncio.sleep(0.2) diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 87360d0e251..fd3ff8613ec 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -11,7 +11,12 @@ import pytest from homeassistant import setup from homeassistant.components.command_line import DOMAIN from homeassistant.components.command_line.sensor import CommandSensor +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.helpers.issue_registry as ir @@ -586,3 +591,54 @@ async def test_updating_to_often( ) await asyncio.sleep(0.2) + + +async def test_updating_manually( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling manual updating using homeassistant udate_entity service.""" + await setup.async_setup_component(hass, HA_DOMAIN, {}) + called = [] + + class MockCommandSensor(CommandSensor): + """Mock entity that updates slow.""" + + async def _async_update(self) -> None: + """Update slow.""" + called.append(1) + # Add waiting time + await asyncio.sleep(1) + + with patch( + "homeassistant.components.command_line.sensor.CommandSensor", + side_effect=MockCommandSensor, + ): + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "sensor": { + "name": "Test", + "command": "echo 1", + "scan_interval": 10, + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert len(called) == 1 + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ["sensor.test"]}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(called) == 2 + + await asyncio.sleep(0.2) diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 88a87588375..e5331fbe7dd 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -14,6 +14,10 @@ import pytest from homeassistant import setup from homeassistant.components.command_line import DOMAIN from homeassistant.components.command_line.switch import CommandSwitch +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SCAN_INTERVAL from homeassistant.const import ( ATTR_ENTITY_ID, @@ -696,3 +700,58 @@ async def test_updating_to_often( ) await asyncio.sleep(0.2) + + +async def test_updating_manually( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test handling manual updating using homeassistant udate_entity service.""" + await setup.async_setup_component(hass, HA_DOMAIN, {}) + called = [] + + class MockCommandSwitch(CommandSwitch): + """Mock entity that updates slow.""" + + async def _async_update(self) -> None: + """Update slow.""" + called.append(1) + # Add waiting time + await asyncio.sleep(1) + + with patch( + "homeassistant.components.command_line.switch.CommandSwitch", + side_effect=MockCommandSwitch, + ): + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "switch": { + "command_state": "echo 1", + "command_on": "echo 2", + "command_off": "echo 3", + "name": "Test", + "scan_interval": 10, + } + } + ] + }, + ) + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(called) == 1 + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ["switch.test"]}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(called) == 2 + + await asyncio.sleep(0.2) From 25a4679266d17e8f6164f9b36b7c0174e8c9a56b Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 12 Jun 2023 21:51:57 +0200 Subject: [PATCH 18/31] Fix reload service in Command Line (#94436) * Fix reload in Command Line * Add read new yaml --- .../components/command_line/__init__.py | 41 ++++++++++--- .../command_line/fixtures/configuration.yaml | 13 ++-- .../fixtures/configuration_empty.yaml | 0 tests/components/command_line/test_cover.py | 44 +------------ tests/components/command_line/test_init.py | 61 ++++++++++++++++++- 5 files changed, 101 insertions(+), 58 deletions(-) create mode 100644 tests/components/command_line/fixtures/configuration_empty.yaml diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index 906e28052da..6f536bf4744 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -46,12 +46,15 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, + SERVICE_RELOAD, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.entity_platform import async_get_platforms +from homeassistant.helpers.reload import async_integration_yaml_config +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN @@ -163,14 +166,39 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Command Line from yaml config.""" - command_line_config: list[dict[str, dict[str, Any]]] = config.get(DOMAIN, []) + + async def _reload_config(call: Event | ServiceCall) -> None: + """Reload Command Line.""" + reload_config = await async_integration_yaml_config(hass, "command_line") + reset_platforms = async_get_platforms(hass, "command_line") + for reset_platform in reset_platforms: + _LOGGER.debug("Reload resetting platform: %s", reset_platform.domain) + await reset_platform.async_reset() + if not reload_config: + return + await async_load_platforms(hass, reload_config.get(DOMAIN, []), reload_config) + + async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config) + + await async_load_platforms(hass, config.get(DOMAIN, []), config) + + return True + + +async def async_load_platforms( + hass: HomeAssistant, + command_line_config: list[dict[str, dict[str, Any]]], + config: ConfigType, +) -> None: + """Load platforms from yaml.""" if not command_line_config: - return True + return _LOGGER.debug("Full config loaded: %s", command_line_config) load_coroutines: list[Coroutine[Any, Any, None]] = [] platforms: list[Platform] = [] + reload_configs: list[tuple] = [] for platform_config in command_line_config: for platform, _config in platform_config.items(): if (mapped_platform := PLATFORM_MAPPING[platform]) not in platforms: @@ -180,6 +208,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: platform_config, PLATFORM_MAPPING[platform], ) + reload_configs.append((PLATFORM_MAPPING[platform], _config)) load_coroutines.append( discovery.async_load_platform( hass, @@ -190,10 +219,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) - await async_setup_reload_service(hass, DOMAIN, platforms) - if load_coroutines: _LOGGER.debug("Loading platforms: %s", platforms) await asyncio.gather(*load_coroutines) - - return True diff --git a/tests/components/command_line/fixtures/configuration.yaml b/tests/components/command_line/fixtures/configuration.yaml index f210b640338..43e6f641966 100644 --- a/tests/components/command_line/fixtures/configuration.yaml +++ b/tests/components/command_line/fixtures/configuration.yaml @@ -1,6 +1,7 @@ -cover: - - platform: command_line - covers: - from_yaml: - command_state: "echo closed" - value_template: "{{ value }}" +command_line: + - "binary_sensor": + "name": "Test" + "command": "echo 1" + "payload_on": "1" + "payload_off": "0" + "command_timeout": 15 diff --git a/tests/components/command_line/fixtures/configuration_empty.yaml b/tests/components/command_line/fixtures/configuration_empty.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index bb41ed16e82..cbbe2e78eac 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -9,7 +9,7 @@ from unittest.mock import patch import pytest -from homeassistant import config as hass_config, setup +from homeassistant import setup from homeassistant.components.command_line import DOMAIN from homeassistant.components.command_line.cover import CommandCover from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, SCAN_INTERVAL @@ -21,7 +21,6 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, - SERVICE_RELOAD, SERVICE_STOP_COVER, ) from homeassistant.core import HomeAssistant @@ -29,7 +28,7 @@ from homeassistant.helpers import entity_registry as er import homeassistant.helpers.issue_registry as ir import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, get_fixture_path +from tests.common import async_fire_time_changed async def test_no_covers_platform_yaml( @@ -214,45 +213,6 @@ async def test_state_value(hass: HomeAssistant) -> None: assert entity_state.state == "closed" -@pytest.mark.parametrize( - "get_config", - [ - { - "command_line": [ - { - "cover": { - "command_state": "echo open", - "value_template": "{{ value }}", - "name": "Test", - } - } - ] - } - ], -) -async def test_reload(hass: HomeAssistant, load_yaml_integration: None) -> None: - """Verify we can reload command_line covers.""" - - entity_state = hass.states.get("cover.test") - assert entity_state - assert entity_state.state == "unknown" - - yaml_path = get_fixture_path("configuration.yaml", "command_line") - with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): - await hass.services.async_call( - "command_line", - SERVICE_RELOAD, - {}, - blocking=True, - ) - await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 1 - - assert not hass.states.get("cover.test") - assert hass.states.get("cover.from_yaml") - - @pytest.mark.parametrize( "get_config", [ diff --git a/tests/components/command_line/test_init.py b/tests/components/command_line/test_init.py index 06d7b8c41dc..53f985961f3 100644 --- a/tests/components/command_line/test_init.py +++ b/tests/components/command_line/test_init.py @@ -2,12 +2,17 @@ from __future__ import annotations from datetime import timedelta +from unittest.mock import patch -from homeassistant.const import STATE_ON, STATE_OPEN +import pytest + +from homeassistant import config as hass_config +from homeassistant.components.command_line.const import DOMAIN +from homeassistant.const import SERVICE_RELOAD, STATE_ON, STATE_OPEN from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, get_fixture_path async def test_setup_config(hass: HomeAssistant, load_yaml_integration: None) -> None: @@ -25,3 +30,55 @@ async def test_setup_config(hass: HomeAssistant, load_yaml_integration: None) -> assert state_sensor.state == "5" assert state_cover.state == STATE_OPEN assert state_switch.state == STATE_ON + + +async def test_reload_service( + hass: HomeAssistant, load_yaml_integration: None, caplog: pytest.LogCaptureFixture +) -> None: + """Test reload serviice.""" + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + + state_binary_sensor = hass.states.get("binary_sensor.test") + state_sensor = hass.states.get("sensor.test") + assert state_binary_sensor.state == STATE_ON + assert state_sensor.state == "5" + + caplog.clear() + + yaml_path = get_fixture_path("configuration.yaml", "command_line") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert "Loading config" in caplog.text + + state_binary_sensor = hass.states.get("binary_sensor.test") + state_sensor = hass.states.get("sensor.test") + assert state_binary_sensor.state == STATE_ON + assert not state_sensor + + caplog.clear() + + yaml_path = get_fixture_path("configuration_empty.yaml", "command_line") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + state_binary_sensor = hass.states.get("binary_sensor.test") + state_sensor = hass.states.get("sensor.test") + assert not state_binary_sensor + assert not state_sensor + + assert "Loading config" not in caplog.text From fa8e9523242cdc4c4680e66331a6ba799bb03aef Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Jun 2023 21:34:09 -0400 Subject: [PATCH 19/31] Set default value for endpoint in zwave device automations (#94445) * Set default value for endpoint in zwave device automations * add test case --- .../components/zwave_js/device_action.py | 2 +- .../components/zwave_js/device_trigger.py | 2 +- .../components/zwave_js/test_device_action.py | 27 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 20c37b5cbb6..18a3ccef7d8 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -101,7 +101,7 @@ RESET_METER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( SET_CONFIG_PARAMETER_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_TYPE): SERVICE_SET_CONFIG_PARAMETER, - vol.Required(ATTR_ENDPOINT): vol.Coerce(int), + vol.Required(ATTR_ENDPOINT, default=0): vol.Coerce(int), vol.Required(ATTR_CONFIG_PARAMETER): vol.Any(int, str), vol.Required(ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(None, int, str), vol.Required(ATTR_VALUE): vol.Coerce(int), diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index a0ac70ccb31..da26e4f293e 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -161,7 +161,7 @@ BASE_VALUE_UPDATED_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( vol.Required(ATTR_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]), vol.Required(ATTR_PROPERTY): vol.Any(int, str), vol.Optional(ATTR_PROPERTY_KEY): vol.Any(None, vol.Coerce(int), str), - vol.Optional(ATTR_ENDPOINT): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_ENDPOINT, default=0): vol.Any(None, vol.Coerce(int)), vol.Optional(ATTR_FROM): VALUE_SCHEMA, vol.Optional(ATTR_TO): VALUE_SCHEMA, } diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index 97631c94501..ccb65c1d8fa 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -196,6 +196,21 @@ async def test_actions( "value": 1, }, }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_config_parameter_no_endpoint", + }, + "action": { + "domain": DOMAIN, + "type": "set_config_parameter", + "device_id": device.id, + "parameter": 1, + "bitmask": None, + "subtype": "3 (Beeper)", + "value": 1, + }, + }, ] }, ) @@ -245,6 +260,18 @@ async def test_actions( assert args[1] == 1 assert args[2] == 1 + with patch( + "homeassistant.components.zwave_js.services.async_set_config_parameter" + ) as mock_call: + hass.bus.async_fire("test_event_set_config_parameter_no_endpoint") + await hass.async_block_till_done() + mock_call.assert_called_once() + args = mock_call.call_args_list[0][0] + assert len(args) == 3 + assert args[0].node_id == 13 + assert args[1] == 1 + assert args[2] == 1 + async def test_actions_multiple_calls( hass: HomeAssistant, From e83f0bb7a5a82981e7e93884b34a90fa40a764cd Mon Sep 17 00:00:00 2001 From: mover85 Date: Tue, 13 Jun 2023 06:58:20 +1200 Subject: [PATCH 20/31] Revert "Bump pydaikin 2.9.1 (#93635)" (#94469) Revert to pydaikin 2.9.0 --- homeassistant/components/daikin/manifest.json | 2 +- homeassistant/components/daikin/switch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 02a8cdbe68f..6f90b0cf5ef 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "loggers": ["pydaikin"], "quality_scale": "platinum", - "requirements": ["pydaikin==2.9.1"], + "requirements": ["pydaikin==2.9.0"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 1b83f7f7330..37b3ec45c4c 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -42,7 +42,7 @@ async def async_setup_entry( [ DaikinZoneSwitch(daikin_api, zone_id) for zone_id, zone in enumerate(zones) - if zone[0] != ("-", "0") + if zone != ("-", "0") ] ) if daikin_api.device.support_advanced_modes: diff --git a/requirements_all.txt b/requirements_all.txt index 22784bd8952..f1a98a39434 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1576,7 +1576,7 @@ pycsspeechtts==1.0.8 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.9.1 +pydaikin==2.9.0 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33d1ed2ebf6..d93a1dd8d39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1164,7 +1164,7 @@ pycoolmasternet-async==0.1.5 pycsspeechtts==1.0.8 # homeassistant.components.daikin -pydaikin==2.9.1 +pydaikin==2.9.0 # homeassistant.components.deconz pydeconz==112 From 576cf525733e5009036efe21a03e3b997fcef459 Mon Sep 17 00:00:00 2001 From: Chris Phillips Date: Tue, 13 Jun 2023 12:37:30 -0700 Subject: [PATCH 21/31] Bump russound_rio to 1.0.0 (#94500) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 728c40121e0..4f35bf69736 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "iot_class": "local_push", "loggers": ["russound_rio"], - "requirements": ["russound_rio==0.1.8"] + "requirements": ["russound-rio==1.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index f1a98a39434..df228272354 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2293,12 +2293,12 @@ rpi-bad-power==0.1.0 # homeassistant.components.rtsp_to_webrtc rtsp-to-webrtc==0.5.1 +# homeassistant.components.russound_rio +russound-rio==1.0.0 + # homeassistant.components.russound_rnet russound==0.1.9 -# homeassistant.components.russound_rio -russound_rio==0.1.8 - # homeassistant.components.ruuvitag_ble ruuvitag-ble==0.1.1 From fd0404bb4afced8807e20809de06fb182d6f358b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 13 Jun 2023 04:52:55 -0400 Subject: [PATCH 22/31] Fix entity and device selector TypedDict's (#94510) --- homeassistant/helpers/selector.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 2e7df07cf04..afd38bf7636 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -189,8 +189,6 @@ class DeviceFilterSelectorConfig(TypedDict, total=False): integration: str manufacturer: str model: str - entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig] - filter: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig] class ActionSelectorConfig(TypedDict): @@ -546,14 +544,12 @@ class DateTimeSelector(Selector[DateTimeSelectorConfig]): return data -class DeviceSelectorConfig(TypedDict, total=False): +class DeviceSelectorConfig(DeviceFilterSelectorConfig, total=False): """Class to represent a device selector config.""" - integration: str - manufacturer: str - model: str entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig] multiple: bool + filter: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig] @SELECTORS.register("device") @@ -622,6 +618,7 @@ class EntitySelectorConfig(EntityFilterSelectorConfig, total=False): exclude_entities: list[str] include_entities: list[str] multiple: bool + filter: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig] @SELECTORS.register("entity") From d557c6e43ec78725807a8bc587d37249c5bce693 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 Jun 2023 06:32:43 -1000 Subject: [PATCH 23/31] Bump yalexs-ble to 2.1.18 (#94547) --- 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 eeaa5f6c622..ca4e799f16b 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -28,5 +28,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.5.1", "yalexs-ble==2.1.17"] + "requirements": ["yalexs==1.5.1", "yalexs-ble==2.1.18"] } diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 8aa795b970e..4822b2d2704 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.1.17"] + "requirements": ["yalexs-ble==2.1.18"] } diff --git a/requirements_all.txt b/requirements_all.txt index df228272354..334dc1be577 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2698,7 +2698,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.1.17 +yalexs-ble==2.1.18 # homeassistant.components.august yalexs==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d93a1dd8d39..53c8a67afc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1968,7 +1968,7 @@ yalesmartalarmclient==0.3.9 # homeassistant.components.august # homeassistant.components.yalexs_ble -yalexs-ble==2.1.17 +yalexs-ble==2.1.18 # homeassistant.components.august yalexs==1.5.1 From a63ce8100e7d039a709aa586166d29d2d162ad12 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 13 Jun 2023 13:37:56 -0600 Subject: [PATCH 24/31] Bump `regenmaschine` to 2023.06.0 (#94554) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 574ca3d7f43..dabae5ff8c6 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -10,7 +10,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["regenmaschine"], - "requirements": ["regenmaschine==2023.05.1"], + "requirements": ["regenmaschine==2023.06.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 334dc1be577..4f0e1957afa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2246,7 +2246,7 @@ rapt-ble==0.1.1 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2023.05.1 +regenmaschine==2023.06.0 # homeassistant.components.renault renault-api==0.1.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 53c8a67afc2..bb5fa31f9fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1630,7 +1630,7 @@ radiotherm==2.1.0 rapt-ble==0.1.1 # homeassistant.components.rainmachine -regenmaschine==2023.05.1 +regenmaschine==2023.06.0 # homeassistant.components.renault renault-api==0.1.13 From 70d33129d4550f65a4c52aa20105f06144b8eebc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 13 Jun 2023 22:27:14 +0200 Subject: [PATCH 25/31] Update Home Assistant base image to 2023.06.0 (#94556) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 11b60a66295..b32aa38dff6 100644 --- a/build.yaml +++ b/build.yaml @@ -1,11 +1,11 @@ image: homeassistant/{arch}-homeassistant shadow_repository: ghcr.io/home-assistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.05.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.05.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.05.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.05.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.05.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.06.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From f67577ebe077b293bbd15ab790aa868053195610 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 14 Jun 2023 21:00:21 +0000 Subject: [PATCH 26/31] Catch InvalidAuthError in `shutdown()` method for Shelly gen2 devices (#94563) * Catch InvalidAuthError in shutdown() method * Add test * Revert unwanted change in tests --- .../components/shelly/coordinator.py | 5 +- tests/components/shelly/test_coordinator.py | 55 ++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 85207ee4475..6d7b3496880 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -535,7 +535,10 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): async def shutdown(self) -> None: """Shutdown the coordinator.""" if self.device.connected: - await async_stop_scanner(self.device) + try: + await async_stop_scanner(self.device) + except InvalidAuthError: + self.entry.async_start_reauth(self.hass) await self.device.shutdown() await self._async_disconnected() diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 2f267a208ca..9039893999d 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -1,6 +1,6 @@ """Tests for Shelly coordinator.""" from datetime import timedelta -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError @@ -335,6 +335,59 @@ async def test_rpc_reload_on_cfg_change( assert hass.states.get("switch.test_switch_0") is None +async def test_rpc_reload_with_invalid_auth( + hass: HomeAssistant, mock_rpc_device, monkeypatch +) -> None: + """Test RPC when InvalidAuthError is raising during config entry reload.""" + with patch( + "homeassistant.components.shelly.coordinator.async_stop_scanner", + side_effect=[None, InvalidAuthError, None], + ): + entry = await init_integration(hass, 2) + + inject_rpc_device_event( + monkeypatch, + mock_rpc_device, + { + "events": [ + { + "data": [], + "event": "config_changed", + "id": 1, + "ts": 1668522399.2, + }, + { + "data": [], + "id": 2, + "ts": 1668522399.2, + }, + ], + "ts": 1668522399.2, + }, + ) + + await hass.async_block_till_done() + + # Move time to generate reconnect + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=RPC_RECONNECT_INTERVAL) + ) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id + + async def test_rpc_click_event( hass: HomeAssistant, mock_rpc_device, events, monkeypatch ) -> None: From d28d9091140701a8d051b9c4c5f35244339d7ff1 Mon Sep 17 00:00:00 2001 From: Ian Foster Date: Wed, 14 Jun 2023 13:06:55 -0700 Subject: [PATCH 27/31] Fix keyboard_remote for python 3.11 (#94570) * started work to update keyboard_remote to work with python 3.11 * updated function names * all checks pass * fixed asyncio for python 3.11 * cleanup * Update homeassistant/components/keyboard_remote/__init__.py Co-authored-by: Martin Hjelmare * Update __init__.py added: from __future__ import annotations * Fix typing --------- Co-authored-by: Martin Hjelmare --- .../components/keyboard_remote/__init__.py | 81 +++++++++++-------- .../components/keyboard_remote/manifest.json | 2 +- requirements_all.txt | 8 +- 3 files changed, 53 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index f0f1497f940..e3d280c2944 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -1,11 +1,14 @@ """Receive signals from a keyboard and use it as a remote control.""" # pylint: disable=import-error +from __future__ import annotations + import asyncio from contextlib import suppress import logging import os +from typing import Any -import aionotify +from asyncinotify import Inotify, Mask from evdev import InputDevice, categorize, ecodes, list_devices import voluptuous as vol @@ -64,9 +67,9 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the keyboard_remote.""" - config = config[DOMAIN] + domain_config: list[dict[str, Any]] = config[DOMAIN] - remote = KeyboardRemote(hass, config) + remote = KeyboardRemote(hass, domain_config) remote.setup() return True @@ -75,12 +78,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class KeyboardRemote: """Manage device connection/disconnection using inotify to asynchronously monitor.""" - def __init__(self, hass, config): + def __init__(self, hass: HomeAssistant, config: list[dict[str, Any]]) -> None: """Create handlers and setup dictionaries to keep track of them.""" self.hass = hass self.handlers_by_name = {} self.handlers_by_descriptor = {} - self.active_handlers_by_descriptor = {} + self.active_handlers_by_descriptor: dict[str, asyncio.Future] = {} + self.inotify = None self.watcher = None self.monitor_task = None @@ -110,16 +114,12 @@ class KeyboardRemote: connected, and start monitoring for device connection/disconnection. """ - # start watching - self.watcher = aionotify.Watcher() - self.watcher.watch( - alias="devinput", - path=DEVINPUT, - flags=aionotify.Flags.CREATE - | aionotify.Flags.ATTRIB - | aionotify.Flags.DELETE, + _LOGGER.debug("Start monitoring") + + self.inotify = Inotify() + self.watcher = self.inotify.add_watch( + DEVINPUT, Mask.CREATE | Mask.ATTRIB | Mask.DELETE ) - await self.watcher.setup(self.hass.loop) # add initial devices (do this AFTER starting watcher in order to # avoid race conditions leading to missing device connections) @@ -134,7 +134,9 @@ class KeyboardRemote: continue self.active_handlers_by_descriptor[descriptor] = handler - initial_start_monitoring.add(handler.async_start_monitoring(dev)) + initial_start_monitoring.add( + asyncio.create_task(handler.async_device_start_monitoring(dev)) + ) if initial_start_monitoring: await asyncio.wait(initial_start_monitoring) @@ -146,6 +148,10 @@ class KeyboardRemote: _LOGGER.debug("Cleanup on shutdown") + if self.inotify and self.watcher: + self.inotify.rm_watch(self.watcher) + self.watcher = None + if self.monitor_task is not None: if not self.monitor_task.done(): self.monitor_task.cancel() @@ -153,11 +159,16 @@ class KeyboardRemote: handler_stop_monitoring = set() for handler in self.active_handlers_by_descriptor.values(): - handler_stop_monitoring.add(handler.async_stop_monitoring()) - + handler_stop_monitoring.add( + asyncio.create_task(handler.async_device_stop_monitoring()) + ) if handler_stop_monitoring: await asyncio.wait(handler_stop_monitoring) + if self.inotify: + self.inotify.close() + self.inotify = None + def get_device_handler(self, descriptor): """Find the correct device handler given a descriptor (path).""" @@ -187,20 +198,21 @@ class KeyboardRemote: async def async_monitor_devices(self): """Monitor asynchronously for device connection/disconnection or permissions changes.""" + _LOGGER.debug("Start monitoring loop") + try: - while True: - event = await self.watcher.get_event() + async for event in self.inotify: descriptor = f"{DEVINPUT}/{event.name}" + _LOGGER.debug("got events for %s: %s", descriptor, event.mask) descriptor_active = descriptor in self.active_handlers_by_descriptor - if (event.flags & aionotify.Flags.DELETE) and descriptor_active: + if (event.mask & Mask.DELETE) and descriptor_active: handler = self.active_handlers_by_descriptor[descriptor] del self.active_handlers_by_descriptor[descriptor] - await handler.async_stop_monitoring() + await handler.async_device_stop_monitoring() elif ( - (event.flags & aionotify.Flags.CREATE) - or (event.flags & aionotify.Flags.ATTRIB) + (event.mask & Mask.CREATE) or (event.mask & Mask.ATTRIB) ) and not descriptor_active: dev, handler = await self.hass.async_add_executor_job( self.get_device_handler, descriptor @@ -208,31 +220,32 @@ class KeyboardRemote: if handler is None: continue self.active_handlers_by_descriptor[descriptor] = handler - await handler.async_start_monitoring(dev) + await handler.async_device_start_monitoring(dev) except asyncio.CancelledError: + _LOGGER.debug("Monitoring canceled") return class DeviceHandler: """Manage input events using evdev with asyncio.""" - def __init__(self, hass, dev_block): + def __init__(self, hass: HomeAssistant, dev_block: dict[str, Any]) -> None: """Fill configuration data.""" self.hass = hass - key_types = dev_block.get(TYPE) + key_types = dev_block[TYPE] self.key_values = set() for key_type in key_types: self.key_values.add(KEY_VALUE[key_type]) - self.emulate_key_hold = dev_block.get(EMULATE_KEY_HOLD) - self.emulate_key_hold_delay = dev_block.get(EMULATE_KEY_HOLD_DELAY) - self.emulate_key_hold_repeat = dev_block.get(EMULATE_KEY_HOLD_REPEAT) + self.emulate_key_hold = dev_block[EMULATE_KEY_HOLD] + self.emulate_key_hold_delay = dev_block[EMULATE_KEY_HOLD_DELAY] + self.emulate_key_hold_repeat = dev_block[EMULATE_KEY_HOLD_REPEAT] self.monitor_task = None self.dev = None - async def async_keyrepeat(self, path, name, code, delay, repeat): + async def async_device_keyrepeat(self, path, name, code, delay, repeat): """Emulate keyboard delay/repeat behaviour by sending key events on a timer.""" await asyncio.sleep(delay) @@ -248,8 +261,9 @@ class KeyboardRemote: ) await asyncio.sleep(repeat) - async def async_start_monitoring(self, dev): + async def async_device_start_monitoring(self, dev): """Start event monitoring task and issue event.""" + _LOGGER.debug("Keyboard async_device_start_monitoring, %s", dev.name) if self.monitor_task is None: self.dev = dev self.monitor_task = self.hass.async_create_task( @@ -261,7 +275,7 @@ class KeyboardRemote: ) _LOGGER.debug("Keyboard (re-)connected, %s", dev.name) - async def async_stop_monitoring(self): + async def async_device_stop_monitoring(self): """Stop event monitoring task and issue event.""" if self.monitor_task is not None: with suppress(OSError): @@ -295,6 +309,7 @@ class KeyboardRemote: _LOGGER.debug("Start device monitoring") await self.hass.async_add_executor_job(dev.grab) async for event in dev.async_read_loop(): + # pylint: disable=no-member if event.type is ecodes.EV_KEY: if event.value in self.key_values: _LOGGER.debug(categorize(event)) @@ -313,7 +328,7 @@ class KeyboardRemote: and self.emulate_key_hold ): repeat_tasks[event.code] = self.hass.async_create_task( - self.async_keyrepeat( + self.async_device_keyrepeat( dev.path, dev.name, event.code, diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index d319ba93ce2..2b298901ca9 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "iot_class": "local_push", "loggers": ["aionotify", "evdev"], - "requirements": ["evdev==1.4.0", "aionotify==0.2.0"] + "requirements": ["evdev==1.6.1", "asyncinotify==4.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4f0e1957afa..7e62831c75e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -222,9 +222,6 @@ aiomusiccast==0.14.8 # homeassistant.components.nanoleaf aionanoleaf==0.2.1 -# homeassistant.components.keyboard_remote -aionotify==0.2.0 - # homeassistant.components.notion aionotion==2023.05.5 @@ -382,6 +379,9 @@ asterisk_mbox==0.5.0 # homeassistant.components.yeelight async-upnp-client==0.33.2 +# homeassistant.components.keyboard_remote +asyncinotify==4.0.2 + # homeassistant.components.supla asyncpysupla==0.0.5 @@ -695,7 +695,7 @@ eternalegypt==0.0.16 eufylife_ble_client==0.1.7 # homeassistant.components.keyboard_remote -# evdev==1.4.0 +# evdev==1.6.1 # homeassistant.components.evohome evohome-async==0.3.15 From 8f437c5833e28743adff51ba66a46d34f3aab1f5 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 14 Jun 2023 02:07:03 -0500 Subject: [PATCH 28/31] Fix failed recovery in roku (#94572) --- homeassistant/components/roku/__init__.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 7f922c2eea5..583d26a4a5b 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -22,13 +22,11 @@ PLATFORMS = [ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)): - coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) - hass.data[DOMAIN][entry.entry_id] = coordinator - + coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -36,7 +34,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok From e6b8e4fd48c06b1264e3ed7ca24b38f4b9c7c0cb Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 14 Jun 2023 02:07:24 -0500 Subject: [PATCH 29/31] Fix failed recovery in ipp (#94573) --- homeassistant/components/ipp/__init__.py | 26 ++++++++++-------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 42dc2b8d93b..9df377b939a 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -19,21 +19,18 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IPP from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)): - # Create IPP instance for this entry - coordinator = IPPDataUpdateCoordinator( - hass, - host=entry.data[CONF_HOST], - port=entry.data[CONF_PORT], - base_path=entry.data[CONF_BASE_PATH], - tls=entry.data[CONF_SSL], - verify_ssl=entry.data[CONF_VERIFY_SSL], - ) - hass.data[DOMAIN][entry.entry_id] = coordinator - + coordinator = IPPDataUpdateCoordinator( + hass, + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + base_path=entry.data[CONF_BASE_PATH], + tls=entry.data[CONF_SSL], + verify_ssl=entry.data[CONF_VERIFY_SSL], + ) await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -41,7 +38,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok From 9cbcfca2cd7209bf4a4a0c09515cf38511b903ca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 Jun 2023 10:26:54 +0200 Subject: [PATCH 30/31] Improve multipan debug logging (#94580) --- .../silabs_multiprotocol_addon.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index 34ab9a3cedb..c5f7049e54f 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -96,19 +96,29 @@ class MultiprotocolAddonManager(AddonManager): ) -> None: """Register a multipan platform.""" self._platforms[integration_domain] = platform - if self._channel is not None or not await platform.async_using_multipan(hass): + + channel = await platform.async_get_channel(hass) + using_multipan = await platform.async_using_multipan(hass) + + _LOGGER.info( + "Registering new multipan platform '%s', using multipan: %s, channel: %s", + integration_domain, + using_multipan, + channel, + ) + + if self._channel is not None or not using_multipan: return - new_channel = await platform.async_get_channel(hass) - if new_channel is None: + if channel is None: return _LOGGER.info( "Setting multipan channel to %s (source: '%s')", - new_channel, + channel, integration_domain, ) - self.async_set_channel(new_channel) + self.async_set_channel(channel) async def async_change_channel( self, channel: int, delay: float From 905bdd0dd5bf94854e6b33005ca771cfda552fea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Jun 2023 21:18:40 -0400 Subject: [PATCH 31/31] Bumped version to 2023.6.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 3392b6dabd8..2d5e5ebb14c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/pyproject.toml b/pyproject.toml index ca4f61a5ccc..ff8898aa90a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.6.1" +version = "2023.6.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"