From d983fa6da7fd32aa64cbf633d0f70de0da11202a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 4 Apr 2024 09:05:08 +0200 Subject: [PATCH 01/25] Update aioairzone-cloud to v0.4.7 (#114761) --- homeassistant/components/airzone_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index 14f02620c91617..b4445f6fe45968 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.4.6"] + "requirements": ["aioairzone-cloud==0.4.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 76dc587d6b9d43..d7b500abdd3280 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.6 +aioairzone-cloud==0.4.7 # homeassistant.components.airzone aioairzone==0.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f329b782aaa86..82aae896b593f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.6 +aioairzone-cloud==0.4.7 # homeassistant.components.airzone aioairzone==0.7.6 From 25289e0ca19c9a9382dda561854d5978b244e399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 5 Apr 2024 02:55:39 +0200 Subject: [PATCH 02/25] Bump myuplink dependency to 0.6.0 (#114767) --- homeassistant/components/myuplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myuplink/manifest.json b/homeassistant/components/myuplink/manifest.json index a76f596ade356c..0e638a72715da8 100644 --- a/homeassistant/components/myuplink/manifest.json +++ b/homeassistant/components/myuplink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/myuplink", "iot_class": "cloud_polling", - "requirements": ["myuplink==0.5.0"] + "requirements": ["myuplink==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d7b500abdd3280..257dea984e6508 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1349,7 +1349,7 @@ mutesync==0.0.1 mypermobil==0.1.8 # homeassistant.components.myuplink -myuplink==0.5.0 +myuplink==0.6.0 # homeassistant.components.nad nad-receiver==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82aae896b593f5..4dcea7c582d127 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1088,7 +1088,7 @@ mutesync==0.0.1 mypermobil==0.1.8 # homeassistant.components.myuplink -myuplink==0.5.0 +myuplink==0.6.0 # homeassistant.components.keenetic_ndms2 ndms2-client==0.1.2 From 450be674069530904862168ca2078ed78f8c6c29 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:56:08 +0100 Subject: [PATCH 03/25] Update romy to 0.0.9 (#114360) --- homeassistant/components/romy/manifest.json | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/romy/manifest.json b/homeassistant/components/romy/manifest.json index 1257c2d1d600fb..7e30c4185995e7 100644 --- a/homeassistant/components/romy/manifest.json +++ b/homeassistant/components/romy/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/romy", "iot_class": "local_polling", - "requirements": ["romy==0.0.7"], + "requirements": ["romy==0.0.9"], "zeroconf": ["_aicu-http._tcp.local."] } diff --git a/pyproject.toml b/pyproject.toml index e0f07fac6b69dc..ac890603ac3c8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -513,8 +513,6 @@ filterwarnings = [ "ignore:invalid escape sequence:SyntaxWarning:.*stringcase", # https://github.com/pyudev/pyudev/pull/466 - >=0.24.0 "ignore:invalid escape sequence:SyntaxWarning:.*pyudev.monitor", - # https://github.com/xeniter/romy/pull/1 - >=0.0.8 - "ignore:with timeout\\(\\) is deprecated, use async with timeout\\(\\) instead:DeprecationWarning:romy.utils", # https://github.com/grahamwetzler/smart-meter-texas/pull/143 - >0.5.3 "ignore:ssl.OP_NO_SSL\\*/ssl.OP_NO_TLS\\* options are deprecated:DeprecationWarning:smart_meter_texas", # https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index 257dea984e6508..a8f9e5877d83b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2459,7 +2459,7 @@ rocketchat-API==0.6.1 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.7 +romy==0.0.9 # homeassistant.components.roomba roombapy==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4dcea7c582d127..34d4b8e8644167 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1893,7 +1893,7 @@ ring-doorbell[listen]==0.8.9 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.7 +romy==0.0.9 # homeassistant.components.roomba roombapy==1.8.1 From a39e1a6428a09d331b32d64360f12d423367ed5e Mon Sep 17 00:00:00 2001 From: Manuel Dipolt Date: Thu, 4 Apr 2024 00:48:35 +0200 Subject: [PATCH 04/25] Update romy to 0.0.10 (#114785) --- homeassistant/components/romy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/romy/manifest.json b/homeassistant/components/romy/manifest.json index 7e30c4185995e7..efb8072ebbcdb0 100644 --- a/homeassistant/components/romy/manifest.json +++ b/homeassistant/components/romy/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/romy", "iot_class": "local_polling", - "requirements": ["romy==0.0.9"], + "requirements": ["romy==0.0.10"], "zeroconf": ["_aicu-http._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index a8f9e5877d83b0..9a28576d836295 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2459,7 +2459,7 @@ rocketchat-API==0.6.1 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.9 +romy==0.0.10 # homeassistant.components.roomba roombapy==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34d4b8e8644167..673d47d02a565b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1893,7 +1893,7 @@ ring-doorbell[listen]==0.8.9 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.9 +romy==0.0.10 # homeassistant.components.roomba roombapy==1.8.1 From ef8e54877fb6283f8104e7c4f917c3fcf5090ba5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 3 Apr 2024 18:20:20 -0600 Subject: [PATCH 05/25] Fix unhandled `KeyError` during Notion setup (#114787) --- homeassistant/components/notion/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index ca45e3a6d168da..1793a0cfd474e9 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (CONF_REFRESH_TOKEN, client.refresh_token), (CONF_USER_UUID, client.user_uuid), ): - if entry.data[key] == value: + if entry.data.get(key) == value: continue entry_updates["data"][key] = value From 3d0bafbdc95b2514ffff276b5fc5354a3e54d772 Mon Sep 17 00:00:00 2001 From: cdheiser <10488026+cdheiser@users.noreply.github.com> Date: Thu, 4 Apr 2024 02:24:02 -0700 Subject: [PATCH 06/25] Fix Lutron light brightness values (#114794) Fix brightness values in light.py Bugfix to set the brightness to 0-100 which is what Lutron expects. --- homeassistant/components/lutron/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index 18b5edd10396b9..eb003fd431ab66 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -141,7 +141,7 @@ def turn_on(self, **kwargs: Any) -> None: else: brightness = self._prev_brightness self._prev_brightness = brightness - args = {"new_level": brightness} + args = {"new_level": to_lutron_level(brightness)} if ATTR_TRANSITION in kwargs: args["fade_time_seconds"] = kwargs[ATTR_TRANSITION] self._lutron_device.set_level(**args) From d8ae7d6955ae199589d57de6fda9e2199e6c7c38 Mon Sep 17 00:00:00 2001 From: Lex Li <425130+lextm@users.noreply.github.com> Date: Fri, 5 Apr 2024 02:41:15 -0400 Subject: [PATCH 07/25] Fix type cast in snmp (#114795) --- homeassistant/components/snmp/sensor.py | 2 +- tests/components/snmp/test_negative_sensor.py | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/components/snmp/test_negative_sensor.py diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index f55cd07effb588..972b91319355e7 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -270,7 +270,7 @@ def _decode_value(self, value): "SNMP OID %s received type=%s and data %s", self._baseoid, type(value), - bytes(value), + value, ) if isinstance(value, NoSuchObject): _LOGGER.error( diff --git a/tests/components/snmp/test_negative_sensor.py b/tests/components/snmp/test_negative_sensor.py new file mode 100644 index 00000000000000..c5ac6460841fc0 --- /dev/null +++ b/tests/components/snmp/test_negative_sensor.py @@ -0,0 +1,79 @@ +"""SNMP sensor tests.""" + +from unittest.mock import patch + +from pysnmp.hlapi import Integer32 +import pytest + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +def hlapi_mock(): + """Mock out 3rd party API.""" + mock_data = Integer32(-13) + with patch( + "homeassistant.components.snmp.sensor.getCmd", + return_value=(None, None, None, [[mock_data]]), + ): + yield + + +async def test_basic_config(hass: HomeAssistant) -> None: + """Test basic entity configuration.""" + + config = { + SENSOR_DOMAIN: { + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.snmp") + assert state.state == "-13" + assert state.attributes == {"friendly_name": "SNMP"} + + +async def test_entity_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + SENSOR_DOMAIN: { + # SNMP configuration + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + # Entity configuration + "icon": "{{'mdi:one_two_three'}}", + "picture": "{{'blabla.png'}}", + "device_class": "temperature", + "name": "{{'SNMP' + ' ' + 'Sensor'}}", + "state_class": "measurement", + "unique_id": "very_unique", + "unit_of_measurement": "°C", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique" + + state = hass.states.get("sensor.snmp_sensor") + assert state.state == "-13" + assert state.attributes == { + "device_class": "temperature", + "entity_picture": "blabla.png", + "friendly_name": "SNMP Sensor", + "icon": "mdi:one_two_three", + "state_class": "measurement", + "unit_of_measurement": "°C", + } From 530725bbfa0274a04b2b0aed72e2592843e0da02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 20:42:57 -1000 Subject: [PATCH 08/25] Handle ambiguous script actions by using action map order (#114825) --- homeassistant/helpers/config_validation.py | 6 ++++++ tests/helpers/test_config_validation.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index f7245607be726d..70de144d5c8678 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1855,6 +1855,12 @@ def determine_script_action(action: dict[str, Any]) -> str: """Determine action type.""" if not (actions := ACTIONS_SET.intersection(action)): raise ValueError("Unable to determine action") + if len(actions) > 1: + # Ambiguous action, select the first one in the + # order of the ACTIONS_MAP + for action_key, _script_action in ACTIONS_MAP.items(): + if action_key in actions: + return _script_action return ACTIONS_MAP[actions.pop()] diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 133e5e80442dd0..9816dc381897c6 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1672,3 +1672,25 @@ def test_color_hex() -> None: with pytest.raises(vol.Invalid, match=msg): cv.color_hex(123456) + + +def test_determine_script_action_ambiguous(): + """Test determine script action with ambiguous actions.""" + assert ( + cv.determine_script_action( + { + "type": "is_power", + "condition": "device", + "device_id": "9c2bda81bc7997c981f811c32cafdb22", + "entity_id": "2ee287ec70dd0c6db187b539bee429b7", + "domain": "sensor", + "below": "15", + } + ) + == "condition" + ) + + +def test_determine_script_action_non_ambiguous(): + """Test determine script action with a non ambiguous action.""" + assert cv.determine_script_action({"delay": "00:00:05"}) == "delay" From 319f76cdc844cb369778f7e3a55527ba5253aa1a Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 4 Apr 2024 13:06:15 -0700 Subject: [PATCH 09/25] Bump opower to 0.4.3 (#114826) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 879aeb0327b175..51ad669733bde4 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.4.2"] + "requirements": ["opower==0.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9a28576d836295..ff3b0a8068e8af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1482,7 +1482,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.2 +opower==0.4.3 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 673d47d02a565b..56703105b7d634 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1176,7 +1176,7 @@ openerz-api==0.3.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.4.2 +opower==0.4.3 # homeassistant.components.oralb oralb-ble==0.17.6 From 0191d3e41b3b655d937aeac92a5c7f9a534f2111 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 09:30:10 -1000 Subject: [PATCH 10/25] Refactor ConfigStore to avoid needing to pass config_dir (#114827) Co-authored-by: Erik --- homeassistant/core.py | 16 +++++++++++----- homeassistant/helpers/storage.py | 13 +++++-------- tests/test_core.py | 3 +++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 4794b284fd2586..d4510e970f914c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -401,6 +401,7 @@ def __init__(self, config_dir: str) -> None: self.services = ServiceRegistry(self) self.states = StateMachine(self.bus, self.loop) self.config = Config(self, config_dir) + self.config.async_initialize() self.components = loader.Components(self) self.helpers = loader.Helpers(self) self.state: CoreState = CoreState.not_running @@ -2589,12 +2590,12 @@ async def _execute_service( class Config: """Configuration settings for Home Assistant.""" + _store: Config._ConfigStore + def __init__(self, hass: HomeAssistant, config_dir: str) -> None: """Initialize a new config object.""" self.hass = hass - self._store = self._ConfigStore(self.hass, config_dir) - self.latitude: float = 0 self.longitude: float = 0 @@ -2645,6 +2646,13 @@ def __init__(self, hass: HomeAssistant, config_dir: str) -> None: # If Home Assistant is running in safe mode self.safe_mode: bool = False + def async_initialize(self) -> None: + """Finish initializing a config object. + + This must be called before the config object is used. + """ + self._store = self._ConfigStore(self.hass) + def distance(self, lat: float, lon: float) -> float | None: """Calculate distance from Home Assistant. @@ -2850,7 +2858,6 @@ async def _async_store(self) -> None: "country": self.country, "language": self.language, } - await self._store.async_save(data) # Circular dependency prevents us from generating the class at top level @@ -2860,7 +2867,7 @@ async def _async_store(self) -> None: class _ConfigStore(Store[dict[str, Any]]): """Class to help storing Config data.""" - def __init__(self, hass: HomeAssistant, config_dir: str) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize storage class.""" super().__init__( hass, @@ -2869,7 +2876,6 @@ def __init__(self, hass: HomeAssistant, config_dir: str) -> None: private=True, atomic_writes=True, minor_version=CORE_STORAGE_MINOR_VERSION, - config_dir=config_dir, ) self._original_unit_system: str | None = None # from old store 1.1 diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 2413a53e60516a..92a31ae9345423 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -95,9 +95,7 @@ def load_old_config(): return config -def get_internal_store_manager( - hass: HomeAssistant, config_dir: str | None = None -) -> _StoreManager: +def get_internal_store_manager(hass: HomeAssistant) -> _StoreManager: """Get the store manager. This function is not part of the API and should only be @@ -105,7 +103,7 @@ def get_internal_store_manager( guaranteed to be stable. """ if STORAGE_MANAGER not in hass.data: - manager = _StoreManager(hass, config_dir or hass.config.config_dir) + manager = _StoreManager(hass) hass.data[STORAGE_MANAGER] = manager return hass.data[STORAGE_MANAGER] @@ -116,13 +114,13 @@ class _StoreManager: The store manager is used to cache and manage storage files. """ - def __init__(self, hass: HomeAssistant, config_dir: str) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize storage manager class.""" self._hass = hass self._invalidated: set[str] = set() self._files: set[str] | None = None self._data_preload: dict[str, json_util.JsonValueType] = {} - self._storage_path: Path = Path(config_dir).joinpath(STORAGE_DIR) + self._storage_path: Path = Path(hass.config.config_dir).joinpath(STORAGE_DIR) self._cancel_cleanup: asyncio.TimerHandle | None = None async def async_initialize(self) -> None: @@ -251,7 +249,6 @@ def __init__( encoder: type[JSONEncoder] | None = None, minor_version: int = 1, read_only: bool = False, - config_dir: str | None = None, ) -> None: """Initialize storage class.""" self.version = version @@ -268,7 +265,7 @@ def __init__( self._atomic_writes = atomic_writes self._read_only = read_only self._next_write_time = 0.0 - self._manager = get_internal_store_manager(hass, config_dir) + self._manager = get_internal_store_manager(hass) @cached_property def path(self): diff --git a/tests/test_core.py b/tests/test_core.py index a0a197096cdcf1..905d8efe6de964 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2288,6 +2288,7 @@ async def test_additional_data_in_core_config( ) -> None: """Test that we can handle additional data in core configuration.""" config = ha.Config(hass, "/test/ha-config") + config.async_initialize() hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, "data": {"location_name": "Test Name", "additional_valid_key": "value"}, @@ -2301,6 +2302,7 @@ async def test_incorrect_internal_external_url( ) -> None: """Test that we warn when detecting invalid internal/external url.""" config = ha.Config(hass, "/test/ha-config") + config.async_initialize() hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, @@ -2314,6 +2316,7 @@ async def test_incorrect_internal_external_url( assert "Invalid internal_url set" not in caplog.text config = ha.Config(hass, "/test/ha-config") + config.async_initialize() hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, From aa14793479a4f481de3508ddd49189fee1d8cfe9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 4 Apr 2024 13:45:44 +0200 Subject: [PATCH 11/25] Avoid blocking IO in downloader initialization (#114841) * Avoid blocking IO in downloader initialization * Avoid blocking IO in downloader initialization --- homeassistant/components/downloader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 94d243e2cf27e6..3ca503a2167b6c 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -83,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not os.path.isabs(download_path): download_path = hass.config.path(download_path) - if not os.path.isdir(download_path): + if not await hass.async_add_executor_job(os.path.isdir, download_path): _LOGGER.error( "Download path %s does not exist. File Downloader not active", download_path ) From 58533f02af7c612abdc51ce7ebcda20093ffe267 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 11:32:59 +0200 Subject: [PATCH 12/25] Fix Downloader YAML import (#114844) --- .../components/downloader/__init__.py | 10 +++- tests/components/downloader/test_init.py | 51 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 tests/components/downloader/test_init.py diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 3ca503a2167b6c..d110c28785aa30 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -43,6 +43,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True + hass.async_create_task(_async_import_config(hass, config), eager_start=True) + return True + + +async def _async_import_config(hass: HomeAssistant, config: ConfigType) -> None: + """Import the Downloader component from the YAML file.""" + import_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -62,7 +69,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, DOMAIN, f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.9.0", + breaks_in_ha_version="2024.10.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, @@ -72,7 +79,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "integration_title": "Downloader", }, ) - return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/tests/components/downloader/test_init.py b/tests/components/downloader/test_init.py new file mode 100644 index 00000000000000..8cd0d00b1abcfa --- /dev/null +++ b/tests/components/downloader/test_init.py @@ -0,0 +1,51 @@ +"""Tests for the downloader component init.""" + +from unittest.mock import patch + +from homeassistant.components.downloader import ( + CONF_DOWNLOAD_DIR, + DOMAIN, + SERVICE_DOWNLOAD_FILE, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_initialization(hass: HomeAssistant) -> None: + """Test the initialization of the downloader component.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_DOWNLOAD_DIR: "/test_dir", + }, + ) + config_entry.add_to_hass(hass) + with patch("os.path.isdir", return_value=True): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE) + assert config_entry.state is ConfigEntryState.LOADED + + +async def test_import(hass: HomeAssistant) -> None: + """Test the import of the downloader component.""" + with patch("os.path.isdir", return_value=True): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DOWNLOAD_DIR: "/test_dir", + }, + }, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {CONF_DOWNLOAD_DIR: "/test_dir"} + assert config_entry.state is ConfigEntryState.LOADED + assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE) From 411e55d0596dac418215e297fd7ec9365bc5b6ef Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 4 Apr 2024 21:01:15 +0200 Subject: [PATCH 13/25] Update frontend to 20240404.0 (#114859) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 1890572bf5a107..75c630b44714d3 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240403.1"] + "requirements": ["home-assistant-frontend==20240404.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6bb6bd4d2d3fa4..d520d7f8f769a4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240403.1 +home-assistant-frontend==20240404.0 home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ff3b0a8068e8af..51de44eaca60da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240403.1 +home-assistant-frontend==20240404.0 # homeassistant.components.conversation home-assistant-intents==2024.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56703105b7d634..eb1fb0583cbd7d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240403.1 +home-assistant-frontend==20240404.0 # homeassistant.components.conversation home-assistant-intents==2024.4.3 From 96003e3562c0cdc91d06556fd1ae06c062bcd237 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Apr 2024 08:28:35 +0200 Subject: [PATCH 14/25] Fix Axis camera platform support HTTPS (#114886) --- homeassistant/components/axis/camera.py | 16 ++++++++-------- homeassistant/components/axis/hub/config.py | 3 +++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 769be676a7832b..025244fb675879 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -56,6 +56,7 @@ def __init__(self, hub: AxisHub) -> None: mjpeg_url=self.mjpeg_source, still_image_url=self.image_source, authentication=HTTP_DIGEST_AUTHENTICATION, + verify_ssl=False, unique_id=f"{hub.unique_id}-camera", ) @@ -74,16 +75,18 @@ def _generate_sources(self) -> None: Additionally used when device change IP address. """ + proto = self.hub.config.protocol + host = self.hub.config.host + port = self.hub.config.port + image_options = self.generate_options(skip_stream_profile=True) self._still_image_url = ( - f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi" - f"/jpg/image.cgi{image_options}" + f"{proto}://{host}:{port}/axis-cgi/jpg/image.cgi{image_options}" ) mjpeg_options = self.generate_options() self._mjpeg_url = ( - f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi" - f"/mjpg/video.cgi{mjpeg_options}" + f"{proto}://{host}:{port}/axis-cgi/mjpg/video.cgi{mjpeg_options}" ) stream_options = self.generate_options(add_video_codec_h264=True) @@ -95,10 +98,7 @@ def _generate_sources(self) -> None: self.hub.additional_diagnostics["camera_sources"] = { "Image": self._still_image_url, "MJPEG": self._mjpeg_url, - "Stream": ( - f"rtsp://user:pass@{self.hub.config.host}/axis-media" - f"/media.amp{stream_options}" - ), + "Stream": (f"rtsp://user:pass@{host}/axis-media/media.amp{stream_options}"), } @property diff --git a/homeassistant/components/axis/hub/config.py b/homeassistant/components/axis/hub/config.py index e6d8378b45c7be..eba706edc837da 100644 --- a/homeassistant/components/axis/hub/config.py +++ b/homeassistant/components/axis/hub/config.py @@ -12,6 +12,7 @@ CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_PROTOCOL, CONF_TRIGGER_TIME, CONF_USERNAME, ) @@ -31,6 +32,7 @@ class AxisConfig: entry: ConfigEntry + protocol: str host: str port: int username: str @@ -54,6 +56,7 @@ def from_config_entry(cls, config_entry: ConfigEntry) -> Self: options = config_entry.options return cls( entry=config_entry, + protocol=config.get(CONF_PROTOCOL, "http"), host=config[CONF_HOST], username=config[CONF_USERNAME], password=config[CONF_PASSWORD], From 618fa08ab2db104c768b772d611f517aaa349c3e Mon Sep 17 00:00:00 2001 From: Jeef Date: Thu, 4 Apr 2024 19:37:54 -0600 Subject: [PATCH 15/25] Bump weatherflow4py to 0.2.20 (#114888) --- homeassistant/components/weatherflow_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weatherflow_cloud/manifest.json b/homeassistant/components/weatherflow_cloud/manifest.json index 8376bd1b50d7ed..361349dcbe881a 100644 --- a/homeassistant/components/weatherflow_cloud/manifest.json +++ b/homeassistant/components/weatherflow_cloud/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud", "iot_class": "cloud_polling", - "requirements": ["weatherflow4py==0.2.17"] + "requirements": ["weatherflow4py==0.2.20"] } diff --git a/requirements_all.txt b/requirements_all.txt index 51de44eaca60da..ca23e4beaf75c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2838,7 +2838,7 @@ watchdog==2.3.1 waterfurnace==1.1.0 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.17 +weatherflow4py==0.2.20 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb1fb0583cbd7d..a3b058f202903f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2185,7 +2185,7 @@ wallbox==0.6.0 watchdog==2.3.1 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.17 +weatherflow4py==0.2.20 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 From 2434a22e4e487c9a654b6f23a365fbc84f83ab36 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Apr 2024 09:47:49 +0200 Subject: [PATCH 16/25] Fix Axis reconfigure step not providing protocols as alternatives but as string (#114889) --- homeassistant/components/axis/config_flow.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 30bc653c2028d5..80872fc9be4cc6 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -168,16 +168,13 @@ async def _redo_configuration( self, entry_data: Mapping[str, Any], keep_password: bool ) -> ConfigFlowResult: """Re-run configuration step.""" + protocol = entry_data.get(CONF_PROTOCOL, "http") + password = entry_data[CONF_PASSWORD] if keep_password else "" self.discovery_schema = { - vol.Required( - CONF_PROTOCOL, default=entry_data.get(CONF_PROTOCOL, "http") - ): str, + vol.Required(CONF_PROTOCOL, default=protocol): vol.In(PROTOCOL_CHOICES), vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str, vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str, - vol.Required( - CONF_PASSWORD, - default=entry_data[CONF_PASSWORD] if keep_password else "", - ): str, + vol.Required(CONF_PASSWORD, default=password): str, vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int, } From 71877fdeda060229db38b40a27cad375144d6c0c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 5 Apr 2024 00:26:07 +0200 Subject: [PATCH 17/25] Update frontend to 20240404.1 (#114890) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 75c630b44714d3..028fb28f01bb75 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240404.0"] + "requirements": ["home-assistant-frontend==20240404.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d520d7f8f769a4..bd35403340fa39 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240404.0 +home-assistant-frontend==20240404.1 home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ca23e4beaf75c4..9bbb5293660e07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240404.0 +home-assistant-frontend==20240404.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3b058f202903f..2125f7e13d0b15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240404.0 +home-assistant-frontend==20240404.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 From 87ffd5ac56750798d222b06c3122f4099e7ffced Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 20:22:00 -1000 Subject: [PATCH 18/25] Ensure all tables have the default table args in the db_schema (#114895) --- homeassistant/components/recorder/db_schema.py | 12 +++++++++++- tests/components/recorder/test_init.py | 7 +++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 5b24448211d4c1..eac743c3d75f87 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -715,6 +715,7 @@ class Statistics(Base, StatisticsBase): "start_ts", unique=True, ), + _DEFAULT_TABLE_ARGS, ) __tablename__ = TABLE_STATISTICS @@ -732,6 +733,7 @@ class StatisticsShortTerm(Base, StatisticsBase): "start_ts", unique=True, ), + _DEFAULT_TABLE_ARGS, ) __tablename__ = TABLE_STATISTICS_SHORT_TERM @@ -760,7 +762,10 @@ def from_meta(meta: StatisticMetaData) -> StatisticsMeta: class RecorderRuns(Base): """Representation of recorder run.""" - __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __table_args__ = ( + Index("ix_recorder_runs_start_end", "start", "end"), + _DEFAULT_TABLE_ARGS, + ) __tablename__ = TABLE_RECORDER_RUNS run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) @@ -789,6 +794,7 @@ class MigrationChanges(Base): """Representation of migration changes.""" __tablename__ = TABLE_MIGRATION_CHANGES + __table_args__ = (_DEFAULT_TABLE_ARGS,) migration_id: Mapped[str] = mapped_column(String(255), primary_key=True) version: Mapped[int] = mapped_column(SmallInteger) @@ -798,6 +804,8 @@ class SchemaChanges(Base): """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES + __table_args__ = (_DEFAULT_TABLE_ARGS,) + change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) schema_version: Mapped[int | None] = mapped_column(Integer) changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) @@ -816,6 +824,8 @@ class StatisticsRuns(Base): """Representation of statistics run.""" __tablename__ = TABLE_STATISTICS_RUNS + __table_args__ = (_DEFAULT_TABLE_ARGS,) + run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index cde2da3cc83c20..206c356bad8bdd 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -27,6 +27,7 @@ DOMAIN, SQLITE_URL_PREFIX, Recorder, + db_schema, get_instance, migration, pool, @@ -2598,3 +2599,9 @@ def run(self, instance: Recorder) -> None: await verify_states_in_queue_future await verify_session_commit_future + + +def test_all_tables_use_default_table_args(hass: HomeAssistant) -> None: + """Test that all tables use the default table args.""" + for table in db_schema.Base.metadata.tables.values(): + assert table.kwargs.items() >= db_schema._DEFAULT_TABLE_ARGS.items() From c39d6f07300d3def56be05cc8a8239c71a9b5a14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 15:28:36 -1000 Subject: [PATCH 19/25] Reduce august polling frequency (#114904) Co-authored-by: TheJulianJES --- homeassistant/components/august/activity.py | 21 +++++++++++- homeassistant/components/august/const.py | 2 +- homeassistant/components/august/subscriber.py | 33 +++++++++---------- tests/components/august/test_lock.py | 23 ++++++++++++- 4 files changed, 58 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index ae920383e408e5..ee180ab548014f 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -5,6 +5,7 @@ from datetime import datetime from functools import partial import logging +from time import monotonic from aiohttp import ClientError from yalexs.activity import Activity, ActivityType @@ -26,9 +27,11 @@ ACTIVITY_STREAM_FETCH_LIMIT = 10 ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 +INITIAL_LOCK_RESYNC_TIME = 60 + # If there is a storm of activity (ie lock, unlock, door open, door close, etc) # we want to debounce the updates so we don't hammer the activity api too much. -ACTIVITY_DEBOUNCE_COOLDOWN = 3 +ACTIVITY_DEBOUNCE_COOLDOWN = 4 @callback @@ -62,6 +65,7 @@ def __init__( self.pubnub = pubnub self._update_debounce: dict[str, Debouncer] = {} self._update_debounce_jobs: dict[str, HassJob] = {} + self._start_time: float | None = None @callback def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None: @@ -70,6 +74,7 @@ def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> Non async def async_setup(self) -> None: """Token refresh check and catch up the activity stream.""" + self._start_time = monotonic() update_debounce = self._update_debounce update_debounce_jobs = self._update_debounce_jobs for house_id in self._house_ids: @@ -140,11 +145,25 @@ def async_schedule_house_id_refresh(self, house_id: str) -> None: debouncer = self._update_debounce[house_id] debouncer.async_schedule_call() + # Schedule two updates past the debounce time # to ensure we catch the case where the activity # api does not update right away and we need to poll # it again. Sometimes the lock operator or a doorbell # will not show up in the activity stream right away. + # Only do additional polls if we are past + # the initial lock resync time to avoid a storm + # of activity at setup. + if ( + not self._start_time + or monotonic() - self._start_time < INITIAL_LOCK_RESYNC_TIME + ): + _LOGGER.debug( + "Skipping additional updates due to ongoing initial lock resync time" + ) + return + + _LOGGER.debug("Scheduling additional updates for house id %s", house_id) job = self._update_debounce_jobs[house_id] for step in (1, 2): future_updates.append( diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index 0cbd21f397e035..6aa033c62b2658 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -40,7 +40,7 @@ # Limit battery, online, and hardware updates to hourly # in order to reduce the number of api requests and # avoid hitting rate limits -MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=1) +MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=24) # Activity needs to be checked more frequently as the # doorbell motion and rings are included here diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py index e800b5cb60420d..9332080d9ad662 100644 --- a/homeassistant/components/august/subscriber.py +++ b/homeassistant/components/august/subscriber.py @@ -49,9 +49,17 @@ def _async_scheduled_refresh(self, now: datetime) -> None: """Call the refresh method.""" self._hass.async_create_task(self._async_refresh(now), eager_start=True) + @callback + def _async_cancel_update_interval(self, _: Event | None = None) -> None: + """Cancel the scheduled update.""" + if self._unsub_interval: + self._unsub_interval() + self._unsub_interval = None + @callback def _async_setup_listeners(self) -> None: """Create interval and stop listeners.""" + self._async_cancel_update_interval() self._unsub_interval = async_track_time_interval( self._hass, self._async_scheduled_refresh, @@ -59,17 +67,12 @@ def _async_setup_listeners(self) -> None: name="august refresh", ) - @callback - def _async_cancel_update_interval(_: Event) -> None: - self._stop_interval = None - if self._unsub_interval: - self._unsub_interval() - - self._stop_interval = self._hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, - _async_cancel_update_interval, - run_immediately=True, - ) + if not self._stop_interval: + self._stop_interval = self._hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, + self._async_cancel_update_interval, + run_immediately=True, + ) @callback def async_unsubscribe_device_id( @@ -82,13 +85,7 @@ def async_unsubscribe_device_id( if self._subscriptions: return - - if self._unsub_interval: - self._unsub_interval() - self._unsub_interval = None - if self._stop_interval: - self._stop_interval() - self._stop_interval = None + self._async_cancel_update_interval() @callback def async_signal_device_id_update(self, device_id: str) -> None: diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 39c1745d967e73..4de931e6979e47 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -4,9 +4,11 @@ from unittest.mock import Mock from aiohttp import ClientResponseError +from freezegun.api import FrozenDateTimeFactory import pytest from yalexs.pubnub_async import AugustPubNub +from homeassistant.components.august.activity import INITIAL_LOCK_RESYNC_TIME from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, STATE_JAMMED, @@ -155,7 +157,9 @@ async def test_one_lock_operation( async def test_one_lock_operation_pubnub_connected( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test lock and unlock operations are async when pubnub is connected.""" lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) @@ -230,6 +234,23 @@ async def test_one_lock_operation_pubnub_connected( == STATE_UNKNOWN ) + freezer.tick(INITIAL_LOCK_RESYNC_TIME) + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=(dt_util.utcnow().timestamp() + 2) * 10000000, + message={ + "status": "kAugLockState_Unlocked", + }, + ), + ) + await hass.async_block_till_done() + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + async def test_lock_jammed(hass: HomeAssistant) -> None: """Test lock gets jammed on unlock.""" From 5d5dc24b3339c9d45f958521ceaf8c8e4def526e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 5 Apr 2024 10:01:51 +0200 Subject: [PATCH 20/25] Show correct model string in myuplink (#114921) --- homeassistant/components/myuplink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/myuplink/__init__.py b/homeassistant/components/myuplink/__init__.py index 5dee46b24cfa0c..42bb90077898e9 100644 --- a/homeassistant/components/myuplink/__init__.py +++ b/homeassistant/components/myuplink/__init__.py @@ -5,7 +5,7 @@ from http import HTTPStatus from aiohttp import ClientError, ClientResponseError -from myuplink import MyUplinkAPI, get_manufacturer, get_system_name +from myuplink import MyUplinkAPI, get_manufacturer, get_model, get_system_name from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -92,7 +92,7 @@ def create_devices( identifiers={(DOMAIN, device_id)}, name=get_system_name(system), manufacturer=get_manufacturer(device), - model=device.productName, + model=get_model(device), sw_version=device.firmwareCurrent, serial_number=device.product_serial_number, ) From ed3daed86935b6aaf2fb625788fd6599c0273471 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 12:32:09 +0200 Subject: [PATCH 21/25] Create right import issues in Downloader (#114922) * Create right import issues in Downloader * Create right import issues in Downloader * Create right import issues in Downloader * Create right import issues in Downloader * Fix * Fix * Fix * Fix * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Fix --------- Co-authored-by: Martin Hjelmare --- .../components/downloader/__init__.py | 57 +++++++++++------ .../components/downloader/config_flow.py | 12 ++-- .../components/downloader/strings.json | 8 +-- .../components/downloader/test_config_flow.py | 16 +++++ tests/components/downloader/test_init.py | 64 ++++++++++++++++++- 5 files changed, 125 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index d110c28785aa30..3fded1215c48ca 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -11,7 +11,11 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + ServiceCall, +) from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -43,7 +47,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True - hass.async_create_task(_async_import_config(hass, config), eager_start=True) + hass.async_create_task(_async_import_config(hass, config)) return True @@ -58,27 +62,40 @@ async def _async_import_config(hass: HomeAssistant, config: ConfigType) -> None: }, ) - translation_key = "deprecated_yaml" if ( import_result["type"] == FlowResultType.ABORT - and import_result["reason"] == "import_failed" + and import_result["reason"] != "single_instance_allowed" ): - translation_key = "import_failed" - - async_create_issue( - hass, - DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.10.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key=translation_key, - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Downloader", - }, - ) + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="directory_does_not_exist", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Downloader", + "url": "/config/integrations/dashboard/add?domain=downloader", + }, + ) + else: + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Downloader", + }, + ) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/downloader/config_flow.py b/homeassistant/components/downloader/config_flow.py index 15af8b56163bf7..94b33f4e93fa58 100644 --- a/homeassistant/components/downloader/config_flow.py +++ b/homeassistant/components/downloader/config_flow.py @@ -46,12 +46,16 @@ async def async_step_user( errors=errors, ) - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Handle a flow initiated by configuration file.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") - return await self.async_step_user(user_input) + try: + await self._validate_input(user_input) + except DirectoryDoesNotExist: + return self.async_abort(reason="directory_does_not_exist") + return self.async_create_entry(title=DEFAULT_NAME, data=user_input) async def _validate_input(self, user_input: dict[str, Any]) -> None: """Validate the user input if the directory exists.""" diff --git a/homeassistant/components/downloader/strings.json b/homeassistant/components/downloader/strings.json index 77dd0abd9d32a0..4cadabf96c6783 100644 --- a/homeassistant/components/downloader/strings.json +++ b/homeassistant/components/downloader/strings.json @@ -37,13 +37,9 @@ } }, "issues": { - "deprecated_yaml": { - "title": "The {integration_title} YAML configuration is being removed", - "description": "Configuring {integration_title} using YAML is being removed.\n\nYour configuration is already imported.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - }, - "import_failed": { + "directory_does_not_exist": { "title": "The {integration_title} failed to import", - "description": "The {integration_title} integration failed to import.\n\nPlease check the logs for more details." + "description": "The {integration_title} integration failed to import because the configured directory does not exist.\n\nEnsure the directory exists and restart Home Assistant to try again or remove the {integration_title} configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." } } } diff --git a/tests/components/downloader/test_config_flow.py b/tests/components/downloader/test_config_flow.py index 5e75a9b33ba333..897fbba0c599fa 100644 --- a/tests/components/downloader/test_config_flow.py +++ b/tests/components/downloader/test_config_flow.py @@ -99,3 +99,19 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: assert result["title"] == "Downloader" assert result["data"] == {} assert result["options"] == {} + + +async def test_import_flow_directory_not_found(hass: HomeAssistant) -> None: + """Test import flow.""" + with patch("os.path.isdir", return_value=False): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_DOWNLOAD_DIR: "download_dir", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "directory_does_not_exist" diff --git a/tests/components/downloader/test_init.py b/tests/components/downloader/test_init.py index 8cd0d00b1abcfa..5832c0402b4207 100644 --- a/tests/components/downloader/test_init.py +++ b/tests/components/downloader/test_init.py @@ -8,7 +8,8 @@ SERVICE_DOWNLOAD_FILE, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -30,7 +31,7 @@ async def test_initialization(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.LOADED -async def test_import(hass: HomeAssistant) -> None: +async def test_import(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None: """Test the import of the downloader component.""" with patch("os.path.isdir", return_value=True): assert await async_setup_component( @@ -49,3 +50,62 @@ async def test_import(hass: HomeAssistant) -> None: assert config_entry.data == {CONF_DOWNLOAD_DIR: "/test_dir"} assert config_entry.state is ConfigEntryState.LOADED assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE) + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + issue_id="deprecated_yaml_downloader", domain=HOMEASSISTANT_DOMAIN + ) + assert issue + + +async def test_import_directory_missing( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the import of the downloader component.""" + with patch("os.path.isdir", return_value=False): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DOWNLOAD_DIR: "/test_dir", + }, + }, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + issue_id="deprecated_yaml_downloader", domain=DOMAIN + ) + assert issue + + +async def test_import_already_exists( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the import of the downloader component.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_DOWNLOAD_DIR: "/test_dir", + }, + ) + config_entry.add_to_hass(hass) + with patch("os.path.isdir", return_value=True): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DOWNLOAD_DIR: "/test_dir", + }, + }, + ) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + issue_id="deprecated_yaml_downloader", domain=HOMEASSISTANT_DOMAIN + ) + assert issue From 9937743863c9135b0865f6548aed4d1c88698567 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 11:44:51 +0200 Subject: [PATCH 22/25] Fix cast dashboard in media browser (#114924) --- homeassistant/components/lovelace/cast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lovelace/cast.py b/homeassistant/components/lovelace/cast.py index 02f5d0c0478ca5..82a92b94ae5d8d 100644 --- a/homeassistant/components/lovelace/cast.py +++ b/homeassistant/components/lovelace/cast.py @@ -179,7 +179,7 @@ async def _get_dashboard_info(hass, url_path): "views": views, } - if config is None: + if config is None or "views" not in config: return data for idx, view in enumerate(config["views"]): From e3c111b1dd0fd41e3d15a6c4fe300bdadfa60885 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Apr 2024 12:34:07 +0200 Subject: [PATCH 23/25] Bump version to 2024.4.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6e08c49f970a87..b642ce6ce8c5f7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index ac890603ac3c8d..2dd3a9632c6f84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0" +version = "2024.4.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 47d9879c0c1da274d9939d530441aff9661d2fb5 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 4 Apr 2024 13:25:35 +0100 Subject: [PATCH 24/25] Pin systembridgemodels to 4.0.4 (#114842) --- homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index b4365fda778d96..aea66d22f62a84 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -10,6 +10,6 @@ "iot_class": "local_push", "loggers": ["systembridgeconnector"], "quality_scale": "silver", - "requirements": ["systembridgeconnector==4.0.3"], + "requirements": ["systembridgeconnector==4.0.3", "systembridgemodels==4.0.4"], "zeroconf": ["_system-bridge._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9bbb5293660e07..a87df9614d1260 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2654,6 +2654,9 @@ synology-srm==0.2.0 # homeassistant.components.system_bridge systembridgeconnector==4.0.3 +# homeassistant.components.system_bridge +systembridgemodels==4.0.4 + # homeassistant.components.tailscale tailscale==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2125f7e13d0b15..1f5d01eb46cd8f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2049,6 +2049,9 @@ switchbot-api==2.0.0 # homeassistant.components.system_bridge systembridgeconnector==4.0.3 +# homeassistant.components.system_bridge +systembridgemodels==4.0.4 + # homeassistant.components.tailscale tailscale==0.6.0 From 95606135a629fd698a26266354a5246bb4713c54 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 14:21:24 +0200 Subject: [PATCH 25/25] Fix ROVA validation (#114938) * Fix ROVA validation * Fix ROVA validation --- homeassistant/components/rova/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index e510bcf0caf92d..f63b9893c02fcb 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -54,7 +54,7 @@ vol.Optional(CONF_HOUSE_NUMBER_SUFFIX, default=""): cv.string, vol.Optional(CONF_NAME, default="Rova"): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=["bio"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + cv.ensure_list, [vol.In(["bio", "paper", "plastic", "residual"])] ), } )